Redis解决并发冲突的方式主要有五个方面,本文重点在后面两种:乐观锁、分布式锁
1、单线程模型:命令执行为单线程,避免了多线程并发修改数据
2、原子命令:如SET、GET、INCR 等单个 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