Redis并发冲突:乐观锁、分布式锁

89 阅读3分钟

Redis解决并发冲突的方式主要有五个方面,本文重点在后面两种:乐观锁、分布式锁

1、单线程模型:命令执行为单线程,避免了多线程并发修改数据

2、原子命令:如SETGETINCR 等单个 Redis 命令是原子执行的,不会被其他命令打断

3、Lua脚本:可嵌入脚本语言,脚本内所有命令在服务端原子性批量执行

4、乐观锁

5、分布式锁

乐观锁:通过 WATCH 监听 key,在事务执行前检测 key 是否被修改,若被改则事务失败,防止干扰其他客户端操作,用于实现乐观并发控制。

测试乐观锁,打开两个redis终端,在一个终端中输入:

127.0.0.1:6379> set count 100
OK 
127.0.0.1:6379> WATCH countOK //在WATCH命令之后,提交事务EXEC之前在另一个客户端 set count 10
127.0.0.1:6379> GET count 
"100" 
127.0.0.1:6379> MULTI    //开启事务
OK 
127.0.0.1:6379> DECR count
QUEUED 
127.0.0.1:6379> EXEC       //提交事务前发现当前版本和WATCH版本不一致,放弃该事务
(nil)

分布式锁:Redisson为例

public class SimpleRedissonLockExample {

    public static void main(String[] args) {
        // 1. 创建 Redisson 客户端配置,连接本地 Redis
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");

        // 2. 创建 Redisson 客户端
        RedissonClient redisson = Redisson.create(config);

        // 3. 定义一个锁,key 是 "myLock"(在 Redis 中的锁名称)
        RLock lock = redisson.getLock("myLock");

        // 4. 模拟一个线程尝试获取锁并执行关键代码
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " 尝试获取锁...");
            try {
                // 尝试加锁,最多等待 10 秒,锁自动释放时间 30 秒
                boolean isLocked = lock.tryLock(10, 30, java.util.concurrent.TimeUnit.SECONDS);
                if (isLocked) {
                    try {
                        System.out.println(Thread.currentThread().getName() + " ✅ 获取锁成功,执行关键业务逻辑");
                        // 模拟业务处理
                        Thread.sleep(3000);
                    } finally {
                        // 确保锁释放
                        lock.unlock();
                        System.out.println(Thread.currentThread().getName() + " 🚀 释放锁");
                    }
                } else {
                    System.out.println(Thread.currentThread().getName() + " ⏳ 获取锁失败,稍后再试");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "Thread-1").start();

        // 可选:再启动一个线程模拟并发争抢锁
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " 尝试获取锁...");
            try {
                boolean isLocked = lock.tryLock(10, 30, java.util.concurrent.TimeUnit.SECONDS);
                if (isLocked) {
                    try {
                        System.out.println(Thread.currentThread().getName() + " ✅ 获取锁成功,执行关键业务逻辑");
                        Thread.sleep(3000);
                    } finally {
                        lock.unlock();
                        System.out.println(Thread.currentThread().getName() + " 🚀 释放锁");
                    }
                } else {
                    System.out.println(Thread.currentThread().getName() + " ⏳ 获取锁失败");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "Thread-2").start();
    }
}

加锁操作:

boolean isLocked = lock.tryLock(10, 30, java.util.concurrent.TimeUnit.SECONDS);

trylock方法源码:

public boolean tryLock(long waitTime,   //当前线程愿意等待时间
                        long leaseTime, //锁持有时间
                         TimeUnit unit  //时间单位
                       ) throws InterruptedException {
        long time = unit.toMillis(waitTime);
        long current = System.currentTimeMillis();
        long threadId = Thread.currentThread().getId();
        Long ttl = this.tryAcquire(waitTime, leaseTime, unit, threadId);
        //其余逻辑
}

tryAcquire方法:去 Redis 执行类似 SET key value NX PX leaseTime 的命令,尝试获取锁,返回null表示没有其他客户,若被占用则返回锁当前剩余时间 。

(除此之外该方法还包含一些机制,例如自动续期剩余时间的看门狗机制,获得锁后启动一个定时任务,定期检查并延长该锁的过期时间**,**防止业务执行时间过长导致锁自动过期,其他客户拿到锁发生冲突)。

NX : Not exists

PX leaseTime :px是毫秒单位,整体就是毫秒单位的剩余时间

可以看到分布式锁底层数据结构就是一个K-V键值对,KEY就是要锁定的资源名称,VALUE是锁持有者信息,通过NX(not exists)来保证如果有一个用户加锁,其他用户无法加锁。

释放锁 :lock.unlock(); 删除对应KEY