一、锁
锁是多线程编程中的一种机制,用于控制对共享资源的访问。它可以防止多个线程同时修改或读取共享资源,从而保证线程安全。
二、分布式锁
线程锁和进程锁仅能满足在单机jvm或者同一个操作系统下,才能有效。跨jvm系统,无法满足。因此就产生了分布式锁,完成锁的工作。分布式锁一般是基于数据库,基于redis(redission),基于zookeeper三种方式实现。
三、这里我们主要说Redission分布式锁失效的问题
失效场景:在事务中使用redission分布式锁(且用户表和流水表中没有添加合适的唯一索引,或者用户余额的计算放在业务中进行计算)
场景描述:用户余额处理,要求同一笔订单不允许重复处理余额
异常代码实现 :
@Transactional(rollbackFor = Exception.class)
public void demo(Form form){
RLock lock = null;
try {
lock = redissonClient.getLock("demo_" + form.getUserUuid());
boolean hasLock = lock.tryLock(10, TimeUnit.SECONDS);
if(!hasLock){
throw new BusinessException( "处理用户余额失败,此用户修改余额获取锁失败");
}
Integer orderCount = orderMapper.selectOrderNoRecordsCount(form.getUserUuid(),form.getOrderNo());
if(orderCount != null && orderCount > 0){
throw new BusinessException( "处理用户余额失败,此订单已处理");
}
//todo--处理用户余额
form.setBalance(beforBalance + consumptionAmount);
userMapper.updateUserBalance(form);
//添加订单流水记录
addOrderNoRecords(form);
//todo--添加订单流水记录
orderMapper.addOrderNoRecords(form);
//todo--其他业务逻辑
} finally {
if(lock != null){
lock.unlock();
}
}
}
实现的代码,看起来貌似没有问题,但是两个并发的请求req1,req2(同一个用户的余额处理)过来时,req1,req2同时查询流水表中是否存在当前订单,因为req1的事务还没有提交,req2此时查询到的orderCount也是0,那么这种情况下两个请求都会继续执行。这时可能会出现以下几种异常情况:
-
req1,req2各自更新自己的余额,因为在业务中处理余额,会导致余额为后提交的余额,从而导致余额异常。
-
Req1,req2如果是同一笔订单,重复提交的请求,这时因为流水表中没有添加唯一索引,这时就会出现流水表出现重复的记录问题
如果你的业务逻辑还相对复杂一些,那么还有可能引起其他异常问题。
解决方法:在使用redission分布式锁的前提下,因为我们需要保证数据的一致性,所以事务不能去除,所以我们可以直接将redission的分布式锁提到controller层,这样就可以解决了。
@PostMapping("demo")
public R demo(@RequestBody Form form){
RLock lock = null;
try {
lock = redissonClient.getLock("demo_" + form.getUserUuid());
boolean hasLock = lock.tryLock(10, TimeUnit.SECONDS);
if(!hasLock){
throw new BusinessException( "处理用户余额失败,此用户修改余额获取锁失败");
}
String result = DemoService.demo(form);
//todo--其他业务逻辑处理
} catch (Exception e) {
System.out.println("打印异常信息");
} finally {
if(lock != null){
lock.unlock();
}
}
//todo--其他业务逻辑处理
return R.failed("处理用户余额异常");
}
Service层:
@Transactional(rollbackFor = Exception.class)
public void demo(RequestForm form){
Integer orderCount = orderMapper.selectOrderNoRecordsCount(form.getUserUuid(),form.getOrderNo());
if(orderCount != null && orderCount > 0){
throw new BusinessException( "处理用户余额失败,此订单已处理");
}
//todo--处理用户余额
form.setBalance(beforBalance + consumptionAmount);
userMapper.updateUserBalance(form);
//添加订单流水记录
addOrderNoRecords(form);
//todo--添加订单流水记录
orderMapper.addOrderNoRecords(form);
//todo--其他业务逻辑
}