目录
1. 在事务内部使用锁,锁在事务提交前释放
1.1 场景
创建付款单,要求不能重复创建相同业务单号的付款单。
为了保证幂等,我们需要判断数据库中是否已经存在相同业务单号的付款单,并且需要加锁处理并发安全性问题。
@Transactional
public void createPaymentOrderInnerLock(PaymentOrder paymentOrder){
RLock lock = redissonClient.getLock(paymentOrder.getBizNo());
//采用的redisson可重入锁,提供watchdog机制,在锁释放前默认每10s重置锁失效时间为30s
lock.lock();
try {
LambdaQueryWrapper<PaymentOrder> paymentOrderLambdaQueryWrapper = new LambdaQueryWrapper<>();
paymentOrderLambdaQueryWrapper.eq(PaymentOrder::getBizNo,paymentOrder.getBizNo());
//判断数据库中是否存在相同业务单号的付款单
long count = this.count(paymentOrderLambdaQueryWrapper);
//存在相同业务单号的付款单则抛异常
if(count>0){
throw new RuntimeException("不可重复提交付款单");
}else{
//无重复数据,创建付款单
this.save(paymentOrder);
//其他DB操作
...
}
} finally {
//释放锁
lock.unlock();
}
}
通过JMeter测试并发下的结果,出现了重复业务单号的数据
1.2 问题分析
1.3 解决方案
为了避免锁在事务提交前释放,我们应该在事务外层使用锁,用法如下:
public void createPaymentOrderOuterLock(PaymentOrder paymentOrder) {
RLock lock = redissonClient.getLock(paymentOrder.getBizNo());
//采用的redisson可重入锁,提供watchdog机制,在锁释放前默认每10s重置锁失效时间为30s
lock.lock();
try {
applicationContext.getBean(PaymentOrderService.class).createPaymentOrderNoLock(paymentOrder);
} finally {
//释放锁
lock.unlock();
}
}
@Transactional
public void createPaymentOrderNoLock(PaymentOrder paymentOrder) {
LambdaQueryWrapper<PaymentOrder> paymentOrderLambdaQueryWrapper = new LambdaQueryWrapper<>();
paymentOrderLambdaQueryWrapper.eq(PaymentOrder::getBizNo,paymentOrder.getBizNo());
long count = this.count(paymentOrderLambdaQueryWrapper);
if(count>0){
log.info("不可重复提交付款单");
throw new RuntimeException("不可重复提交付款单");
}else{
this.save(paymentOrder);
//其他DB操作
...
}
}
2. 业务未执行完,锁超时释放
2.1 场景
@Override
public void createPaymentOrderRenault(List<PaymentOrder> paymentOrderList){
if(!CollectionUtils.isEmpty(paymentOrderList)){
for (PaymentOrder paymentOrder : paymentOrderList) {
/**
* 采用公司框架提供的分布式锁
* 10---等待锁释放时间
* 1---尝试获取锁时间间隔
* 5---锁失效时间
* 注意:此处设置锁失效时间为5秒,在createPaymentOrderNoLock中睡眠5秒模拟耗时操作,此时会出现业务未执行完,锁超时释放的问题
*/
try (AutoReleaseLock lock = acquireLock(paymentOrder.getBizNo(), 10, 1, 5, TimeUnit.SECONDS)) {
if(lock != null) {
paymentOrderService.createPaymentOrderNoLock(paymentOrder);
} else {
log.info("未获取到锁!");
}
}catch (CacheParamException e) {
log.info("获取锁失败");
}
}
}
}
@Override
@Transactional
public void createPaymentOrderNoLock(PaymentOrder paymentOrder) {
LambdaQueryWrapper<PaymentOrder> paymentOrderLambdaQueryWrapper = new LambdaQueryWrapper<>();
paymentOrderLambdaQueryWrapper.eq(PaymentOrder::getBizNo,paymentOrder.getBizNo());
long count = this.count(paymentOrderLambdaQueryWrapper);
if(count>0){
log.info("不可重复提交付款单");
throw new RuntimeException("不可重复提交付款单");
}else{
this.save(paymentOrder);
//模拟耗时操作...
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
2.2 解决方案
- 把失效时间设置长一点,确实可以。但设置多长合适呢,设置过长有可能存在拿到锁的客户端宕掉了,此时就要等锁过期才能释放,其他节点处于阻塞状态,降低了系统吞吐。
- 可以采用类似Redisson的watchdog机制给锁续命。另外,注意减小锁的粒度,把存在并发安全性问题的关键代码锁住即可,增加系统吞吐量。同时也要注意减小事务的粒度,把查询操作、甚至一些远程调用放到事务外部(注意读写分离的情况),避免出现大事务问题。
3. redis主从切换
3.1 场景
- 客户端A从master获取到锁
- 在master将锁同步到slave之前,master宕掉了。
- slave节点被晋升为master节点
- 客户端B取得了同一个资源被客户端A已经获取到的同一个锁
3.2 解决方案
假设有N个redis的master节点,这些节点是相互独立的(不需要主从或者其他协调的系统)。N推荐为奇数~
客户端在获取锁时,需要做以下操作:
获取当前时间戳,以微妙为单为。
使用相同的lockName和lockValue,尝试从N个节点获取锁。(在获取锁时,要求等待获取锁的时间远小于锁的释放时间,如锁的lease_time为10s,那么wait_time应该为5-50毫秒;避免因为redis实例挂掉,客户端需要等待更长的时间才能返回,即需要让客户端能够fast_fail;如果一个redis实例不可用,那么需要继续从下个redis实例获取锁)
当从N个节点获取锁结束后,如果客户端能够从多数节点(N/2 + 1)中成功获取锁,且获取锁的时间小于失效时间,那么可认为,客户端成功获得了锁。(获取锁的时间=当前时间戳 - 步骤1的时间戳)
客户端成功获得锁后,那么锁的实际有效时间 = 设置锁的有效时间 - 获取锁的时间。
客户端获取锁失败后,N个节点的redis实例都会释放锁,即使未能加锁成功。
RedLock存在的争议
Redlock 必须「强依赖」多个节点的时钟是保持同步的,一旦有节点时钟发生错误,那这个算法模型就失效了。
客户端 A 获取节点 1、2、3 上的锁。由于网络问题,无法访问 4 和 5。
节点 3 上的时钟向前跳跃,导致锁到期。
客户端 B 获取节点 3、4、5 上的锁。由于网络问题,无法访问 1 和 2。
客户端 A 和 B 现在都相信他们持有锁。
3.3 扩展
redis ap 可用性
zookeeper cp一致性(当超过半数节点获取到同步消息时才返回信息)
来源:https://blog.csdn.net/ZDK_csdn/article/details/122487945
红锁:https://www.jianshu.com/p/8d929ea3c5c6