【Redis | 第五篇】分布式锁

目录

[一、 为什么需要分布式锁?(它解决了什么问题?)](#一、 为什么需要分布式锁?(它解决了什么问题?))

二、分布式锁的实现方案

[方案一:基于 Redis(最常用,性能极高)](#方案一:基于 Redis(最常用,性能极高))

[方案二:基于 Zookeeper(可靠性极高)](#方案二:基于 Zookeeper(可靠性极高))

[方案三:基于 数据库(例如 MySQL)](#方案三:基于 数据库(例如 MySQL))

三、基于Redis实现分布式锁

[3.1 最基础的加锁](#3.1 最基础的加锁)

[3.2 引入过期时间](#3.2 引入过期时间)

[3.3 锁的验证](#3.3 锁的验证)

[3.4 判断和删除原子化](#3.4 判断和删除原子化)


一、 为什么需要分布式锁?(它解决了什么问题?)

在传统的单机应用中,如果多个线程要同时修改同一个数据(比如扣减库存),为了防止数据错乱,我们会在代码里加一个本地锁 (例如 Java 中的 synchronizedReentrantLock)。

随着业务发展,你的应用变成了分布式系统 。为了抗住高并发,你的应用被部署在了多台服务器上。单机锁只能锁住自己所在的那个 JVM(进程),它管不到其他服务器上的进程

这时候就需要一个公共的锁也就是分布式锁来控制部署在不同服务器上的进程。

二、分布式锁的实现方案

方案一:基于 Redis(最常用,性能极高)

利用 Redis 的单线程处理机制和 SETNX(Set if Not eXists)命令。

  • 原理: 尝试在 Redis 里写入一个 Key,如果 Key 不存在,就写入成功(代表加锁成功);如果 Key 已经存在,就写入失败(代表别人已经加锁了)。

  • 优点: 性能极高,支持高并发,实现相对简单。

  • 缺点: 极端情况下(如 Redis 主从切换时),可能会出现锁丢失的问题。通常会使用 Redisson 这样的成熟框架,它内置了"看门狗(Watchdog)"机制来自动为还在执行的业务续期锁的时间,非常省心。

方案二:基于 Zookeeper(可靠性极高)

利用 Zookeeper 的"临时顺序节点"特性。

  • 原理: 每个请求都在 Zookeeper 下创建一个临时节点,序号最小的那个节点获得锁。操作完后删除节点。如果客户端宕机,Zookeeper 会自动删除它的临时节点(释放锁)。

  • 优点: 非常可靠,不存在 Redis 的锁过期时间难以把控的问题,天生具备强一致性。

  • 缺点: 性能比 Redis 差一些,因为频繁创建和删除节点开销较大。

方案三:基于 数据库(例如 MySQL)

  • 原理: 利用数据库的唯一索引(Unique Key)或者行锁(FOR UPDATE)。试图插入一条记录,插入成功的获得锁。

  • 优点: 不需要引入额外的中间件,如果系统本身就有数据库就能做。

  • 缺点: 性能最差,数据库的连接资源非常宝贵,抗不住高并发。极少在大型互联网项目的高并发场景中作为首选。

三、基于Redis实现分布式锁

接下来,将以版本迭代的方式来介绍Redis实现分布式锁的完整过程。

3.1 最基础的加锁

Redis 提供了一个原生命令:SETNX(Set if Not eXists,如果不存在则设置)。

  • 加锁: SETNX lock_key 1。如果返回 1,说明 key 之前不存在,加锁成功;如果返回 0,说明 key 已存在,别人正在用,加锁失败。

  • 解锁: DEL lock_key。业务执行完,删掉这个 key。

缺陷:死锁问题,如果服务器 A 抢到了锁(执行了 SETNX),但在执行业务逻辑时突然断电宕机了,没来得及执行 DEL。这个锁就会永远留在 Redis 里,其他所有服务器无限期等待,系统死锁。

3.2 引入过期时间

为了解决上面的死锁问题,我们可以给锁提供一个过期时间,来给锁释放做一个兜底的操作。

SET 命令提供了扩展参数。我们可以把加锁和设置过期时间合并成一条原子命令:

复制代码
SET lock_key 1 NX PX 30000

缺陷:锁被误删问题,

假设设置了 30 秒过期:

  1. 线程 A 拿到锁,但业务执行极其缓慢,花了 40 秒。

  2. 第 30 秒时,Redis 自动删除了锁。

  3. 此时线程 B 顺利拿到了锁,开始执行业务。

  4. 第 40 秒时,线程 A 执行完了,执行了一句 DEL lock_key

  5. 线程 A 把线程 B 刚加的锁给删了,接着线程C又进来了。

3.3 锁的验证

为了解决上面锁被误删问题,我们的解决方案是给锁加上一个身份ID,每当一个线程加上锁之后会在Redis中记录一个锁的ID,为了后面验证这是自己加上的锁,如果不是自己加上的锁,那么就不会删除。

上锁:

复制代码
SET lock_key "UUID-ThreadA" NX PX 30000

释放锁:加一个判断

java 复制代码
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);

        if(threadId.equals(id)){
            stringRedisTemplate.delete(KEY_PREFIX + name);

缺陷:判断和删除不是原子操作,在线程A判断是自己上的锁之后,接下来本来应该要执行释放锁的操作,但是在释放之前阻塞了,在阻塞期间锁刚好到期自动释放,并且被别的线程抢走了,当线程A恢复之后还是要执行释放操作,依然会发生误删。

3.4 判断和删除原子化

为了保证"判断并删除"这两步操作的绝对原子性,我们需要向 Redis 发送一段 Lua 脚本。Redis 会把整个 Lua 脚本作为一个整体执行,执行期间绝对不会被打断。

解锁的 Lua 脚本如下:

Lua 复制代码
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end
相关推荐
欢呼的太阳5 小时前
数据库设计Step by Step (10)——范式化
服务器·数据库·oracle
喜欢的名字被抢了7 小时前
MySQL基础入门:从零理解数据库与SQL
数据库·mysql·教程
Elastic 中国社区官方博客7 小时前
如何比较两个 Elasticsearch 索引并找出缺失的文档
大数据·运维·数据库·elasticsearch·搜索引擎
DLYSB_8 小时前
生命通道:如何用 HIS 医疗系统直连网络声光终端,打造“零延误”的危急值响应网关?
java·网络·数据库·报警灯
儒雅的大叔8 小时前
MySQL数据库InnoDB数据恢复工具使用总结
数据库·mysql·adb
观远数据8 小时前
ChatBI选型对比:从意图识别到SQL修复,六个维度打分决定是否值得投产
数据库·人工智能·sql
嘉&年华8 小时前
【第008篇】通过dexp和dimp命令导出和导入dmp文件(适用于达梦数据库)
数据库·sql
哥是强混10 小时前
Means:基于 .NET 10 打造的开源自部署 S3 兼容对象存储服务
网络·数据库·.net
ffqws_10 小时前
redis面试:如何保证双写一致
数据库·redis·缓存
水无痕simon10 小时前
6 数据库同义词
数据库