vlambda博客
学习文章列表

分布式锁(一)Redis分布式锁注解灵活实现

  • 前言

在日常开发中,为了防止高并发,在不依赖过多的中间件的情况下,最常使用的分布式锁之一是 Redis锁。使用Redis锁就不得不面临一个问题,就是在业务代码中要控制Redis加锁、释放锁等等,对代码的侵入性较强。本文将详细介绍Redis分布式锁的实现原理以及已注解的形式灵活控制锁。

  • 优点:

  1. 线程间锁互斥。在同一时间内,仅有一个线程持有锁,避免多个线程同时执行逻辑,出现并发情况。

  2. 可重试。若一个线程第一次没有拿到锁,将会等待N秒后,重新尝试获得锁,重试次数可自定义,避免某一线程没有第一时间获得锁直接失败。

  3. 无死锁。即使某一线程中断没能释放锁,在到达指定的时间后,程序会自动释放锁。

  4. 锁唯一独有。加锁和释放锁必须由同一线程执行,不会出现A线程加锁后,B线程将锁释放。

  5. 无侵入。通过注解实现加锁和释放锁,代码中只需关注业务实现,无须关心“锁”问题,避免代码侵入。

  6. 支持多种方式传参做key。通过注解指定参数名称和类型,通过反射,灵活获得指定的参数。

  • 代码实现

spring相关依赖不在具体粘代码了,操作Redis用到了 Jedis 组件,因用到了jedis 2.9版本的功能,引入组件时,请注意版本。

        <dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>

RedisLock 注解类

代码解释:

  1. 注解需传入businessFlag,标识当前加锁的业务,保证锁在当前业务逻辑下有效。

  2. 使用业务唯一的参数,作为key,保证锁的唯一。

  3. 通过反射实现支持多种取参方式,如直接取值、json/map取值,以及业务封装的对象取值。使用对象的属性做key时,需通过fieldName指定属性名称

  4. 可根据业务自定锁时间,有默认值,保证即使线程中断,到达指定时间后会自动释放锁,避免出现死锁。

代码

package com.lsz.common.annotation;

import java.lang.annotation.*;

/**
* Redis锁注解
*
* @author lishuzhen
* @date 2021/6/1 21:12
*/
@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisLock {

/**
* key取值方式
*/
enum KeyType {
/**
* 直接取值
* <p>
* 如 String,Integer...
*/
VALUE,
/**
* 通过对象属性取值
* <p>
* 如 User,Order...
*/
CLASS,
/**
* 序列化后通过 key:value 取值
* <p>
* 如 JSONObject,Map...
*/
JSON
}


/**
* 方法签名中,将要作为key的参数名称
*/
String keyName();

/**
* key取值方式
*
* @return
*/
KeyType keyType() default KeyType.VALUE;

/**
* key 参数类型
*
* @return
*/
Class keyClass() default String.class;

/**
* 业务标识
* <p>
* 防止key重复
* <p>
* 如 订单锁 传入 ORDER 等等
*
* @return
*/
String businessFlag();

/**
* 锁自动释放时间,默认30s自动释放锁
*
* @return
*/
long lockTime() default 30L;

/**
* 若 KeyType = JSON, 通过 fieldName 作为 key 取 JSON 中的值,作为 LockKey
* <p>
* 若 KetType = ClASS, 通过获取对象的field,属性取值
*
* @return
*/
String fieldName() default "";


}

RedisLockAspect 使用AOP切面处理注解,实现功能

代码解释:

  1. 生成uuid作为requestId,意为当前线程的请求ID,将其作为value存入redis中,在释放锁时通过LUA脚本检查 requestId 是否一致,保证谁加的锁由谁来释放,确保锁唯一独有,解铃还须系铃人。

  2. 对Redis进行操作时,设置SET_IF_NOT_EXIST = NX, 保证当key不存在时,才进行set操作;key存在则不执行任何操作。

  3. 对Redis进行操作时,设置 SET_WITH_EXPIRE_TIME = PX,给当前key设置过期时间,保证不会出现死锁,过期时间从RedisLock注解中获取,默认最长30s,可自定义。

  4. 定义 MAX_RETRY_GET_LOCK 和 WAIT_LOCK_TIME ,控制获得锁重试的次数和每次等待的时间。

  5. LUA脚本解释:KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId;功能是先通过key获得value,再检查value和requestId是否一致,如何一致则删除当前key,也就是释放锁;不一致则表明加锁线程不是当前线程,不可以释放锁。使用LUA可以包保证上诉操作是原子性的。简单来说就是在jedis.eval()方法执行LUA脚本时,会将其作为一条命令执行,并且直到命令执行完毕,Redis才会执行下一条命令。

代码

package com.lsz.common.aspect;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.lsz.common.annotation.RedisLock;
import com.lsz.common.exception.BusinessError;
import com.lsz.common.exception.BusinessRuntimeException;
import com.lsz.common.utils.JoinPointUtils;
import com.lsz.common.utils.IdUtils;
import com.lsz.common.redis.JedisPoolUtil;
import org.apache.commons.lang.StringUtils;
import org.apache.logging.log4j.LogManager;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.util.Collections;


/**
* Redis锁 切面
*
* @author lishuzhen
* @date 2021/6/1 21:45
*/

@Aspect
@Component
public class RedisLockAspect {
private static org.apache.logging.log4j.Logger logger = LogManager.getLogger();

/**
* redis key
*/

private static final String KEY_PRE = "LOCK_";

/**
* 获取锁 最大重试次数
*/

private static final Integer MAX_RETRY_GET_LOCK = 3;

/**
* 等待锁的时间 5s
*/

private static final Long WAIT_LOCK_TIME = 5000L;

/**
* 当key不存在时,进行set操作
*/

private static final String SET_IF_NOT_EXIST = "NX";

/**
* 过期的设置
*/

private static final String SET_WITH_EXPIRE_TIME = "PX";

/**
* lua脚本 释放锁
*/

private static final String REDIS_DEL_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";


@Autowired
private JedisPoolUtil jedisPoolUtil;


/**
* redis 加锁切入点
*/

@Pointcut(value = "@annotation(com.lsz.common.annotation.RedisLock) && args(..)")
public void redisLockPointCut() {
}

/**
* 使用环绕通知,控制加锁和释放锁
*
* @param joinPoint
* @return
* @throws Throwable
*/

@Around("redisLockPointCut()")
public Object redisLockAction(ProceedingJoinPoint joinPoint) throws Throwable {
Object result = new Object();
String lockKey = "";
String requestId = IdUtils.uuid32();
try {
RedisLock redisLock = JoinPointUtils.getMethodAnnotation(joinPoint, RedisLock.class);
Object obj = JoinPointUtils.getParamByName(joinPoint, redisLock.keyName(), redisLock.keyClass());
lockKey = getRedisKey(redisLock, obj);

// 检查key是否可以加锁
if (checkingLock(lockKey))
{
// 一直被锁,不可以上锁
logger.info("目前此 lockKey => {} 一直被锁,无法获得锁,导致程序中断", lockKey);
// 锁替换为随机码,防止将原有的锁释放
lockKey = IdUtils.uuid();
throw new BusinessRuntimeException(BusinessError.LOCK_WAIT_TIME_OUT);
}

jedisPoolUtil.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, redisLock.lockTime() * 1000);
logger.info("此 lockKey => {} 已获得redis锁, requestId = {}", lockKey, requestId);

result = joinPoint.proceed();
} catch (Throwable e) {
if (e instanceof BusinessRuntimeException) {
freedLock(lockKey, requestId);
throw e;
}
logger.error("服务器异常", e);
} finally {
freedLock(lockKey, requestId);
}

return result;
}


/**
* 检查锁,尝试检查 MAX_RETRY_GET_LOCK 此
*
* @param lockKey
* @return 是否一直有锁
* @throws InterruptedException
*/

private Boolean checkingLock(String lockKey) throws InterruptedException {
Boolean isLock = true;
// 尝试 MAX_RETRY_GET_LOCK 次 检查锁
for (int count = 1; count <= MAX_RETRY_GET_LOCK; count++) {
isLock = isLock(lockKey);
if (isLock) {
logger.info("第 {} 检查,目前此 lockKey => {}已上锁,等待 {}ms 再次检查", count, lockKey, WAIT_LOCK_TIME);
Thread.sleep(WAIT_LOCK_TIME);
} else {
logger.info("第 {} 检查,目前此 lockKey => {}未锁定,可以加锁", count, lockKey);
return false;
}
}

return isLock;
}


/**
* 是否已加锁
*
* @param key
* @return
*/

private boolean isLock(String key) {
String lock = jedisPoolUtil.get(key);
if (StringUtils.isBlank(lock) || "null".equals(lock)) {
return false;
}
return true;
}

/**
* 释放锁
*
* @param key
*/

private void freedLock(String key, String requestId) {
jedisPoolUtil.eval(REDIS_DEL_LOCK_SCRIPT, Collections.singletonList(key), Collections.singletonList(requestId));
logger.info("释放redis锁,lockKey = {}, requestId = {}", key, requestId);
}



/**
* 根据key不同的参数类型,获取key值,并拼接返回 Redis Key
*
* @param redisLock
* @param obj
* @return
*/

private String getRedisKey(RedisLock redisLock, Object obj) throws NoSuchFieldException, IllegalAccessException {
StringBuffer key = new StringBuffer(KEY_PRE + redisLock.businessFlag() + "_");
if (RedisLock.KeyType.VALUE.equals(redisLock.keyType())) {
key.append(obj);
} else if (RedisLock.KeyType.JSON.equals(redisLock.keyType())) {
JSONObject json = JSONObject.parseObject(JSON.toJSONString(obj));
key.append(json.getString(redisLock.fieldName()));
} else if (RedisLock.KeyType.CLASS.equals(redisLock.keyType())) {
Field field = obj.getClass().getDeclaredField(redisLock.fieldName());
field.setAccessible(true);
key.append(field.get(obj));
}

return key.toString();
}



}

JoinPointUtils 操作Spring JoinPoint的工具类 。若是boot项目,无须配置,此工具类可直接使用。若是spring mvc项目,有可能会出现无法获取实现类方法签名等情况,请检查spring配置文件中是否有此配置  

<aop:aspectj-autoproxy proxy-target-class="true" />
package com.lsz.common.utils;

import org.apache.commons.lang3.ArrayUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.reflect.MethodSignature;

import java.lang.annotation.Annotation;

/**
* JoinPoint 工具类
*
* @author lishuzhen
* @date 2020/10/10 18:27
*/

public class JoinPointUtils {

/**
* 从 joinPoint 中 根据 参数名称 获取参数
*
* @param joinPoint
* @param paramName
* @return
* @author Lishuzhen
*/

public static <T> T getParamByName(JoinPoint joinPoint, String paramName, Class<T> clazz) {
Object[] args = joinPoint.getArgs();
MethodSignature methodSignature = getMethodSignature(joinPoint);
String[] parameterNames = methodSignature.getParameterNames();
int index = ArrayUtils.indexOf(parameterNames, paramName);

if (index < 0) {
return null;
}

Object obj = args[index];
if (clazz.isInstance(obj)) {
return clazz.cast(obj);
}

return (T) obj;
}


/**
* 从 joinPoint 获取 方法上的注解
*
* @param joinPoint
* @return
*/

public static <T extends Annotation> T getMethodAnnotation(JoinPoint joinPoint, Class<T> annotationClass) throws NoSuchMethodException {
return getMethodSignature(joinPoint).getMethod().getAnnotation(annotationClass);
}


/**
* 在 joinPoint 中获取 MethodSignature
*
* @param joinPoint
* @return
*/

public static MethodSignature getMethodSignature(JoinPoint joinPoint) {
return (MethodSignature) joinPoint.getSignature();
}
}

JedisPoolUtils Jedis工具类 ,这里就不在粘代码了,就是简单封装了一下获取连接和释放连接, 调用jedis方法直接透传。

Demo

手写Demo代码进行测试。

package com.lsz.common;

import com.lsz.common.annotation.RedisLock;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.Map;

/**
* RedisLock Demo
*
* @author lishuzhen
* @date 2021/6/1 22:07
*/

@ResponseBody
@RequestMapping("redisLock/test")
public class TestController {


@RedisLock(businessFlag = "payOrder", keyName = "orderNo")
@RequestMapping("testValue")
public Object testValue(String orderNo) {
System.out.println("Test Value This is my order " + orderNo);
return "ok";
}

@RedisLock(businessFlag = "payOrder", keyName = "order",
keyType = RedisLock.KeyType.CLASS,
keyClass = Order.class,
fieldName = "orderNo")

@RequestMapping("testBean")
public Object testBean(Order order) {
System.out.println("Test Bean This is my order " + order.getOrderNo());
return "ok";
}


@RedisLock(businessFlag = "payOrder", keyName = "paramMap",
keyType = RedisLock.KeyType.JSON,
fieldName = "orderNo")

@RequestMapping("testJson")
public Object testJson(Map<String, Object> paramMap) {
System.out.println("Test json, This is my order " + paramMap.get("orderNo"));
return "ok";
}

@RedisLock(businessFlag = "payOrder", keyName = "orderNo", lockTime = 20)
@RequestMapping("testRetry")
public Object testRetry(String orderNo) {
System.out.println("start pay order" + orderNo);
try {
// 模拟业务逻辑用时
Thread.sleep(6000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("end pay order" + orderNo);

return "ok";
}

}

自定义一bean,demo使用

package com.lsz.common;

import java.io.Serializable;

/**
* Demo bean
* @author lishuzhen
* @date 2021/6/1 22:11
*/

public class Order implements Serializable {
private static final long serialVersionUID = 6213874569224877786L;


private String orderNo;
private String userNo;

// ...

public String getOrderNo() {
return orderNo;
}

public void setOrderNo(String orderNo) {
this.orderNo = orderNo;
}

public String getUserNo() {
return userNo;
}

public void setUserNo(String userNo) {
this.userNo = userNo;
}
}

测试注解值取参方式,访问 testValue ,查看日志

2021-06-01 22:26:11.364 INFO  com.lsz.common.aspect.RedisLockAspect 134 checkingLock - 第 1 检查,目前此 lockKey => LOCK_payOrder_Test001未锁定,可以加锁
2021-06-01 22:26:11.382 INFO  com.lsz.common.aspect.RedisLockAspect 102 redisLockAction - 此 lockKey => LOCK_payOrder_Test001 已获得redis锁, requestId = ff36ccdf29e849cca5b5d565939f7c09
Test Value This is my order Test001
2021-06-01 22:26:11.416 INFO  com.lsz.common.aspect.RedisLockAspect 163 freedLock - 释放redis锁,lockKey = LOCK_payOrder_Test001, requestId = ff36ccdf29e849cca5b5d565939f7c09

测试注解对象取参方式,访问 testBean

2021-06-01 22:28:09.936 INFO  com.lsz.common.aspect.RedisLockAspect 134 checkingLock - 第 1 检查,目前此 lockKey => LOCK_payOrder_Test002未锁定,可以加锁
2021-06-01 22:28:09.960 INFO  com.lsz.common.aspect.RedisLockAspect 102 redisLockAction - 此 lockKey => LOCK_payOrder_Test002 已获得redis锁, requestId = db2189b9702349b889b334027ce4e6d3
Test Bean This is my order Test002
2021-06-01 22:28:09.984 INFO  com.lsz.common.aspect.RedisLockAspect 163 freedLock - 释放redis锁,lockKey = LOCK_payOrder_Test002, requestId = db2189b9702349b889b334027ce4e6d3

测试注解JSON/Map取参方式,访问TestJson

2021-06-01 22:31:25.791 INFO  com.lsz.common.aspect.RedisLockAspect 134 checkingLock - 第 1 检查,目前此 lockKey => LOCK_payOrder_Test003未锁定,可以加锁
2021-06-01 22:31:25.812 INFO  com.lsz.common.aspect.RedisLockAspect 102 redisLockAction - 此 lockKey => LOCK_payOrder_Test003 已获得redis锁, requestId = 6d142ded58bb46cb868a72dc0f174a4f
Test json, This is my order Test003
2021-06-01 22:31:25.846 INFO  com.lsz.common.aspect.RedisLockAspect 163 freedLock - 释放redis锁,lockKey = LOCK_payOrder_Test003, requestId = 6d142ded58bb46cb868a72dc0f174a4f

测试重试获得锁,两个客户端访问 testRetry

2021-06-01 23:02:04.558 INFO  com.lsz.common.aspect.RedisLockAspect 134 checkingLock - 第 1 检查,目前此 lockKey => LOCK_payOrder_Test003未锁定,可以加锁
2021-06-01 23:02:04.587 INFO  com.lsz.common.aspect.RedisLockAspect 102 redisLockAction - 此 lockKey => LOCK_payOrder_Test003 已获得redis锁, requestId = 89a4b362896d4f64a8ff9191d27a953d
start pay orderTest003
2021-06-01 23:02:05.897 INFO  com.lsz.common.aspect.RedisLockAspect 131 checkingLock - 第 1 检查,目前此 lockKey => LOCK_payOrder_Test003已上锁,等待 5000ms 再次检查
end pay orderTest003
2021-06-01 23:02:10.646 INFO  com.lsz.common.aspect.RedisLockAspect 163 freedLock - 释放redis锁,lockKey = LOCK_payOrder_Test003, requestId = 89a4b362896d4f64a8ff9191d27a953d
2021-06-01 23:02:10.924 INFO  com.lsz.common.aspect.RedisLockAspect 134 checkingLock - 第 2 检查,目前此 lockKey => LOCK_payOrder_Test003未锁定,可以加锁
2021-06-01 23:02:10.948 INFO  com.lsz.common.aspect.RedisLockAspect 102 redisLockAction - 此 lockKey => LOCK_payOrder_Test003 已获得redis锁, requestId = 51057fa3168d452d97f4f36e9078ebb7
start pay orderTest003
end pay orderTest003
2021-06-01 23:02:16.986 INFO  com.lsz.common.aspect.RedisLockAspect 163 freedLock - 释放redis锁,lockKey = LOCK_payOrder_Test003, requestId = 51057fa3168d452d97f4f36e9078ebb7

总结:

以上记录博主是在用Redis做分布式锁时考虑到问题及解决方案,实现了博客开头提及的几个优点,目前满足大部分的业务场景。同时对业务代码零侵入,开发人员无需关心“锁”问题,一个注解轻松解决。