【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
相关推荐
我有满天星辰2 小时前
Mac 安装 Redis + Spring Boot 整合 Redis(完整实战指南)
spring boot·redis·macos
Java爱好狂.2 小时前
Redis高级笔记:深入浅出Java面试高频考点!
java·数据库·redis·后端·java面试·java程序员·java八股文
念恒123062 小时前
MySQL事务(2)---事务隔离级别
数据库·mysql
rising start2 小时前
深度解析 Redis 主从复制
数据库·redis·主从复制
运维栈记2 小时前
Ceph 入门:一文读懂分布式存储的“瑞士军刀”
分布式·ceph
网管NO.12 小时前
SQL 企业实战全流程|全覆盖前置基础 + 核心语法(MySQL8.0 可直接运行)
数据库·oracle
头歌实践平台2 小时前
HBase 完全分布式安装(新)
数据库·分布式·hbase
大尚来也2 小时前
主键、外键、索引,一篇讲透
java·数据库·oracle
j7~2 小时前
【MYSQL】表的内外连接--详解(重点)
数据库·mysql·内连接·左外连接·右外连接