目录
原理:分布式锁原理
1-1 什么是分布式锁?
在并发编程中,锁是一种同步机制,用于控制多个线程或进程对共享资源的访问,确保在同一时间内只有一个线程或进程可以访问该资源。当这种机制应用于分布式系统中时,我们称之为分布式锁。分布式系统由多个相互通信、物理分隔的组件组成,它们共同工作以完成一项任务。在这样的系统中,确保跨不同节点的操作的互斥性变得尤为重要。
1-2 为什么需要分布式锁?
抢票场景:
在这张图片中,我们看到了一个典型的抢票应用场景,涉及到两个不同的Java虚拟机(JVM1和JVM2),它们分别运行着不同的Java服务(Java服务1和Java服务2)。这两个服务都试图修改同一份资源(票数)的状态。在这种情况下,我们需要保证在任何时刻,只有一个服务能够成功地修改票数,从而防止数据不一致的发生。这就是分布式锁需要被用到的场景。
为了详细解释这个需求,让我们假设JVM1和JVM2都试图将票数减少1。如果没有适当的同步机制,以下情况可能会发生:
- 数据竞争:如果两个服务几乎同时读取票数并试图更新它,它们可能基于同一初始值计算出新值。例如,如果最初有1张票,两个服务都读取到1张,然后都试图将其减少1,最终结果应该是-1张票,发生超卖。
分布式锁解决的问题:
- 互斥访问:分布式锁保证当一个服务正在修改票数时,其他服务不能执行修改操作。
- 顺序控制:分布式锁强制服务按顺序进行操作,第一个获得锁的服务先进行操作,完成后释放锁,然后第二个服务才能获得锁并进行操作。
在这个场景中,假设Java服务1首先获得分布式锁,然后减少票数,更新完毕后释放锁。这时Java服务2获得锁,并基于Java服务1更新后的票数再次减少。这样可以确保票数的每次减少都是基于最新的、正确的值,最终用户获得的票数是准确和一致的。这对于任何处理共享资源和必须避免数据不一致性的分布式应用来说都是至关重要的。
1-3 Redis作分布式锁的解决方案
方案1:最朴素方案
使用 Redis 的 setnx
指令,使用 setnx key value
的方式获取一个锁,
- 如果命令返回成功(即键之前不存在),则服务获取了锁,可以继续执行。
- 如果键已经存在,表示锁已经被另一个服务持有,当前服务需要等待或重试。
- 一旦服务完成其操作,它将删除或过期该键,从而释放锁。
缺点:
- 没有超时机制:最简单的
SETNX
实现没有超时机制,这意味着如果获得锁的服务崩溃或因其他原因未能释放锁,锁将永远不会被释放,导致其他服务永远无法获取锁,从而造锁泄漏。
方案2:为锁加过期时间
为了避免锁泄漏,通常会为锁设定一个过期时间(TTL),以确保即使出现服务崩溃或其他异常情况,锁也能在一段时间后自动释放,使得其他进程可以重新竞争获取锁。
Redis基于该场景考虑,可以使用 SET key value NX EX seconds
,来获取锁。
SET
是基本的设置键值对的命令。key
是要设置的键。value
是键key对应的值。NX
是一个条件选项,它指定操作必须检查键是否存在,只有键不存在时才进行设置。EX
是一个选项,后跟参数 seconds,用于设置键的过期时间,单位是秒。
这个命令在分布式锁的上下文中非常有用,因为它允许一个服务设置一个锁,但如果这个服务因任何原因未能释放锁(比如崩溃),锁将在指定的seconds秒后自动释放,这样其他服务就可以再次尝试获取锁了。这种机制避免了因为单个服务的故障而导致的锁永久性地被占用。
存在的问题:
- 锁过早释放:如果锁在业务操作完成前就过期了,那么其他进程或线程可能会认为锁已经可用,从而获得锁并开始执行它们的业务逻辑。这将导致多个进程或线程同时修改同一个资源,破坏了锁机制应有的互斥特性。
- 锁误释放问题:原本的进程在业务操作完成后可能会尝试释放锁,如果此时锁已经被其他进程获得,那么释放操作可能会意外地释放了其他进程持有的锁,进一步导致数据安全问题。
为了解上述问题,一般会采用以下策略:
- 锁续期:在业务处理时监控锁的剩余生存时间(TTL),如果接近过期,则对锁进行续期。Redisson客户端提供了这样的功能,称为“看门狗”。
1-4 浅析Redisson原理
以下图片引用自:程序员进阶
上述流程图展示了Redisson分布式锁的工作机制,我们可以根据图示来描述Redisson的工作原理:
-
锁请求:
当一个客户端(比如一个Java应用)需要获取锁时,它会向Redis服务器发送一个锁请求。 -
加锁尝试:
Redisson客户端会尝试通过执行Redis命令(如 SETNX 或 Lua脚本)在Redis中创建一个唯一的锁。这个锁是通过一个特定的键来表示的,通常与一个随机的值或者线程标识符结合,以保证锁的唯一性。 -
获取锁成功:
如果锁键不存在,这意味着没有其他客户端持有锁,当前客户端会成功设置锁键,并继续执行其业务逻辑。 -
获取锁失败:
如果锁键已存在,表明锁已被另一个客户端持有。在这种情况下,客户端可能会进入等待,定期重试获取锁。 -
锁续期(Watch Dog):
一旦客户端成功获取锁,Redisson内部的“看门狗”服务会启动。这个服务会定时(如每隔10秒)检查锁的持有时间,并在锁即将到期时自动对其进行续期,确保客户端在执行业务逻辑的过程中锁不会失效。 -
业务逻辑执行:
客户端执行它的业务逻辑。在此期间,如果业务逻辑执行的时间超过了锁的初始过期时间,由于“看门狗”服务的存在,锁仍然会被保持。 -
释放锁:
完成业务逻辑后,客户端会向Redis发送命令以释放锁。这通常是通过删除Redis中对应的键来实现的。
实践:使用 Redisson 实现分布式锁
步骤1:添加依赖项
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.17.5</version>
</dependency>
步骤2:配置Redis
# application.yml
spring:
redis:
host: your_redis_host
port: 6379
步骤3:创建Redisson配置类
@Configuration
public class RedissonConfig {
@Value("${spring.redis.host}")
private String redisHost;
@Value("${spring.redis.port}")
private int redisPort;
@Value("${spring.redis.password}")
private String redisPassword;
@Bean(destroyMethod="shutdown")
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://" + redisHost + ":" + redisPort)
.setPassword(redisPassword)
.setDatabase(0);
return Redisson.create(config);
}
}
步骤 4: 使用Redisson分布式锁
@Service
public class DistributedLockService {
@Autowired
private RedissonClient redissonClient;
public void performTaskWithLock() {
RLock lock = redissonClient.getLock("myLock");
try {
if (lock.tryLock(100, 10, TimeUnit.SECONDS)) {
try {
// 执行业务逻辑
} finally {
lock.unlock();
}
} else {
// 无法获取锁
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}