一、引言
在项目中使用博主自己封装的分布式代理锁已经很久了,慢慢发展已经比较完善通用,现在开源共享。
github地址:
GitHub - SongTing0711/distributed-proxy-lock
gitee地址:distributed-proxy-lock: 分布式代理锁,动态的锁后缀采用ThreadLocal或者参数名获取,锁粒度自定义选择
相比较博主之前在Redis分布式代理锁的两种实现_tingmailang的博客-CSDN博客封装的工具,开源的工具除了支持redisson之外还支持spring redis加锁,通过参数获取锁后缀的方式也更加灵活,可以看出工具演变的过程。
二、代码解析
1、注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DistributedProxyLock {
String key();
long waitOut() default 60;
long executeOut() default 60;
TimeUnit timeUnit() default TimeUnit.SECONDS;
boolean atuoRemove() default true;
String suffixKeyTypeEnum() default DistributedProxyLockCommonUtil.NO_SUFFIX;
String objectName() default "";
String paramName() default "";
String lockConnectionEnum() default DistributedProxyLockCommonUtil.REDISSON;
}
2、连接工具枚举
@Getter
public enum LockConnectionEnum {
REDISSON("redisson", "使用redisson"),
SPRING_REDIS("spring_redis", "使用springredis"),
;
private final String code;
private final String desc;
LockConnectionEnum(String code, String desc) {
this.code = code;
this.desc = desc;
}
@JsonCreator
public static LockConnectionEnum of(String key) {
Optional<LockConnectionEnum> assetStatusEnum = Arrays.stream(LockConnectionEnum.values())
.filter(c -> c.getCode().equals(key)).findFirst();
return assetStatusEnum.orElse(null);
}
}
3、锁后缀获取方式枚举
三种方式获取锁后缀
@Getter
public enum DistributedProxyLockSuffixKeyTypeEnum {
NO_SUFFIX("no_suffix", "没有后缀"),
THREAD_LOCAL("thread_local", "通过ThreadLocal获取后缀"),
PARAM("param", "通过参数获取后缀"),
;
private final String code;
private final String desc;
DistributedProxyLockSuffixKeyTypeEnum(String code, String desc) {
this.code = code;
this.desc = desc;
}
@JsonCreator
public static DistributedProxyLockSuffixKeyTypeEnum of(String key) {
Optional<DistributedProxyLockSuffixKeyTypeEnum> assetStatusEnum = Arrays.stream(DistributedProxyLockSuffixKeyTypeEnum.values())
.filter(c -> c.getCode().equals(key)).findFirst();
return assetStatusEnum.orElse(null);
}
}
4、ThreadLocal工具类
public class DistributedProxyLockUtil {
static ThreadLocal<String> LOCK_KEY = new ThreadLocal<String>();
public static void set(String key) {
LOCK_KEY.set(key);
}
public static String get() {
return LOCK_KEY.get();
}
public static void remove() {
LOCK_KEY.remove();
}
}
5、共用类
public class DistributedProxyLockCommonUtil {
public static final String REDISSON = "redisson";
public static final String SPRING_REDIS = "spring_redis";
public static final String NO_SUFFIX = "no_suffix";
public static final String THREAD_LOCAL = "thread_local";
public static final String PARAM = "param";
public static final String GET = "get";
public static Object getFieldValueByName(String fieldName, Object o) {
try {
String firstLetter = fieldName.substring(0, 1).toUpperCase();
String getter = GET + firstLetter + fieldName.substring(1);
Method method = o.getClass().getMethod(getter, new Class[]{});
Object value = method.invoke(o, new Object[]{});
return value;
} catch (Exception e) {
throw new DistributedProxyLockException("获取属性值失败" + e);
}
}
}
6、异常类
public class DistributedProxyLockException extends RuntimeException {
public DistributedProxyLockException() {
super();
}
public DistributedProxyLockException(String message) {
super("DistributedProxyLockException:" + message);
}
public DistributedProxyLockException(String message, Throwable cause) {
super("DistributedProxyLockException:" + message, cause);
}
public DistributedProxyLockException(Throwable cause) {
super(cause);
}
}
7、切面
根据使用者在注解设置的后缀获取方式进行锁拼接,默认无后缀
根据使用者设置的redis链接工具进行加锁,默认redisson
@Aspect
@Component
public class DistributedProxyLockAspect {
@Resource
private DistributedProxyLockService distributedProxyLockService;
@Pointcut("@annotation(com.annotation.DistributedProxyLock)")
public void lockPointCut() {
}
@Around("lockPointCut() && @annotation(distributedProxyLock)")
public Object around(ProceedingJoinPoint joinPoint, DistributedProxyLock distributedProxyLock) throws Throwable {
String lockKey;
try {
DistributedProxyLockSuffixKeyTypeEnum suffixKeyTypeEnum = DistributedProxyLockSuffixKeyTypeEnum.of(distributedProxyLock.suffixKeyTypeEnum());
switch (suffixKeyTypeEnum) {
case PARAM:
lockKey = distributedProxyLockService.getKeyWithParam(joinPoint, distributedProxyLock);
break;
case NO_SUFFIX:
lockKey = distributedProxyLock.key();
break;
case THREAD_LOCAL:
lockKey = distributedProxyLockService.getKeyWithThreadLocal(joinPoint, distributedProxyLock);
break;
default:
throw new DistributedProxyLockException("未知后缀获取类型" + distributedProxyLock.suffixKeyTypeEnum());
}
} catch (Exception e) {
throw new DistributedProxyLockException("获取后缀key失败" + e);
}
LockConnectionEnum lockConnectionEnum = LockConnectionEnum.of(distributedProxyLock.lockConnectionEnum());
switch (lockConnectionEnum) {
case REDISSON:
return distributedProxyLockService.lockByRedisson(lockKey, joinPoint, distributedProxyLock);
case SPRING_REDIS:
return distributedProxyLockService.lockBySpringRedis(lockKey, joinPoint, distributedProxyLock);
default:
throw new DistributedProxyLockException("未知redis工具" + distributedProxyLock.lockConnectionEnum());
}
}
}
8、代理锁业务处理
public interface DistributedProxyLockService {
String getKeyWithThreadLocal(ProceedingJoinPoint joinPoint, DistributedProxyLock distributedProxyLock);
String getKeyWithParam(ProceedingJoinPoint joinPoint, DistributedProxyLock distributedProxyLock);
Object lockByRedisson(String lockKey, ProceedingJoinPoint joinPoint, DistributedProxyLock distributedProxyLock) throws Throwable;
Object lockBySpringRedis(String lockKey, ProceedingJoinPoint joinPoint, DistributedProxyLock distributedProxyLock) throws Throwable;
}
@Service
@Slf4j
public class DistributedProxyLockKeyServiceImpl implements DistributedProxyLockService {
@Resource
private RedissonClient redissonClient;
@Resource
private RedisTemplate redisTemplate;
@Override
public String getKeyWithThreadLocal(ProceedingJoinPoint joinPoint, DistributedProxyLock distributedProxyLock) {
String lockKey = distributedProxyLock.key();
if (StringUtil.isBlank(DistributedProxyLockUtil.get())) {
throw new DistributedProxyLockException("DistributedProxyLockUtil为空");
}
return lockKey;
}
@Override
public String getKeyWithParam(ProceedingJoinPoint joinPoint, DistributedProxyLock distributedProxyLock) {
String objectName = distributedProxyLock.objectName();
if (StringUtil.isBlank(objectName)) {
throw new DistributedProxyLockException("objectName为空");
}
String paramName = distributedProxyLock.paramName();
Object[] args = joinPoint.getArgs();
String[] objectNames = ((CodeSignature) joinPoint.getSignature()).getParameterNames();
Map<String, Object> objectHashMap = new HashMap<>();
for (int i = 0; i < objectNames.length; i++) {
objectHashMap.put(objectNames[i], args[i]);
}
if (!objectHashMap.containsKey(objectName)) {
throw new DistributedProxyLockException("入参不包含该对象" + objectName);
}
Object o = objectHashMap.get(objectName);
if (StringUtil.isBlank(paramName)) {
return distributedProxyLock.key() + o.toString();
}
String lockKey = distributedProxyLock.key() + DistributedProxyLockCommonUtil.getFieldValueByName(paramName, o);
return lockKey;
}
public Object lockBySpringRedis(String lockKey, ProceedingJoinPoint joinPoint, DistributedProxyLock distributedProxyLock) throws Throwable {
try {
if (redisTemplate.opsForValue().setIfAbsent(lockKey, lockKey)) {
redisTemplate.expire(lockKey, distributedProxyLock.executeOut(), distributedProxyLock.timeUnit());
log.debug("代理加锁成功:{}", lockKey);
return joinPoint.proceed();
} else {
log.debug("代理加锁失败:{}", lockKey);
}
} catch (InterruptedException e) {
log.error("获取代理锁异常:{}", e);
throw e;
} finally {
redisTemplate.delete(lockKey);
}
return null;
}
public Object lockByRedisson(String lockKey, ProceedingJoinPoint joinPoint, DistributedProxyLock distributedProxyLock) throws Throwable {
RLock lock = redissonClient.getLock(lockKey);
try {
if (lock.tryLock(distributedProxyLock.waitOut(), distributedProxyLock.executeOut(), distributedProxyLock.timeUnit())) {
log.debug("代理加锁成功:{}", lockKey);
return joinPoint.proceed();
} else {
log.debug("代理加锁失败:{}", lockKey);
}
} catch (InterruptedException e) {
log.error("获取代理锁异常:{}", e);
throw e;
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
log.debug("代理解锁:{}", lockKey);
}
//如果方法注解中开启自动清除,就去除
if (distributedProxyLock.atuoRemove()) {
DistributedProxyLockUtil.remove();
log.debug("自动清除DistributedProxyLockUtil:{}", lockKey);
}
}
return null;
}
}
三、依赖pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.7</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.ting</groupId>
<artifactId>redis-lock</artifactId>
<version>1.0.0</version>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 包含自动配置的代码-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<!-- 配置文件点击可以跳转实体-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.10</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>2.15.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
四、使用
1. 无后缀(redisson加锁)
@DistributedProxyLock(key = "SHOP_LOCK_KEY")
public void test(ShopChainDTO shopChainDTO) {
for (int i = 0; i < 6; i++) {
log.info("测试加锁:{}", DistributedProxyLockUtil.get() + i);
}
}
2. 参数中获取(redisson加锁)
①从某个入参对象的某个参数获取
@DistributedProxyLock(key = "SHOP_LOCK_KEY",
suffixKeyTypeEnum = DistributedProxyLockCommonUtil.PARAM,
objectName = "shopChainDTO",
paramName = "shopId")
public void test(ShopChainDTO shopChainDTO) {
for (int i = 0; i < 6; i++) {
log.info("测试加锁:{}", DistributedProxyLockUtil.get() + i);
}
}
②从某个入参对象获取
@DistributedProxyLock(key = "SHOP_LOCK_KEY",
suffixKeyTypeEnum = DistributedProxyLockCommonUtil.PARAM,
objectName = "shopId")
public void test(LocalDateTime onlineTime, String shopId) {
for (int i = 0; i < 6; i++) {
log.info("测试加锁:{}", DistributedProxyLockUtil.get() + i);
}
}
3. 使用ThreadLocal获取(redisson加锁)
@Slf4j
@Service
public class ShopServiceImpl implements ShopService {
@DistributedProxyLock(key = "SHOP_LOCK_KEY",
suffixKeyTypeEnum = DistributedProxyLockCommonUtil.THREAD_LOCAL)
public void test(ShopChainDTO shopChainDTO) {
for (int i = 0; i < 6; i++) {
log.info("测试加锁:{}", DistributedProxyLockUtil.get() + i);
}
}
}
@RestController
@RequestMapping("shop")
public class ShopController {
@Resource
private ShopService shopService;
@PostMapping("/online")
@MethodLogger
public void run(@RequestBody @Validated ShopChainDTO dto) {
DistributedProxyLockUtil.set(dto.getShopId());
shopService.test(dto);
}
}
4. 无后缀(Spring redis加锁)
@DistributedProxyLock(key = "SHOP_LOCK_KEY",
lockConnectionEnum = DistributedProxyLockCommonUtil.SPRING_REDIS)
public void test(ShopChainDTO shopChainDTO) {
for (int i = 0; i < 6; i++) {
log.info("测试加锁:{}", DistributedProxyLockUtil.get() + i);
}
}
5. 参数中获取(Spring redis加锁)
①从某个入参对象的某个参数获取
@DistributedProxyLock(key = "SHOP_LOCK_KEY",
suffixKeyTypeEnum = DistributedProxyLockCommonUtil.PARAM,
objectName = "shopChainDTO",
paramName = "shopId",
lockConnectionEnum = DistributedProxyLockCommonUtil.SPRING_REDIS)
public void test(ShopChainDTO shopChainDTO) {
for (int i = 0; i < 6; i++) {
log.info("测试加锁:{}", DistributedProxyLockUtil.get() + i);
}
}
②从某个入参对象获取
@DistributedProxyLock(key = "SHOP_LOCK_KEY",
suffixKeyTypeEnum = DistributedProxyLockCommonUtil.PARAM,
objectName = "shopId",
lockConnectionEnum = DistributedProxyLockCommonUtil.SPRING_REDIS)
public void test(LocalDateTime onlineTime, String shopId) {
for (int i = 0; i < 6; i++) {
log.info("测试加锁:{}", DistributedProxyLockUtil.get() + i);
}
}
6. 使用ThreadLocal获取(Spring redis加锁)
@Slf4j
@Service
public class ShopServiceImpl implements ShopService {
@DistributedProxyLock(key = "SHOP_LOCK_KEY",
suffixKeyTypeEnum = DistributedProxyLockCommonUtil.THREAD_LOCAL,
lockConnectionEnum = DistributedProxyLockCommonUtil.SPRING_REDIS)
public void test(ShopChainDTO shopChainDTO) {
for (int i = 0; i < 6; i++) {
log.info("测试加锁:{}", DistributedProxyLockUtil.get() + i);
}
}
}
@RestController
@RequestMapping("shop")
public class ShopController {
@Resource
private ShopService shopService;
@PostMapping("/online")
@MethodLogger
public void run(@RequestBody @Validated ShopChainDTO dto) {
DistributedProxyLockUtil.set(dto.getShopId());
shopService.test(dto);
}
}
五、总结
使用工具之后基本上加锁只需要一个注解就可以。
考虑到通用性redislock支持了多种redis链接工具,如果有其他常用的链接工具,同学们不妨提个issue,博主再去封装。
同时分布式锁除了通过redis实现,还有zk等其他工具可以实现,但是博主这里暂时没有使用,有兴趣的同学可以加入这个工具的完善。