WatchDog+Redis实现分布式锁

代码说明:
watchDog机制主要是用来对redis中商品key进行锁续命,防止业务处理时间过长导致的误删key值。
lua脚本则用来对redis中指令的原子性操作,注意 lua脚本中不能有复杂逻辑,防止阻塞redis

/**
     * 创建定时任务线程工厂
     */
    private static final ThreadFactory THREAD_FACTORY = new ThreadFactoryBuilder().setNameFormat("watchDog-").get();
	
	/**
	* 创建定时任务线程
	*/
    private static final ScheduledExecutorService SCHEDULED_EXECUTOR_SERVICE = new ScheduledThreadPoolExecutor(10,THREAD_FACTORY);

    /**
     * 存放看门狗返回的线程对象
     */
    private static final ConcurrentHashMap<String,ScheduledFuture> CONCURRENT_HASH_MAP = new ConcurrentHashMap<>(16);


 @GetMapping("/normal")
    public String normalRedisLock() throws InterruptedException {
        //给每一个线程都设置对应的UUID
        String productId = "product_huawei_p30";
        String stock = "stock";
        String clientId = UUID.randomUUID().toString();
        try {
            //如果线程已经被加锁,死循环等待释放锁
            while (true){
                Integer stockNum = Integer.parseInt(stringRedisTemplate.opsForValue().get(stock));
                if(stockNum <= 0){
                    return "商品已经卖完";
                }
                //线程加锁,为10秒钟,设置为对应的客户端ID
                Boolean setIfAbsent = stringRedisTemplate.opsForValue().setIfAbsent(productId, clientId, 10, TimeUnit.SECONDS);
                if(Objects.nonNull(setIfAbsent) && setIfAbsent){
                    break;
                }
            }
            System.out.println("----------------------------------开始扣减库存----------------------------------");
            /**
             * 看门狗机制,目的是在线程业务处理时间过长时,导致锁被提前释放,导致处理完成时错误的释放掉另外线程的锁
             */
            WatchDogThread watchDogThread = new WatchDogThread(productId,clientId,stringRedisTemplate,CONCURRENT_HASH_MAP,SCHEDULED_EXECUTOR_SERVICE);
            ScheduledFuture<?> scheduledFuture = SCHEDULED_EXECUTOR_SERVICE.scheduleAtFixedRate(watchDogThread, 1, 5, TimeUnit.SECONDS);
            /**
             * 采用ConcurrentHaspMap用来存储,watchDog任务,并且停止指定的watchDog任务
             */
            CONCURRENT_HASH_MAP.put(clientId,scheduledFuture);
            //执行业务逻辑
            int stockNum = Integer.parseInt(stringRedisTemplate.opsForValue().get(stock));
            if(stockNum > 0){
                /*System.out.println("模拟业务处理时间过长,看门狗续命机制.....");
                Thread.sleep(20000);*/
                stringRedisTemplate.opsForValue().set(stock,String.valueOf(stockNum-1));
                System.out.println("扣减库存成功库存数量为:"+stringRedisTemplate.opsForValue().get(stock));
            }else {
                System.out.println("库存扣减失败。。。。");
            }
        } catch (Exception e) {
            /**
             * 抛出异常时,获取到对应客户端ID的看门狗线程,并且停止看门狗机制
             */
            ScheduledFuture scheduledFuture = CONCURRENT_HASH_MAP.get(clientId);
            if(scheduledFuture != null){
                System.out.println("异常信息,移除看门狗线程。。。");
                scheduledFuture.cancel(true);
                CONCURRENT_HASH_MAP.remove(clientId);
            }
        } finally {
            //释放锁
            stringRedisTemplate.delete(productId);
            System.out.println("----------------------------------业务执行完成----------------------------------");
        }
        return "";

    }

WatchDog实现机制

public class WatchDogThread implements Runnable {

    private String productId;
    private String clientId;
    private StringRedisTemplate stringRedisTemplate;
    private ConcurrentHashMap<String, ScheduledFuture> cacheMap;
    //获取到线程池的引用
    private ScheduledExecutorService scheduledExecutorService;

    /**
     * lua脚本,目的原子操作,获取到商品锁如果等于当前客户端ID,执行锁续命
     */
    private static final String SCRIPT = "if redis.call('get',KEYS[1]) == ARGV[1] then" +
            " local ttl = tonumber(redis.call('ttl',KEYS[1]));" +
            " redis.call('expire',KEYS[1],ttl+ARGV[2]) return redis.call('ttl',KEYS[1]) end";

    public WatchDogThread(String productId,String clientId, StringRedisTemplate stringRedisTemplate, ConcurrentHashMap<String, ScheduledFuture> concurrentHashMap,ScheduledExecutorService scheduledExecutorService) {
        this.clientId = clientId;
        this.productId = productId;
        this.stringRedisTemplate = stringRedisTemplate;
        this.cacheMap = concurrentHashMap;
        this.scheduledExecutorService = scheduledExecutorService;
    }

    @Override
    public void run() {
        String lock = stringRedisTemplate.opsForValue().get(productId);
        try {
            //如果获取到锁为空,或者获取到的锁不等于当前客户端ID,那么就直接停止看门狗
            if (StringUtils.isEmpty(lock) || !clientId.equals(lock)) {
                ScheduledFuture scheduledFuture = cacheMap.get(clientId);
                if (scheduledFuture != null) {
                    System.out.println("库存扣减完成,关闭开门狗。。。");
                    scheduledFuture.cancel(true);
                    cacheMap.remove(clientId);
                }
                return;
            }
            System.out.println("执行续命任务ID:"+lock);
            //执行lua脚本,用来原子性执行锁续命
            stringRedisTemplate.execute(new DefaultRedisScript(SCRIPT, Long.class), Collections.singletonList(productId),clientId,"10");
            Long expire = stringRedisTemplate.getExpire(productId, TimeUnit.SECONDS);
            System.out.println("续命后时间;"+expire);
        } catch (Exception e) {
            System.out.println("watchdog执行失败"+e.getMessage());
            /**
             * 如果watchDog执行续命任务出现异常,直接设置30秒过期时间,防止key值失效,导致误删
             */
            this.stringRedisTemplate.expire(productId,30,TimeUnit.SECONDS);
            /*WatchDogThread watchDogThread = new WatchDogThread(productId,clientId,stringRedisTemplate,this.cacheMap,this.scheduledExecutorService);
            this.scheduledExecutorService.scheduleAtFixedRate(watchDogThread, 1, 5, TimeUnit.SECONDS);*/
        }
    }
}

心得:

如果并发量过高,可以将商品库存进行拆分,如果是redis-cluster架构,可以通过一致性hash,将商品库存分配到不同的redis中进行存储,用来提高并发量。

<think>好的,用户想了解Spring Boot中如何使用Redis实现分布式锁。首先,我需要回忆一下分布式锁的基本概念和用途。分布式锁主要是为了解决在分布式系统中多个服务实例同时访问共享资源时的并发问题,确保同一时间只有一个实例能够执行特定操作。 接下来,我需要考虑Redis实现分布式锁的常见方法。记得最常用的方式是使用SET命令结合NX和PX参数,这样可以原子性地设置键值对并设置过期时间,避免死锁。不过可能会有一些陷阱,比如锁的误删问题,所以在释放锁时要验证客户端标识,确保只有锁的持有者才能删除。 然后,用户可能想知道在Spring Boot中的具体实现步骤。需要提到如何配置RedisTemplate,或者使用Redisson这样的客户端库来简化操作。Redisson提供了RLock接口,支持可重入锁、自动续期等功能,可能比原生实现更可靠。 还要考虑示例代码的结构,比如如何加锁、释放锁,以及处理异常情况。可能需要展示一个基本的代码片段,使用RedisTemplate执行SET命令,或者展示Redisson的使用方式。同时,要强调正确设置超时时间和唯一客户端标识的重要性。 另外,用户可能关心分布式锁的高可用性和性能问题。比如Redis的主从架构下可能出现的问题,这时候可能需要提到RedLock算法,不过也要说明其争议性。或者推荐使用Redisson的MultiLock,但需要权衡复杂性和实际需求。 最后,需要总结使用Redis实现分布式锁的最佳实践,比如设置合理的超时时间、确保释放锁的原子性、避免长时间阻塞等。可能还要提到测试和监控的重要性,确保锁机制在实际环境中的可靠性。 整个过程中要注意用词准确,步骤清晰,让用户能够一步步理解和实现。同时要提醒常见的错误和注意事项,帮助用户避免典型问题。可能还需要比较不同的实现方式,帮助用户根据项目需求选择最合适的方案。</think>在Spring Boot中使用Redis实现分布式锁的常见方式及实现步骤如下: 一、核心原理 通过Redis的原子性操作(SETNX命令或SET命令扩展参数)实现互斥锁机制,确保分布式系统中多个服务实例对共享资源的互斥访问。 二、推荐实现方式(基于SET命令) ```java public class RedisDistributedLock { @Autowired private StringRedisTemplate redisTemplate; private static final String LOCK_PREFIX = "lock:"; private static final long DEFAULT_EXPIRE_TIME = 30000; // 30秒 private static final String LOCK_SUCCESS = "OK"; private static final String SET_IF_NOT_EXIST = "NX"; private static final String SET_WITH_EXPIRE_TIME = "PX"; /** * 尝试获取分布式锁 * @param lockKey 锁键 * @param clientId 客户端唯一标识(建议UUID+线程ID) * @param expireTime 锁持有时间(毫秒) * @return 是否获取成功 */ public boolean tryLock(String lockKey, String clientId, long expireTime) { return redisTemplate.execute((RedisCallback<Boolean>) connection -> { RedisSerializer<String> serializer = redisTemplate.getStringSerializer(); String result = connection.execute("SET", serializer.serialize(LOCK_PREFIX + lockKey), serializer.serialize(clientId), serializer.serialize(SET_IF_NOT_EXIST), serializer.serialize(SET_WITH_EXPIRE_TIME), serializer.serialize(String.valueOf(expireTime))); return LOCK_SUCCESS.equals(result); }); } /** * 释放分布式锁 * @param lockKey 锁键 * @param clientId 客户端唯一标识 */ public void unlock(String lockKey, String clientId) { String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " + "return redis.call('del', KEYS[1]) " + "else " + "return 0 " + "end"; redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Collections.singletonList(LOCK_PREFIX + lockKey), clientId); } } ``` 三、关键要点说明 1. 原子性加锁:使用`SET key value NX PX expireTime`命令保证操作的原子性 2. 唯一客户端标识:使用UUID+线程ID组合,防止不同客户端/线程误删锁 3. 锁续期机制(可选):通过守护线程定期延长锁的有效期 4. 释放锁的原子性:使用Lua脚本保证验证和删除操作的原子性 四、使用示例 ```java // 生成客户端唯一标识 String clientId = UUID.randomUUID().toString() + ":" + Thread.currentThread().getId(); try { if (redisLock.tryLock("order_lock", clientId, 30000)) { // 执行业务逻辑 processOrder(); } } finally { redisLock.unlock("order_lock", clientId); } ``` 五、推荐改进方案 1. 使用Redisson客户端(推荐生产环境使用) ```java // 配置Redisson @Bean public RedissonClient redissonClient() { Config config = new Config(); config.useSingleServer().setAddress("redis://127.0.0.1:6379"); return Redisson.create(config); } // 使用示例 RLock lock = redissonClient.getLock("order_lock"); try { if (lock.tryLock(10, 30, TimeUnit.SECONDS)) { // 执行业务逻辑 } } finally { lock.unlock(); } ``` 六、注意事项 1. 避免设置过长的锁超时时间 2. 确保业务处理时间小于锁过期时间 3. 推荐使用第三方成熟方案(Redisson/Apache Curator) 4. 考虑Redis集群模式下的可靠性问题 5. 做好锁等待超时处理机制 6. 添加重试机制时需要设置最大重试次数 七、优缺点对比 ✅ 优点: - 实现相对简单 - 性能较高 - 支持高并发场景 ❌ 缺点: - 需要自行处理锁续期 - Redis主从架构存在数据不一致风险 - 需要处理网络分区等异常情况 建议生产环境优先使用Redisson等成熟框架,它们已经处理了锁续期、重试、Watchdog等复杂逻辑,能提供更可靠的分布式锁实现
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值