redis分布式锁最佳实践(并实现锁续期机制)
文章目录
1. 分布式锁是什么?
在单体项目进入分布式项目之后,各个业务被拆分成多个微型服务,核心微服务还可以搭建集群,那么搭建集群之后问题就来了,以前是单体项目,如果涉及共享资源得一些操作我们可以使用ReentrantLock来进行加锁,那么如今微服务集群项目下,做不到了,那么我么其实就可以使用一些中间件,基于中间件得原子性操作来实现分布式锁,今天我们就来使用redis搭建分布式锁实践。
2. setnx 和 AQS state
在redis中有setnx命令 ,该命令使用逻辑如下:
可以看到我们使用setnx添加一个key-value得值,第一次增加之后,第二次再去增加发现返回值变成了0。意味着setnx命令就如同Java中unsfae类得CAS操作一样。
这里简单说一下AQS得机制,AQS是Java同步器得基石,如我们常用得ReentranLock和ReadWriteReetranLock还有seameple信号量等都是基于AQS来实现,在AQS有一个state得状态值,当state状态值为0时表示没有人获取该锁,=1 表示有线程获取了该锁,其他线程lock时会进入阻塞队列中等待。
而redis中得setnx和javaAQS得state就有异曲同工之处,我们就可以借助setnx来实现分布式锁,当然这一前提是建立在redis得业务处理是单线程模式执行。
3. jedis完成分布式锁得设计
#3.1 v1版本
package com.xzq.lock;
/**
- @Author xzq
- @Description // 分布式锁实践
- @Date 2021/11/27 9:45
- @Version 1.0.0
**/
public class RedisLock {
private Logger logger = LoggerFactory.getLogger(RedisLock.class);
private Jedis jedis = JedisPoolManager.getJedis();
static class JedisPoolManager{
private static JedisPool jedisPool;
static {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(10);
jedisPool = new JedisPool(config, "127.0.0.1", 6379);
}
public static Jedis getJedis() {
Jedis jedis = jedisPool.getResource();
return jedis;
}
}
public boolean lock(String key, String value) {
if (jedis.setnx(key, value)==1) {
return true;
}
return false;
}
public boolean unlock(String key) {
if (jedis.del(key)==1) {
return true;
}
return false;
}
}
一个简单得redis分布式锁就完成了,但是这就完了吗?其中有没有一些问题呢?
- 当上层应用得业务逻辑出现异常,并且没有try catch机制进行unlock,那么锁将一直被占有,其它分布式服务永远得不到锁。这其实也是一种死锁。
好办呀,加个过期时间就可以了,redis中不是有expire命令吗?
3.2 v2版本
为了解决v1版本得问题我们将获取锁得代码改造如下:
public boolean lock(String key, String value,long ttl,long timeOut) {
//结束时间,表示超时时间,3s内获取不到锁则结束
long end = System.currentTimeMillis() + timeOut;
while (System.currentTimeMillis() < end) {
if (acquire(key, value,ttl)) {
return true;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return false;
}
private boolean acquire(String key, String value, long ttl) {
if (jedis.setnx(key, value)==1) {
//加锁成功,设置过期时间
jedis.expire(key, ttl);
return true;
}
return false;
}
public boolean unlock(String key) {
if (jedis.del(key)==1) {
return true;
}
return false;
}
v2版本我们不仅设置了过期时间ttl,还进行了一个超时等待得自旋优化,当获取锁失败后进行一个指定时间得自旋重试,如果超过这个时间那么就返回。
看着好像也没啥问题了,如果我业务系统出现某些问题,但是我的锁有过期时间并不会形成死锁。
但是啊,我们得这个unlock其实有一些问题
直接拿着key去删合适吗?
会不会我加锁得时候,有人拿着RedisLock直接先调用个unlock,就把我得锁给删了。所以说我们要对del删除锁时进行一个比对,那么比对得值其实就是value,我们可以给value设置一个唯一值,当进行删除得时候比对value,value比对成功才能进行删除。
对应得代码上其实就是一个 get-del得操作也就是先get锁得value,在比较,比较完成在del.但这是两步操作,可能会出现多应用下得指令交错,所以我们必须实现原子性,借助于lua脚本
local lock_key=KEYS[1]
local lock_value=ARGV[1]
local current_value=redis.call('get',lock_key)
local result=0
if lock_value==current_value then
redis.call('del',lock_key)
result=1
end
return result
private final String unlockLua = jedis.scriptLoad("local lock_key=KEYS[1]\n" +
"local lock_value=ARGV[1]\n" +
"\n" +
"local current_value=redis.call('get',lock_key)\n" +
"local result=0\n" +
"if lock_value==current_value then\n" +
" redis.call('del',lock_key)\n" +
" result=1\n" +
"end\n" +
" return result");
/**
* 释放锁得操作 使用Lua脚本保证原子性
* @param uuid
*/
public boolean unlock(String key,String uuid) {
if ((long)jedis.evalsha(unlockLua, Arrays.asList(key), Arrays.asList(uuid))==1) {
scheduledExecutorService.shutdown();
return true;
}
return false;
}
}
我们就可以把释放锁得代码改造如上。
3.3 v3版本
那么这样就大功告成了吗?并没有。
假设我们得ttl设置的是30s, 可是我的业务系统却执行了40s, 在第30s锁就已经释放,其他线程已经可以进来了。
那么如何解决这个问题呢?
在redission中有一个watchDog看门狗机制其实就是应对这种情况,实现锁的一个续期处理。这里我们就不演示redission如何实现的了。
我们自己来实现watchDog相同思想的操作,给锁续期。
其实思路也很简单我们可以new 一条线程,定期检查锁的过期时间,如果快过期了,那么就进行续费操作。
我们这里就简单一点搞一个定时任务,根据ttl的最后几秒钟来设置执行的速率,当临近ttl快过期的时候,我们重新设置ttl.实现续期操作。
package com.xzq.lock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* @Author xzq
* @Description // 分布式锁实践
* @Date 2021/11/27 9:45
* @Version 1.0.0
**/
public class RedisLock {
private Logger logger = LoggerFactory.getLogger(RedisLock.class);
private Jedis jedis = JedisPoolManager.getJedis();
private ScheduledThreadPoolExecutor scheduledExecutorService = new ScheduledThreadPoolExecutor(1);
static class JedisPoolManager{
private static JedisPool jedisPool;
static {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(10);
jedisPool = new JedisPool(config, "127.0.0.1", 6379);
}
public static Jedis getJedis() {
Jedis jedis = jedisPool.getResource();
return jedis;
}
}
public boolean lock(String key, String value,long ttl) {
//结束时间,表示超时时间,3s内获取不到锁则结束
long end = System.currentTimeMillis() + 1000 * 3;
//自旋重试
while (System.currentTimeMillis() < end) {
if (acquire(key, value,ttl)) {
watchDog(key, value,ttl);
return true;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return false;
}
private void watchDog(String key,String value,long ttl) {
//获取续命速率
long rate = getRate(ttl);
if (scheduledExecutorService.isShutdown()) {
scheduledExecutorService=new ScheduledThreadPoolExecutor(1);
}
scheduledExecutorService.scheduleAtFixedRate(new watchDogThread(scheduledExecutorService,Arrays.asList(key),Arrays.asList(value,String.valueOf(ttl))),
1, rate, TimeUnit.SECONDS);
}
class watchDogThread implements Runnable{
private ScheduledThreadPoolExecutor poolExecutor;
private List<String> keys;
private List<String> args;
public watchDogThread(ScheduledThreadPoolExecutor poolExecutor, List<String> keys, List<String> args) {
this.poolExecutor = poolExecutor;
this.keys = keys;
this.args = args;
}
@Override
public void run() {
logger.info("进行续期");
if ((long)jedis.evalsha(watchLua, keys,args)==0) {
//续期失败 可能是业务系统发生异常并且没有进行异常捕捉,没有进行释放锁操作
poolExecutor.shutdown();
}
}
}
private long getRate(long ttl) {
if (ttl - 5 > 0) {
return ttl - 5;
} else if (ttl - 1 > 0) {
return ttl - 1;
}
throw new RuntimeException("ttl 不允许小于1");
}
public boolean lock(String key, String value,long ttl,long timeOut) {
//结束时间,表示超时时间,3s内获取不到锁则结束
long end = System.currentTimeMillis() + timeOut;
while (System.currentTimeMillis() < end) {
if (acquire(key, value,ttl)) {
watchDog(key, value,ttl);
return true;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return false;
}
private boolean acquire(String key, String value, long ttl) {
if (jedis.setnx(key, value)==1) {
//加锁成功,设置过期时间
jedis.expire(key, ttl);
return true;
}
return false;
}
private final String watchLua = jedis.scriptLoad("local lock_key=KEYS[1]\n" +
"local lock_value=ARGV[1]\n" +
"local lock_ttl=ARGV[2]\n" +
"local current_value=redis.call('get',lock_key)\n" +
"local result=0;\n" +
"if lock_value==current_value then\n" +
" result=1;\n" +
" redis.call('expire',lock_key,lock_ttl)\n" +
"end\n" +
"return result");
private final String unlockLua = jedis.scriptLoad("local lock_key=KEYS[1]\n" +
"local lock_value=ARGV[1]\n" +
"\n" +
"local current_value=redis.call('get',lock_key)\n" +
"local result=0\n" +
"if lock_value==current_value then\n" +
" redis.call('del',lock_key)\n" +
" result=1\n" +
"end\n" +
" return result");
/**
* 释放锁得操作 使用Lua脚本保证原子性
* @param uuid
*/
public boolean unlock(String key,String uuid) {
if ((long)jedis.evalsha(unlockLua, Arrays.asList(key), Arrays.asList(uuid))==1) {
scheduledExecutorService.shutdown();
return true;
}
return false;
}
}
4. 测试
package com.xzq;
import com.xzq.lock.RedisLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.UUID;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* @Author xzq
* @Description //TODO
* @Date 2021/11/27 10:20
* @Version 1.0.0
**/
public class LockTest {
private static Logger logger = LoggerFactory.getLogger(LockTest.class);
private static final String LOCK_PREV = "REIDS:LOCK:";
private static final long LOCK_TTL = 5 ;
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(() -> {
something();
}).start();
}
}
public static void something(){
RedisLock redisLock = new RedisLock();
String uuid = UUID.randomUUID().toString();
String key = LOCK_PREV + "something";
if (redisLock.lock(key, uuid, LOCK_TTL, 1000 * 2)) {
try {
doSomething();
} catch (Exception e) {
e.printStackTrace();
redisLock.unlock(key, uuid);
}finally {
redisLock.unlock(key, uuid);
}
}else{
logger.error("获取锁失败");
}
}
private static void doSomething() {
logger.info("做一些业务操作.......");
try {
Thread.sleep(1000 * 15);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
如图所示,我们直接new 5个线程 去争抢锁,并设置业务执行时间为15s,但是锁的过期时间设置为5s。
可以看到五个线程中只有一个线程获取到了锁,并且进行了续期操作,并且其他线程是在自旋2s后退出的。