我们之前讲了秒杀模块的实现,使用了sychronized互斥锁,但是在集群模式下因为不同服务器有不同jvm,所以synchronized互斥锁失效了。
redis实现秒杀超卖问题的解决方案:(仅限于单体项目)-CSDN博客
这时就要找到一个多台服务器都能识别的锁,即redis中的setNX充当互斥锁,来控制秒杀的一人一单
在redis缓存击穿中,使用逻辑过期就用过互斥锁,这里原理一摸一样,只不过这里存储的value为UUID+线程ID
setNX互斥锁的使用:
场景1:(会导致一个用户创建多个订单)
注*线程1和线程2的userID相同,所以创建的redis锁key值相同,但是value不相同,释放锁时如果不进行验证value值,很有可能会出现场景1的情况。
场景2:
注*在线程1检查锁后,发现自己的锁过期了,该锁不是自己创建的,说明其他相同userID的线程也在创建订单,这时应该回滚,撤销之前数据库操作。(在调用减库存创建订单的方法中回滚)
代码实现:
锁工具:
创建后不能注册为Bean,用的时候new对象即可,如果想注册为Bean使用keywords和value都要作为参数传递,否者会出现多线程随意修改该值的情况
public class RedisLock {StringRedisTemplate template;String keywords;String value;public RedisLock(StringRedisTemplate template,String keywords){this.keywords=keywords;this.template=template;}//尝试创建锁public boolean tryLock(Integer timeOutSecond){//给该线程生成唯一标识,作为valuevalue=UUID.randomUUID().toString().replace("-","")+"-"+Thread.currentThread().getId();return template.opsForValue().setIfAbsent("lock:"+keywords,value,timeOutSecond, TimeUnit.SECONDS);}//尝试删除锁public void delLovk(){//释放锁之前先验证锁是否过期,是为自己的锁String result = template.opsForValue().get("lock:" + keywords);if(result!=null || result.equals(value)){template.delete("lock:"+keywords);}}
}
之所以不将该类注册为bean使用,是因为创建锁时,要获取UUID+线程的ID,删除锁时也需要该值,所以这个值只能使用一个全局变量来记录。
如果注册为bean后,所有线程的操作都使用该对象中value属性去进行赋值和删除操作,就会导致value被不断修改,keywords也会被不断修改,最终导致程序逻辑错误,应该一个线程使用一个独有的value属性,所以不能将该工具类注册为Bean,用的时候new即可
业务逻辑代码:有原来的synchronized改为分布式锁控制线程创建订单
@AutowiredApplicationContext context;//模仿秒杀减库存,创建订单@Overridepublic Boolean killInSecond(Integer userID,Integer productID){//检查库存是否>0Product product = pm.selectByPrimaryKey(productID);if(product.getSales()<=0){throw new MyExceptionHandler("库存不足");}//调用2-4步骤方法Boolean result=false;//同步锁
// synchronized (userID.toString().intern()){
// //使用代理对象调用事务方法
// ProductServiceImpl bean = context.getBean(ProductServiceImpl.class);
// result=bean.ProductAndOrder(userID,productID);
// }//分布式锁RedisLock redisLock = new RedisLock(template, "order:" + userID);//获取锁result=redisLock.tryLock(30);if(!result){throw new RuntimeException("该用户只能创建一个订单");}//使用代理对象调用事务方法ProductServiceImpl bean = context.getBean(ProductServiceImpl.class);result=bean.ProductAndOrder(userID,productID);//释放锁redisLock.delLovk();return result;}
减库存创建订单方法:
@AutowiredOrderMapper om;@AutowiredRedisIdIncrement redisId;//创建订单,减库存操作@Transactionalpublic Boolean ProductAndOrder(Integer userID,Integer productID){//检查数据库中书否存在该用户订单Integer orderCount = om.selectOrderByUserIdAndProductId(userID, productID);if(orderCount>0){throw new MyExceptionHandler("用户已下单");}//订单不存在减库存,宽松乐观锁Integer result = pm.updateProductBysale(productID);if(result!=1){throw new MyExceptionHandler("库存不足");}//创建订单//获取redis唯一IDLong orderId = redisId.getRedisID("order");//封装订单Order order=new Order(orderId.toString(),userID,"","",productID,"",null,1,0,null,null,null,null,new BigDecimal(100));result = om.insertCompleteOrder(order);if(result!=1){return false;}return true;}
不足:
虽然我们通过检查锁的value值判断该锁是否为本线程创建的锁,控制了误删锁的可能,但是这里依然会没有解决多个相同userID的线程,会创建多个订单的情况。
情况一:
在线程1检查锁后,发现自己的锁过期了,该锁不是自己创建的,说明其他相同userID的线程也在创建订单,这时应该回滚,撤销之前数据库操作。(在调用减库存创建订单的方法中回滚)
情况2:
删除锁时发现自己的锁过期了,缓存中没有该锁,说明
1.没有其他相同userID用户执行创建订单的逻辑,不需回滚直接结束程序即可
2.有其他线程执行了操作,但是已经执行完毕,订单也已经创建完毕,继续执行程序即可,因为创建订单时发现订单已存在,自会回滚