redis分布式锁

redis实现分布式锁

概要

什么是锁?

多线程编程下一种机制,能够确保,在多核多线程某一个时间点只能又一个线程进入临界区代码,从而确保多线程下数据安全。

分布式锁?

上面提到的锁是单体系统下,对于集群、分布式环境并不适用,于是就有了分布式锁的概念。分布式锁目前主要有三种方式实现,redis、zookeeper、数据库表(性能太差)。这里,主要介绍redis实现思路。

实现

加锁主要利用redis指令:

SET key value [EX seconds|PX milliseconds] [NX|XX]
# EX|PX 设置超时时间
# NX:只有当key不存在的时候才会设置

# 伪代码
# 加锁
String uuid = UUID.randomUUID().toString().replaceAll("-","");
set lock uuid NX EX 30
# 解锁,使用lua脚本,确保原子性
if (redis.call('get', KEYS[1]) == ARGV[1])
    then return redis.call('del', KEYS[1])
else return 0
end

EX设置超时时间主要是保证了key会在超时后自动删除,防止死锁;NX可以保证在这个key不存在的情况下写入成功.以上两个参数,保证同一线程下同一时刻只会有一个线程获取锁,确保不会出现死锁(最坏情况下也不过是超时删除key)。

注意:value一定要设置,这个值保证了,每一次请求获取的锁对对象,防止锁的误解除。栗子:如果不加value,第一次请求线程A获取锁,设置过期时间30s,锁过期,线程B获取了锁;随后,A业务代码执行完成,线程A执行del命令释放锁,此时,线程B的锁还没有执行完成,线程A释放的锁实际上是线程B的

目前为止,看起来基本实现了我们需要的锁功能。但是,实际上上述的代码存在一些问题:

1. 解锁超时,引起并发

redis解锁超时引起并发问题

解决方案:

  • 过期时间设置足够长,确保代码逻辑在锁释放之前执行完成
  • 为获取锁的线程增加守护线程,为将要过期但未释放的锁增加有效时间(redission watch dog就是这样的方案)

2. 锁不可重入

public class Demo1 {
    public  void functionA(){
        //业务A加锁
        functionB();
        //业务A解锁
    }
    public  void functionB(){
        //业务B加锁解锁过程
    }
}

假设有上面伪代码的场景,那么业务A、B加锁了,且获取同一把锁;这时,首先A获取锁,这时B尝试获取锁就会失败(如果我们之前不设置超时时间,这里就会死锁),等到超时B获取锁,执行完之后,A的锁已经失效,会引起并发问题

解决方案:

  • 使用hset构造分布式锁,结构类似key:{str:’123131’, count:2},str确保防止误解锁,count计数器加锁一次就+1

3. 无法等待锁的释放

上述命令执行都是立即返回的,获取不到流程就执行完了,对于程序的健壮性不好。

解决方案:

  • 轮询,获取不到锁,间隔一段时间轮询
  • 通过redis发布订阅机制,当获取锁失败时,订阅锁释放消息,获取锁成功后释放时,发送锁释放消息,如图:

redis发布订阅等待锁释放

4. 高可用集群环境下锁的问题

上面思路在单机版redis下面是基本没问题的,但是涉及到高可用集群下就会更加复杂,举个栗子:

客户端A在redis客户端master节点拿到锁;master宕机了,key没有同步到slave节点;同时,slave节点升级为master;客户端B又从新的master(原slave)拿到锁。这样,客户端A、B都拿到锁,安全性被打破

解决方案:

redis官网,作者提出了一种分布式锁的解决规范RedLock。但是这只是一种标准,而不是实现,具体实现可以参考Redission,它还基于RedLock增加了其他新的特性,使之使用于生产。下面是网上找的一张redission的原理图:
redission分布式锁

RedLock并不100%保证锁的安全性,在一些极端情况下,也会出现锁不安全的问题。如果,业务需求严格的锁安全,那么还是需要数据库锁作为补充。总之,性能与安全的取舍具体业务具体分析