文章目录
一、分布式锁的由来和定义
在分布式系统中,多个进程可能会同时访问同一个临界资源,导致数据竞争问题。例如,在秒杀活动中,多个用户同时下单会导致库存扣减的并发冲突。为了解决这一问题,需要引入分布式锁,确保同一时刻只有一个进程能够访问共享资源,从而避免数据不一致问题。
分布式锁的实现方式有多种,包括基于数据库、Redis、ZooKeeper 等方案。其中,数据库实现方式虽然简单,但性能较低,适用于低并发场景。因此,在高并发的应用场景中,通常使用 Redis 和 ZooKeeper 实现分布式锁。
二、用数据库实现分布式锁(不推荐)
用数据库实现分布式锁比较简单,就是创建一张锁表。要锁住临界资源并对其访问时,在锁表中增加一条记录即可;删除某条记录就可释放相应的临界资源。数据库对临界资源做了唯一性约束,如果有访问临界资源的请求同时提交到数据库,数据库会保证只有一个请求能够得到锁,然后只有得到锁的这个请求才可以访问临界资源。
由于此类操作属于数据库 IO 操作,效率不高,而且频繁操作会增大数据库的开销,因此这种方式在高并发、对性能要求较高的场景中使用得并不多。
三、 通过 Redis 缓存实现分布式锁
前面提到库存作为临界资源会遭遇高并发的请求访问,为了提高效率,可以将库存信息放到缓存中。以流行的 Redis 为例,用其存放库存信息,当多个进程同时请求访问库存时会出现资源争夺现象,也就是分布式程序争夺唯一资源。为了解决这个问题,需要实现分布式锁。
一个进程持有锁后,就可以访问 Redis 中的库存资源,且在其访问期间其他进程是不能访问的。
如下图:假设有多个扣减服务用于响应用户的下单请求,这些服务接收到请求后会去访问Redis 缓存中存放的库存信息,每接收一次用户请求,就将 Redis 中存放的库存量减去 1。
超时时间的考虑
如果该进程长期没有释放锁,就会造成其他进程饥饿,因此需要考虑锁的过期时间,设置超时时间。
Redis 通过 SETNX(set if not exists)命令和 EXPIRE(设置过期时间)命令可以实现互斥访问。这里的超时时间需要考虑两方面问题。
- 资源本身的超时时间,一旦资源被使用一段时间后还没有被释放,Redis 就会自动释放该资源,给其他服务使用。
- 服务访问资源的超时时间,如果一个服务访问资源超过一段时间,那么不管这个服务是否处理完,都要马上释放资源给其他服务使用。
下单服务中的扣减操作属于核心操作,因此会用到第二种方式。如果到达规定时间,下单服务还没有处理完库存资源,就重新申请资源并且延长持有锁的时间。
代码示例(基于 Redisson 实现 Redis 分布式锁)
这里以 Redisson 为例介绍如何实现分布式锁。
public void testLock() {
Config config = new Config();
config.useSingleServer()