☆* o(≧▽≦)o *☆嗨~我是小奥🍹
📄📄📄个人博客:小奥的博客
📄📄📄CSDN:个人CSDN
📙📙📙Github:传送门
📅📅📅面经分享(牛客主页):传送门
🍹文章作者技术和水平有限,如果文中出现错误,希望大家多多指正!
📜 如果觉得内容还不错,欢迎点赞收藏关注哟! ❤️
文章目录
- Redis分布式锁实现
- 一、分布式锁
- 二、 基于Redis的分布式锁
- 2.1 初级版本
- 2.2 解决分布式锁误删问题
- 2.3 分布式锁的原子性问题
- 2.4 Redis分布式锁存在的问题
Redis分布式锁实现
一、分布式锁
分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁。
分布式锁的核心是实现多进程之间互斥,而满足这一点的方式有很多,常见的有三种:
MySQL | Redis | Zookeeper | |
---|---|---|---|
互斥 | 利用MySQL本身的互斥锁机制 | 利用setnx互斥命令 | 利用节点的唯一性和有序性实现互斥 |
高可用 | 好 | 好 | 好 |
高性能 | 一般 | 好 | 一般 |
安全性 | 断开连接,自动释放锁 | 利用锁超时时间,到期释放 | 临时节点,断开连接自动释放 |
二、 基于Redis的分布式锁
2.1 初级版本
实现基于Redis的分布式锁需要实现两个基本的方法:
获取锁
互斥:确保只有一个线程获取锁
// 添加锁,利用setnx的互斥特性
setnx lock thread1
// 添加锁过期时间,避免服务器宕机引起的死锁
expire lock 10
// 添加锁,NX是互斥,EX是设置超时时间
set lock thread1 NX EX 10
释放锁
- 手动释放:删除key即可
- 超时释放:获取锁时添加一个超时时间
// 释放锁,删除即可
del key
基本逻辑
- 业务开始时尝试获取锁
- 获取锁失败,直接返回
- 获取锁成功,开始执行业务
- 如果业务超时或服务宕机,自动释放锁
- 否则执行完业务手动释放锁
存在的问题 :分布式锁误删问题
比如如下的场景:
- 线程1首先获取锁,正常获取到锁,开始执行自己的业务。
- 由于某种原因,线程1的业务被阻塞,并且阻塞时间超过了锁的过期时间,导致锁被自动释放。
- 锁被释放后,线程2再来获取锁,因为线程1的锁被释放了,所以线程2也能成功获取到锁,然后开始执行自己的业务。
- 在线程2执行业务时,线程1的业务完成,线程1手动释放锁,此时,线程1释放的是线程2的锁。
- 这时候,线程3也来获取锁,因为线程2的锁被线程1释放了,所以线程3也能成功获取到锁,然后开始执行自己的业务。
- 此时,线程2和线程3是并行执行业务的,会出现错误的结果。
产生原因:
- 没有唯一的锁标识,所以会产生误删的情况。
2.2 解决分布式锁误删问题
要想解决分布式锁误删的问题,主要的关键思路在于:如何判断是不是自己的锁。
这时就可以使用**线程标识 **(即线程ID)来当作分布式锁标识:
- 如果线程标识与当前线程一致,则释放锁;
- 如果线程标识与当前线程不一致,则不做处理;
处理逻辑
业务开始时尝试获取锁,锁的标识可以是线程ID:
- 获取锁之前先比较当前线程ID与锁标识是否一致:
- 不一致,获取锁失败,直接返回
- 一致,获取锁成功,开始执行业务
- 如果业务超时或服务宕机,自动释放锁
- 业务执行完成,比较当前线程ID与锁标识是否一致
- 一致,手动释放锁
- 不一致,不做任何处理
存在的问题:分布式锁原子性问题
比如如下的场景:
- 线程1首先获取锁,正常获取到锁,开始执行自己的业务。
- 线程1业务执行完毕,需要手动释放锁,获取锁标识并判断是否与自己一致,结果一致,执行手动释放锁的动作,假设此时产生了阻塞,并且阻塞时间超过了锁的过期时间,导致锁被自动释放。
- 锁被释放后,线程2再来获取锁,因为线程1的锁被释放了,所以线程2也能成功获取到锁,然后开始执行自己的业务。
- 在线程2执行业务过程中,线程1的阻塞结束了,此时继续执行手动释放锁动作,线程1手动释放锁,此时,线程1释放的是线程2的锁。
- 这时候,线程3也来获取锁,因为线程2的锁被线程1释放了,所以线程3也能成功获取到锁,然后开始执行自己的业务。
- 此时,线程2和线程3是并行执行业务的,会出现错误的结果。
产生原因:
- 判断锁标识和释放锁是两个操作,没有保证原子性。
2.3 分布式锁的原子性问题
要想解决操作原子性问题,我们可以使用Lua脚本解决多条命令原子性问题。
Redis提供的调用函数,语法如下:
# 执行redis的命令
redis.call('命令名称','key','其他参数',...)
Lua脚本
-- 锁的key
local key = KEYS[1]
-- 当前线程标识
local threadId = ARGV[1]
-- 比较线程标识与锁中的标识是否一致
if (redis.call('get', KEYS[1]) == ARGV[1]) then
-- 释放锁
return redis.call('del', KEYS[1])
end
return 0
Lua脚本处理逻辑
- 获取锁中的线程标识
- 判断是否与当前线程标识一致
- 一致,手动删除锁
- 不一致,不做处理
2.4 Redis分布式锁存在的问题
上面的实现逻辑呢,其实就已经能满足简单的分布式锁的功能了,但是由于它是我们自己实现的,所以还是会有一些问题:
(1)不可重入:同一个线程只能获取一次锁,无法多次获取同一把锁,如果我们有嵌套业务都需要用到分布式锁,那么这种Redis实现的锁就不可以使用了。
(2)不可重试:获取锁时如果获取失败就直接返回了,没有重试机制,这种对性能来说其实并不友好。
(3)超时释放:锁超时释放虽然可以避免死锁,但是如何业务执行耗时较长也会导致锁超时释放,存在不可预估的问题。
(4)主从一致性无法保证:如果Redis提供了主从集群,因为主从同步存在延迟,并且一般都是主写从读。如果线程在主节点获取了锁,并且尚未同步给从节点的过程中,突然主节点宕机,虽然Redis会重新选取一个从节点作为新的主节点,但是新主节点中没有锁的标识,所以也会出现多线程并行执行业务的情况(情况出现概率极低)。