Spring Boot 3集成Redisson分布式锁实现与接口防重复提交设计实战

分布式锁是微服务架构中解决并发资源竞争的核心组件。在高并发设计和分布式事务场景下,基于数据库的唯一约束或Redis的SETNX命令实现的锁方案存在可用性和可靠性短板。Redisson作为Java生态中最成熟的Redis客户端框架,提供了开箱即用的分布式锁实现,包含自动续期、可重入、公平锁和读写锁等完整能力。本文以Spring Boot 3项目为例,给出Redisson分布式锁的集成方案和接口防重复提交的工程化实践。

Redisson核心依赖与Spring Boot 3配置

Spring Boot框架集成Redisson需要引入redisson-spring-boot-starter依赖,并配置Redis连接参数。Redisson 3.x版本全面支持Spring Boot 3和Java 17+:

<!-- pom.xml -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.31.0</version>
</dependency>

<!-- Spring Boot 3.3.x -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

Redisson支持单节点、集群、哨兵和主从四种部署模式。生产环境推荐集群模式配置:

# application.yml
spring:
  data:
    redis:
      host: 192.168.1.100
      port: 6379
      password: yourPassword
      database: 0

# Redisson配置
redisson:
  config: |
    clusterServersConfig:
      nodeAddresses:
        - "redis://192.168.1.101:6379"
        - "redis://192.168.1.102:6379"
        - "redis://192.168.1.103:6379"
      password: yourPassword
      scanInterval: 1000
      readMode: "SLAVE"
      subscriptionMode: "SLAVE"
      slaveConnectionMinimumIdleSize: 10
      slaveConnectionPoolSize: 64
      masterConnectionMinimumIdleSize: 10
      masterConnectionPoolSize: 64
      idleConnectionTimeout: 10000
      connectTimeout: 10000
      timeout: 3000
      retryAttempts: 3
      retryInterval: 1500
    threads: 16
    nettyThreads: 32
    codec: !<org.redisson.codec.JsonJacksonCodec> {}

Redisson分布式锁的底层原理与RLock接口

Redisson的分布式锁基于Redis的Hash结构和Lua脚本实现,具备可重入性和自动续期能力。理解其底层机制有助于在服务治理层面正确使用:

// Redisson加锁的Lua脚本(简化版)
// KEYS[1] = 锁名称
// ARGV[1] = 锁超时时间
// ARGV[2] = 客户端ID:线程ID

// 1. 判断锁是否存在
if redis.call('exists', KEYS[1]) == 0 then
    // 锁不存在,创建锁,设置重入计数为1
    redis.call('hset', KEYS[1], ARGV[2], 1)
    redis.call('pexpire', KEYS[1], ARGV[1])
    return nil
end
// 2. 判断当前线程是否持有锁
if redis.call('hexists', KEYS[1], ARGV[2]) == 1 then
    // 可重入,计数+1
    redis.call('hincrby', KEYS[1], ARGV[2], 1)
    redis.call('pexpire', KEYS[1], ARGV[1])
    return nil
end
// 3. 锁被其他线程持有,返回剩余TTL
return redis.call('pttl', KEYS[1])

RLock接口的核心用法:

@Service
public class OrderService {

    @Autowired
    private RedissonClient redissonClient;

    public void createOrder(String userId, String productId) {
        // 构造锁Key
        String lockKey = "order:lock:" + userId + ":" + productId;
        RLock lock = redissonClient.getLock(lockKey);

        try {
            // 尝试加锁,最多等待5秒,锁自动30秒过期
            boolean locked = lock.tryLock(5, 30, TimeUnit.SECONDS);
            if (!locked) {
                throw new BusinessException("操作过于频繁,请稍后重试");
            }

            // 执行业务逻辑:检查库存、创建订单、扣减库存
            doCreateOrder(userId, productId);

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new BusinessException("加锁被中断");
        } finally {
            // 确保释放锁
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    private void doCreateOrder(String userId, String productId) {
        // 业务实现
    }
}

Watchdog自动续期机制是Redisson的核心特性。当不指定leaseTime时,Redisson启动一个后台定时任务,每10秒(默认看门狗超时时间30秒的1/3)检查锁是否仍被当前线程持有,如果是则续期至30秒:

// 不指定leaseTime,启用Watchdog自动续期
RLock lock = redissonClient.getLock("myLock");
lock.lock();  // 默认30秒TTL,Watchdog每10秒续期
try {
    // 长时间业务操作(超过30秒也不会丢锁)
    Thread.sleep(60000);
} finally {
    lock.unlock();
}

// 指定leaseTime则禁用Watchdog(锁在指定时间后自动释放)
lock.lock(10, TimeUnit.SECONDS);  // 10秒后自动释放,不续期
// 适合业务确定在短时间内完成的场景

基于注解+AOP的防重复提交方案

接口防重复提交是API接口规范中的常见需求。通过自定义注解和AOP切面,将分布式锁逻辑从业务代码中解耦:

// 1. 定义防重注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PreventRepeat {
    String key() default "";           // SpEL表达式,支持从参数提取Key
    int waitTime() default 3;          // 等待获取锁时间(秒)
    int leaseTime() default 10;        // 锁持有时间(秒)
    String message() default "请勿重复提交";
}

// 2. AOP切面实现
@Aspect
@Component
public class PreventRepeatAspect {

    @Autowired
    private RedissonClient redissonClient;

    @Autowired
    private SpelExpressionParser parser;

    @Around("@annotation(preventRepeat)")
    public Object around(ProceedingJoinPoint joinPoint, PreventRepeat preventRepeat) throws Throwable {
        // 构造锁Key
        String lockKey = buildLockKey(joinPoint, preventRepeat);

        RLock lock = redissonClient.getLock(lockKey);
        boolean locked = lock.tryLock(
            preventRepeat.waitTime(),
            preventRepeat.leaseTime(),
            TimeUnit.SECONDS
        );

        if (!locked) {
            throw new BusinessException(preventRepeat.message());
        }

        try {
            return joinPoint.proceed();
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    private String buildLockKey(ProceedingJoinPoint joinPoint, PreventRepeat preventRepeat) {
        String keyExpr = preventRepeat.key();
        if (keyExpr.isEmpty()) {
            // 默认使用方法签名+参数作为Key
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            String argsHash = Arrays.hashCode(joinPoint.getArgs()) + "";
            return "prevent:" + signature.getDeclaringTypeName() + ":" + signature.getName() + ":" + argsHash;
        }

        // 解析SpEL表达式
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        EvaluationContext context = new StandardEvaluationContext();
        String[] paramNames = signature.getParameterNames();
        Object[] args = joinPoint.getArgs();
        for (int i = 0; i < paramNames.length; i++) {
            context.setVariable(paramNames[i], args[i]);
        }

        Expression expression = parser.parseExpression(keyExpr);
        String keyValue = expression.getValue(context, String.class);
        return "prevent:" + signature.getDeclaringTypeName() + ":" + method.getName() + ":" + keyValue;
    }
}

在Controller中使用注解实现防重复提交:

@RestController
@RequestMapping("/api/orders")
public class OrderController {

    // 基于用户ID和商品ID防重
    @PreventRepeat(
        key = "#request.userId + ':' + #request.productId",
        waitTime = 0,
        leaseTime = 5,
        message = "订单正在处理中,请勿重复提交"
    )
    @PostMapping("/create")
    public Result<OrderVO> createOrder(@RequestBody @Valid OrderRequest request) {
        OrderVO order = orderService.createOrder(request);
        return Result.success(order);
    }

    // 全局防重(同一方法+相同参数在5秒内不可重复调用)
    @PreventRepeat(leaseTime = 5, message = "操作过于频繁")
    @PostMapping("/query")
    public Result<List<OrderVO>> queryOrders(@RequestBody OrderQueryRequest request) {
        return Result.success(orderService.queryOrders(request));
    }
}

读写锁与信号量在并发场景中的应用

Redisson提供RReadWriteLock和RSemaphore等高级同步工具,适用于更复杂的业务中台建设场景:

// 读写锁:缓存重建场景
@Service
public class CacheService {

    @Autowired
    private RedissonClient redissonClient;

    public String getDataFromCache(String key) {
        RReadWriteLock rwLock = redissonClient.getReadWriteLock("cache:rw:" + key);

        // 读锁:允许多个线程同时读
        RLock readLock = rwLock.readLock();
        readLock.lock();
        try {
            String cachedValue = redisTemplate.opsForValue().get(key);
            if (cachedValue != null) {
                return cachedValue;
            }
        } finally {
            readLock.unlock();
        }

        // 缓存未命中,获取写锁重建
        RLock writeLock = rwLock.writeLock();
        writeLock.lock();
        try {
            // 双重检查
            String cachedValue = redisTemplate.opsForValue().get(key);
            if (cachedValue != null) {
                return cachedValue;
            }

            // 从数据库加载
            String dbValue = loadFromDatabase(key);
            redisTemplate.opsForValue().set(key, dbValue, 30, TimeUnit.MINUTES);
            return dbValue;
        } finally {
            writeLock.unlock();
        }
    }

    // 信号量:限流控制
    public void executeWithLimit(String resource, int maxConcurrent, Runnable task) {
        RSemaphore semaphore = redissonClient.getSemaphore("semaphore:" + resource);
        semaphore.trySetPermits(maxConcurrent);

        try {
            // 尝试获取许可
            boolean acquired = semaphore.tryAcquire(3, TimeUnit.SECONDS);
            if (!acquired) {
                throw new BusinessException("系统繁忙,请稍后重试");
            }

            task.run();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            semaphore.release();
        }
    }
}

Redisson锁的异常处理与生产级注意事项

分布式锁在高并发设计中的可靠性与异常处理直接相关。几个关键的工程化注意事项:

// 1. 锁释放前检查持有者,避免释放非自身持有的锁
RLock lock = redissonClient.getLock(key);
try {
    if (lock.tryLock(5, 30, TimeUnit.SECONDS)) {
        doBusiness();
    }
} finally {
    // 锁可能已过期被其他线程获取,直接unlock会抛IllegalMonitorStateException
    if (lock.isHeldByCurrentThread()) {
        lock.unlock();
    }
}

// 2. 处理Redis宕机场景的降级策略
@Configuration
public class LockFallbackConfig {

    @Bean
    @ConditionalOnProperty(name = "lock.strategy", havingValue = "database")
    public DistributedLock databaseLock(DataSource dataSource) {
        // 降级到数据库行锁方案
        return new DatabaseDistributedLock(dataSource);
    }
}

// 3. 监控锁等待和持有时间(消息中间件告警)
@Aspect
@Component
public class LockMonitorAspect {

    @Around("execution(* org.redisson.api.RLock.tryLock(..))")
    public Object monitorLock(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = pjp.proceed();
        long elapsed = System.currentTimeMillis() - start;

        if (elapsed > 2000) {
            metricsClient.recordTimer("distributed.lock.wait", elapsed);
            if (elapsed > 5000) {
                alertService.sendAlert("锁等待超时: " + elapsed + "ms");
            }
        }
        return result;
    }
}

// 4. 避免锁粒度过大导致性能瓶颈
// 反模式:单个全局锁
String lockKey = "global:order:lock";  // 所有订单操作共享一把锁

// 正确模式:细粒度锁
String lockKey = "order:lock:" + userId;  // 按用户维度加锁

Redisson的RedLock算法在多节点Redis场景下提供了更高可用性,但在实际使用中需权衡一致性和可用性。对于多数业务场景,单节点Redisson锁配合合理的超时配置和降级策略已能满足需求。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/springboot3-ji-cheng-redisson-fen-bu-shi-suo-shi-xian-yu/

(0)
小编小编
上一篇 2小时前
下一篇 2小时前

相关推荐