redission分布式锁实现

实现分布式锁,通常是采用两种方式:1、setnx设置过期时间,配合lua脚本;2、使用redission框架

Redis 实现分布式锁的演变和升级:
1、setnx:set if Not eXists -> setnx key value -> 问题:因为没有设置过期时间,所以可能存在死锁的问题(执行一半程序崩溃,后面的释放锁流程没有执行)。
2、set nx ex/px:Redis 2.6+ set key value nx px 3000 -> 问题:不能解决锁续期和锁重入。
3、Lua 脚本:解决锁重入的问题,但存在问题:实现复杂、不能实现锁续期。
4、Redisson 框架:实现简单、可以实现锁重入和锁续期问题。
使用setnx实现分布式锁有几个小问题:1、没有设置过期时间,该如何控制锁持有时间(这个可以通过expire设置过期时间解决);2、锁操作分为2步,判断是否是你的锁,是的话进行释放,这是非原子操作(这个可以通过lua脚本解决);3、设置的过期时间达到,但业务还没执行完成,还需要继续拥有锁,而此时因为设置了过期时间就自动释放锁资源了,别的线程获取到资源,就可能会导致并发问题,即数据不一致,而且还极难复现(这个可以自行在代码中进行锁续期,也就是看门狗机制的实现,当发现业务代码还没执行完成,那么就继续对这个锁加时间)。
使用redission框架实现的分布式锁,1、它底层也是自行集成了lua脚本(即针对锁误删的问题,就是判断锁是否是当前线程和删除这两个的原子操作);2、锁续期底层也是通过看门狗的机制去实现的,只要时间到了,代码还未执行完成,就会完成续期让其继续执行,直到完成业务代码。

本文介绍下redission实现分布式锁:

一、添加依赖:

<!-- Redisson -->
<!-- https://mvnrepository.com/artifact/org.redisson/redisson-spring-boot-starter -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.25.2</version> <!-- 请根据实际情况使用最新版本 -->
</dependency>

二、配置redissionClient对象

将 RedissonClient 重写,存放到 IoC 容器,并且配置连接的 Redis 服务器信息
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RedissonConfig {
    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        // 也可以将 redis 配置信息保存到配置文件
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        return Redisson.create(config);
    }
}

三、使用分布式锁

Redisson 分布式锁的操作和 Java 中的 ReentrantLock(可重入锁)的操作很像,都是先使用 tryLock 尝试获取(非公平)锁,最后再通过 unlock 释放锁
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;
@RestController
public class LockController {
    @Autowired
    private RedissonClient redissonClient;
    @GetMapping("/lock")
    public String lockResource() throws InterruptedException {
        String lockKey = "myLock";
        // 获取 RLock 对象
        RLock lock = redissonClient.getLock(lockKey);
        try {
            // 尝试获取锁(尝试加锁)(锁超时时间是 30 秒)
            boolean isLocked = lock.tryLock(30, TimeUnit.SECONDS);
            if (isLocked) {
                // 成功获取到锁
                try {
                    // 模拟业务处理
                    TimeUnit.SECONDS.sleep(5);
                    return "成功获取锁,并执行业务代码";
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // 释放锁
                    lock.unlock();
                }
            } else {
                // 获取锁失败
                return "获取锁失败";
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "获取锁成功";
    }
}

贴上一段实际的使用代码,可以参考下:

public ResponseEntity chat(String question) throws Exception {
        if (!StringUtils.hasLength(question)){
            return ResponseEntity.error("问题不能为空");
        }
        long uid = SecurityUserInfoUtil.getSecurityUserDetails().getUid();
        String modelLockKey = AppVariable.getModelLockKey(uid, AiModelEnum.OPENAI.getCode(), AiTypeEnum.CHAT.getCode());
        // 尝试获取锁
        RLock lock = redissonClient.getLock(modelLockKey);
        boolean tryLock = lock.tryLock(30, TimeUnit.SECONDS);
        // 获取锁失败,则返回错误信息
        if (!tryLock){
            return ResponseEntity.error("请勿重复提问");
        }
        // 获取锁成功,则执行逻辑
        try {
            String call = chatModel.call(question);
            Answer answer = new Answer();
            answer.setTitle(question);
            answer.setContent(call);
            answer.setModel(AiModelEnum.OPENAI.getCode());
            answer.setType(AiTypeEnum.CHAT.getCode());
            answer.setUid(uid);
            boolean save = answerService.save(answer);
            if (save){return ResponseEntity.success(call);
            }
        }catch (Exception e){
            e.printStackTrace();
            return ResponseEntity.error("请求失败");
        } finally {
            // 释放锁
            lock.unlock();
        }
        return ResponseEntity.error("数据保存失败,请重试");
    }

key的获取

public class AppVariable {

    // 获取首页缓存的数据的 key
    public static String getListCacheKey(Long uid,int model,int type){
        return "LIST_CACHE_KEY" + uid + "_" + model + "_" + type;
    }

}

以上代码中:使用了锁的超时时间,那么redission的看门狗机制就会失效,会使用你设置的过期时间,到了时间会自动释放锁资源。而如果你想用redission框架的自动锁续期,就使用下面的代码

 RLock lock = redissonClient.getLock(lockKey);
        
        try {
            // 加锁,默认加锁时间 30 秒,Redisson 的 Watch Dog 机制会自动续期(若业务未完成)
            lock.lock(); 
            
            // 执行业务逻辑,如操作共享资源、更新数据库等
            System.out.println("执行受锁保护的业务逻辑...");
            
        } finally {
            // 解锁,必须在 finally 中确保释放,避免死锁
            lock.unlock(); 
            System.out.println("锁已释放");
        }

即不设置过期时间,让redission框架自己控制。

 

可重入性:同一线程可多次获取同一把锁(lock.lock() 多次调用),解锁时需对应次数 unlock 才会真正释放,也可直接调用 unlock() 一次释放(内部会处理重入计数)。

Watch Dog 机制:加锁后,若业务执行时间超过默认 30 秒,Redisson 会每隔 10 秒(internalLockLeaseTime / 3 ,internalLockLeaseTime 默认 30 秒 )检查线程是否仍持有锁,若持有则延长锁的过期时间,保证业务能完成。

四、实现公平锁

Redisson 默认创建的分布式锁是非公平锁(出于性能的考虑)
RLock lock = redissonClient.getFairLock(lockKey);  // 获取公平锁

五、实现读写锁

RReadWriteLock lock = redissonClient.getReadWriteLock(lockKey); // 获取读写锁
lock.readLock();  // 读锁
lock.writeLock(); // 写锁
读写锁的特点就是并发性能高(读读共享、读写/写写不共享),它是允许多个线程同时获取读锁进行读操作的,也就是说在没有写锁的情况下,读取操作可以并发执行,提高了系统的并行度。
但写锁则是独占式的,同一时间只有一个线程可以获得写锁,无论是读还是写都无法与写锁并存,这样就确保了数据修改时的数据一致性。

 

posted @ 2025-06-28 11:24  多多指教~  阅读(516)  评论(0)    收藏  举报