一.前言
在但进程中,我们可以用到synchronized、lock之类的同步操作去解决,但是对于分布式架构下多进程的情况下,如何做到跨进程的锁。就需要借助一些第三方手段来完成,本文介绍redis分布锁的合理使用.
二.相关介绍
线程锁:主要用来给方法、代码块加锁。当某个方法或代码使用锁,在同一时刻仅有一个线程执行该方法或该代码段。线程锁只在同一JVM中有效果,因为线程锁的实现在根本上是依靠线程之间共享内存实现的,比如synchronized是共享对象头,显示锁Lock是共享某个变量(state)。
进程锁:为了控制同一操作系统中多个进程访问某个共享资源,因为进程具有独立性,各个进程无法访问其他进程的资源,因此无法通过synchronized等线程锁实现进程锁。
三.开始使用
1.使用思路
(1)加锁
redis中有一个setNx命令,这个命令只有在key不存在的情况下为key设置值,并同时设置失效时间,写值成功即为加锁成功。
(2)解锁
删除redis上的key对应value的数据,要保证获取数据、判断一致以及删除数据三个操作是原子的。
Lua脚本:
当客户端完成了对共享资源的操作之后,执行下面的Redis Lua脚本来释放锁
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
2.注意事项
(1) 为了避免死锁 需要设设置一个失效时间,时间一般为执行业务时间的2倍.
(2) 为了避免锁误删,需在加锁的时候通过生成一个随机值字符串来当成value加锁.
(3)写入随机值与设置失效时间要同时进行,保证加锁的原子性。
3.使用示例
本例使用springboot框架
1.声明redis操作接口RedisServiceInter
public interface RedisServiceInter {
boolean setNx(String key,String value,long time);
boolean releaseLock(String key,String value);//lu脚本
}
2.接口实现类RedisServiceImpl
@Service
public class RedisServiceImpl implements RedisServiceInter {
/**
* @Author lss0555
* @Description setNx操作
**/
@Override
public boolean setNx(String key,String value, long time) {
try {
RedisCallback<String> callback = (connection) -> {
JedisCommands commands = (JedisCommands) connection.getNativeConnection();
return commands.set(key, value, "NX", "PX", time);
};
String result = redisTemplate.execute(callback);
return !StringUtils.isEmpty(result);
} catch (Exception e) {
logger.error("set redis occured an exception", e);
}
return false;
}
//lu脚本,删除key
@Override
public boolean releaseLock(String key,String value) {
// 释放锁的时候,有可能因为持锁之后方法执行时间大于锁的有效期,此时有可能已经被另外一个线程持有锁,所以不能直接删除
try {
List<String> keys = new ArrayList<>();
keys.add(key);
List<String> args = new ArrayList<>();
args.add(value);
// 使用lua脚本删除redis中匹配value的key,可以避免由于方法执行时间过长而redis锁自动过期失效的时候误删其他线程的锁
// spring自带的执行脚本方法中,集群模式直接抛出不支持执行脚本的异常,所以只能拿到原redis的connection来执行脚本
RedisCallback<Long> callback = (connection) -> {
Object nativeConnection = connection.getNativeConnection();
// 集群模式和单机模式虽然执行脚本的方法一样,但是没有共同的接口,所以只能分开执行
// 集群模式
if (nativeConnection instanceof JedisCluster) {
return (Long) ((JedisCluster) nativeConnection).eval(UNLOCK_LUA, keys, args);
}
// 单机模式
else if (nativeConnection instanceof Jedis) {
return (Long) ((Jedis) nativeConnection).eval(UNLOCK_LUA, keys, args);
}
return 0L;
};
Long result = redisTemplate.execute(callback);
return result != null && result > 0;
} catch (Exception e) {
logger.error("release lock occured an exception", e);
} finally {
// 清除掉ThreadLocal中的数据,避免内存溢出
//lockFlag.remove();
}
return false;
}
public static final String UNLOCK_LUA;
static {
StringBuilder sb = new StringBuilder();
sb.append("if redis.call(\"get\",KEYS[1]) == ARGV[1] ");
sb.append("then ");
sb.append(" return redis.call(\"del\",KEYS[1]) ");
sb.append("else ");
sb.append(" return 0 ");
sb.append("end ");
UNLOCK_LUA = sb.toString();
}
}
3.声明RedisLock锁实现Lock接口
@Component
public class RedisLock implements Lock {
private Logger logger = LoggerFactory.getLogger(RedisLock.class);
public static final String UNLOCK_LUA;
static {
StringBuilder sb = new StringBuilder();
sb.append("if redis.call(\"get\",KEYS[1]) == ARGV[1] ");
sb.append("then ");
sb.append(" return redis.call(\"del\",KEYS[1]) ");
sb.append("else ");
sb.append(" return 0 ");
sb.append("end ");
UNLOCK_LUA = sb.toString();
}
@Resource
RedisServiceInter redisServiceInter;
private static final String KEY="KEY";
private ThreadLocal<String> local=new ThreadLocal<>();
//加锁
@Override
public void lock() {
//1.尝试加锁
if(tryLock()){
return;
}
//2.加锁失败 当前任务休眠一段时间
try {
Thread.sleep(100);
}catch (Exception e){
e.printStackTrace();
}
//3.递归调用,再次重新加锁
lock();
}
//尝试加锁
@Override
public boolean tryLock() {
//产生随机值
String uuid=UUID.randomUUID().toString();
//设置随机值以及设置失效时间
boolean ret = redisServiceInter.setNx(KEY, uuid, 100);
if(ret){
//加锁成功后,为当前threadlocal设置随机值,以便解锁的时候使用
local.set(uuid);
}
return ret;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
//解锁
@Override
public void unlock() {
boolean b = redisServiceInter.releaseLock(KEY, local.get());
//清理ThreadLocal中的数据,避免内存溢出
local.remove();
if(!b){
logger.error("解锁失败");
}
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public Condition newCondition() {
return null;
}
}
4.在Controller中接口测试下
模拟有10个线程在不断的争抢1000张票
@RestController
public class RedisLockController {
private Logger logger = LoggerFactory.getLogger(RedisLockController.class);
ExecutorService executorService= Executors.newFixedThreadPool(10);
@Resource
RedisLock lock;
private static int num=1000;
@GetMapping("/lock")
public String redislockTest(){
num=1000;
for (int i=0;i<10;i++){
executorService.submit(new Runnable() {
@Override
public void run() {
locks();
}
});
}
return "ok";
}
private void locks() {
while (num>0){
lock.lock();
try {
if(num>0){
logger.info("抢到第"+num--+"个");
}
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
}
结果
018-12-06 10:46:23 25049 [pool-1-thread-6] INFO c.e.s.c.RedisLockController - 抢到第1000个
2018-12-06 10:46:23 25049 [pool-1-thread-6] INFO c.e.s.c.RedisLockController - 抢到第999个
2018-12-06 10:46:23 25050 [pool-1-thread-6] INFO c.e.s.c.RedisLockController - 抢到第998个
…
…
…
2018-12-06 10:46:23 25050 [pool-1-thread-6] INFO c.e.s.c.RedisLockController - 抢到第10个
2018-12-06 10:46:23 25063 [pool-1-thread-6] INFO c.e.s.c.RedisLockController - 抢到第9个
2018-12-06 10:46:23 25063 [pool-1-thread-6] INFO c.e.s.c.RedisLockController - 抢到第8个
2018-12-06 10:46:23 25064 [pool-1-thread-6] INFO c.e.s.c.RedisLockController - 抢到第7个
2018-12-06 10:46:23 25065 [pool-1-thread-6] INFO c.e.s.c.RedisLockController - 抢到第6个
2018-12-06 10:46:23 25065 [pool-1-thread-6] INFO c.e.s.c.RedisLockController - 抢到第5个
2018-12-06 10:46:23 25066 [pool-1-thread-6] INFO c.e.s.c.RedisLockController - 抢到第4个
2018-12-06 10:46:23 25067 [pool-1-thread-6] INFO c.e.s.c.RedisLockController - 抢到第3个
2018-12-06 10:46:23 25067 [pool-1-thread-6] INFO c.e.s.c.RedisLockController - 抢到第2个
2018-12-06 10:46:23 25068 [pool-1-thread-6] INFO c.e.s.c.RedisLockController - 抢到第1个