分布式锁工具类

业务背景:

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

相关推荐
n***84071 小时前
Spring Boot(快速上手)
java·spring boot·后端
Wgrape1 小时前
一文了解常见AI搜索方案的代码实现
人工智能·后端
小兔崽子去哪了1 小时前
Docker部署ZLMediaKit流媒体服务器并自定义配置指南
java·后端·容器
程序猿小蒜1 小时前
基于springboot的人口老龄化社区服务与管理平台
java·前端·spring boot·后端·spring
aiopencode1 小时前
iOS 开发者工具推荐,构建从调试到性能优化的多维度生产力工具链(2025 深度工程向)
后端
iOS开发上架哦2 小时前
iOS APP 抓包全流程解析,HTTPS 调试、网络协议分析与多工具组合方案
后端
用户69371750013842 小时前
6.Kotlin 流程控制:循环控制:while 与 do/while
android·后端·kotlin
文心快码BaiduComate2 小时前
下周感恩节!文心快码助力感恩节抽奖页快速开发
前端·后端·程序员
_小九2 小时前
【开源】耗时数月、我开发了一款功能全面的AI图床
前端·后端·图片资源