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

基于redis的分布式锁 lua脚本解决原子性

基于redis的分布式锁 lua脚本解决原子性

之前我们实现的乐观锁和悲观锁来控制超卖有一定效果,但它们都只能在单机环境下生效。在分布式系统中,我们需要更强大的锁机制来确保跨多个服务实例的数据一致性。Redisson是一个在Redis基础上实现的Java分布式服务,它提供了强大的分布式锁实现,可以帮助我们解决分布式环境下的并发控制问题。

分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁

分布式锁的核心思想就是让大家都使用同一把锁,只要大家使用的是同一把锁,那么我们就能锁住线程,不让线程进行,让程序串行执行,这就是分布式锁的核心思路

事实上我们有很多实现分布式锁的方法 但redis优势更大一些 接下来我们用redis来实现一下分布式锁

主要分两步

  • 获取锁:
    • 互斥:确保只能有一个线程获取锁
    • 非阻塞:尝试一次,成功返回true,失败返回false
  • 释放锁:
    • 手动释放
    • 超时释放:获取锁时添加一个超时时间

我们利用redis 的setNx 方法,当有多个线程进入时,我们就利用该方法,第一个线程进入时,redis 中就有这个key 了,返回了1,如果结果是1,则表示他抢到了锁,那么他去执行业务,然后再删除锁,退出锁逻辑,没有抢到锁的哥们,等待一定时间后重试即可

public class SimpleRedisLock implements ILock{//不同的业务应该有不同的所private String name;private StringRedisTemplate stringRedisTemplate;//给锁加一个前缀private static final String KEY_PREFIX="lock:";//接收用户传递给我们的参数public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {this.name = name;this.stringRedisTemplate = stringRedisTemplate;}/*** 尝试获取锁** @param timeoutSec* @return*/@Overridepublic boolean tryLock(long timeoutSec) {//获取线程标识long thresdId = Thread.currentThread().getId();//获取锁——如果不存在才执行:nxBoolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX+name,threadId+"", timeoutSec, TimeUnit.SECONDS);/*** 若success是true,这里返回true* 若sucess是false,这里返回false* 若这里为空,返回的也是false* 避免空指针异常*/return Boolean.TRUE.equals(success);}/*** 释放锁*/@Overridepublic void unlock() {stringRedisTemplate.delete(KEY_PREFIX+name);}
}
//足够,创建订单Long userId = UserHolder.getUser().getId();//创建锁对象SimpleRedisLock lock = new SimpleRedisLock("order" + userId, stringRedisTemplate);//获取锁boolean isLock = lock.tryLock(1200);//判断是否获取锁成功if (!isLock) {//获取锁失败,返回错误return Result.fail("不允许重复下单!");}try {//获取代理对象(事务)IVoucherOrderService proxy= (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(voucherId);} finally {//释放锁lock.unlock();}

这样我们在测试的时候就会发现 我们虽然部署两台tomcat服务器 但锁不仅仅是在一个jvm中的 而是作用域全局的

但还有一些问题 例如 持有锁的线程在锁的内部出现了阻塞,导致他的锁自动释放,这时其他线程,线程2来尝试获得锁,就拿到了这把锁,然后线程2在持有锁执行过程中,线程1反应过来,继续执行,而线程1执行过程中,走到了删除锁逻辑,此时就会把本应该属于线程2的锁进行删除,这就是误删别人锁的情况说明

解决起来也很简单 解决方案就是在每个线程释放锁的时候,去判断一下当前这把锁是否属于自己,如果属于自己,则不进行锁的删除,假设还是上边的情况,线程1卡顿,锁自动释放,线程2进入到锁的内部执行逻辑,此时线程1反应过来,然后删除锁,但是线程1,一看当前这把锁不是属于自己,于是不进行删除锁逻辑,当线程2走到删除锁逻辑时,如果没有卡过自动释放锁的时间点,则判断当前这把锁是属于自己的,于是删除这把锁。(是不是很像版本号法解决)

  1. 修改之前的分布式锁实现,满足:在获取锁时存入线程标示(可以用UUID表示)
    在释放锁时先获取锁中的线程标示,判断是否与当前线程标示一致
  2. 如果一致则释放锁 如果不一致则不释放锁
    核心逻辑:在存入锁时,放入自己线程的标识,在删除锁时,判断当前这把锁的标识是不是自己存入的,如果是,则进行删除,如果不是,则不进行删除。
    /*** 释放锁*/@Overridepublic void unlock() {//获取线程标识String thresdId = ID_PREFIX+Thread.currentThread().getId();//获取锁中的标识String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);//判断标识是否一致if (threadId.equals(id)) {stringRedisTemplate.delete(KEY_PREFIX+name);}}

这样就解决我们刚刚的问题了

假设还有更极端的一种情况

线程1现在持有锁之后,在执行业务逻辑过程中,他正准备删除锁,而且已经走到了条件判断的过程中,比如他已经拿到了当前这把锁确实是属于他自己的,正准备删除锁,但是此时他的锁到期了,那么此时线程2进来,但是线程1他会接着往后执行,当他卡顿结束后,他直接就会执行删除锁那行代码,相当于条件判断并没有起到作用,这就是删锁时的原子性问题,之所以有这个问题,是因为线程1的拿锁,比锁,删锁,实际上并不是原子性的,我们要防止刚才的情况发生

就需要lua登场了 lua是一种脚本语言 Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。这里重点介绍Redis提供的调用函数,我们可以使用lua去操作redis,又能保证他的原子性,这样就可以实现拿锁比锁删锁是一个原子性动作了

这里重点介绍Redis提供的调用函数,语法如下:

redis.call('命令名称', 'key', '其它参数', ...)
# 先执行 set name jack
redis.call('set', 'name', 'Rose')
# 再执行 get name
local name = redis.call('get', 'name')
# 返回
return name

如果脚本中的key、value不想写死,可以作为参数传递。key类型参数会放入KEYS数组,其它参数会放入ARGV数组,在脚本中可以从KEYS和ARGV数组获取这些参数:

EVAL "return redis.call('set',KEYS[1],ARGV[1])" 1 name tom

因此我们可以改造一下 我们希望最后两步保证原子性 因此只需要把最后两步写入lua脚本 执行即可

-- 比较线程标示与锁中的标示是否一致
if(redis.call('get', KEYS[1]) ==  ARGV[1]) then-- 释放锁 del keyreturn redis.call('del', KEYS[1])
end
return 0
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;static {UNLOCK_SCRIPT = new DefaultRedisScript<>();UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));UNLOCK_SCRIPT.setResultType(Long.class);}public void unlock() {// 调用lua脚本stringRedisTemplate.execute(UNLOCK_SCRIPT,Collections.singletonList(KEY_PREFIX + name),ID_PREFIX + Thread.currentThread().getId());
}

基于Redis的分布式锁实现思路:

  • 利用set nx ex获取锁,并设置过期时间,保存线程标示
  • 释放锁时先判断线程标示是否与自己一致,一致则删除锁
    • 特性:
      • 利用set nx满足互斥性
      • 利用set ex保证故障时锁依然能释放,避免死锁,提高安全性
      • 利用Redis集群保证高可用和高并发特性
http://www.xdnf.cn/news/15600.html

相关文章:

  • 免杀学习篇(1)—— 工具使用
  • 网页源码保护助手 海洋网页在线加密:HTML 源码防复制篡改,密文安全如铜墙铁壁
  • 基于华为欧拉系统安装FileGator文件管理器
  • 【Android】日志的使用
  • 深度学习中的激活函数:从原理到 PyTorch 实战
  • python 基于 httpx 的流式请求
  • 场景设计题+智力题
  • [Science]论文 视黄素与细胞修复
  • C++回顾 Day7
  • PyCharm 高效入门指南:从安装到效率倍增
  • [面试] 手写题-对象数组根据某个字段进行分组
  • 学习嵌入式的第二十八天-数据结构-(2025.7.15)进程和线程
  • P3842 [TJOI2007] 线段
  • Web攻防-PHP反序列化字符逃逸增多减少成员变量属性解析不敏感Wakeup绕过
  • 高等数学强化——导学
  • Android中Launcher简介
  • deepseekAI对接大模型的网页PHP源码带管理后台(可实现上传分析文件)
  • ASP .NET Core 8结合JWT轻松实现身份验证和授权
  • SpringBoot 实现 Redis读写分离
  • “C21988-谷物烘干机(2D+3D+说明书+运动仿真)8张cad+设计说明书
  • pytorch学习笔记(四)-- TorchVision 物体检测微调教程
  • 常用高频指令总结
  • iOS App 上架工具选型与跨平台开发 iOS 上架流程优化实录
  • 视频HDR技术全解析:从原理到应用的深度探索
  • 【时时三省】(C语言基础)通过指针引用多维数组
  • 视频编码中熵编码之基于上下文的变长编码(Huffman霍夫曼编码和指数哥伦布)
  • 网络编程-epoll模型/udp通信
  • css 边框颜色渐变
  • 【linux V0.11】init/main.c
  • JAVA青企码协会模式系统源码支持微信公众号+微信小程序+H5+APP