redis实现分布式锁
在单体项目中通常使用synchronize或是lock来实现锁功能,但在集群环境下,之前的锁同步就没办法保证共享数据的同步了,这时候我们就需要使用分布式锁,目前分布式锁的实现主要有三种方式,数据库的乐观锁,redis分布式锁,基于zk的分布式锁,本文主要说一说关于redis分布式锁的实现
redis分布式锁实现条件
- 互斥性:在集群环境下,不同的tomcat服务器共享同一个共享变量,那么在不同服务器的请求下,需要通过使他们之间对共享变量的访问具有互斥性来保证共享数据的一致性
- 不会发生死锁:在获取锁后,如果没有及时的释放锁,也要保证锁可以被正常释放
- 加锁和解锁具有唯一性:解锁操作只能通过加锁的客户端来解锁,不能任由别的线程进行解锁
基于单节点redis分布式锁
代码实现
获取锁方法
public class DistributedLock {
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";
private static final Long RELEASE_SUCCESS = 1L;
private static void validParam(JedisPool jedisPool, String lockKey, String requestId, int expireTime) {
if (null == jedisPool) {
throw new IllegalArgumentException("jedisPool obj is null");
}
if (null == lockKey || "".equals(lockKey)) {
throw new IllegalArgumentException("lock key is blank");
}
if (null == requestId || "".equals(requestId)) {
throw new IllegalArgumentException("requestId is blank");
}
if (expireTime < 0) {
throw new IllegalArgumentException("expireTime is not allowed less zero");
}
}
/**
* 加锁操作
* @param lockKey
* @param requestId
* @param expireTime
* @return
*/
public static boolean tryLock(RedisPool redisPool, String lockKey, String requestId, int expireTime) {
//validParam(jedisPool, lockKey, requestId, expireTime);
Jedis jedis = null;
try {
jedis = redisPool.getJedis();
//lockKey是set的key值
//requestId被用作解锁时的操作,是唯一值,用以保证解锁操作是加锁操作的,使用UUID.randomUUID()获取
//SET_IF_NOT_EXIST,不存在的时候添加,存在的时候不做操作
//SET_WITH_EXPIRE_TIME,设置过期时间
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
} catch (Exception e) {
throw e;
} finally {
if (null != jedis) {
jedis.close();
}
}
return false;
}
/**
* @param lockKey
* @param requestId
* @param expireTime
*/
public static void lock(RedisPool redisPool, String lockKey, String requestId, int expireTime) {
//validParam(jedisPool, lockKey, requestId, expireTime);
while (true) {
if (tryLock(redisPool, lockKey, requestId, expireTime)) {
return;
}
}
}
/**
* @param lockKey
* @param requestId
* @return
*/
public static void unLock(JedisPool jedisPool, String lockKey, String requestId) {
validParam(jedisPool, lockKey, requestId, 1);
//lua脚本语言
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
Object result = jedis.eval(script, Collections.singletonList(lockKey),
Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {
System.out.println("relese lock ok ");
}
} catch (Exception e) {
throw e;
} finally {
if (null != jedis) {
jedis.close();
}
}
}
}
在上面的代码中,tryLock是用来获取redis分布式锁的,原理就是在redis中设置一个key为lockKey的string数据,如果key已经被设置了,则不做任何操作,在tryLock方法中,成功获取锁则返回true,否则返回false,key为lockKey的value对应的值为requestid,为一个不重复的随机数,通过UUID.randomUUID()获取,目的是保证redis分布式锁实现条件中的加锁和解锁具有唯一性
redis解锁
在上面的代码中,unlock为解锁代码,在unlock的代码内容中主要分为两部分,第一个为lua脚本语言,第二是将脚本语言交给redis的eval进行执行,这行脚本语言的意思就是获取key为KEYS[1]的value值,如果和ARGV[1]相同,则进行删除操作,否则返回0
单节点redis分布式锁结语
针对单节点的分布式锁,实际上就是通过在redis中设置用于业务区分的string来保证获取锁的唯一性,互斥性,保证用于分布式数据唯一性的条件即可
基于多redis节点的分布式锁实现
在上面的单节点redis实现分布式锁中,当发生redis宕机的时候,就会出现获取锁失败的情况,为了避免单个redis宕机,我们可以使用redis主从复制的方式,当主redis宕机的时候切换到slave的redis服务器上,这种方法有个问题,就是当客户端A在主redis上设置了锁后,还未来及复制到从redis的时候发生了宕机的情况,这时候客户端B就会在从的redis服务器上设置锁,当发生这种情况的时候,就会有发生锁丢失
redlock解决单机模式分布式锁
redlock算法是当我们有多个redis服务器的时候,这些服务器并不会发生主从复制或是任何协调同步操作,我们需要保证所有redis节点接收同样redis加锁和解锁操作,保证当某个redis节点宕机以后,其余redis节点都会保证同样的操作
redlock算法基本原则
- 获取当前时间
- 获取锁的请求同时向每台redis服务器发出请求,每台服务器都进行同样的加锁解锁操作,为加锁设置超时时间限制,如果获取锁的时间超过延迟时间,则退出当前redis节点的锁操作
- 设置锁的有效时间为锁的默认有效时间减去获取锁的延迟时间,只有获取超过一半以上的redis服务器,才能认定锁获取成功,否则需要释放之间获取的锁
- redis获取锁失败后,需要释放zhi所有获取实例的锁
redisson对redlock算法的封装
看过AQS相关代码的可能会感觉很熟悉,这是类似于一个重入锁的写法,
写法也比较简单,需要注意的是RLock lock = redisson.getLock("test");方法传入的test会被构造成RedissonLock的name,也是锁的key值
public class w2g_redis_lock {
public static void main(String[] args) throws Exception {
Redisson redisson = Redisson.create();
//此处设置锁的key值
RLock lock = redisson.getLock("test");
// 500ms是获取锁的超时时间,10000ms即10s是获取锁后的失效时间,实际失效时间是减去获取锁的时间
boolean isLock = lock.tryLock(500, 10000, TimeUnit.MILLISECONDS);
try {
System.out.println("test");
}
finally {
lock.unlock();
}
redisson.shutdown();
}
}
redis实现过程中需要注意的地方
锁获取失败
一个线程成功获取到锁后,其他线程此时获取锁失败,对于获取锁失败的线程来说,要么就是放弃获取锁,要么就是继续轮询获取锁,在同步方法中,所获取操作将会被阻塞,实现类似lock方式,所以可以通过异步的方式对锁的获取进行轮询
可重入性
可重入性是指一个已经获取锁的线程再次请求获取锁,如果redis分布式锁需要支持可重入的方式,可以使用threadlocal进行锁资源的记录,通过threadlocal来存储锁的信息
redisson分布式锁相关博客
单redis服务器实现分布式锁:
https://mp.weixin.qq.com/s?__biz=MzU2NjIzNDk5NQ==&mid=2247485960&idx=1&sn=9d2fc991f4e7057a44a8c67a8e885cdd&chksm=fcaed014cbd959020f9841482527e552e53af1cbada9064dd47cb4c25af37f539cd8dbabed8d&scene=0&xtrack=1&key=e8a88acc138eabf4b438b5af1940c70916688ef4834a49bfaef24adb1483997a1feeb235f30b06c0b77a59d88794ba3b0d0148822f51ca454c6cc063fed4789dd3d6c124ed2059b8f723a876b9f56e96&ascene=1&uin=MTkyMDIyMzc0Mg%3D%3D&devicetype=Windows+10&version=62060739&lang=zh_CN&pass_ticket=eK7xM4rLn8yHwOZ7%2FcrBEM281oiebbbYOl%2FyQz7autXj%2F1qPS3xQwGjcmAytkKZk
redisson源码解析:
https://blog.csdn.net/qq_14828239/article/details/80578830
redis实现分布式锁需要注意的问题:
https://mp.weixin.qq.com/s/w7OPv66j_8f3nQL94A-Prg