Spring Boot 集成 Redis 实现看门狗 Lua 脚本分布式锁

162 阅读10分钟

Spring Boot 集成 Redis 实现看门狗 Lua 脚本分布式锁

公司实战项目分布式锁,偷偷给你们看,怎么使用的,好多人都不会也不懂

一、概述

1.1 什么是看门狗锁?

看门狗锁(Watchdog Lock) 是一种基于Redis实现的分布式锁优化方案,它在传统分布式锁的基础上增加了自动续期机制,防止业务执行时间超过锁过期时间而导致的锁提前释放问题。

1.2 基本原理

  1. 获取锁时:设置一个较短的初始过期时间(如30秒)
  2. 后台线程(看门狗) :定期检查锁是否仍被持有,如果是则自动续期
  3. Lua脚本保证原子性:所有关键操作使用Lua脚本,确保在Redis端的原子执行
  4. 锁释放:业务完成后手动释放,或线程结束时自动清理

1.3 核心优势 vs 传统分布式锁

特性传统分布式锁看门狗锁
过期时间固定,需要预估业务时间动态续期,自适应业务耗时
业务超时风险高(业务超时导致锁失效)低(自动续期)
实现复杂度简单中等
资源占用需要额外线程维护

二、应用场景

2.1 适用场景

  1. 长时间任务:批处理、数据迁移等耗时不确定的操作
  2. 事务性操作:分布式事务中的资源锁定
  3. 定时任务防重:确保分布式环境下只有一个节点执行
  4. 秒杀/限流:高并发场景下的资源争用控制

2.2 不适用场景

  1. 极短时间锁(< 100ms):续期开销可能大于收益
  2. 对延迟极其敏感的场景:看门狗检查可能引入额外延迟

三、实现方案详述

3.1 架构设计

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│  业务线程       │    │  看门狗线程     │    │  Redis Server   │
│                 │    │                 │    │                 │
│ 1.获取锁        │───▶│                 │───▶│ 执行Lua脚本     │
│                 │    │                 │    │ 设置锁+过期时间 │
│ 2.执行业务逻辑  │    │ 3.启动看门狗    │    │                 │
│                 │    │   定期检查      │    │                 │
│                 │◀───│   自动续期      │◀───│ 执行续期Lua    │
│ 4.释放锁        │───▶│                 │───▶│ 删除Key        │
└─────────────────┘    └─────────────────┘    └─────────────────┘

3.2 关键特性

  1. 可重入性:同一线程可重复获取锁
  2. 自动续期:后台线程定期延长锁有效期
  3. 防死锁:设置最大续期次数,避免异常情况无限续期
  4. 异常恢复:客户端异常断开时,锁会自动过期

四、Spring Boot 集成实现

4.1 环境准备

Maven 依赖
<!-- Spring Boot Starter Data Redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- Redis客户端(使用Lettuce) -->
<dependency>
    <groupId>io.lettuce.core</groupId>
    <artifactId>lettuce-core</artifactId>
</dependency>

<!-- JSON处理 -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
</dependency>

<!-- AOP -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
application.yml 配置
spring:
  redis:
    host: localhost
    port: 6379
    password: 
    database: 0
    lettuce:
      pool:
        max-active: 8
        max-idle: 8
        min-idle: 0
        max-wait: -1ms
    timeout: 5000ms

# 分布式锁配置
distributed:
  lock:
    watch-dog:
      enable: true                    # 是否启用看门狗
      check-interval: 10000          # 检查间隔(ms)
      lease-time: 30000              # 租约时间(ms)
      max-renew-times: 10            # 最大续期次数

4.2 核心实现类

4.2.1 Redis 配置类
package com.example.lock.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
@ConfigurationProperties(prefix = "distributed.lock.watch-dog")
public class RedisLockConfig {
    
    private boolean enable = true;
    private long checkInterval = 10000;
    private long leaseTime = 30000;
    private int maxRenewTimes = 10;
    
    // getters and setters
    
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.afterPropertiesSet();
        return template;
    }
    
    @Bean
    public DefaultRedisScript<Long> lockScript() {
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        script.setScriptText(
            "if (redis.call('exists', KEYS[1]) == 0) then " +
            "    redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
            "    redis.call('pexpire', KEYS[1], ARGV[1]); " +
            "    return 1; " +
            "end; " +
            "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
            "    redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
            "    redis.call('pexpire', KEYS[1], ARGV[1]); " +
            "    return 1; " +
            "end; " +
            "return 0;"
        );
        script.setResultType(Long.class);
        return script;
    }
    
    @Bean
    public DefaultRedisScript<Long> unlockScript() {
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        script.setScriptText(
            "if (redis.call('hexists', KEYS[1], ARGV[1]) == 0) then " +
            "    return 0; " +
            "end; " +
            "local counter = redis.call('hincrby', KEYS[1], ARGV[1], -1); " +
            "if (counter > 0) then " +
            "    redis.call('pexpire', KEYS[1], ARGV[2]); " +
            "    return 1; " +
            "else " +
            "    redis.call('del', KEYS[1]); " +
            "    return 1; " +
            "end; " +
            "return 0;"
        );
        script.setResultType(Long.class);
        return script;
    }
    
    @Bean
    public DefaultRedisScript<Long> renewScript() {
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        script.setScriptText(
            "if (redis.call('hexists', KEYS[1], ARGV[1]) == 1) then " +
            "    redis.call('pexpire', KEYS[1], ARGV[2]); " +
            "    return 1; " +
            "end; " +
            "return 0;"
        );
        script.setResultType(Long.class);
        return script;
    }
}
4.2.2 分布式锁实现类
package com.example.lock.service;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;

import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

@Slf4j
@Component
public class RedisWatchdogLock {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private DefaultRedisScript<Long> lockScript;
    
    @Autowired
    private DefaultRedisScript<Long> unlockScript;
    
    @Autowired
    private DefaultRedisScript<Long> renewScript;
    
    @Autowired
    private RedisLockConfig config;
    
    // 线程本地存储:存储当前线程持有的锁信息
    private final ThreadLocal<LockInfo> currentLock = new ThreadLocal<>();
    
    // 全局锁映射:lockKey -> 锁信息
    private final ConcurrentHashMap<String, LockInfo> lockMap = new ConcurrentHashMap<>();
    
    // 看门狗线程池
    private final ScheduledExecutorService watchdogExecutor = 
        new ScheduledThreadPoolExecutor(1, r -> {
            Thread t = new Thread(r, "redis-lock-watchdog");
            t.setDaemon(true);
            return t;
        });
    
    @Data
    private static class LockInfo {
        private String lockKey;
        private String lockValue;
        private long leaseTime;
        private int holdCount;
        private long lastRenewTime;
        private int renewCount;
        private ReentrantLock localLock = new ReentrantLock();
    }
    
    /**
     * 尝试获取锁(阻塞版本)
     */
    public boolean tryLock(String lockKey, long waitTime, TimeUnit unit) throws InterruptedException {
        long startTime = System.currentTimeMillis();
        long timeout = unit.toMillis(waitTime);
        String lockValue = UUID.randomUUID().toString();
        
        // 检查是否已经持有锁(可重入)
        LockInfo existingLock = currentLock.get();
        if (existingLock != null && existingLock.getLockKey().equals(lockKey)) {
            existingLock.setHoldCount(existingLock.getHoldCount() + 1);
            return true;
        }
        
        // 本地锁,防止同一JVM内多个线程同时竞争
        ReentrantLock localLock = new ReentrantLock();
        localLock.lock();
        try {
            while (true) {
                // 尝试获取Redis锁
                Long result = redisTemplate.execute(
                    lockScript,
                    Collections.singletonList(lockKey),
                    String.valueOf(config.getLeaseTime()),
                    lockValue
                );
                
                if (result != null && result == 1) {
                    // 获取成功,创建锁信息
                    LockInfo lockInfo = new LockInfo();
                    lockInfo.setLockKey(lockKey);
                    lockInfo.setLockValue(lockValue);
                    lockInfo.setLeaseTime(config.getLeaseTime());
                    lockInfo.setHoldCount(1);
                    lockInfo.setLastRenewTime(System.currentTimeMillis());
                    
                    lockMap.put(lockKey, lockInfo);
                    currentLock.set(lockInfo);
                    
                    // 启动看门狗
                    if (config.isEnable()) {
                        startWatchdog(lockInfo);
                    }
                    
                    log.debug("Lock acquired: {} by {}", lockKey, lockValue);
                    return true;
                }
                
                // 检查是否超时
                if (System.currentTimeMillis() - startTime > timeout) {
                    log.debug("Lock acquisition timeout: {}", lockKey);
                    return false;
                }
                
                // 短暂休眠后重试
                Thread.sleep(100);
            }
        } finally {
            localLock.unlock();
        }
    }
    
    /**
     * 启动看门狗线程
     */
    private void startWatchdog(LockInfo lockInfo) {
        watchdogExecutor.scheduleWithFixedDelay(() -> {
            try {
                renewLock(lockInfo);
            } catch (Exception e) {
                log.error("Watchdog error for lock: {}", lockInfo.getLockKey(), e);
            }
        }, config.getCheckInterval() / 2, config.getCheckInterval(), TimeUnit.MILLISECONDS);
    }
    
    /**
     * 续期锁
     */
    private void renewLock(LockInfo lockInfo) {
        if (lockInfo.getRenewCount() >= config.getMaxRenewTimes()) {
            log.warn("Lock {} reached max renew times", lockInfo.getLockKey());
            return;
        }
        
        try {
            Long result = redisTemplate.execute(
                renewScript,
                Collections.singletonList(lockInfo.getLockKey()),
                lockInfo.getLockValue(),
                String.valueOf(config.getLeaseTime())
            );
            
            if (result != null && result == 1) {
                lockInfo.setLastRenewTime(System.currentTimeMillis());
                lockInfo.setRenewCount(lockInfo.getRenewCount() + 1);
                log.debug("Lock renewed: {}", lockInfo.getLockKey());
            } else {
                log.warn("Lock renewal failed: {}", lockInfo.getLockKey());
            }
        } catch (Exception e) {
            log.error("Lock renewal error: {}", lockInfo.getLockKey(), e);
        }
    }
    
    /**
     * 释放锁
     */
    public boolean unlock(String lockKey) {
        LockInfo lockInfo = currentLock.get();
        if (lockInfo == null || !lockInfo.getLockKey().equals(lockKey)) {
            log.warn("Current thread does not hold lock: {}", lockKey);
            return false;
        }
        
        lockInfo.getLocalLock().lock();
        try {
            lockInfo.setHoldCount(lockInfo.getHoldCount() - 1);
            
            if (lockInfo.getHoldCount() > 0) {
                // 还有重入,只减少计数
                return true;
            }
            
            // 完全释放Redis锁
            Long result = redisTemplate.execute(
                unlockScript,
                Collections.singletonList(lockKey),
                lockInfo.getLockValue(),
                String.valueOf(config.getLeaseTime())
            );
            
            if (result != null && result == 1) {
                // 清理资源
                lockMap.remove(lockKey);
                currentLock.remove();
                log.debug("Lock released: {}", lockKey);
                return true;
            }
            
            return false;
        } finally {
            lockInfo.getLocalLock().unlock();
        }
    }
    
    /**
     * 尝试获取锁(非阻塞版本)
     */
    public boolean tryLock(String lockKey) {
        try {
            return tryLock(lockKey, 0, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        }
    }
    
    /**
     * 强制释放锁(用于异常情况)
     */
    public void forceUnlock(String lockKey) {
        LockInfo lockInfo = lockMap.get(lockKey);
        if (lockInfo != null) {
            redisTemplate.delete(lockKey);
            lockMap.remove(lockKey);
            currentLock.remove();
            log.warn("Force unlocked: {}", lockKey);
        }
    }
    
    /**
     * 清理线程本地存储
     */
    public void cleanup() {
        LockInfo lockInfo = currentLock.get();
        if (lockInfo != null) {
            unlock(lockInfo.getLockKey());
        }
    }
}
4.2.3 AOP 注解支持
package com.example.lock.annotation;

import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedisLock {
    
    /**
     * 锁的key,支持SpEL表达式
     */
    String key();
    
    /**
     * 锁的key前缀
     */
    String prefix() default "lock:";
    
    /**
     * 等待锁的最长时间
     */
    long waitTime() default 5;
    
    /**
     * 等待时间单位
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;
    
    /**
     * 获取失败时的错误信息
     */
    String message() default "系统繁忙,请稍后再试";
}

package com.example.lock.aop;

import com.example.lock.annotation.RedisLock;
import com.example.lock.service.RedisWatchdogLock;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

@Slf4j
@Aspect
@Component
public class RedisLockAspect {
    
    @Autowired
    private RedisWatchdogLock redisWatchdogLock;
    
    @Around("@annotation(redisLock)")
    public Object around(ProceedingJoinPoint joinPoint, RedisLock redisLock) throws Throwable {
        String lockKey = parseKey(redisLock.key(), joinPoint);
        String fullKey = redisLock.prefix() + lockKey;
        
        boolean locked = false;
        try {
            locked = redisWatchdogLock.tryLock(fullKey, redisLock.waitTime(), redisLock.timeUnit());
            if (!locked) {
                throw new RuntimeException(redisLock.message());
            }
            
            return joinPoint.proceed();
        } finally {
            if (locked) {
                redisWatchdogLock.unlock(fullKey);
            }
        }
    }
    
    private String parseKey(String key, ProceedingJoinPoint joinPoint) {
        if (!key.contains("#")) {
            return key;
        }
        
        try {
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            Method method = signature.getMethod();
            String[] parameterNames = signature.getParameterNames();
            Object[] args = joinPoint.getArgs();
            
            ExpressionParser parser = new SpelExpressionParser();
            StandardEvaluationContext context = new StandardEvaluationContext();
            
            for (int i = 0; i < parameterNames.length; i++) {
                context.setVariable(parameterNames[i], args[i]);
            }
            
            Expression expression = parser.parseExpression(key);
            return expression.getValue(context, String.class);
        } catch (Exception e) {
            log.error("Parse lock key error: {}", key, e);
            return key;
        }
    }
}

4.3 使用示例

4.3.1 业务服务类
package com.example.lock.service;

import com.example.lock.annotation.RedisLock;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Slf4j
@Service
public class OrderService {
    
    /**
     * 注解方式使用分布式锁
     */
    @RedisLock(key = "'order:' + #orderId", waitTime = 10, timeUnit = TimeUnit.SECONDS)
    public void processOrder(Long orderId) {
        try {
            log.info("开始处理订单: {}", orderId);
            // 模拟耗时操作
            Thread.sleep(15000);
            log.info("订单处理完成: {}", orderId);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException(e);
        }
    }
    
    /**
     * 编程方式使用分布式锁
     */
    public void processOrderManual(Long orderId) {
        String lockKey = "order:" + orderId;
        
        try {
            boolean locked = redisWatchdogLock.tryLock(lockKey, 10, TimeUnit.SECONDS);
            if (!locked) {
                throw new RuntimeException("获取订单锁失败");
            }
            
            try {
                log.info("开始处理订单: {}", orderId);
                // 模拟耗时操作
                Thread.sleep(15000);
                log.info("订单处理完成: {}", orderId);
            } finally {
                redisWatchdogLock.unlock(lockKey);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException(e);
        }
    }
}
4.3.2 控制器
package com.example.lock.controller;

import com.example.lock.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/order")
public class OrderController {
    
    @Autowired
    private OrderService orderService;
    
    @PostMapping("/process/{orderId}")
    public String processOrder(@PathVariable Long orderId) {
        orderService.processOrder(orderId);
        return "success";
    }
    
    @PostMapping("/process/manual/{orderId}")
    public String processOrderManual(@PathVariable Long orderId) {
        orderService.processOrderManual(orderId);
        return "success";
    }
}

五、测试与验证

5.1 单元测试

package com.example.lock;

import com.example.lock.service.RedisWatchdogLock;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

@SpringBootTest
class RedisWatchdogLockTest {
    
    @Autowired
    private RedisWatchdogLock redisWatchdogLock;
    
    private int counter = 0;
    
    @Test
    void testReentrantLock() throws InterruptedException {
        String lockKey = "test:reentrant";
        
        // 第一次获取锁
        boolean firstLock = redisWatchdogLock.tryLock(lockKey, 5, TimeUnit.SECONDS);
        assert firstLock;
        
        // 第二次获取(重入)
        boolean secondLock = redisWatchdogLock.tryLock(lockKey, 5, TimeUnit.SECONDS);
        assert secondLock;
        
        // 释放一次
        redisWatchdogLock.unlock(lockKey);
        
        // 再释放一次
        boolean result = redisWatchdogLock.unlock(lockKey);
        assert result;
    }
    
    @Test
    void testConcurrentAccess() throws InterruptedException {
        String lockKey = "test:concurrent";
        int threadCount = 5;
        ExecutorService executor = Executors.newFixedThreadPool(threadCount);
        CountDownLatch latch = new CountDownLatch(threadCount);
        
        for (int i = 0; i < threadCount; i++) {
            executor.submit(() -> {
                try {
                    if (redisWatchdogLock.tryLock(lockKey, 2, TimeUnit.SECONDS)) {
                        try {
                            // 临界区操作
                            int current = counter;
                            Thread.sleep(100);
                            counter = current + 1;
                        } finally {
                            redisWatchdogLock.unlock(lockKey);
                        }
                    }
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    latch.countDown();
                }
            });
        }
        
        latch.await();
        executor.shutdown();
        
        // 验证最终结果
        assert counter == 1 : "并发测试失败,counter=" + counter;
    }
    
    @Test
    void testWatchdogRenewal() throws InterruptedException {
        String lockKey = "test:watchdog";
        
        boolean locked = redisWatchdogLock.tryLock(lockKey, 5, TimeUnit.SECONDS);
        assert locked;
        
        // 持有锁超过初始租期,验证看门狗是否续期
        Thread.sleep(40000); // 40秒
        
        // 尝试在另一个线程获取锁(应该失败)
        Thread otherThread = new Thread(() -> {
            boolean otherLock = redisWatchdogLock.tryLock(lockKey);
            assert !otherLock : "锁应该还在被持有";
        });
        
        otherThread.start();
        otherThread.join();
        
        // 释放锁
        redisWatchdogLock.unlock(lockKey);
    }
}

5.2 集成测试

# 启动Redis服务
docker run -d --name redis-test -p 6379:6379 redis:alpine

# 启动Spring Boot应用
./mvnw spring-boot:run

# 测试并发请求
# 使用ab或wrk进行压力测试
wrk -t4 -c100 -d30s http://localhost:8080/order/process/123

六、性能优化与监控

6.1 性能优化建议

  1. 合理设置过期时间:根据业务平均耗时调整leaseTime
  2. 调整检查间隔:根据业务波动性调整checkInterval
  3. 避免锁粒度太细:减少锁竞争和续期开销
  4. 使用连接池:确保Redis连接高效复用

6.2 监控指标

package com.example.lock.monitor;

import com.example.lock.service.RedisWatchdogLock;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;
import java.util.concurrent.atomic.AtomicInteger;

@Configuration
public class LockMonitor {
    
    @Autowired
    private RedisWatchdogLock redisWatchdogLock;
    
    @Autowired
    private MeterRegistry meterRegistry;
    
    private final AtomicInteger activeLocks = new AtomicInteger(0);
    
    @PostConstruct
    public void init() {
        Gauge.builder("redis.lock.active.count", activeLocks, AtomicInteger::get)
            .description("当前活跃的分布式锁数量")
            .register(meterRegistry);
    }
}

6.3 告警配置

# Prometheus告警规则
groups:
  - name: redis_lock_alerts
    rules:
      - alert: RedisLockRenewFailure
        expr: increase(redis_lock_renew_failure_total[5m]) > 10
        for: 1m
        labels:
          severity: warning
        annotations:
          summary: "Redis锁续期失败"
          description: "锁 {{ $labels.lock_key }} 续期失败次数超过阈值"
      
      - alert: RedisLockHoldTooLong
        expr: redis_lock_hold_duration_seconds > 300
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "Redis锁持有时间过长"
          description: "锁 {{ $labels.lock_key }} 已持有超过5分钟"

七、常见问题与解决方案

7.1 问题排查表

问题现象可能原因解决方案
获取锁超时锁竞争激烈或业务持有时间过长1. 增加waitTime 2. 优化业务逻辑 3. 减小锁粒度
看门狗未续期看门狗线程异常终止1. 检查线程池配置 2. 增加异常监控 3. 设置线程未捕获异常处理器
锁释放失败网络异常或Redis故障1. 实现重试机制 2. 设置锁的最大持有时间 3. 添加强制释放接口
内存泄漏ThreadLocal未清理1. 确保finally块中清理 2. 使用拦截器统一清理

7.2 Redis集群环境

在Redis集群环境下,需要考虑以下调整:

// 配置Redisson客户端支持集群
@Bean
public RedissonClient redissonClient() {
    Config config = new Config();
    config.useClusterServers()
        .addNodeAddress("redis://127.0.0.1:7001", "redis://127.0.0.1:7002")
        .setPassword("password")
        .setScanInterval(2000);
    return Redisson.create(config);
}

八、总结

8.1 优点

  1. 自动续期:解决业务执行时间不确定性问题
  2. 高可靠性:Lua脚本保证原子性,避免竞争条件
  3. 可重入性:支持同一线程多次获取
  4. 易于集成:提供注解和编程两种使用方式
  5. 可监控:完善的监控和告警机制

8.2 缺点

  1. 实现复杂度高:需要维护看门狗线程和锁状态
  2. 资源消耗:每个锁需要额外线程进行续期
  3. 网络依赖:完全依赖Redis可用性
  4. 时钟同步问题:分布式环境下需要保证时钟同步

8.3 最佳实践建议

  1. 锁命名规范:使用业务前缀,如 order:lock:{orderId}
  2. 设置合理的超时时间:根据业务99线耗时设置
  3. 实现降级策略:Redis不可用时使用本地锁
  4. 定期清理:实现锁的自动清理机制
  5. 文档化:记录锁的使用场景和注意事项

部署注意事项

  1. 生产环境建议使用Redis Sentinel或Cluster保证高可用
  2. 建议设置合理的JVM参数,避免看门狗线程过多导致资源耗尽
  3. 定期检查锁的使用情况,优化锁粒度和持有时间
  4. 在容器化部署时,注意线程池的合理配置