Redis专题(分布式锁)
1、为什么会出现分布式锁
-
效率:使用分布式锁可以避免不同节点重复相同的工作,这些工作会浪费资源。比如用户付了钱之后有可能不同节点会发出多封短信。
-
正确性:加分布式锁同样可以避免破坏正确性的发生,如果两个节点在同一条数据上面操作,比如多个节点机器对同一个订单操作不同的流程有可能会导致该笔订单最后状态出现错误,造成损失。
2、分布式锁一般有三种实现方式
-
基于 MySQL
中的锁:MySQL
本身有自带的悲观锁for update
关键字,也可以自己实现悲观/乐观锁来达到目的; -
基于 Zookeeper
有序节点:Zookeeper
允许临时创建有序的子节点,这样客户端获取节点列表时,就能够当前子节点列表中的序号判断是否能够获得锁; -
基于 Redis
的单线程:由于Redis
是单线程,所以命令会以串行的方式执行,并且本身提供了像SETNX(set if not exists)
这样的指令,本身具有互斥性; -
自研分布式锁:如谷歌的 Chubby
。
每个方案都有各自的优缺点,例如 MySQL
虽然直观理解容易,但是实现起来却需要额外考虑 锁超时、加事务 等,并且性能局限于数据库,诸如此类我们在此不作讨论,重点关注 Redis
。
使用Redis
作为分布式锁的优点:对于Redis
实现简单,性能对比 Zookeeper
和Mysql
较好。如果不需要特别复杂的要求,那么自己就可以利用setNx
进行实现,如果自己需要复杂的需求的话那么可以利用或者借鉴Redission
。对于一些要求比较严格的场景来说的话可以使用RedLock
。
使用redis
作为分布式锁的缺点:需要维护Redis
集群,如果要实现RedLock
那么需要维护更多的集群。
3、分布式锁的条件
为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:
-
互斥性:在任意时刻,只有一个客户端能持有锁;
-
不会发生死锁:即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁;
-
具有容错性:只要大部分的
Redis
节点正常运行,客户端就可以加锁和解锁; -
解铃还须系铃人:加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
3.1加锁的代码实现
setNx resourceName value
若加锁了之后如果机器宕机那么这个锁就不会得到释放所以会加入过期时间,加入过期时间需要和setNx
同一个原子操作,在Redis2.8
之前我们需要使用Lua
脚本达到我们的目的,之前的setnx
和expire
无法保证原子性,但是Redis2.8
之后Redis
支持nx
和ex
操作是同一原子操作。
这是Redis
2.8之后扩展了set
方法的参数:
public class RedisTool {
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
/**
* 尝试获取分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @param expireTime 超期时间
* @return 是否获取成功
*/
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
其实加锁就一行代码:
jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
可以看到,我们加锁就一行代码:jedis.set(String key, String value, String nxxx, String expx, int time)
,这个set()
方法一共有五个形参:
-
第一个为 key
,我们使用key
来当锁,因为key
是唯一的。 -
第二个为 value
,我们传的是requestId
,很多童鞋可能不明白,有key
作为锁不就够了吗,为什么还要用到value
?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件 解铃还须系铃人,通过给value
赋值为requestId
,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId
可以使用UUID.randomUUID().toString()
方法生成。 -
第三个为 nxxx
,这个参数我们填的是NX
,意思是SET IF NOT EXIST
,即当key
不存在时,我们进行set
操作;若key
已经存在,则不做任何操作; -
第四个为 expx
,这个参数我们传的是PX
,意思是我们要给这个key
加一个过期的设置,具体时间由第五个参数决定。 -
第五个为 time
,与第四个参数相呼应,代表key
的过期时间。
3.2、解锁的代码实现:
public class RedisTool {
private static final Long RELEASE_SUCCESS = 1L;
/**
* 释放分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @return 是否释放成功
*/
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
4、Redission
4.1、应用背景
Jedis
出现的时间比较长了,接触Redis
比较早的人可能使用的都是Jedis
,但是随着现代系统的多核和异步,为了不断提高的吞吐量,异步非阻塞线程模型大行其道,这里面非常热门的框架就是Netty
,Netty
因其设计优秀,应用面广,实际使用的场景广泛,很多大型框架比如hadoop
,dubbo
等许多的底层都是通过Netty
来实现的通信。Redission
就是Redis
基于Netty
封装的通信客户端。
4.2、使用
<!--Maven-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.10.4</version>
</dependency>
// 1. Create config object
Config = ...
// 2. Create Redisson instance
RedissonClient redisson = Redisson.create(config);
// 3. Get Redis based object or service you need
RMap<MyKey, MyValue> map = redisson.getMap("myMap");
RLock lock = redisson.getLock("myLock")
lock.lock();
//业务代码
lock.unlock();
4.3、优缺点
-
优点 支持
Redis
单实例、Redis
哨兵、Redis cluster
、Redis master-slave
等各种部署架构,基于Redis
所以具有Redis
功能使用的封装,功能齐全。许多公司试用后可以用到企业级项目中,社区活跃度高。 -
缺点 最大的问题,就是如果你对某个
Redis master
实例,写入了myLock
这种锁key
的value
,此时会异步复制给对应的master slave
实例。但是这个过程中一旦发生Redis master
宕机,主备切换,Redis slave
变为了Redis master
,接着就会导致,客户端2来尝试加锁的时候,在新的Redis master
上完成了加锁,而客户端1也以为自己成功加了锁。此时就会导致多个客户端对一个分布式锁完成了加锁。这时系统在业务语义上一定会出现问题,导致脏数据的产生。所以这个就是
Redis cluster
,或者是Redis master-slave
架构的主从异步复制导致的Redis
分布式锁的最大缺陷:在Redis master
实例宕机的时候,可能导致多个客户端同时完成加锁。