Spring Boot Starter实现基于 Redis + Lua 的分布式锁封装与实践


一、涉及的技术知识点

1.1 Redis 分布式锁核心原理

知识点说明
Redis 单线程模型保证命令按序执行,天然支持原子操作
SETNX(SET if Not eXists)只有 key 不存在时才设置成功,是分布式锁的基础
PEXPIRE设置 key 毫秒级过期时间,防止死锁
HSET/HGET用 Hash 结构存储锁持有者标识(requestId),支持安全释放
Lua 脚本多个 Redis 命令在服务端原子执行,保证加锁/解锁操作的原子性
锁超时自动释放持有者崩溃时锁不会永久占用
可重入防护通过 requestId 标识持有者,只有持有者能释放锁

1.2 Spring Boot 自动配置机制

知识点说明
spring.factoriesSpring Boot SPI 加载机制,声明自动配置类
@Configuration + @Bean声明式注册 Spring Bean
@Autowired / @Resource自动注入 StringRedisTemplate
条件装配可配合 @ConditionalOnClass 实现按需加载

1.3 Java 语言特性

知识点说明
Closeable 接口实现自动资源管理,支持 try-with-resources
Lambda 表达式RedisCallback 用 Lambda 简化回调写法
自旋等待 + 超时退出tryLock 内循环重试直到超时
包级访问控制构造器和 setter 使用包级权限,只允许通过 Provider 创建

1.4 设计模式

模式应用
工厂模式DistributedLockProvider 作为工厂创建 DistributedLock 实例
模板方法StringRedisTemplate.execute(RedisCallback) 统一连接管理
策略模式不同 TimeUnit + leaseTime 组合适应不同场景

注:

博客:

https://blog.csdn.net/badao_liumang_qizhi

二、封装 Spring Boot Starter JAR 包的流程

2.1 项目结构

xxx-xxx-lib-lock/
├── pom.xml
└── src/main/
    ├── java/xx/xx/lib/lock/
    │   ├── LockConfig.java                 // 自动配置类
    │   ├── DistributedLockProvider.java    // 锁工厂(对外暴露)
    │   └── DistributedLock.java            // 锁实现(核心逻辑)
    └── resources/META-INF/
        └── spring.factories                // 自动配置声明

2.2 封装步骤

  1. 定义核心接口/类 — 确定对外暴露的 API(Provider + Lock)
  2. 实现核心逻辑 — Redis Lua 脚本加锁解锁
  3. 编写自动配置 — @Configuration 类注册 Bean
  4. 声明 spring.factories — 让 Spring Boot 自动发现配置类
  5. Maven 打包发布 — mvn deploy 到私有仓库

2.3 引入方使用流程

  1. pom.xml 添加依赖
  2. 确保项目已配置 spring.redis(StringRedisTemplate 可用)
  3. 注入 DistributedLockProvider 直接使用
  4. 无需任何额外配置或注解开启

三、通用示例代码

3.1 Starter 封装侧

pom.xml
<project>
    <groupId>com.example</groupId>
    <artifactId>example-distributed-lock-starter</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <scope>provided</scope>
        </dependency>
    </dependencies>
</project>
DistributedLock.java
package com.example.lock;

import java.io.Closeable;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.StringRedisTemplate;

/**
 * 基于Redis的分布式锁实现.
 * 支持自旋等待、超时释放、安全解锁.
 */
public class DistributedLock implements Closeable {

    private static final Logger log = LoggerFactory.getLogger(DistributedLock.class);
    private static final String LOCK_PREFIX = "app-locks:{%s}";
    private static final String REQUEST_ID_KEY = "rid-{%s}";

    /** 加锁Lua脚本:key不存在时设置hash并设过期时间. */
    private static final String LOCK_SCRIPT =
        "if (redis.call('exists', KEYS[1]) == 0) then "
            + "redis.call('hset', KEYS[1], KEYS[2], ARGV[1]); "
            + "redis.call('pexpire', KEYS[1], ARGV[2]); "
            + "return 1; "
            + "else return 0; end";

    /** 解锁Lua脚本:校验requestId匹配后删除key. */
    private static final String UNLOCK_SCRIPT =
        "if redis.call('hget', KEYS[1], KEYS[2]) == ARGV[1] then "
            + "redis.call('del', KEYS[1]); return 1; "
            + "else return 0; end;";

    private final StringRedisTemplate redisTemplate;
    private final String key;
    private final String lockKey;
    private final String requestIdKey;
    private final String requestId;
    private TimeUnit leaseTimeUnit = TimeUnit.MINUTES;
    private long leaseTime = 10;
    private boolean locked = false;

    DistributedLock(StringRedisTemplate redisTemplate, String key, String requestId) {
        this.redisTemplate = redisTemplate;
        this.key = key;
        this.requestId = (requestId == null || requestId.isEmpty())
            ? java.util.UUID.randomUUID().toString().replace("-", "")
            : requestId;
        this.lockKey = String.format(LOCK_PREFIX, key);
        this.requestIdKey = String.format(REQUEST_ID_KEY, key);
    }

    void setLeaseTimeUnit(TimeUnit leaseTimeUnit) {
        if (leaseTimeUnit != null) {
            this.leaseTimeUnit = leaseTimeUnit;
        }
    }

    void setLeaseTime(long leaseTime) {
        if (leaseTime > 0) {
            this.leaseTime = leaseTime;
        }
    }

    /**
     * 尝试获取锁,支持超时等待.
     *
     * @param waitTimeUnit 等待时间单位
     * @param waitTime     最大等待时间
     * @return true-获取成功,false-超时未获取到
     */
    public boolean tryLock(TimeUnit waitTimeUnit, long waitTime) {
        Boolean result = redisTemplate.execute(connection -> {
            long waitMillis = waitTimeUnit.toMillis(waitTime);
            long startTime = System.currentTimeMillis();
            long deadline = startTime + waitMillis;

            // 第一次尝试
            if (doLock(connection)) {
                return Boolean.TRUE;
            }

            // 自旋重试直到超时
            while (System.currentTimeMillis() < deadline) {
                if (doLock(connection)) {
                    return Boolean.TRUE;
                }
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    log.error("获取锁等待时被中断", e);
                    Thread.currentThread().interrupt();
                    return Boolean.FALSE;
                }
            }
            return Boolean.FALSE;
        });

        this.locked = Boolean.TRUE.equals(result);
        log.debug("获取锁结果 - {} - {}", key, locked);
        return this.locked;
    }

    /**
     * 释放锁.
     *
     * @return true-释放成功,false-释放失败(非持有者或已过期)
     */
    public boolean unlock() {
        if (!locked) {
            log.info("未锁,无需操作 - {}", key);
            return true;
        }
        log.info("准备释放锁 - {}", key);
        Boolean result = redisTemplate.execute(connection -> {
            long expireMillis = leaseTimeUnit.toMillis(leaseTime);
            Long evalResult = (Long) connection.eval(
                UNLOCK_SCRIPT.getBytes(),
                org.springframework.data.redis.connection.ReturnType.INTEGER,
                2,
                lockKey.getBytes(),
                requestIdKey.getBytes(),
                requestId.getBytes()
            );
            log.info("释放锁数量 - {} - {} - {}", requestId, key, evalResult);
            return evalResult != null && evalResult != 0 ? Boolean.TRUE : Boolean.FALSE;
        });

        this.locked = !Boolean.TRUE.equals(result);
        return Boolean.TRUE.equals(result);
    }

    @Override
    public void close() {
        unlock();
    }

    private boolean doLock(org.springframework.data.redis.connection.RedisConnection connection) {
        long expireMillis = leaseTimeUnit.toMillis(leaseTime);
        Long evalResult = (Long) connection.eval(
            LOCK_SCRIPT.getBytes(),
            org.springframework.data.redis.connection.ReturnType.INTEGER,
            2,
            lockKey.getBytes(),
            requestIdKey.getBytes(),
            requestId.getBytes(),
            String.valueOf(expireMillis).getBytes()
        );
        return evalResult != null && evalResult != 0;
    }
}
DistributedLockProvider.java
package com.example.lock;

import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;

/**
 * 分布式锁工厂.
 * 通过注入此类获取锁实例.
 */
public class DistributedLockProvider {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 获取分布式锁(默认过期10分钟).
     *
     * @param key 业务锁标识
     * @return 锁实例
     */
    public DistributedLock getLock(String key) {
        return new DistributedLock(redisTemplate, key, getCurrentRequestId());
    }

    /**
     * 获取分布式锁(自定义过期时间).
     *
     * @param key           业务锁标识
     * @param leaseTimeUnit 过期时间单位
     * @param leaseTime     过期时间值
     * @return 锁实例
     */
    public DistributedLock getLock(String key, TimeUnit leaseTimeUnit, long leaseTime) {
        DistributedLock lock = new DistributedLock(redisTemplate, key, getCurrentRequestId());
        lock.setLeaseTime(leaseTime);
        lock.setLeaseTimeUnit(leaseTimeUnit);
        return lock;
    }

    private String getCurrentRequestId() {
        // 从链路追踪上下文获取requestId,保证同一请求内的锁标识一致
        // 实际项目中可替换为 MDC.get("requestId") 或自定义实现
        return java.util.UUID.randomUUID().toString().replace("-", "");
    }
}
LockConfig.java
package com.example.lock;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 分布式锁自动配置类.
 */
@Configuration
public class LockConfig {

    @Bean
    public DistributedLockProvider distributedLockProvider() {
        return new DistributedLockProvider();
    }
}
META-INF/spring.factories
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.lock.LockConfig

3.2 引入方使用侧

pom.xml 添加依赖
<dependency>
    <groupId>com.example</groupId>
    <artifactId>example-distributed-lock-starter</artifactId>
    <version>1.0.0</version>
</dependency>
application.yml(确保 Redis 已配置)
spring:
  redis:
    host: localhost
    port: 6379
    password: your_password
    database: 0
使用示例1:try-with-resources 自动释放(最佳实践)
@Service
public class OrderService {

    @Resource
    private DistributedLockProvider distributedLockProvider;

    public void processOrder(String orderId) {
        String lockKey = "order_process_" + orderId;
        // 锁过期5分钟,等待获取锁最多3分钟
        try (DistributedLock lock = distributedLockProvider.getLock(
                lockKey, TimeUnit.MINUTES, 5)) {
            if (lock.tryLock(TimeUnit.MINUTES, 3)) {
                // 获取锁成功,安全执行业务逻辑
                doProcessOrder(orderId);
            } else {
                // 超时未获取到锁
                throw new RuntimeException("订单正在处理中,请勿重复操作");
            }
        }
        // try 块结束自动调用 close() -> unlock()
    }
}
使用示例2:非阻塞立即返回
public void submitTask(String taskId) {
    String lockKey = "task_submit_" + taskId;
    try (DistributedLock lock = distributedLockProvider.getLock(lockKey)) {
        // 等待时间为0,立即返回结果
        if (lock.tryLock(TimeUnit.MILLISECONDS, 0)) {
            executeTask(taskId);
        } else {
            throw new RuntimeException("任务已在执行中");
        }
    }
}
使用示例3:事务提交后释放锁
@Transactional
public void updateStock(Integer itemId) {
    String key = "stock_update_" + itemId;
    DistributedLock lock = distributedLockProvider.getLock(key, TimeUnit.MINUTES, 3);
    if (!lock.tryLock(TimeUnit.MINUTES, 1)) {
        throw new RuntimeException("库存操作冲突");
    }
    // 在事务提交后再释放锁,保证其他线程读到的是已提交数据
    TransactionSynchronizationManager.registerSynchronization(
        new TransactionSynchronization() {
            @Override
            public void afterCommit() {
                lock.unlock();
            }
        });
    // 执行库存扣减
    doUpdateStock(itemId);
}
使用示例4:MQ 消费防重复处理
@Component
public class OrderMqConsumer {

    @Resource
    private DistributedLockProvider distributedLockProvider;

    public void onMessage(Map<String, Object> message) {
        String orderId = (String) message.get("orderId");
        String lockKey = "mq_consume_order_" + orderId;
        // 锁10分钟,等待8分钟
        try (DistributedLock lock = distributedLockProvider.getLock(
                lockKey, TimeUnit.MINUTES, 10)) {
            if (lock.tryLock(TimeUnit.MINUTES, 8)) {
                processMessage(message);
            } else {
                log.warn("消息正在被其他消费者处理: {}", orderId);
            }
        }
    }
}

四、设计要点总结

要点实现方式收益
零配置使用spring.factories 自动装配引入依赖即可用,无需 @Enable 注解
防死锁锁自带过期时间进程崩溃不会永久阻塞
防误释放requestId 校验A 加的锁不会被 B 释放
原子操作Lua 脚本避免 check-then-act 竞态条件
资源安全Closeable + try-with-resources确保锁一定被释放
灵活度工厂模式 + 可配参数不同场景可设不同超时
可观测性关键操作打日志方便排查锁竞争问题
最小依赖仅依赖 spring-data-redis + slf4j轻量无侵入
Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐