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

基于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.lryc.cn/news/588635.html

相关文章:

  • 银河麒麟服务器版挂载镜像文件
  • sqli-labs靶场通关笔记:第18-19关 HTTP头部注入
  • exe直接传输会导致文件损坏
  • 【html常见页面布局】
  • AI应用服务
  • Axios 完整功能介绍和完整示例演示
  • 分布式全局唯一ID生成:雪花算法 vs Redis Increment,怎么选?
  • gRPC实战指南:像国际快递一样调用跨语言服务 —— 解密Protocol Buffer与HTTP/2的完美结合
  • TCP可靠性设计的核心机制与底层逻辑
  • Java基础(八):封装、继承、多态与关键字this、super详解
  • Java全栈工程师面试实录:从电商系统到AIGC的层层递进
  • 通用综合文字识别联动 MES 系统:OCR 是数据流通的核心
  • 在百亿流量面前,让“不存在”无处遁形——Redis 缓存穿透的极限攻防实录
  • 【Ubuntu22.04】repo安装方法
  • 1.2 vue2(组合式API)的语法结构以及外部暴露
  • 如何把手机ip地址切换到外省
  • 【深度学习优化算法】06:动量法
  • 从springcloud-gateway了解同步和异步,webflux webMvc、共享变量
  • iOS V2签名网站系统源码/IPA在线签名/全开源版本/亲测
  • iOS 抓包工具精选对比:不同调试需求下的工具适配策略
  • 项目总体框架(servlet+axios+Mybatis)
  • 【解决】联想电脑亮度调节
  • iOS高级开发工程师面试——多线程
  • Axios 和 Promise 区别对比
  • Supervisor 使用教程:进程守护的最佳实践指南
  • 【Git】详解git commit --amend用法以及使用遇到的问题
  • eVTOL分布式电推进(DEP)适航审定探究
  • Python 操作Excel工作表:添加、删除、移动、隐藏
  • redis集群的部署
  • 线性代数小述(三)