共计 1400 个字符,预计需要花费 4 分钟才能阅读完成。
分布式锁在多实例部署,分布式系统中经常会使用到,这是因为基于 jvm 的锁无法满足多实例中锁的需求,本篇将讲下 Redis 如何通过 Lua 脚本实现分布式锁,不同于网上的 redission,完全是手动实现的。
我们先来看一个无锁的情况下会导致什么问题:
这是一个普通的更新用户年龄的功能, 各层代码如下, 访问 controller 层, 一个更新, 一个查询
这是 service 层, 我们使用 contdownlatch 发令枪来模拟线程同时并发的情况, 发令枪设为 32, 即 32 个线程同时去请求修改年龄,
这里使用线程池来提交多线程任务, 看代码知道, 这里我们已经有了判断年龄的操作, 当查询用户查询大于 0 时, 才去调更新用户年龄 - 1 的方法, 等下看看有没有用
这里是 sql, 可以看到两个 sql, 一个查询用户年龄, 一个会执行用户年龄每次减 1 ,
这里是用户数据, 我们可以看到, 用户 UID 为 UR12324 的用户, 他的年龄是 30, 接着我们来调 32 个线程来操作减他年龄
我们请求下这个方法
然后看看结果:
可以看到库中年龄已被减为 -2, 在未加锁的情况下, 查询较验并没有什么作用, 此时如果加个 synchronized 或 lock 锁肯定能避免这种情况, 但我们本文讨论的是多实例或分布式环境中, 此加锁方式仍然会产生问题, 感兴趣的可以试下是不是
下面我们开始实现一个 redis 分布式锁, 来避免这种情况发生, 先说说实现思路:
1, 线程请求访问前先调用加锁的方法, 加锁就去里生成一个随机数同时保存在线程本地变量和 redis 的某 key 中, 此 key 设有效期为 200ms, 具体值根据业务执行时间自行调整, 加锁成功;
2. 其它线程试着访问拿出它本地变量与 redis 中某 key 进行比较, 如果不一致, 则说明有锁, 此线程休眠一段时间, 再试着加锁;
3. 加锁成功的线程在操作结束后删掉它持有锁 (用 lua 实现, 保证原子性, 在它比对和删除锁的过程中, 其它线程不会加锁成功), 让其它线程再次加锁以执行任务;
说明: 锁的时间为 200ms 可预防线程挂掉之后死锁,200ms 后会自动释放
下面看看我们写的锁代码:
片段 1: 使用 redislock 实现 lock 来复写它的方法
片段 2: 试着加锁的方法
片段 3: 解锁方法, 此处首先从线程本地变量获取它的随机数, 然后调用 lua 脚本, 与 redis 中 key 相比较, 如果相同则删除, 否则返回 0;
此为 lua 脚本方法, 用此方法可以保证判断和删除的原子性, 在此过程中没有线程可以操作此 key
到此为止, 我们锁基本写完, 来测试下有没有用:
我们在此方法前后分别加入加锁和解锁方法, 使用方式和 lock 锁一样, 我们重新把年龄恢复到 30 后来测试一下吧
先看看日志
这里可以看到各个线程争夺锁的情况, 再看看执行结果
这里我们可以看到虽然是 32 个线程并发执行, 但此值并不会变为负数, 加锁成功.
我们可以看到最后 2 个线程并没有执行方法
在具体生产环境中, 比如典型的用户余额扣减, 我们可以用用户 UID 作 KEY, 这样就不会造成 100 个用户,500 个线程争夺一个锁的情况发生,100 个用户会有 100 个锁, 此时假如每个用户 5 个请求, 一个锁只处理 5 个线程
大大提高锁的效率.
此时说明加锁成功, 大家可以在分布式环境中测试更明显, 有关极端情况下解锁失败后应该做什么也可以由我们自己决定, 比 redission 要灵活, 带锁的 redis 最好是单实例, 在集群中可能会出问题, 有机会我们再用 zk 实现下。
: