当前位置: 首页 > news >正文

redis中分布式锁的应用

我们之前讲了秒杀模块的实现,使用了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.有其他线程执行了操作,但是已经执行完毕,订单也已经创建完毕,继续执行程序即可,因为创建订单时发现订单已存在,自会回滚

情况三:使用Lua脚本保证redis验证和删除操作为原子性操作(即不可被打断)

分布式锁工具代码可知,通过(get("键名"))获取锁的value值进行对比验证,和del删除锁是两个操作。有可能验证完条件就被阻塞

        //释放锁之前先验证锁是否过期,是为自己的锁String result = template.opsForValue().get("lock:" + keywords);if(result!=null || result.equals(value)){//阻塞,刚验证完条件就被阻塞template.delete("lock:"+keywords);}}

在验证完value值,允许删除锁后,线程也可能被垃圾回收机制CG给阻塞几毫秒,这时删除锁的操作就会延迟执行。

当延迟到锁过期,就可能出现删除其他锁的情况:所以要保证验证value和删除锁是一个原子操作

Lua脚本的使用:

Lua 教程 | 菜鸟教程

http://www.lryc.cn/news/621446.html

相关文章:

  • 面试题:如何用Flink实时计算QPS
  • 解锁AI潜能:五步写出让大模型神级指令
  • 宋红康 JVM 笔记 Day01|JVM介绍
  • 嵌入式开发学习———Linux环境下网络编程学习(一)
  • 【数据分享】351个地级市农业相关数据(2013-2022)-有缺失值
  • 速通C++类型转换(代码+注释)
  • AI测试自动化:智能软件质量守护者
  • 带root权限_贝尔RG020ET-CA融合终端S905L处理器当贝纯净版刷机教程
  • ROS机器人云实践案例博客建议和范文-AI版本
  • DAY 22|算法篇——贪心四
  • linux初始化配置
  • 【Linux系统】进程的生命旅程:从创建到独立的演绎
  • vue+moment将分钟调整为5的倍数(向下取整)
  • 人工智能——卷积神经网络自定义模型全流程初识
  • 18.12 BERT问答系统核心难题:3步攻克Tokenizer答案定位与动态填充实战
  • httpx 设置速率控制 limit 时需要注意 timeout 包含 pool 中等待时间
  • C语言指针使用
  • Day57--图论--53. 寻宝(卡码网)
  • 使用免费API开发口播数字人
  • 计算机视觉Open-CV
  • 新手入门 Makefile:FPGA 项目实战教程(一)
  • 经典蓝牙(BR/EDR)配对连接全过程:从 HCI 命令到 Profile 交互
  • PHP持久连接与普通连接的区别
  • 上网行为组网方案
  • Linux软件下载菜单脚本
  • 2025 年电赛 C 题 发挥部分 1:多正方形 / 重叠正方形高精度识别与最小边长测量
  • 待办事项小程序开发
  • Multimodal RAG Enhanced Visual Description
  • 容器运行时支持GPU,并使用1panel安装ollama
  • 【嵌入式C语言】四