分布式锁工具类

业务背景:

在多节点部署、事件回调或定时任务并发执行的场景中,需要同一份业务数据只被处理一次,否则会导致重复推送、资源竞争或脏数据。传统 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即可解决。

相关推荐
真上帝的左手2 分钟前
15. 实时数据-SpringBoot集成WebSocket
spring boot·后端·websocket
han_hanker4 分钟前
springboot 封装的比较好的 统一的返回类型 工具类
java·spring boot·后端
韩立学长4 分钟前
基于Springboot流浪动物救助系统cqy142wz(程序、源码、数据库、调试部署方案及开发环境)系统界面展示及获取方式置于文档末尾,可供参考。
数据库·spring boot·后端
怪只怪满眼尽是人间烟火5 分钟前
springboot数据上链FISCO BCOS
java·spring boot·后端
她说..8 分钟前
Spring AOP场景5——异常处理(附带源码)
java·数据库·后端·spring·springboot·spring aop
BingoGo15 分钟前
PHP 值对象实战指南:避免原始类型偏执
后端·php
JaguarJack15 分钟前
PHP 值对象实战指南:避免原始类型偏执
后端·php
IT_陈寒25 分钟前
SpringBoot3.0性能优化:这5个冷门配置让我节省了40%内存占用
前端·人工智能·后端
填满你的记忆30 分钟前
Gemini使用教程
java·后端·ai编程
源代码•宸32 分钟前
goframe框架签到系统项目(安装 redis )
服务器·数据库·经验分享·redis·后端·缓存·golang