在分布式环境中,实现分布式锁是比较复杂的,不能像传统的
synchronized
、ReentrantLock
这种使用进程锁的方式,因为如果你的项目部署了多个副本,使用这种方式上锁,都是锁定的当前进程的锁对象,如果请求进入不同的副本中,对不同的副本锁是无效的。
Redis分布式锁的问题
不设置过期时间
我们知道,可以使用 Redis 的
setNx
命令实现 Redis的分布式锁,使用 Redis 的lua
脚本实现解锁, Spring data Redis中的RedisConnection#setNX(byte[] key, byte[] value)
接收两个参数,分别为 key,和 value,此方法不能设置key的过期时间。如果key不设置过期时间,就可能造成死锁,如应用程序没有正常的解锁,就会导致后续的需要获取此key的线程一直拿不到锁,造成死锁。所以,应该给这个key设置合理的过期时间。
过期时间设置太短
同样,如果一个key的过期时间设置太短,被锁定的临界区代码还没有执行完,key就自动过期了,会导致其它线程也能获取到这个key的锁,此时,两个线程同时进入被锁定的临界区代码块中,锁失效。
配合 @Transactional
事物注解使用不当的影响
如下一段代码,在高并发的情况会出现问题,解决方法:
将
lock.lock();
加锁的逻辑放在调用此方法调用者,谁要调用此方法,自己先加锁,在加锁的临界区代码中调用此方法。如果有100个地方要调用此方法,那就要写100遍
lock.lock()
类似的代码,你会非常不爽…其实,我们只要保证
lock.lock()
加锁方法在事物AOP 拦截之前执行,且lock.unlock()
解锁方法在事物 AOP 拦截提交之后执行即可
@Override
@Transactional
public void transactionalAndRedisLockTest(String keyId) {
Lock lock = new RedisLock("transactionalAndRedisLockTest:" + keyId);
lock.lock();
try {
// 执行业务逻辑,先查询数据库记录,判断库存是否满足,如果满足减去库存,再执行update操作
} finally {
// 执行完业务逻辑,解锁后,此时事物还没有提交(因为有Transactional,需要整个方法执行完后,才会提交事物),数据库的记录并没有发生变化,
// 此时,另一个线程是可以获取到锁的,那它可以进入到try语句中,如果这个线程查询是前一个线程还没有提交的数据,此时,锁的意义失效
lock.unlock();
}
}
终极Redis分布式锁的代码实现
定时任务
此定时任务是定时刷新key过期时间,防止 key设置的过期时间太短,业务方法还没有执行完,导致其它线程进入加锁的临界区代码中。
package com.hk.core.redis.locks;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* 定义此Schedule,是防止Redis 锁key过期时间设置太短,导致key自动过期后,加锁的代码块还没有执行完成,导致其它线程也获取锁
*
* @author Kevin
*/
class RedisLockSchedule {
private static final ConcurrentHashMap<String, ScheduledFuture<?>> FUTURE_MAP;
private static final ScheduledThreadPoolExecutor POOL_EXECUTOR;
static {
var creator = new CustomizableThreadCreator();
creator.setDaemon(false);
creator.setThreadGroupName("Redis-Lock-");
POOL_EXECUTOR = new ScheduledThreadPoolExecutor(2, creator::createThread);
FUTURE_MAP = new ConcurrentHashMap<>(128);
}
static void register(String key, Runnable runnable, long period, TimeUnit timeUnit) {
ScheduledFuture<?> fixedRate = POOL_EXECUTOR.scheduleAtFixedRate(runnable, 0, period, timeUnit);
ScheduledFuture<?> oldFuture = FUTURE_MAP.put(key, fixedRate);
if (Objects.nonNull(oldFuture)) {
oldFuture.cancel(true);
}
}
static void cancel(String key) {
ScheduledFuture<?> future = FUTURE_MAP.remove(key);
if (Objects.nonNull(future)) {
future.cancel(true);
}
}
}
Redis锁实现核心类
------------------------------------------------------------------------
package com.hk.core.redis.locks;
import com.hk.commons.util.ArrayUtils;
import com.hk.commons.util.Lazy;
import com.hk.commons.util.ObjectUtils;
import com.hk.commons.util.SpringContextHolder;
import com.hk.core.redis.scripting.RedisScriptSource;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalTime;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* 基于 Redis setNx 与 lua 脚本实现分布式锁,支持可重入。
*
* @author Kevin
*/
public class RedisLock implements Lock {
/**
* 线程睡眠时间,单位:毫秒
*/
private static final long SLEEP_TIME = 10;
/**
* <pre>
* 默认过期时间: 20 秒
* </pre>
*/
private static final long EXPIRE_SECONDS = 20;
/**
* lua 脚本内容,lua 脚本能保证原子性执行
*/
private static final DefaultRedisScript<Long> LUA_SCRIPT;
/**
* 非事物的 redisTemplate
*/
private final static Lazy<StringRedisTemplate> STRING_REDIS_TEMPLATE_LAZY =
Lazy.of(() -> SpringContextHolder.getBean("stringRedisTemplateDisabledTransactionSupport",
StringRedisTemplate.class));
static {
LUA_SCRIPT = new DefaultRedisScript<>();
/*
* 这里 LOCK是一段lua脚本,内容为:
* if redis.call("get",KEYS[1]) == ARGV[1] then
* return redis.call("del",KEYS[1])
* else
* return 0
* end
*/
LUA_SCRIPT.setScriptSource(RedisScriptSource.LOCK);
LUA_SCRIPT.setResultType(Long.class);
}
/**
* redis Key
*/
private final String key;
/**
* key 过期时间,防止死锁
*/
private final long expire;
/**
* 默认过期时间 2 秒
*
* @param key key
*/
public RedisLock(String key) {
this(key, EXPIRE_SECONDS);
}
/**
* 默认过期时间 2 秒
*
* @param key key
*/
public RedisLock(String key, long expire) {
this.key = key;
this.expire = expire <= 0 ? EXPIRE_SECONDS : expire;
}
/**
* 获取锁
*/
@Override
public void lock() {
while (!tryLock()) {
if (SLEEP_TIME >= 0) {
try {
TimeUnit.MILLISECONDS.sleep(SLEEP_TIME);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
/**
* 可被打断的获取 lock
*/
@Override
public void lockInterruptibly() throws InterruptedException {
if (Thread.interrupted()) {//如果当前线程已中段
throw new InterruptedException();
}
while (!tryLock()) {
TimeUnit.MILLISECONDS.sleep(SLEEP_TIME);
}
}
/**
* 尝试获取锁,立即返回。如果返回 true ,加锁成功
*
* @return true or false
*/
@Override
public boolean tryLock() {
/*
* 只有当Key 不存在时,设置key的值,
* 注意,redisTemplate.setIfAbsent方法,在 pipeline / transaction 的方法调用此方法时, 会返回 null,
* 所以这里使用 stringRedisTemplateDisabledTransactionSupport 的 redisTemplate
*/
var result = ObjectUtils.defaultIfNull(STRING_REDIS_TEMPLATE_LAZY.get().opsForValue().setIfAbsent(key,
Long.toString(Thread.currentThread().getId()), expire, TimeUnit.SECONDS), Boolean.FALSE);
if (result && expire > 1) { //过期时间大于1秒才设置开启线程定时刷新过期时间
registerSchedule();
}
return result;
}
/**
* 初始化线程并启动
*/
private void registerSchedule() {
cancelSchedule();
long period;
TimeUnit timeUnit;
if (expire > 2) {
period = expire - 2;
timeUnit = TimeUnit.SECONDS;
} else {
period = 500;
timeUnit = TimeUnit.MILLISECONDS;
}
RedisLockSchedule.register(key, () -> {
//刷新过期时间,如果没有成功,可能是key不存在了,直接结束此任务
if (!ObjectUtils.defaultIfNull(STRING_REDIS_TEMPLATE_LAZY.get().expire(key, expire, TimeUnit.SECONDS),
Boolean.FALSE)) {
cancelSchedule();
}
}, period, timeUnit);
}
private void cancelSchedule() {
RedisLockSchedule.cancel(key);
}
/**
* 在指定的时间段获取锁,超出指定的时间立即返回
*/
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
if (Thread.interrupted()) {
throw new InterruptedException();
}
long max = System.nanoTime() + unit.toNanos(time);
while (System.nanoTime() < max) {
if (tryLock()) {
return true;
}
}
return false;
}
/**
* 解锁
*/
@Override
public void unlock() {
STRING_REDIS_TEMPLATE_LAZY.get().execute(LUA_SCRIPT, ArrayUtils.asArrayList(key),
Long.toString(Thread.currentThread().getId()));
cancelSchedule();
}
@Override
public Condition newCondition() {
throw new UnsupportedOperationException("Redis Lock newCondition");
}
}
Redis锁使用注解方式
package com.hk.core.redis.annotations;
import java.lang.annotation.*;
/**
* redis 锁使用注解的方式
*
* @author Kevin
* @see com.hk.core.redis.locks.RedisLock
* @see com.hk.core.redis.aspect.RedisLockInterceptor
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedisLock {
/**
* <pre>
* 锁的 Key,支持 SPEL 表达式
* SPEL格式如:
* #参数名,如方法为method(int value) ,则可以使用 #value 获取
* 也可以使用计算公式: 如 "1+1" -> 结果为2
* 方法参数也可以使用对象,如 method(int value, Object obj) ,如果想以obj的属性作为key,可以写成 #obj.属性名。
* 如果方法参数有数组,如 method(int[]arr) ,可以使用 #arr[0] 获取第几个元素作为key
* 如果方法参数有map,如 method(int[]arr,Map<String,Object> map) ,可以使用 #map[key] 获取map指定key的值作为锁的key
* 如果方法参数有list,如 method(List<String> list) ,可以使用 #list[0] 获取list指定的索引作为锁的key
* </pre>
*/
String key();
/**
* 过期时间,单位为秒,默认20 秒
*/
long expireSeconds() default 20;
}
Redis锁注解 AOP
注意:类上加了
@Order
注解,此AOP 会在TransactionInterceptor
的前面。
package com.hk.core.redis.aspect;
import com.hk.commons.util.StringUtils;
import com.hk.core.redis.annotations.RedisLock;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.SimpleEvaluationContext;
import java.util.Objects;
/**
* <pre>
*
* 加 Order 注解,必需要在 事务之前执行(也就是这个Aspect 拦截器要在 {@link org.springframework.transaction.interceptor.TransactionInterceptor} 之前),事务提交之后,再释放类。
* 先使用redis上锁、再开启事物、再执行业务代码,再提交事物 ,再释放redis 锁,才能让下一个线程能获取到锁。
* </pre>
* <p>
* {@link RedisLock} aspect
*
* @author Kevin
*/
@Aspect
@Order(value = Integer.MIN_VALUE)
public class RedisLockInterceptor {
@Pointcut("@annotation(com.hk.core.redis.annotations.RedisLock)")
public void redisLock() {
}
@Around("redisLock()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
if (pjp.getSignature() instanceof MethodSignature signature) {
var method = signature.getMethod();
var redisLock = method.getAnnotation(RedisLock.class);
if (Objects.nonNull(redisLock)) {
String key = redisLock.key();
var parser = new SpelExpressionParser();
var parameters = signature.getMethod().getParameters();
var args = pjp.getArgs();
var builder = SimpleEvaluationContext.forReadWriteDataBinding();
int argLen = args.length;
if (argLen > 1) {
builder.withRootObject(args[0]);
}
var context = builder.build();
for (int i = 0; i < argLen; i++) {
context.setVariable(parameters[i].getName(), args[i]);
}
try {
key = parser.parseExpression(redisLock.key()).getValue(context, String.class);
} catch (Exception e) {
// can not parse spel,use original key.
}
if (StringUtils.isEmpty(key)) {
key = pjp.getTarget().getClass().getName() + method.getName();
}
var lock = new com.hk.core.redis.locks.RedisLock(key, redisLock.expireSeconds());
lock.lock();
try {
return pjp.proceed();
} finally {
//保证释放锁时,事物已提交
lock.unlock();
}
}
}
return pjp.proceed();
}
}
测试
基于注解方式
@Transactional
@RedisLock(key = "#keyId")
public void transactionalAndRedisLockTest(String keyId) {
//您的业务逻辑
}
基于编程式
主要是使用
transactionTemplate
执行
private final TransactionTemplate transactionTemplate;
/**
* 编程式方式
*/
public void transactionalAndRedisLockTest(String keyId) {
Lock lock = new RedisLock(keyId);
lock.lock();
try {
transactionTemplate.execute(new TransactionCallback<Object>() {
@Override
public Object doInTransaction(TransactionStatus status) {
//要执行的业务逻辑
return null;
}
});
} finally {
lock.unlock();
}
}