场景
前情提要:https://segmentfault.com/a/11...
为了解决max(id) 引发的事务可见性问题,最初是通过间隙锁完成,但是间隙锁锁的是表,对并发影响太大,因此考虑使用粒度更精细的分布式读写锁。
读写锁
读写锁原理,读读并发,读写、写写不行
根据我们业务规则,可以并发写,但是写的时候不能读,读的时候不能写
因此写操作的时候获取的是读锁,读操作的时候获取的是写锁
引入Redisson
pom.xml
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.0</version>
</dependency>
装配
@Bean(destroyMethod = "shutdown")
public RedissonClient getRedissonClient() {
//创建配置
Config config = new Config();
//指定编码,默认编码为org.redisson.codec.JsonJacksonCodec
//之前使用的spring-data-redis,用的客户端jedis,编码为org.springframework.data.redis.serializer.StringRedisSerializer
//改用redisson后为了之间数据能兼容,这里修改编码为org.redisson.client.codec.StringCodec
config.setCodec(new org.redisson.client.codec.StringCodec());
//指定使用单节点部署方式
config.useSingleServer()
.setAddress(String.format("redis://%s:%s", redisConfiguration.getHost(), redisConfiguration.getPort()))
.setPassword(redisConfiguration.getPassword())
.setConnectionPoolSize(redisConfiguration.getPool().getMaxActive())
.setConnectionMinimumIdleSize(redisConfiguration.getPool().getMinIdle())
.setIdleConnectionTimeout(redisConfiguration.getPool().getIdleTimeout())
.setConnectTimeout(redisConfiguration.getConnectTimeout())
.setTimeout(redisConfiguration.getReadTimeout());
return Redisson.create(config);
}
Lock 服务
代码逻辑比较简单,不明白评论区留言,及时回复。
@Service
@Slf4j
public class LockService {
@Resource
private RedissonClient redissonClient;
@Value("${LOCK_RETRY_TIME:3}")
private Integer lockReTryTime;
/**
* 获取读锁用于存数据
*
* @param computeUuids
*/
public void getReadLock(List<String> computeUuids) {
getLock(computeUuids, true);
}
/**
* 释放读锁
*
* @param computeUuids
*/
public void unReadLock(List<String> computeUuids) {
unLock(computeUuids, true);
}
/**
* 获取读锁用于存数据
*
* @param computeUuids
*/
public void getWriteLock(List<String> computeUuids) {
getLock(computeUuids, false);
}
/**
* 释放读锁
*
* @param computeUuids
*/
public void unWriteLock(List<String> computeUuids) {
unLock(computeUuids, false);
}
/**
* 获取读锁用于存数据
*
* @param computeUuids
*/
public void getLock(List<String> computeUuids, boolean read) {
log.info("批量获取{}锁 computeUuid:{}", read ? "读" : "写", JSON.toJSONString(computeUuids));
List<RLock> lockedList = new ArrayList<>();
try {
for (String computeUuid : computeUuids) {
String lockName = RedisCacheKey.DEVICE_AUTHORITY_MODIFY_SYNC_LOCK.build(computeUuid);
RLock rLock;
if (read) {
rLock = redissonClient.getReadWriteLock(lockName).readLock();
} else {
rLock = redissonClient.getReadWriteLock(lockName).writeLock();
}
boolean isLocked = false;
int count = 0;
log.info("lockName:{} 开始获取{}锁", lockName, read ? "读" : "写");
// 尝试3次获取,获取失败全部释放
do {
try {
// 尝试获取锁(等待时间,过期时间,时间单位)
isLocked = rLock.tryLock(RedisCacheKey.DEVICE_AUTHORITY_MODIFY_SYNC_LOCK.getWaitTime(), RedisCacheKey.DEVICE_AUTHORITY_MODIFY_SYNC_LOCK.getExpireSecs(), TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
log.info("获取锁失败:{}", rLock.getName());
count++;
}
} while (!isLocked && count < lockReTryTime);
if (count == 3) {
log.info("================={}锁申请失败{},开始释放============================", read ? "读" : "写", lockName);
break;
} else {
lockedList.add(rLock);
log.info("================={}锁申请成功{}============================", read ? "读" : "写", lockName);
}
}
} finally {
if (computeUuids.size() != lockedList.size()) {
log.info("=================存在{}锁申请失败,开始释放============================", read ? "读" : "写");
lockedList.forEach(lock -> {
lock.unlock();
log.info("================={}锁释放成功{}============================", read ? "读" : "写", lock.getName());
});
}
}
}
/**
* @param computeUuids
*/
public void unLock(List<String> computeUuids, boolean read) {
log.info("批量释放{}锁 computeUuid:{}", read ? "读" : "写", JSON.toJSONString(computeUuids));
for (String computeUuid : computeUuids) {
String lockName = RedisCacheKey.DEVICE_AUTHORITY_MODIFY_SYNC_LOCK.build(computeUuid);
log.info("lockName:{} 开始释放{}锁", lockName, read ? "读" : "写");
RLock rLock;
try {
if (read) {
rLock = redissonClient.getReadWriteLock(lockName).readLock();
} else {
rLock = redissonClient.getReadWriteLock(lockName).writeLock();
}
rLock.unlock();
} catch (Exception e) {
log.info("释放时获取锁失败,继续获取下一把:{}", lockName);
}
log.info("lockName:{} 释放{}锁成功", lockName, read ? "读" : "写");
}
log.info("================={}锁释放成功============================", read ? "读" : "写");
}
}
Filter 优化放锁
插入数据可以通过统一入口,在固定位置执行,但是放锁需要在各个业务出口处理
由于出口众多,一个一个写无疑是最容易想到的,但是也是最...emmm....憨憨的
于是想到了 webFilter ,每一个请求的线程都需要经过Filter,于是就想到了利用 Filter + ThreadLocal处理
全局 ThreadLocal
public class ComputeLockThreadLocal {
private static final ThreadLocal<List<String>> threadLocal = new NamedThreadLocal<>("computeLockList");
public static void set(List<String> computeLockList) {
threadLocal.set(computeLockList);
}
public static List<String> get() {
return threadLocal.get();
}
public static void remove() {
threadLocal.remove();
}
}
定义 WebFilter
@Slf4j
public class PassWebFilter implements Filter {
@Resource
private LockService lockService;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
try {
filterChain.doFilter(servletRequest, servletResponse);
} finally {
List<String> computeLockList = ComputeLockThreadLocal.get();
log.debug("*********************获取的读写锁 computeLockList:{}", JSON.toJSONString(computeLockList));
if (CollectionUtils.isNotEmpty(computeLockList)) {
log.info("*********************释放的读写锁 computeLockList:{}", JSON.toJSONString(computeLockList));
lockService.unReadLock(computeLockList);
ComputeLockThreadLocal.remove();
}
}
}
@Override
public void destroy() {
}
}
配置 WebFilter
@Configuration
public class WebConfiguration {
@Value("${web.filter.urlPatterns:/*}")
private String webPrefix;
@Bean("passWebFilter")
public Filter webFilter() {
return new PassWebFilter();
}
@Bean
public FilterRegistrationBean webFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new DelegatingFilterProxy("passWebFilter"));
registration.setName("passWebFilter");
registration.addUrlPatterns(webPrefix);// 配置拦截的url,全部拦截
registration.setOrder(5);
return registration;
}
@Bean
public RequestContextListener requestContextListener() {
return new RequestContextListener();
}
}
这样就可以通过加锁时放入锁key,然后在会话结束时通过 WebFilter 时统一判断是否要放锁,减少修改量
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。