public class DayEntity implements java.io.Serializable {
/**主键*/
private String id;
/**预约额度*/
private Integer totalNum;
/**已预约人数*/
private Integer bookNum;
}
public class AppointmentUserEntity implements java.io.Serializable {
/**主键*/
private String id;
private Date createDate;
/**预约时段*/
private String appointmentTime;
}
DayEntity dayEntity = this.get(DayEntity.class, dayId);//从库中找数据
if(RedissonLockUtil.tryLock(CacheKeys.BOOK_SYNC_LOCK+dayId , 300, 500)){//分布式锁
setAppointmentByDay(entity,dayEntity);//业务逻辑:会更改DayEntity 的bookNum值,并在AppointmentUserEntity表新建一条数据
尴尬了,认真一看锁的位置确实不太对,从数据库拿锁后再加锁,并发情况下,下次库中拿到的数据dayEntity有可能是旧的。于是改下这段代码再试试
if(RedissonLockUtil.tryLock(CacheKeys.BOOK_SYNC_LOCK+dayId , 300, 500)){//分布式锁
DayEntity dayEntity = this.get(DayEntity.class, dayId);//从库中找数据
setAppointmentByDay(entity,dayEntity);//业务逻辑:会更改DayEntity 的bookNum值,并在AppointmentUserEntity表新建一条数据
private UserEntity setAppointmentByDay(UserEntity entity, DayEntity dayEntity) {
//已预约人数 小于 预约额度
if(dayEntity.getTotalNum > dayEntity.getBookNum ){
dayEntity.setBookNum(dayEntity.getBookNum()+1);//预约额度 +1
dayService.update(dayEntity);
this.save(entity);//预约用户加一
}
//...其他逻辑
return entity;
}
这次本地在idea启动多一个服务试试,发现移出去后确保了第一个线程完成之前,第二个线程才能获取值(保证获取到的是新bookNum值)。
发现当并发上来竟然会有问题,还是出现了50个号,但是预约人数超过了50人。
![在这里插入图片描述](https://img-blog.csdnimg.cn/9d53669e541f48aa81a7fc3107c4e67f.png
把线程名称和获取到的已预约数量拿出来,发觉不同的线程会拿到DayEntity旧的已预约人数bookNum的值。
if(RedissonLockUtil.tryLock(CacheKeys.BOOK_SYNC_LOCK+dayId , 300, 500)){
Thread.sleep(30);//refresh db 让这种方式尝试让数据库恢复最新值
DayEntity dayEntity = this.get(DayEntity.class, dayId);
setAppointmentByDay(entity,dayEntity);
鉴于以上两点并测试发现睡眠适量时间后并发情况的数据超预约问题可以解决,以为分布式锁成功只是并发情况下更新了数量立马读出来。于是尝试了另外一个思路,既然数据库io性能比较慢 那么何不换redis 来记录每次的已预约记录数DayEntity.bookNum。
private UserEntity setAppointmentByDay(UserEntity entity, DayEntity dayEntity) {
//已预约人数 小于 预约额度
if(dayEntity.getTotalNum > redisValue ){ //这里每次判断从redis拿值判断
dayEntity.setBookNum(dayEntity.getBookNum()+1);//预约额度 +1
dayService.update(dayEntity);
userService.save(entity);//预约用户加一
}
//...其他逻辑
return entity;
}
这样每次对比库存中的值是否满了都从redis拿redisValue来判断,对redis的key的操作是单线程的保证拿到的值是唯一并且是最新的。通过引入redis后解决了并发问题,JMeter无论调多高都能正确返回数据了。
怀疑是跟事务有关系 ,首先重新写了一个例子,重新看代码测试一下。
短点后直接进入@Transactional的切面
if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
// Standard transaction demarcation with getTransaction and commit/rollback calls.
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
Object retVal = null;
try {
// This is an around advice: Invoke the next interceptor in the chain.
// This will normally result in a target object being invoked.
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// target invocation exception
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
cleanupTransactionInfo(txInfo);
}
commitTransactionAfterReturning(txInfo);
return retVal;
}
进入切面后代码后突然恍然大悟了,定位到Spring源码org.springframework.transaction.interceptor.TransactionAspectSupport#invokeWithinTransaction, 可以发现锁在事务提交前释放了。
子类继承父类后也有事务,相当于加了@Transactional,在spring的@Transactional切面源码中看到它是先执行切面代码 最后提交事务,执行切面代码时分布式锁已经释放 而因为事务还没提交数据库的值还没发生改变,所以其他线程能够拿到旧值 所以并发情况下有数据不一致问题,解决办法:把锁放在事务外层
由于这个类继承的一个公共类加了一个@Transaction方法了(这个jeecg框架的公共类层面加了感觉粒度有点大),所以其实这个子类也有事务了的,上面截图等价于:
@Transactional
@Override
public ResultVo test() throws BaseBizException {
/**
* 尝试获取锁
*
* @param lockKey
* @param waitTime 最多等待时间
* @param leaseTime 上锁后自动释放锁时间
* @return
*/
public static boolean tryLock(String lockKey, int waitTime, int leaseTime) {
return redissLock.tryLock(lockKey, TimeUnit.SECONDS, waitTime, leaseTime);
}
解决方案为可以把锁失效时间设置长一点
或者可以采用类似Redisson的watchdog机制给锁续命