Redis 实现分布式锁
一、背景:
业务架构中没有使用 Zookeeper,只使用了 Redis,但是业务中又需要使用到分布式锁,还好 redis 提供了很多原子操作,可以利用这些原子操作来实现分布式锁
二、设计分布式锁需要考虑的点:
互斥性
在任意时刻,只有一个客户端能持有锁。
不会发生死锁
即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
具有容错性
只要大部分的 Redis 节点正常运行,客户端就可以加锁和解锁。
解铃还须系铃人
加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
三、流程:
redis分布式锁流程
1.两个进程同时获取锁,但是 A 进程得到了锁,所以 B 进程阻塞;
2.B 进程等待超时重试,失败,继续等待;
3.A 进程执行完毕,释放锁;
4.B 进程等待超时继续获取锁,获得锁,执行逻辑。
四、Java 实现:
redis包结构
DistributedRedisLock.java
package com.gameboys;
import java.util.Arrays;
import java.util.Collections;
import java.util.UUID;
import redis.clients.jedis.Jedis;
/**
*
*
*
*
* redis版本必须高于 Redis 2.6.12 版本
*
* jedis版本也对应的需要升级,目前使用的是2.8.1
*
* redis官方说明 http://doc.redisfans.com/script/eval.html
*
* @author sniper
* @date 2019年10月30日
*/
public class DistributedRedisLock {
// 超时时间
private static final int DEFAULT_TIME_OUT_MILLIONS = 3000;
private static final int DEFAULT_RETRY_TIMES = 3;// 重试次数
private static final int DEFAULT_RETRY_TIME_OUT_MILLIONS = 100;// 重试超时时间
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";
public DistributedRedisLock() {
}
/**
* 这个方法会阻塞线程,
*
* @param key
* @param lockLogic
* @param successParams
* @param failParams
* @param retryTimes
* 重试次数
* @param tryTime
* 重试时间
*/
public void lock(Jedis jedis,String key, ILockLogic lockLogic, Object[] successParams, Object[] failParams, int retryTimes, int retryTimeOut) {
final String reqID = UUID.randomUUID().toString();
boolean lock = this.tryLockAndDoAction(jedis,key, reqID, DEFAULT_TIME_OUT_MILLIONS, lockLogic, successParams);
if (lock) {
return;
}
// 获取失败,阻塞一下再次获取
for (int i = 0; i < retryTimes; i++) {
synchronized (this) {
try {
this.wait(retryTimeOut);
lock = this.tryLockAndDoAction(jedis,key, reqID, DEFAULT_TIME_OUT_MILLIONS, lockLogic, successParams);
if (lock) {
return;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 如果到这里还没有拿到锁,执行失败逻辑
lockLogic.onFailDo(failParams);
}
/**
* 这个方法会阻塞线程,默认重试3次,每次100ms
*
*
* @param key
* @param lockLogic
* @param successParams
* @param failParams
*/
public void lock(Jedis jedis,String key, ILockLogic lockLogic, Object[] successParams, Object[] failParams) {
this.lock(jedis,key, lockLogic, successParams, failParams, DEFAULT_RETRY_TIMES, DEFAULT_RETRY_TIME_OUT_MILLIONS);
}
private boolean tryLockAndDoAction(Jedis jedis, String lockKey, String requestId, int expireTime, ILockLogic lockLogic, final Object[] successParams) {
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
lockLogic.onSuccessDo(successParams);
this.unlock(jedis, lockKey, requestId);
return true;
}
return false;
}
private static final Long RELEASE_SUCCESS = 1L;
/**
* 释放分布式锁,这个可看成原子操作
*
* @param jedis
* Redis客户端
* @param lockKey
* 锁
* @param requestId
* 请求标识
* @return 是否释放成功
*/
private boolean unlock(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;
}
public static void main(String[] args) {
//redis按照自己的项目初始化
Jedis jedis = null;
DistributedRedisLock redisLock = new DistributedRedisLock();
redisLock.lock(jedis, "gameboysTest", new ILockLogic() {
@Override
public void onSuccessDo(Object[] params) {
System.out.println(Thread.currentThread().getName() + "成功执行:" + Arrays.toString(params));
}
@Override
public void onFailDo(Object[] params) {
System.out.println(Thread.currentThread().getName() + "失败执行:" + Arrays.toString(params));
}
}, new String[] { "gameboys", "nice" }, new String[] { "lisa", "nice" });
}
}
ILockLogic.java
package com.gameboys;
/**
* Description:
*
* @author gameboys
* @date 2019年10月30日
*/
public interface ILockLogic {
void onSuccessDo(Object[] params);
void onFailDo(Object[] params);
}
ParamsRunnable.java
package com.gameboys;
/**
* Description:
*
* @author gameboys
* @date 2019年9月30日
*/
public abstract class ParamsRunnable implements Runnable {
// 参数
protected Object[] params;
public ParamsRunnable(Object... params) {
this.params = params;
}
@Override
public void run() {
try {
this.doAction(params);
} catch (Exception e) {
}
}
public abstract void doAction(Object[] params);
public Object[] getParams() {
return params;
}
}
五、总结:
Redis 能实现分布式锁,得益于 redis 官方提供的 set 和 eval 命令,具体解释如下:
set 命令:
SET key value [EX seconds] [PX milliseconds] [NX|XX]
从 Redis 2.6.12 版本开始, SET 命令的行为可以通过一系列参数来修改:
EX second :设置键的过期时间为 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。
PX millisecond :设置键的过期时间为 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。
NX :只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。
**XX ** :只在键已经存在时,才对键进行设置操作。
eval 命令:
EVAL script numkeys key [key ...] arg [arg ...]
从 Redis 2.6.0 版本开始,通过内置的 Lua 解释器,可以使用 EVAL 命令对 Lua 脚本进行求值。
详情查看官网:http://doc.redisfans.com/script/eval.html