业务背景:
在多节点部署、事件回调或定时任务并发执行的场景中,需要同一份业务数据只被处理一次,否则会导致重复推送、资源竞争或脏数据。传统 synchronized 或数据库锁难以满足跨进程、跨机器的互斥需求。通常的做法都是使用Redisson 加一个分布式锁。
每次加分布式锁要写一堆代码,特别的麻烦,而且由于不断有新人进组,导致加分布式锁的代码并不规范,会导致加锁的异常处理存在安全风险,因此就封装了一个工具类给项目组成员使用,通过这个工具类,一来可以避免每次加锁写很多的代码,二来通过工具类可以保证加锁的安全性。
技术设计
1、分布式锁还是使用Redisson来作为核心技术组件;
2、由于加锁时有一些参数需要调用方传入,如果放在方法里面作为参数会导致参数过多,因此封装了一个LockCallOptions类作为参数信息类,并为参数提供默认值,大部分情况下只需要使用默认值即可。这些参数信息有:
go
`lockKey`:加锁的键值;
`lockSeconds`:锁过期时间,默认 5 秒,可自定义(需在 1~1000 秒区间)。
`errorLogMsg`:加锁和业务执行时如果抛出异常会打印日志,这个参数表示打印的日志信息内容。
`throwWhenLockFail`:未抢到锁时是否抛异常,默认 true。
`throwMsgWhenLockFail`:未抢到锁时如果抛出异常,内容就是这个参数。
LockCallOptions再实现一个checkParams方法检查参数值的合法性。
3、业务方法可以使用java的函数来实现,比如Function等,但由于业务方法需要传入的参数可能不同,比如有的不传参数,有的一个参数,有的两个参数,因此可以实现多个方法,分别用Supplier、Function、BiFunction来实现。
如果有更多参数怎么办,第一种办法是继续函数式接口,然后再实现一个方法,比如针对三个参数的可以定义这个函数式接口:
csharp
@FunctionalInterface
public interface ThreeFunction<T, U, V, R> {
R apply(T t, U u, V v);
}
还有一种方式是可以定义一个map,将所有的参数都推入这个map,然后在工具类方法中从map中取参数;
关键代码
csharp
private static RedissonClient getRedissonClient() {
RedissonClient result = SpringContextHolder.getBean(RedissonClient.class);
if (Objects.isNull(result)) {
throw new RuntimeException("获取分布式锁失败");
}
return result;
}
public static <R> R callWithLock(LockCallOptions options, Supplier<R> bizFunc) {
//参数检查
options.check();
RLock lock = null;
boolean locked = false;
try {
lock = getRedissonClient().getLock(options.getLockKey());
locked = lock.tryLock(options.getLockSeconds(), TimeUnit.SECONDS);
if (!locked) {
//没获取到锁则丢弃
log.error("获取分布式锁失败,键值key: {}", options.getLockKey());
if (options.getThrowWhenLockFail()) {
throw new RuntimeException(options.getThrowMsgWhenLockFail());
}
return null;
}
//业务处理
return bizFunc.get();
} catch (Exception ex) {
log.error("{}: 异常信息:{}", options.getErrorLogMsg(), ex.getMessage(), ex);
return null;
} finally {
if (locked && lock != null && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
使用示例
java
String lockKey = String.format("lcok:%s:%s", type, userId);
LockCallOptions callOptions = LockCallOptions.builder()
.lockKey(lockKey)
.errorLogMsg("业务处理异常")
.throwWhenLockFail(false)
.build();
LockUtils.callWithLock(callOptions, () -> handler.process(requestData));
注意:
LockCallOptions的定义如下:
java
@Data
@Builder
public class LockCallOptions {
// 锁的键值
private String lockKey;
//锁的秒数
private long lockSeconds = 5;
//加锁异常时记录的错误日志信息
private String errorLogMsg = "加锁执行方法异常";
// 加锁失败时是否抛异常
private Boolean throwWhenLockFail = true;
// 加锁失败时抛异常信息内容(throwWhenLockFail = true时才会使用这个值)
private String throwMsgWhenLockFail = "请稍后重试";
}
由于使用了@Builder注解,在使用LockCallOptions.builder().build()时发现创建的LockCallOptions对象并没有上面定义时指定的默认值,要解决这个办法就是在有默认值的属性定义上加上注解:@Builder.Default即可解决。