什么是分布式锁
分布式锁是一种用于在分布式系统中协调多个客户端对共享资源的访问的同步机制。目标是确保在分布式环境中,同一时间只有一个客户端能够访问或修改某个共享资源,从而避免数据不一致或冲突。
分布式锁的使用场景
- 资源分配:确保共享资源(如数据库连接、线程池)在分配时不会被多个客户端同时占用。
- 数据同步:保证多个客户端在访问或修改同一数据项时的一致性和正确性。
- 事务控制:协调跨多个服务或数据库的操作,确保事务的原子性和一致性。
- 任务调度:避免任务重复执行或遗漏执行,确保任务按预期顺序执行。
- 高并发控制:在抢购、秒杀等高并发场景中,确保商品不被超卖,库存保持一致。
- 避免重复操作:防止多个客户端重复执行同一操作,如重复发送提醒信息。
demo测试
redis实现分布式锁
引入依赖
<!-- 集成web依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 集成redis依赖 -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.10.6</version>
</dependency>
设置配置信息
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
// 此处使用的是单机版本的redis
config.useSingleServer()
// 根据实际情况调整
.setAddress("redis://localhost:6379")
.setPassword("123456");
return Redisson.create(config);
}
}
编写测试案例
模拟场景
500人抢10张票
使用jmeter模拟用户请求。
无锁
测试代码
/**
* 模拟的总票数
**/
int ticketCount = 10;
@Resource
private RedissonClient redissonClient;
@GetMapping("/buyTicketWithoutLock")
public String buyTicketWithoutLock() {
String msg;
// 业务处理
if (ticketCount > 0) {
msg = Thread.currentThread().getName() + "抢到了第" + ticketCount + "张票,剩余票数:" + --ticketCount;
} else {
msg = "票已售罄";
}
System.out.println(msg);
return msg;
}
测试结果
出现超卖现象
http-nio-8080-exec-5抢到了第2张票,剩余票数:1
http-nio-8080-exec-33抢到了第1张票,剩余票数:0
票已售罄
票已售罄
http-nio-8080-exec-25抢到了第-1张票,剩余票数:-2
http-nio-8080-exec-19抢到了第-3张票,剩余票数:-4
http-nio-8080-exec-45抢到了第-2张票,剩余票数:-3
票已售罄
http-nio-8080-exec-49抢到了第0张票,剩余票数:-1
...
有锁
测试代码
@GetMapping("/buyTicketWithLock")
public String buyTicketWithLock() {
// 设置锁的名称,可实际情况调整,一般为固定前缀+唯一标识
RLock lock = redissonClient.getLock("ticketLock");
lock.lock();
String msg;
try {
// 业务处理
if (ticketCount > 0) {
msg = Thread.currentThread().getName() + "抢到了第" + ticketCount + "张票,剩余票数:" + --ticketCount;
} else {
msg = "票已售罄";
}
} finally {
lock.unlock();
}
System.out.println(msg);
return msg;
}
测试结果
剩余票数为0后后续请求都提示”票已售罄“
http-nio-8080-exec-3抢到了第9张票,剩余票数:8
http-nio-8080-exec-4抢到了第7张票,剩余票数:6
http-nio-8080-exec-1抢到了第8张票,剩余票数:7
http-nio-8080-exec-5抢到了第10张票,剩余票数:9
http-nio-8080-exec-7抢到了第6张票,剩余票数:5
http-nio-8080-exec-6抢到了第5张票,剩余票数:4
http-nio-8080-exec-8抢到了第3张票,剩余票数:2
http-nio-8080-exec-5抢到了第4张票,剩余票数:3
http-nio-8080-exec-9抢到了第1张票,剩余票数:0
http-nio-8080-exec-2抢到了第2张票,剩余票数:1
票已售罄
票已售罄
...
参考链接
常见问题及解决方案
如何保证锁的唯一性
- 问题描述:分布式锁需要确保在分布式环境下,同一资源只能被一个进程或线程访问和操作。如果锁不具备唯一性,就可能导致多个进程或线程同时访问和操作同一资源,从而引发数据安全问题。
- 解决方案:使用Redis的
SETNX
(Set if Not Exist)命令,该命令只能被一个客户端占坑,如果Redis实例存在唯一键(key),则无法再在该键上设置值。
如何避免锁的超时与死锁
- 问题描述:如果分布式锁没有设置超时时间,一旦持有锁的客户端出现异常或崩溃,锁就可能无法被释放,导致其他客户端无法获取锁,形成死锁。
- 解决方案:在加锁的同时设置超时时间,使用Redis的
EXPIRE
命令或SET
命令的EX
选项来设置锁的过期时间。
如何保证锁的创建与超时设置的原子性
- 问题描述:如果锁的创建和超时时间的设置不是原子性的,就可能在两个操作之间发生客户端崩溃,导致锁无法被释放且没有设置超时时间,从而引发死锁。
- 解决方案:使用Redis的
SET
命令的NX
和EX
选项来一次性完成锁的创建和超时时间的设置,确保这两个操作的原子性。
锁到期后如何续期
- 问题描述:如果分布式锁的过期时间设置得过短,而业务逻辑的执行时间又较长,就可能导致在业务逻辑完成前锁已经过期,从而引发数据不一致的问题。
- 解决方案:使用Redis客户端(如Redisson)的自动续期功能,通过注册一个定时任务来监听锁的状态,并在锁即将过期时对其进行续期。
如何避免释放错误的锁
- 问题描述:如果多个客户端使用相同的锁键(key)但不同的值(value)来尝试获取锁,就可能导致一个客户端错误地释放了另一个客户端持有的锁。
- 解决方案:在释放锁时,需要确保只有持有锁的客户端(即设置了正确value的客户端)才能释放锁。这可以通过在释放锁之前检查锁的value来实现。
集群环境下的分布式锁问题
- 问题描述:在Redis集群环境下,如果主节点发生故障,而锁的数据还没有同步到从节点,就可能导致其他客户端获取到相同的锁,从而引发数据不一致的问题。
- 解决方案:使用Redlock算法或类似的分布式锁算法,该算法通过向多个Redis节点请求加锁来确保锁的可靠性。当超过半数的节点成功加锁时,才认为获取锁成功。
总结
- 合理设置锁的超时时间:根据业务逻辑的执行时间合理设置锁的超时时间,确保在业务逻辑完成前锁不会过期。
- 优化网络环境:确保分布式系统中的节点之间的网络连接稳定、低延迟,以减少因网络问题导致的锁失效或续期失败。
- 监控Redis节点状态:定期检查Redis节点的运行状态,确保节点稳定可用。如果发生节点故障,及时进行处理和恢复。
- 升级和修复客户端问题:确保使用的Redis客户端版本是最新的,并且已经修复了已知的问题。如果客户端存在内存溢出、线程阻塞等问题,需要及时解决。