说明
分布式锁一般有三种实现方式:1. 数据库乐观锁;2. 基于 Redis 的分布式锁;3. 基于 ZooKeeper 的分布式锁。本文介绍基于 Redis 实现分布式锁。
关于实现分布式锁的三种方式,可以参考之前的博文: 分布式锁简单入门以及三种实现方式介绍
本文中的分布式锁通过注解的方式实现,可以自定义重试次数,锁超时时间等。
可靠性
首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:
- 互斥性。在任意时刻,只有一个客户端能持有锁。
- 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
- 具有容错性。只要大部分的 Redis 节点正常运行,客户端就可以加锁和解锁。
- 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
并发问题
在没有使用分布式锁之前,如果有两个线程并发操作同一条数据,可能会出现并发问题(脏读、不可重复读、幻读)。
举个例子,下面是一段将数据的值 +1 的代码:
public void addNum() {
try {
// 查询数据
TE te = teManager.findOne(1L);
System.out.println("start:" + te.getNum());
// 模拟耗时操作
Thread.sleep(2000);
// 值加1
te.setNum(te.getNum() + 1);
// 保存数据
teManager.save(te);
System.out.println("end:" + te.getNum());
} catch (Exception e) {
}
}
有两个线程同时访问:
15:56:06,241 INFO [stdout] (default task-39) start:0
15:56:07,548 INFO [stdout] (default task-40) start:0
15:56:08,242 INFO [stdout] (default task-39) end:1
15:56:09,555 INFO [stdout] (default task-40) end:1
可以看到 task-39 和 task-40 两个线程读取到的值均是 0,执行两次后,值为 1 ,并不是想要的 2。
具体执行的情况如下:
task-39 | task-40 |
---|---|
读取 num,num = 0 | |
读取 num,num = 0 | |
num = num + 1 ,num 值变为 1 | |
num = num + 1 ,num 值变为 1 | |
将 1 存入库中 | |
将 1 存入库中 |
如果是单机部署,那么可以用多线程的 18 般武艺来解决并发问题,比如加锁等,改动如下:
public synchronized void addNum() {
try {
// 查询数据
TE te = teManager.findOne(1L);
System.out.println("start:" + te.getNum());
// 模拟耗时操作
Thread.sleep(2000);
// 值加1
te.setNum(te.getNum() + 1);
// 保存数据
teManager.save(te);
System.out.println("end:" + te.getNum());
} catch (Exception e) {
}
}
加了一个关键字 synchronized 就能解决单机下的并发问题,结果如下:
16:09:49,539 INFO [stdout] (default task-46) start:0
16:09:51,541 INFO [stdout] (default task-46) end:1
16:09:51,592 INFO [stdout] (default task-47) start:1
16:09:53,597 INFO [stdout] (default task-47) end:2
task-46 | task-47 |
---|---|
读取 num,num = 0 | |
num = num + 1 ,num 值变为 1 | |
将 1 存入库中 | |
读取 num,num = 1 | |
num = num + 1 ,num 值变为 2 | |
将 2 存入库中 |
如果集群部署的话,这种方式就无法解决了(每台机器的 JVM 无法共享,无法加锁),只能使用分布式锁。
分布式锁的实现
pom.xml:
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>1.8.4.RELEASE</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.10.2</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.8.9</version<