Redisson分布式锁详细教程
Redisson分布式锁详细教程
1. 什么是分布式锁?
生活中的例子
想象一下你去银行的ATM机取钱。当你插入银行卡开始操作时,这台ATM机就被你”锁定”了,其他人即使有卡也无法在这台机器上操作,必须等你完成交易后才能使用。这就是生活中”锁”的概念——确保同一时间只有一个人能使用某个资源。
再比如,公司的会议室预约系统。当有人预约了下午2-3点的会议室后,系统就会”锁定”这个时间段,其他人就不能再预约同一时间的同一会议室了。如果没有这个”锁”机制,可能会出现多个团队同时到达会议室的尴尬场面。
分布式锁就像是一个”全公司通用的会议室预约系统”。不管你在北京办公室还是上海办公室,预约系统都是同一个,确保不会有冲突。
技术层面的理解
在技术层面,分布式锁是一种跨多个服务器、多个进程之间的同步机制。它确保在分布式系统中,多个节点在访问共享资源时能够互斥访问。
传统的Java锁(如synchronized、ReentrantLock)只能在单个JVM进程内起作用。就像家里的门锁只能管住自己家的门,管不了邻居家的门。而分布式锁就像小区的门禁系统,可以统一管理所有住户的出入权限。
简单来说:
单机锁:只能管理一个JVM进程内的线程同步分布式锁:可以管理多个服务器上的多个进程的同步
2. 为什么需要分布式锁?
单体应用 vs 分布式应用
让我们通过一个电商秒杀的例子来理解为什么需要分布式锁。
单体应用(简单)
在单体应用时代,所有的代码都运行在一台服务器的一个进程中。处理并发很简单:
public class StockService {
private int stock = 100; // 商品库存
// 使用synchronized关键字就能保证线程安全
public synchronized boolean reduceStock() {
if (stock > 0) {
stock--;
System.out.println("扣减库存成功,剩余:" + stock);
return true;
}
System.out.println("库存不足!");
return false;
}
}
这种方式在单体应用中完美运行。synchronized关键字确保同一时间只有一个线程能执行这个方法,不会出现超卖。
分布式应用(复杂)
但是当业务量增长,我们需要部署多个服务器来分担压力时,问题就来了:
用户请求 → 负载均衡器 → 服务器A(有自己的JVM)
→ 服务器B(有自己的JVM)
→ 服务器C(有自己的JVM)
每个服务器都有自己独立的JVM进程,synchronized只能管住自己服务器内的线程,管不了其他服务器。这就像三个收银台同时卖同一件商品,每个收银台都以为自己看到的库存是准确的,结果可能出现:
服务器A:看到库存1,卖出1件服务器B:也看到库存1,也卖出1件服务器C:还是看到库存1,又卖出1件
结果:库存是1,却卖出了3件!这就是典型的”超卖”问题。
这时候,我们就需要一个”全局的锁”——分布式锁,来协调所有服务器的操作。
3. 分布式锁的核心要求
一个合格的分布式锁,就像一个优秀的保安系统,需要满足以下要求:
3.1 互斥性(Mutual Exclusion)
通俗解释:就像厕所的门锁,有人进去了就要锁上,其他人就进不去了。
在任意时刻,只能有一个客户端持有锁。这是最基本的要求。如果两个人能同时拿到锁,那这个锁就失去意义了。
// 正确的互斥性
线程A获取锁 → 成功
线程B获取锁 → 失败(因为A持有)
线程A释放锁
线程B获取锁 → 成功
3.2 安全性(Safety)
通俗解释:自己的锁只能自己开,别人不能随便开你的锁。
谁加的锁,就必须由谁来解锁。不能出现A加的锁被B解开的情况。这就像你的家门钥匙,只有你自己有,邻居不能随便开你家的门。
3.3 活性(Liveness)
通俗解释:不能出现”死锁”,即使持锁人突然”失联”,锁也要能自动释放。
分为两个方面:
无死锁:即使持有锁的客户端崩溃了,锁也必须能被释放容错性:只要大部分Redis节点活着,客户端就应该能够获取和释放锁
就像智能门锁,即使你的手机没电了,过一段时间门锁也会自动重置,不会永远锁死。
3.4 高可用(High Availability)
通俗解释:锁服务要一直可用,不能说今天锁坏了,大家都进不了门。
锁服务本身必须是高可用的。不能因为提供锁服务的Redis挂了,整个系统就瘫痪了。这就像大楼的门禁系统,必须24小时稳定运行。
4. Redisson分布式锁基础
4.1 基本概念
Redisson是Redis官方推荐的Java客户端,它不仅仅是一个Redis客户端,更像是一个”分布式工具箱”。如果说Redis是一把瑞士军刀,那Redisson就是教你如何优雅地使用这把刀的说明书。
Redisson提供的分布式锁特点:
简单易用:几行代码就能实现分布式锁功能完善:支持可重入锁、公平锁、读写锁等自动续期:有看门狗机制,不用担心业务没执行完锁就过期了高性能:基于Redis,性能优秀
4.2 快速开始
首先,添加依赖(就像安装软件):
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.24.3</version>
</dependency>
配置连接(就像设置WiFi密码):
spring:
redis:
host: localhost
port: 6379
password: 123456 # 如果没密码可以不写
创建配置类(初始化工具):
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
// 单机模式,就像连接一台电脑
config.useSingleServer()
.setAddress("redis://localhost:6379")
.setPassword("123456"); // 如果有密码
return Redisson.create(config);
}
}
4.3 核心API解析
Redisson分布式锁的核心API非常简单,主要就这几个:
// 1. 获取锁对象(就像拿到一把锁)
RLock lock = redissonClient.getLock("myLock");
// 2. 加锁(就像锁门)
lock.lock(); // 一直等待直到获取锁
lock.tryLock(); // 尝试获取,立即返回结果
lock.tryLock(10, TimeUnit.SECONDS); // 最多等10秒
// 3. 解锁(就像开门)
lock.unlock();
// 4. 检查锁状态
lock.isLocked(); // 锁是否被占用
lock.isHeldByCurrentThread(); // 是否是我持有的锁
5. 基本分布式锁详解
5.1 简单示例:库存扣减
让我们用一个最常见的电商库存扣减场景来演示分布式锁的使用:
@Service
public class StockService {
@Autowired
private RedissonClient redissonClient;
// 商品库存(实际应该存在数据库)
private Integer stock = 100;
/**
* 扣减库存 - 不加锁版本(有问题!)
*/
public String reduceStockWithoutLock() {
if (stock > 0) {
stock--;
return "扣减成功,剩余库存:" + stock;
}
return "库存不足!";
}
/**
* 扣减库存 - 加分布式锁版本(正确!)
*/
public String reduceStockWithLock() {
// 1. 获取锁对象,名字叫"stock-lock"
RLock lock = redissonClient.getLock("stock-lock");
try {
// 2. 尝试加锁,最多等待3秒,锁定后10秒自动解锁
boolean isLocked = lock.tryLock(3, 10, TimeUnit.SECONDS);
if (isLocked) {
// 3. 获取锁成功,执行业务逻辑
System.out.println(Thread.currentThread().getName() + " 获取锁成功");
// 检查库存
if (stock > 0) {
stock--;
System.out.println("扣减成功,剩余库存:" + stock);
// 模拟业务处理时间
Thread.sleep(50);
return "购买成功!剩余库存:" + stock;
} else {
return "库存不足!";
}
} else {
// 获取锁失败
return "系统繁忙,请稍后重试!";
}
} catch (InterruptedException e) {
e.printStackTrace();
return "系统异常!";
} finally {
// 4. 释放锁(很重要!)
// 先判断是否是自己的锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
System.out.println(Thread.currentThread().getName() + " 释放锁");
}
}
}
}
执行流程图
代码解析:
:获取一把名为”stock-lock”的锁
getLock("stock-lock")
:
tryLock(3, 10, TimeUnit.SECONDS)
第一个参数3:最多等待3秒第二个参数10:获取锁后,10秒后自动释放(防止死锁)
:检查是不是自己的锁,只有自己的锁才能释放
isHeldByCurrentThread()
5.2 并发测试验证
让我们写个测试,模拟100个用户同时抢购:
@RestController
public class StockController {
@Autowired
private StockService stockService;
/**
* 模拟秒杀场景
*/
@GetMapping("/seckill")
public void seckill() {
// 模拟100个用户同时抢购
for (int i = 0; i < 100; i++) {
new Thread(() -> {
String result = stockService.reduceStockWithLock();
System.out.println(result);
}, "用户" + i).start();
}
}
/**
* 不加锁的测试(会出现超卖)
*/
@GetMapping("/seckill-no-lock")
public void seckillNoLock() {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
String result = stockService.reduceStockWithoutLock();
System.out.println(result);
}, "用户" + i).start();
}
}
}
测试结果:
不加锁:可能出现负库存(超卖)加锁:库存准确,不会超卖
5.3 可重入锁示例
可重入锁(Reentrant Lock):同一个线程可以多次获取同一把锁,而不会造成死锁。
可重入锁的好处:避免死锁,同一个线程可以多次获取同一把锁。
可重入锁就像你家的大门,你进去后还可以再开卧室门、厨房门,不会因为你已经在家里就不让你开其他门了。
@Service
public class ReentrantLockService {
@Autowired
private RedissonClient redissonClient;
/**
* 方法A:获取锁后调用方法B
*/
public void methodA() {
RLock lock = redissonClient.getLock("reentrant-lock");
try {
lock.lock();
System.out.println("方法A获取锁成功");
// 调用方法B(需要同一把锁)
methodB();
System.out.println("方法A执行完成");
} finally {
lock.unlock();
System.out.println("方法A释放锁");
}
}
/**
* 方法B:也需要获取同一把锁
*/
public void methodB() {
RLock lock = redissonClient.getLock("reentrant-lock");
try {
lock.lock(); // 可重入,同一线程可以再次获取
System.out.println("方法B获取锁成功(重入)");
// 执行业务逻辑
Thread.sleep(1000);
System.out.println("方法B执行完成");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println("方法B释放锁");
}
}
}
运行结果:
方法A获取锁成功
方法B获取锁成功(重入)
方法B执行完成
方法B释放锁
方法A执行完成
方法A释放锁
6. 公平锁vs非公平锁
6.1 概念对比
公平锁(Fair Lock)
定义:严格按照线程申请锁的时间顺序来分配锁特点:先来先得,像银行排队取号一样原则:FIFO(First In First Out,先进先出)
非公平锁(Non-Fair Lock)
定义:不保证申请锁的时间顺序特点:谁抢到算谁的,像抢座位一样原则:谁运气好、速度快谁先得
想象你在银行排队:
非公平锁:像没有排队系统的银行,谁抢到谁先办。可能后来的人反而先办理业务。
优点:效率高,不需要维护队列缺点:可能有人一直抢不到,造成”饥饿”
公平锁:像有叫号系统的银行,先到先得,按顺序办理。
优点:公平,不会饥饿缺点:需要维护队列,性能略低
6.2 非公平锁示例
@Service
public class NonFairLockService {
@Autowired
private RedissonClient redissonClient;
/**
* 非公平锁演示
*/
public void nonFairLockDemo(String userName) {
// 获取非公平锁(默认就是非公平锁)
RLock lock = redissonClient.getLock("non-fair-lock");
try {
System.out.println(userName + " 开始尝试获取锁...");
// 尝试获取锁
boolean isLocked = lock.tryLock(10, 5, TimeUnit.SECONDS);
if (isLocked) {
System.out.println(userName + " 获取锁成功!");
// 模拟业务处理
Thread.sleep(2000);
System.out.println(userName + " 处理完成");
} else {
System.out.println(userName + " 获取锁失败");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
System.out.println(userName + " 释放锁");
}
}
}
}
6.3 公平锁示例
@Service
public class FairLockService {
@Autowired
private RedissonClient redissonClient;
/**
* 公平锁演示
*/
public void fairLockDemo(String userName) {
// 获取公平锁(需要明确指定)
RLock lock = redissonClient.getFairLock("fair-lock");
try {
System.out.println(userName + " 开始排队获取锁...");
long startTime = System.currentTimeMillis();
// 尝试获取锁(会按照请求顺序排队)
lock.lock();
long waitTime = System.currentTimeMillis() - startTime;
System.out.println(userName + " 等待了" + waitTime + "ms后获取锁成功!");
// 模拟业务处理
Thread.sleep(2000);
System.out.println(userName + " 处理完成");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println(userName + " 释放锁");
}
}
/**
* 测试公平性
*/
public void testFairness() {
// 创建5个线程模拟5个用户
for (int i = 1; i <= 5; i++) {
final String userName = "用户" + i;
new Thread(() -> {
fairLockDemo(userName);
}).start();
try {
// 确保按顺序发起请求
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
测试结果对比:
非公平锁:用户获取锁的顺序可能是乱的公平锁:严格按照请求顺序获取锁(用户1→用户2→用户3…)
6.4 选择建议
场景 | 建议 | 原因 |
---|---|---|
高并发秒杀 | 非公平锁 | 性能更好,吞吐量大 |
排队取号 | 公平锁 | 需要保证顺序 |
普通业务 | 非公平锁 | 默认即可,性能优先 |
任务调度 | 公平锁 | 避免某些任务饿死 |
7. 读写锁详解
7.1 读写锁的特性
读写锁:一种特殊的锁,允许多个读操作同时进行,但写操作是独占的。
读写锁就像图书馆的规则:
读锁(共享锁):多个人可以同时看同一本书(只要不拿走)写锁(独占锁):要修改书的内容时,必须独占这本书
特性总结:
多个线程可以同时获取读锁写锁是独占的,获取写锁时其他读锁和写锁都要等待适合读多写少的场景
7.2 缓存更新场景
最典型的应用场景是缓存更新:
@Service
public class CacheService {
@Autowired
private RedissonClient redissonClient;
// 模拟本地缓存
private Map<String, String> cache = new HashMap<>();
/**
* 读取缓存(可以多个线程同时读)
*/
public String readCache(String key) {
// 获取读写锁
RReadWriteLock rwLock = redissonClient.getReadWriteLock("cache-lock-" + key);
RLock readLock = rwLock.readLock();
try {
// 获取读锁
readLock.lock();
System.out.println(Thread.currentThread().getName() + " 获取读锁,开始读取缓存");
// 模拟读取耗时
Thread.sleep(1000);
String value = cache.get(key);
System.out.println(Thread.currentThread().getName() + " 读取完成:" + value);
return value;
} catch (InterruptedException e) {
e.printStackTrace();
return null;
} finally {
readLock.unlock();
System.out.println(Thread.currentThread().getName() + " 释放读锁");
}
}
/**
* 更新缓存(独占操作)
*/
public void updateCache(String key, String value) {
// 获取读写锁
RReadWriteLock rwLock = redissonClient.getReadWriteLock("cache-lock-" + key);
RLock writeLock = rwLock.writeLock();
try {
// 获取写锁
writeLock.lock();
System.out.println(Thread.currentThread().getName() + " 获取写锁,开始更新缓存");
// 模拟更新耗时
Thread.sleep(2000);
cache.put(key, value);
System.out.println(Thread.currentThread().getName() + " 更新完成");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
writeLock.unlock();
System.out.println(Thread.currentThread().getName() + " 释放写锁");
}
}
}
7.3 读写锁测试
@RestController
public class CacheController {
@Autowired
private CacheService cacheService;
/**
* 测试读写锁
*/
@GetMapping("/test-rw-lock")
public void testReadWriteLock() {
String key = "user:1001";
// 先写入一个值
cacheService.updateCache(key, "张三");
// 启动3个读线程
for (int i = 1; i <= 3; i++) {
new Thread(() -> {
cacheService.readCache(key);
}, "读线程" + i).start();
}
// 等待1秒后,启动一个写线程
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
cacheService.updateCache(key, "李四");
}, "写线程").start();
// 再启动2个读线程
for (int i = 4; i <= 5; i++) {
new Thread(() -> {
cacheService.readCache(key);
}, "读线程" + i).start();
}
}
}
运行结果:
读线程1 获取读锁,开始读取缓存
读线程2 获取读锁,开始读取缓存 // 可以同时读
读线程3 获取读锁,开始读取缓存 // 可以同时读
读线程1 读取完成:张三
读线程1 释放读锁
读线程2 读取完成:张三
读线程2 释放读锁
读线程3 读取完成:张三
读线程3 释放读锁
写线程 获取写锁,开始更新缓存 // 等所有读锁释放后才能写
写线程 更新完成
写线程 释放写锁
读线程4 获取读锁,开始读取缓存
读线程5 获取读锁,开始读取缓存
读线程4 读取完成:李四 // 读到更新后的值
读线程5 读取完成:李四
7.4 读写锁的性能优势
对比普通锁和读写锁的性能:
@Service
public class PerformanceCompareService {
@Autowired
private RedissonClient redissonClient;
/**
* 使用普通锁(读也要排队)
*/
public void normalLockTest() {
RLock lock = redissonClient.getLock("normal-lock");
long startTime = System.currentTimeMillis();
// 10个读操作
for (int i = 0; i < 10; i++) {
lock.lock();
try {
Thread.sleep(100); // 模拟读取
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
long endTime = System.currentTimeMillis();
System.out.println("普通锁耗时:" + (endTime - startTime) + "ms");
// 结果:约1000ms(串行执行)
}
/**
* 使用读写锁(读可以并行)
*/
public void rwLockTest() throws InterruptedException {
RReadWriteLock rwLock = redissonClient.getReadWriteLock("rw-lock");
long startTime = System.currentTimeMillis();
CountDownLatch latch = new CountDownLatch(10);
// 10个读操作并行
for (int i = 0; i < 10; i++) {
new Thread(() -> {
RLock readLock = rwLock.readLock();
readLock.lock();
try {
Thread.sleep(100); // 模拟读取
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock();
latch.countDown();
}
}).start();
}
latch.await();
long endTime = System.currentTimeMillis();
System.out.println("读写锁耗时:" + (endTime - startTime) + "ms");
// 结果:约100ms(并行执行)
}
}
性能对比结果:读写锁在读多写少场景下性能提升明显!
8. 联锁(多重锁)
8.1 什么是联锁?
联锁:同时获取多个锁的机制,要么全部获取成功,要么全部失败,具有原子性。
联锁就像办理一个复杂业务需要多个部门盖章:必须所有部门都同意(获取所有锁)才能办理。
举个例子:银行转账
需要锁定转出账户(防止余额被其他操作修改)需要锁定转入账户(防止并发问题)只有两个账户都锁定成功,才能进行转账
8.2 转账业务实现
@Service
public class TransferService {
@Autowired
private RedissonClient redissonClient;
// 模拟账户余额
private Map<String, BigDecimal> accounts = new HashMap<>();
{
// 初始化账户
accounts.put("account:001", new BigDecimal("1000"));
accounts.put("account:002", new BigDecimal("1000"));
}
/**
* 转账操作(需要同时锁定两个账户)
*/
public String transfer(String fromAccount, String toAccount, BigDecimal amount) {
// 获取两个账户的锁
RLock lock1 = redissonClient.getLock(fromAccount);
RLock lock2 = redissonClient.getLock(toAccount);
// 创建联锁(MultiLock)
RLock multiLock = redissonClient.getMultiLock(lock1, lock2);
try {
// 同时获取两个锁,最多等待5秒,锁定10秒后自动释放
boolean isLocked = multiLock.tryLock(5, 10, TimeUnit.SECONDS);
if (isLocked) {
System.out.println("成功锁定两个账户,开始转账");
// 检查余额
BigDecimal fromBalance = accounts.get(fromAccount);
if (fromBalance.compareTo(amount) < 0) {
return "余额不足,转账失败!";
}
// 执行转账
accounts.put(fromAccount, fromBalance.subtract(amount));
BigDecimal toBalance = accounts.get(toAccount);
accounts.put(toAccount, toBalance.add(amount));
// 模拟转账处理时间
Thread.sleep(1000);
System.out.println("转账成功!");
System.out.println(fromAccount + " 余额:" + accounts.get(fromAccount));
System.out.println(toAccount + " 余额:" + accounts.get(toAccount));
return "转账成功!";
} else {
return "系统繁忙,请稍后重试!";
}
} catch (InterruptedException e) {
e.printStackTrace();
return "转账失败!";
} finally {
// 释放联锁(会同时释放所有锁)
if (multiLock.isHeldByCurrentThread()) {
multiLock.unlock();
System.out.println("释放所有锁");
}
}
}
/**
* 测试并发转账
*/
public void testConcurrentTransfer() {
// 模拟两个人同时转账
// 张三:account:001 → account:002 转100
// 李四:account:002 → account:001 转100
Thread t1 = new Thread(() -> {
String result = transfer("account:001", "account:002", new BigDecimal("100"));
System.out.println("张三转账结果:" + result);
});
Thread t2 = new Thread(() -> {
String result = transfer("account:002", "account:001", new BigDecimal("100"));
System.out.println("李四转账结果:" + result);
});
t1.start();
t2.start();
}
}
8.3 避免死锁
使用联锁时要注意避免死锁。Redisson的MultiLock会按照一定顺序获取锁,避免死锁:
@Service
public class DeadlockAvoidanceService {
@Autowired
private RedissonClient redissonClient;
/**
* 错误示例:可能死锁
*/
public void wrongWay() {
// 线程1:先锁A,再锁B
Thread t1 = new Thread(() -> {
RLock lockA = redissonClient.getLock("lockA");
RLock lockB = redissonClient.getLock("lockB");
lockA.lock();
System.out.println("线程1获取lockA");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
lockB.lock(); // 等待lockB,但lockB被线程2持有
System.out.println("线程1获取lockB");
lockB.unlock();
lockA.unlock();
});
// 线程2:先锁B,再锁A
Thread t2 = new Thread(() -> {
RLock lockA = redissonClient.getLock("lockA");
RLock lockB = redissonClient.getLock("lockB");
lockB.lock();
System.out.println("线程2获取lockB");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
lockA.lock(); // 等待lockA,但lockA被线程1持有
System.out.println("线程2获取lockA");
lockA.unlock();
lockB.unlock();
});
t1.start();
t2.start();
// 结果:死锁!
}
/**
* 正确示例:使用MultiLock避免死锁
*/
public void rightWay() {
Thread t1 = new Thread(() -> {
RLock lockA = redissonClient.getLock("lockA");
RLock lockB = redissonClient.getLock("lockB");
// MultiLock会统一管理,避免死锁
RLock multiLock = redissonClient.getMultiLock(lockA, lockB);
multiLock.lock();
System.out.println("线程1获取所有锁");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
multiLock.unlock();
System.out.println("线程1释放所有锁");
});
Thread t2 = new Thread(() -> {
RLock lockA = redissonClient.getLock("lockA");
RLock lockB = redissonClient.getLock("lockB");
RLock multiLock = redissonClient.getMultiLock(lockA, lockB);
multiLock.lock();
System.out.println("线程2获取所有锁");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
multiLock.unlock();
System.out.println("线程2释放所有锁");
});
t1.start();
t2.start();
// 结果:正常执行,不会死锁
}
}
9. 红锁(RedLock)
9.1 红锁的背景
红锁:Redis官方推荐的分布式锁算法,通过在多个独立的Redis实例上获取锁来保证高可用性。
普通的分布式锁有一个潜在问题:如果Redis主节点挂了怎么办?
场景分析:
客户端A在Redis主节点获取锁成功主节点挂了,还没来得及同步到从节点从节点被提升为新的主节点客户端B在新的主节点又获取了同一把锁结果:A和B都认为自己有锁!
红锁(RedLock)是Redis作者提出的解决方案:不依赖单个Redis节点,而是同时在多个独立的Redis节点上加锁。
原理:假设有5个独立的Redis节点
客户端尝试在所有节点上加锁只要在超过半数(3个)节点上加锁成功,就认为获取锁成功这样即使有2个节点挂了,锁依然有效
9.2 红锁配置
首先需要准备多个独立的Redis实例(不是主从,是完全独立的):
@Configuration
public class RedLockConfig {
/**
* Redis实例1
*/
@Bean
public RedissonClient redissonClient1() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://192.168.1.101:6379")
.setPassword("123456")
.setDatabase(0);
return Redisson.create(config);
}
/**
* Redis实例2
*/
@Bean
public RedissonClient redissonClient2() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://192.168.1.102:6379")
.setPassword("123456")
.setDatabase(0);
return Redisson.create(config);
}
/**
* Redis实例3
*/
@Bean
public RedissonClient redissonClient3() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://192.168.1.103:6379")
.setPassword("123456")
.setDatabase(0);
return Redisson.create(config);
}
/**
* Redis实例4
*/
@Bean
public RedissonClient redissonClient4() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://192.168.1.104:6379")
.setPassword("123456")
.setDatabase(0);
return Redisson.create(config);
}
/**
* Redis实例5
*/
@Bean
public RedissonClient redissonClient5() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://192.168.1.105:6379")
.setPassword("123456")
.setDatabase(0);
return Redisson.create(config);
}
}
9.3 红锁使用示例
@Service
public class RedLockService {
@Autowired
private RedissonClient redissonClient1;
@Autowired
private RedissonClient redissonClient2;
@Autowired
private RedissonClient redissonClient3;
@Autowired
private RedissonClient redissonClient4;
@Autowired
private RedissonClient redissonClient5;
/**
* 使用红锁处理重要业务
*/
public void importantBusiness() {
String lockKey = "important-business-lock";
// 在5个Redis实例上创建锁
RLock lock1 = redissonClient1.getLock(lockKey);
RLock lock2 = redissonClient2.getLock(lockKey);
RLock lock3 = redissonClient3.getLock(lockKey);
RLock lock4 = redissonClient4.getLock(lockKey);
RLock lock5 = redissonClient5.getLock(lockKey);
// 创建红锁
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3, lock4, lock5);
try {
// 尝试获取红锁
// 会尝试在5个实例上加锁,超过半数(3个)成功才算成功
boolean isLocked = redLock.tryLock(5, 30, TimeUnit.SECONDS);
if (isLocked) {
System.out.println("成功获取红锁,开始处理重要业务");
// 处理重要业务,比如:
// - 大额转账
// - 修改系统核心配置
// - 处理订单状态变更
// 模拟业务处理
processImportantBusiness();
System.out.println("重要业务处理完成");
} else {
System.out.println("获取红锁失败,无法处理业务");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放红锁
redLock.unlock();
System.out.println("释放红锁");
}
}
/**
* 模拟重要业务处理
*/
private void processImportantBusiness() {
try {
// 这里处理真正的业务逻辑
System.out.println("正在处理重要业务...");
Thread.sleep(3000);
System.out.println("业务处理成功!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 测试红锁的容错性
*/
public void testRedLockFaultTolerance() {
System.out.println("测试场景:5个Redis实例中有2个宕机");
String lockKey = "fault-tolerance-test";
// 假设实例4和5宕机了,只使用前3个
RLock lock1 = redissonClient1.getLock(lockKey);
RLock lock2 = redissonClient2.getLock(lockKey);
RLock lock3 = redissonClient3.getLock(lockKey);
// 创建红锁(只有3个实例)
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
try {
boolean isLocked = redLock.tryLock(5, 30, TimeUnit.SECONDS);
if (isLocked) {
System.out.println("即使有节点宕机,依然获取锁成功!");
// 3个节点全部成功,满足"大多数"原则
Thread.sleep(1000);
} else {
System.out.println("获取锁失败");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
redLock.unlock();
}
}
}
9.4 红锁 vs 普通分布式锁
对比项 | 普通分布式锁 | 红锁(RedLock) |
---|---|---|
可靠性 | 依赖单个Redis | 多个独立Redis,更可靠 |
性能 | 高(单次网络请求) | 较低(多次网络请求) |
复杂度 | 简单 | 复杂,需要多个Redis |
成本 | 低(1个Redis) | 高(多个Redis) |
使用场景 | 普通业务 | 金融、支付等关键业务 |
使用建议:
普通业务场景:使用普通分布式锁 + Redis主从 + 哨兵模式足够一般重要业务:使用普通分布式锁 + Redis集群模式极其重要业务:使用红锁,或考虑使用ZooKeeper性能敏感场景:不建议使用红锁,性能开销较大
实际案例:
电商库存扣减:普通分布式锁即可用户积分变更:普通分布式锁即可银行转账:建议使用红锁配置中心更新:建议使用红锁
总结
通过这个教程,我们学习了:
分布式锁的概念:解决分布式系统中的并发问题为什么需要分布式锁:单机锁无法跨JVM工作核心要求:互斥性、安全性、活性、高可用基本使用:lock、tryLock、unlock的正确用法可重入锁:同一线程可多次获取同一把锁公平锁:保证获取顺序,避免饥饿读写锁:提高读多写少场景的性能联锁:同时获取多个锁,避免死锁红锁:提供更高的可靠性保证
最佳实践建议:
始终设置锁的超时时间,防止死锁释放锁前检查是否是自己的锁根据业务场景选择合适的锁类型不要过度设计,普通场景普通锁就够了做好监控和日志,便于问题排查
记住:分布式锁是工具,理解业务场景选择合适的工具才是关键!