❝分布式锁其实就是,控制分布式系统不同进程共同访问共享资源的一种锁的实现。如果不同的系统或同一个系统的不同主机之间共享了某个临界资源,往往需要互斥来防止彼此干扰,以保证一致性。❞ |
特征:
- 「互斥性」: 任意时刻,只有一个客户端能持有锁。
- 「锁超时释放」:持有锁超时,可以释放,防止不必要的资源浪费,也可以防止死锁。
- 「可重入性」:一个线程如果获取了锁之后,可以再次对其请求加锁。
- 「高性能和高可用」:加锁和解锁需要开销尽可能低,同时也要保证高可用,避免分布式锁失效。
- 「安全性」:锁只能被持有的客户端删除,不能被其他客户端删除
方案一:SETNX + EXPIRE
提到Redis的分布式锁,很多小伙伴马上就会想到setnx+ expire命令。即先用setnx来抢锁,如果抢到之后,再用expire给锁设置一个过期时间,防止锁忘记了释放。
❝SETNX 是SET IF NOT EXISTS的简写.日常命令格式是SETNX key value,如果 key不存在,则SETNX成功返回1,如果这个key已经存在了,则返回0。❞ |
假设某电商网站的某商品做秒杀活动,key可以设置为key_resource_id,value设置任意值,伪代码如下:
if(jedis.setnx(key_resource_id,lock_value) == 1){ //加锁 |
但是这个方案中,setnx和expire两个命令分开了,「不是原子操作」。如果执行完setnx加锁,正要执行expire设置过期时间时,服务器宕机或者要重启维护了,那么这个锁就“长生不老”了,「别的线程永远获取不到锁啦」。
方案三:使用Lua脚本(包含SETNX + EXPIRE两条指令)
实际上,我们还可以使用Lua脚本来保证原子性(包含setnx和expire两条指令),lua脚本如下:
if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then |
加锁代码如下:
String lua_scripts = "if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then" + |
这个方案,跟方案二对比,你觉得哪个更好呢?
方案四:SET的扩展命令(SET EX PX NX)
除了使用,使用Lua脚本,保证SETNX + EXPIRE两条指令的原子性,我们还可以巧用Redis的SET指令扩展参数!(SET key value[EX seconds][PX milliseconds][NX|XX]),它也是原子性的!
❝ SET key value[EX seconds][PX milliseconds][NX|XX]
❞ |
伪代码demo如下:
if(jedis.set(key_resource_id, lock_value, "NX", "EX", 100s) == 1){ //加锁try { |
但是呢,这个方案还是可能存在问题:
- 问题一:「锁过期释放了,业务还没执行完」。假设线程a获取锁成功,一直在执行临界区的代码。但是100s过去后,它还没执行完。但是,这时候锁已经过期了,此时线程b又请求过来。显然线程b就可以获得锁成功,也开始执行临界区的代码。那么问题就来了,临界区的业务代码都不是严格串行执行的啦。
- 问题二:「锁被别的线程误删」。假设线程a执行完后,去释放锁。但是它不知道当前的锁可能是线程b持有的(线程a去释放锁时,有可能过期时间已经到了,此时线程b进来占有了锁)。那线程a就把线程b的锁释放掉了,但是线程b临界区业务代码可能都还没执行完呢。
方案五:SET EX PX NX + 校验唯一随机值,再删除
既然锁可能被别的线程误删,那我们给value值设置一个标记当前线程唯一的随机数,在删除的时候,校验一下,不就OK了嘛。伪代码如下:
if(jedis.set(key_resource_id, uni_request_id, "NX", "EX", 100s) == 1){ //加锁try { |
在这里,「判断是不是当前线程加的锁」和「释放锁」不是一个原子操作。如果调用jedis.del()释放锁的时候,可能这把锁已经不属于当前客户端,会解除他人加的锁。
为了更严谨,一般也是用lua脚本代替。lua脚本如下:
if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) elsereturn 0end; |
方案六:Redisson框架
方案五还是可能存在「锁过期释放,业务没执行完」的问题。有些小伙伴认为,稍微把锁过期时间设置长一些就可以啦。其实我们设想一下,是否可以给获得锁的线程,开启一个定时守护线程,每隔一段时间检查锁是否还存在,存在则对锁的过期时间延长,防止锁过期提前释放。
当前开源框架Redisson解决了这个问题。我们一起来看下Redisson底层原理图吧:
只要线程一加锁成功,就会启动一个watch dog看门狗,它是一个后台线程,会每隔10秒检查一下,如果线程1还持有锁,那么就会不断的延长锁key的生存时间。因此,Redisson就是使用Redisson解决了「锁过期释放,业务没执行完」问题。
方案七:多机实现的分布式锁Redlock+Redisson
前面六种方案都只是基于单机版的讨论,还不是很完美。其实Redis一般都是集群部署的:
如果线程一在Redis的master节点上拿到了锁,但是加锁的key还没同步到slave节点。恰好这时,master节点发生故障,一个slave节点就会升级为master节点。线程二就可以获取同个key的锁啦,但线程一也已经拿到锁了,锁的安全性就没了。
为了解决这个问题,Redis作者 antirez提出一种高级的分布式锁算法:Redlock。Redlock核心思想是这样的:
❝搞多个Redis master部署,以保证它们不会同时宕掉。并且这些master节点是完全相互独立的,相互之间不存在数据同步。同时,需要确保在这多个master实例上,是与在Redis单实例,使用相同方法来获取和释放锁。❞ |
我们假设当前有5个Redis master节点,在5台服务器上面运行这些Redis实例。
RedLock的实现步骤:如下
❝
❞ |
简化下步骤就是:
- 按顺序向5个master节点请求加锁
- 根据设置的超时时间来判断,是不是要跳过该master节点。
- 如果大于等于3个节点加锁成功,并且使用的时间小于锁的有效期,即可认定加锁成功啦。
- 如果获取锁失败,解锁!
@RedisLock实现
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Method;
import java.util.concurrent.atomic.AtomicInteger;
/**
* paramIndex:标识使用方法中第几个参数作为对象锁,数值从0开始,标识使用第一个,选择的参数目前仅支持基本类型和String
* type:分布式锁应用的业务领域,String类型,该值可自定义
* waitSecondsWhenConflict:当获取分布式锁失败时最大等待时间,获取失败时抛出LockFailException异常
* timeOutSeconds:线程持有锁的最大超时时间,避免线程僵死导致死锁
*/
public class RedisLockInterceptor {
private static final String LOCK_CACHE="_inner_redislock";
private static final Logger logger = LoggerFactory.getLogger(RedisLockInterceptor.class);
private static final String lock_prefix = "$redislock_";
@SuppressWarnings({"rawtypes", "unchecked"})
public Object filterRuleMethod(FilterChain fc, RuleServiceFilterContext ctx) throws Throwable {
//获取redisLock注解
RedisLock redisLock = parseRedisLock(ctx.getMethod());
if (redisLock == null) {
//无注解执行后续流程
return fc.next(ctx);
} else {
//posName clazz=类名,method=方法名
String posName = ((ctx.getObject() == null ? "" : ("clazz=" + ctx.getObject().getClass().getName() + ","))
+ "method=" + ctx.getMethod().getName());
if (logger.isDebugEnabled()) {
logger.debug(posName + " 进入redislock注解拦截器");
}
//获取key
String lockId = resolverLockName(posName, redisLock.type(), redisLock.paramIndex(), ctx.getArgs());
//获取新的版本id
String vid = RequestIdKit.newSpanId();
//尝试加锁
boolean lock = tryGetLock(posName, lockId, redisLock.waitSecondsWhenConflict(), redisLock.timeOutSeconds(), vid);
if (lock) {
//加锁成功
try {
//执行后续流程
return fc.next(ctx);
} finally {
//释放锁
releaseLock(posName, lockId, vid);
if (logger.isDebugEnabled()) {
logger.debug(posName + " 退出redislock注解拦截器");
}
}
} else {
throw new RuntimeException(posName + " redis分布式锁lockId=[" + lockId + "]竞争失败!");
}
}
}
private void releaseLock(String tips, String lockId, String vid) {
IKMEMCache cache = KMEMFactory.instance.getCache(LOCK_CACHE);
try {
//释放锁 计数器减1
incrOrdecrLockCounter(lockId, false);
String val = cache.read(lockId);
if (!StringKit.isEmpty(val) && val.equals(vid)) {
//删除缓存key
cache.deleteSingle(lockId);
}
} catch (Exception e) {
logger.error(tips + " 释放redis分布式锁lockId=[" + lockId + "]异常:" + e.getMessage());
}
}
private boolean tryGetLock(String tips, String lockId, int waitSecondsWhenConflict, int timeOutSeconds,
String vid) {
//第一次加锁
boolean lock = tryLock(tips, lockId, timeOutSeconds, vid);
if (!lock && waitSecondsWhenConflict > 0) {
//加锁失败 且等待时间大于0
//获取当前时间
long currTime = System.currentTimeMillis();
//获取最大等待时间
long deadline = currTime + waitSecondsWhenConflict * 1000;
long waitInterval = 100;// 100毫秒周期
do {
//最大等待时间-当前时间
long temp = (deadline - currTime);
//获取线程等待时间
long wait = Math.min(waitInterval, temp);
//线程睡眠wait毫秒
waitInmills(wait);
//再次尝试加锁
lock = tryLock(tips, lockId, timeOutSeconds, vid);
if (lock) {
//加锁成功
break;
}
//加锁失败 继续循环
currTime = System.currentTimeMillis();
} while (currTime < deadline);
}
return lock;
}
private void waitInmills(long timeInmils) {
if (timeInmils > 0) {
try {
Thread.sleep(timeInmils);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
private boolean tryLock(String tips, String lockId, int timeOutSeconds, String vid) {
if (isCurrentThreadHoldLock(lockId)) {
//当前线程持有锁 计数+1
incrOrdecrLockCounter(lockId, true);
return true;
}
IKMEMCache cache = KMEMFactory.instance.getCache(LOCK_CACHE);
try {
//未持有锁 加锁 NX -- Only set the key if it does not already exist.
boolean lock = cache.writeWithExpireByKeyExistorNot(lockId, vid, false, timeOutSeconds);
if (lock) {
// 锁成功context中添加线程的标识
incrOrdecrLockCounter(lockId, true);
}
return lock;
} catch (Exception e) {
logger.error(tips + " 获取redis分布式锁lockId=[" + lockId + "]异常:" + e.getMessage());
}
return false;
}
private String resolverLockName(String tips, String type, int paramIndex, Object[] args) {
//无参方法
if (null == args && paramIndex == -1) {
//参数为空 或者 paramIndex为-1 缓存key为 appCode|type|clazz=类名,method=方法名
return AbstractConfig.getAppcode() + "|" + type + "|" + tips;
}
if (args == null || args.length <= paramIndex) {
//长度不匹配 抛异常
throw new ArrayIndexOutOfBoundsException(tips + " redisLock paramIndex=" + paramIndex
+ " out of args length : " + (args == null ? 0 : args.length));
}
Object obj = args[paramIndex];
if (!validParamType(obj)) {
//非基本数据类型
throw new IllegalArgumentException(tips + " redisLock paramIndex=" + paramIndex
+ " of args value is not type of String or primitive : type="
+ (obj == null ? null : obj.getClass().getName()));
}
//缓存key为 appCode|type|uid
return AbstractConfig.getAppcode() + "|" + type + "|" + (obj == null ? "null" : obj);
}
private RedisLock parseRedisLock(Method m) {
if (m == null) {
return null;
}
return m.getAnnotation(RedisLock.class);
}
private boolean validParamType(Object obj) {
// 校验参数类型是否有效
if (obj == null) {
return true;
} else if (obj instanceof String) {
return true;
} else if (obj.getClass().isPrimitive()) {
return true;
} else if (obj.getClass().isAssignableFrom(Integer.class)) {
return true;
} else if (obj.getClass().isAssignableFrom(Double.class)) {
return true;
} else if (obj.getClass().isAssignableFrom(Long.class)) {
return true;
} else if (obj.getClass().isAssignableFrom(Short.class)) {
return true;
} else if (obj.getClass().isAssignableFrom(Boolean.class)) {
return true;
} else if (obj.getClass().isAssignableFrom(Character.class)) {
return true;
} else if (obj.getClass().isAssignableFrom(Byte.class)) {
return true;
} else if (obj.getClass().isAssignableFrom(Float.class)) {
return true;
}
return false;
}
// 判断对象锁是不是已经被当前线程持有
private boolean isCurrentThreadHoldLock(String lockId) {
AtomicInteger counter = (AtomicInteger) (ThreadLocalContextManager.instance.getContext()
.getAttribute(getLocalId(lockId)));
return counter != null && counter.get() > 0;
}
private int incrOrdecrLockCounter(String lockId, boolean incr) {
//$redislock_+缓存key
String attrId = getLocalId(lockId);
AtomicInteger counter = (AtomicInteger) (ThreadLocalContextManager.instance.getContext().getAttribute(attrId));
if (counter == null) {
//计数
counter = new AtomicInteger(0);
//放入上下文
ThreadLocalContextManager.instance.getContext().setAttribute(attrId, counter);
}
if (incr) {
//获取锁 自增
return counter.incrementAndGet();
}
//释放锁 减1
return counter.decrementAndGet();
}
private String getLocalId(String lockId) {
//$redislock_+缓存key
return lock_prefix + lockId;
}
}
/*
public boolean writeWithExpireByKeyExistorNot(String key, Object value, boolean exist, long expireTime) throws Exception {
if (StringKit.isEmpty(key)) {
//key为空 加锁失败
return false;
} else {
JedisCluster j = null;
boolean var11;
try {
j = this.getJedisCluster();
//config.getBid() + currentTenantId + "_" + key
String redisKey = this.convert(key);
String type = "";
if (exist) {
type = "XX";
} else {
type = "NX";
}
String result = "";
String val = "";
if (value.getClass().isAssignableFrom(String.class)) {
val = (String)value;
} else {
val = JsonKit.object2Json(value);
}
result = j.set(redisKey, val, type, "EX", expireTime);
var11 = "OK".equalsIgnoreCase(result);
} catch (Exception var15) {
log.error(var15);
this.fillException(var15);
throw var15;
} finally {
this.monitorStat();
}
return var11;
}
}*/