手撸Redis分布式锁(8个版本的渐进式源码实践解读)

2022-12-02 15:31:34 浏览数 (1)

前言

分布式锁相对应的是本地锁,像我们熟悉的synchronized和ReentrantLock都是本地锁,本地锁是作用于JVM内部,单个进程内的操作共享资源互斥。而现在主流都是分布式和微服务架构,会部署多个服务(多个JVM),为此分布式锁也就应运而生了。

分布式锁主流实现有3种:基于Redis、Zookeeper或Mysql等数据库。

Redis实现分布式锁使用得非常广泛,也是面试的重要考点之一,很多同学都知道这个知识,也大致知道分布式锁的原理,但是具体到细节的掌握上,往往并不完全正确。所以下面就让我们手写Redis分布式锁,以版本迭代的方式,渐进式的解读遇到的问题和对应的解决方案,帮你彻底理解Reids分布式锁。


一、v1 初出茅庐

setnx (SET if Not eXists) 命令在指定的 key 不存在时,为 key 设置指定的值。

两个客户端进程可以执行这个命令,达到互斥,就可以实现一个分布式锁。

基本语法:setnx key value 例如两个进程同时加锁,只能成功1个:

释放锁,直接使用 DEL 命令删除这个 key 即可:

基于这个思路,使用RedisTemplate我写下了v1版本的实现代码:

模拟减库存调用如下:


二、v2 小心死锁

1. 业务逻辑异常导致死锁

解决方案:当lock成功后,对执行的业务逻辑加**try finally**,保证即使业务逻辑异常,也可以unlock。

2. 服务宕机导致死锁

在执行业务逻辑时,虽然我们加了try finally,但假设获得锁的服务宕机,还没有来的及解锁,那么这个锁将一直被占有,其它客户端也将永远拿不到这个锁了。

解决方案设置过期时间,服务宕机的话key也会在指定时间内自动过期,不会永远占有锁。

我们修改lock方法,对key加上expire 10s:

代码语言:javascript复制
public boolean lock() {
    // 等价于原生命令 setnx key 1
    Boolean ok = stringRedisTemplate.opsForValue().setIfAbsent(key, "1");
    boolean res = Boolean.TRUE.equals(ok);
    if (res) {
        // 等价于原生命令 expire key 10
        stringRedisTemplate.expire(key, 10, TimeUnit.SECONDS);
    }
    return res;
}

三、v3 彻底搞定死锁

v2的思路是对的,但是这里还有个小问题,如果setnx成功但expire失败呢? 依然会有死锁的可能,这个问题的根源在于setnx和expire是两条指令而不是原子指令,所以解决原子性问题我们可以采用lua脚本,详见我写的 Redis使用Lua脚本:保证原子性【项目案例分享】

另外,在Redis2.8版本中,作者加入了set指令的扩展参数ex nx,使得setnx和expire指令可以一起执行:

基本语法: set key value ex secords nx 例如:

我们修改lock方法如下:

代码语言:javascript复制
public boolean lock() {
    // 等价于原生命令 set key 1 ex 10 nx
    Boolean ok = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
    return Boolean.TRUE.equals(ok);
}

这样就彻底解决了**死锁**问题

0 人点赞