基于Redis的Redisson分布式锁实现方案

 
基于redis的分布式锁(redisson)

 

 

1、分布式锁介绍:

为了保证一个方法或属性在高并发的情况下同一时间只能被同一个线程执行,在传统单机部署的情况下,可以使用Java并发处理相关的API(如ReentrantLockSynchronized)进行互斥控制。但是,随之业务发展的需要,原单机部署的系统演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同的机器上,这将原来的单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。
为了解决这个问题,就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!

分布式锁应该具备哪些条件

  • 在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;
  • 高可用、高性能的获取锁与释放锁;
  • 具备可重入特性;
  • 具备锁失效机制、防止死锁;
  • 具备非阻塞锁特性,即没有获取到锁直接返回获取锁失败;

2、分布式锁的实现方式

关于分布式锁的实现,目前主流方案有以下三类:

1、基于数据库的乐观锁;

2、基于redis实现的锁服务;

3、基于zookeeper的实现;

 

3、这里主要介绍基于Redis的分布式锁的实现:

redisson: https://github.com/redisson/redisson/wiki/8.-%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E5%92%8C%E5%90%8C%E6%AD%A5%E5%99%A8

引入依赖:

<dependency>
     <groupId>org.redisson</groupId>
     <artifactId>redisson-spring-boot-starter</artifactId>
     <version>3.10.1</version>
</dependency>
   <!--redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

 

3、封装工具类:

/**
 * 基于Redisson的分布式锁实现
 */
@Component
public class RedissonLockUtil {

    @Resource
    private RedissonClient redissonClient;
    
    /**
     * 获取锁对象
     * 
     * @param lockKey 锁键
     * @return
     */
    private RLock getLock(String lockKey) {
        return redissonClient.getLock(lockKey);
    }

    /**
     * 加锁 - 默认会尝试锁定,如果锁定失败则会阻塞直到获取锁
     *
     * @param lockKey
     * @return
     */
    public RLock lock(String lockKey) {
        RLock lock = this.getLock(lockKey);
// 锁过期时间默认30秒,但当前线程没执行完会自动续期 lock.lock();
return lock; } /** * 加锁,过期自动释放(不会自动续期) * * @param lockKey * @param leaseTime 自动释放锁时间 * @return */ public RLock lock(String lockKey, long leaseTime) { RLock lock = this.getLock(lockKey); lock.lock(leaseTime, TimeUnit.SECONDS); return lock; } /** * 加锁,过期自动释放,时间单位传入 * * @param lockKey * @param unit 时间单位 * @param leaseTime 上锁后自动释放时间 * @return */ public RLock lock(String lockKey, TimeUnit unit, long leaseTime) { RLock lock = this.getLock(lockKey); lock.lock(leaseTime, unit); return lock; } /** * 尝试加锁 * * @param unit 时间单位 * @param waitTime 最多等待时间 * @param leaseTime 上锁后自动释放时间 * @return */ public boolean tryLock(RLock lock, TimeUnit unit, long waitTime, long leaseTime) { try { return lock.tryLock(waitTime, leaseTime, unit); } catch (InterruptedException e) { return false; } } /** * 尝试加锁 * * @param lock 锁对象 * @param waitTime 最多等待时间 * @param leaseTime 上锁后自动释放锁时间 * @return */ public boolean tryLock(RLock lock, long waitTime, long leaseTime) { try { return lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS); } catch (InterruptedException e) { return false; } } /** * 释放锁 * * @param lock */ public void unlock(RLock lock) { lock.unlock(); } }

 

使用示例:

 @Autowired
    private RedissionLockUtil redissionLockUtil;

    String lockKey = "only_id";
     try{
        //超过2S自动释放锁
        redissionLockUtil.lock(lockKey, 2L);
        //业务处理

    } finally{
        redissionLockUtil.unlock(lockKey);  //释放锁
    }
 
4、读写锁
@Autowired
 RedissonClient redisson;
 
 @Autowired
 RedisTemplate redisTemplate;

//写锁保证一定能读到最新数据,修改期间,写锁是一个排他锁(互诉锁,独享锁)。读锁是一个共享锁
//写锁没释放,读就必须等待
//写 + 读 (写的时候进行读操作):等待写锁释放
//写 + 写 (写的时候进行写操作):阻塞方式
//读 + 写 (读的时候进行写操作):等待读锁释放
//读 + 读 :相当于无锁,并发读,只会在redis中记录好,所有当前的读锁。他们都会同时加锁成功。
//总结:只要有写的存在,就必须等待前面的锁释放。
@ResponseBody
@RequestMapping("/write")
public String writeLock(){
    RReadWriteLock lock = redisson.getReadWriteLock("rw-lock");
    String s = "";
    RLock rLock = lock.writeLock();
    try {
        // 改数据加写锁,读数据加读锁

        rLock.lock();
        s = UUID.randomUUID().toString();
        redisTemplate.opsForValue().set("writerValue",s);
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        rLock.unlock();
    }
    return s;
}

@ResponseBody
@RequestMapping("/read")
public String readLock(){
    RReadWriteLock lock = redisson.getReadWriteLock("rw-lock");
    RLock rLock = lock.readLock();
    String s = "";
    try {
        //加读锁
        rLock.lock();
        s = redisTemplate.opsForValue().get("writerValue").toString();
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        rLock.unlock();
    }
    return s;
}

 

5、闭锁
/**
 * 下班了,关门回家
 * 1、部门没人了
 * 2、5个部门全部走完,锁门回家
 * @return
 * @throws InterruptedException
 */
@ResponseBody
@RequestMapping("/lockDoor")
public String lockDoor() throws InterruptedException {
    RCountDownLatch door = redisson.getCountDownLatch("door");
    door.trySetCount(5);
    door.await(); // 等待闭锁都完成
    return "下班了。。。。";
}

@ResponseBody
@RequestMapping("/gogogo/{id}")
public String gogogo(@PathVariable("id") String id){
    RCountDownLatch door = redisson.getCountDownLatch("door");
    door.countDown(); //计数减1
    return id + "部门下班了";
}

 

6、信号量

注:信号量也可以用作分布式限流

/**
 * 车库停车(信号量)
 * 3车位
 * 信号量也可以用作分布式限流
 * @return
 */
@ResponseBody
@RequestMapping("/park")
public String park() throws InterruptedException {
    RSemaphore park = redisson.getSemaphore("park");
    //park.acquire(); //获取一个信号,获取一个值,占一个车位(阻塞方式)
    boolean b = park.tryAcquire();//直接运行之后的代码,非阻塞
    if (b){
        //执行业务
    }
    return "ok";
}

@ResponseBody
@RequestMapping("/gogo")
public String gogo(){
    RSemaphore park = redisson.getSemaphore("park");
    park.release(); //释放一个车位,车开走了
    return "ok";
}

 

7、Redisson实现原理详解

Redisson 分布式锁的实现逻辑非常经典,它基于 Redis 的 Lua 脚本和 Pub/Sub 机制,确保了在高并发场景下的安全性和可靠性。
其核心逻辑可以分为两部分:加锁释放锁/续期

执行逻辑如下:

image

 

1、加锁逻辑
加锁的核心是通过执行一段 Lua 脚本 来保证原子性。

-- KEYS[1]: 锁的Key(如 "my_lock"-- ARGV[1]: 锁的过期时间(毫秒)默认30秒
-- ARGV[2]: 客户端唯一标识 + 线程ID (如 `8743c9c0-0795-4907-87fd-6c719a6c4582:1`)

-- 检查锁Key是否存在
if (redis.call('exists', KEYS[1]) == 0) then
    -- 不存在,首次加锁。创建一个Hash结构,并设置重入次数为1。
    redis.call('hset', KEYS[1], ARGV[2], 1);
    -- 为锁设置过期时间
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return nil;
end;

-- 锁Key已经存在,检查是否是当前线程持有的
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;

-- 锁Key存在,但不是当前线程持有的,返回这个锁的剩余存活时间
return redis.call('pttl', KEYS[1]);

如果加锁失败(脚本返回了剩余存活时间):

  • 1. 当前线程会 订阅 Redis 的一个频道(例如:`redisson_lock__channel:{my_lock}`)。
  • 2. 进入一个循环,不断重试获取锁。
  • 3. 一旦锁被释放,发布者会发布消息,所有订阅的客户端都会被唤醒并再次尝试抢锁。这避免了无效的轮询,减轻了 Redis 压力。

 

2、看门狗机制

这是 Redisson 锁的一个**核心特性**,用于解决锁的长期持有问题。
触发条件:只有在加锁时未指定 `leaseTime`(锁自动释放时间)的情况下才会启动。

工作原理:

  • 1. 看门狗在加锁成功后启动,它是一个后台定时任务。
  • 2. 它默认每隔 `锁过期时间 / 3` (比如默认 30秒 / 3 = 10秒)就去检查一下锁是否还被当前线程持有。
  • 3. 如果锁还存在,就通过 Lua 脚本 **重置锁的过期时间**,相当于“续期”。

目的:防止业务执行时间过长,导致锁因为超时而被自动释放,进而被其他线程获取,引发数据错乱。

如果你指定了 `leaseTime`,看门狗就不会启动,锁在到达 `leaseTime` 后会自动释放,无论业务是否执行完毕。

 

3. 释放锁逻辑

释放锁同样通过 Lua 脚本来保证原子性,确保不会误删其他线程的锁。
Lua 脚本释放锁逻辑:

-- KEYS[1]: 锁的Key
-- KEYS[2]: 锁释放消息的频道
-- ARGV[1]: 锁释放消息
-- ARGV[2]: 过期时间
-- ARGV[3]: 客户端唯一标识 + 线程ID

-- 检查锁是否存在,且是不是当前线程持有的
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
    return nil;
end;

-- 是当前线程持有的锁,将重入次数减1
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
if (counter > 0) then
    -- 如果重入次数还大于0,说明还没完全释放,只需更新过期时间
    redis.call('pexpire', KEYS[1], ARGV[2]);
    return 0;
else
    -- 如果重入次数减为0,则彻底删除这个锁Key
    redis.call('del', KEYS[1]);
    -- 并向频道发布解锁消息,通知其他等待的客户端
    redis.call('publish', KEYS[2], ARGV[1]);
    return 1;
end;

return nil;

 

总结

image

 

posted @ 2020-05-24 00:53  邓维-java  阅读(753)  评论(0)    收藏  举报