Redis分布式锁实现及使用
Redis在业内解决秒杀等业务场景有非常广的应用,如何设计实现一个分布式锁是解决超卖、一人一单问题非常重要…
分布式锁
分布式锁是控制分布式系统之间同步访问共享资源的一种方式。 在 分布式系统 中,常常需要协调他们的动作。 如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证 一致性 ,这个时候,便需要使用到分布式锁。
全局ID生成器
实现一个分布式锁,在分布式系统中用来生成全局唯一ID的工具,需要满足唯一性、高可用、高性能、递增性、安全性。
常见的一些解决方案:
- 数据库自增AUTO_INCREMENT
如果使用数据库自增ID就存在id的规律性太过明显,而且也会受到单表数据量的限制。
-
UUID
-
Redis生成ID
Redis提供了incr和incrby这样的自增原子命令,能够保证生成的ID肯定是唯一有序的。
- 雪花算法snowflake
算法算法的有点是高性能,低延迟,按时间有序,缺点主要是需要独立的开发和部署,依赖机器的时钟。
一人一单实现
超卖问题
在单线程的环境下,进行下单是没有问题的,但是在多线程的条件下,很容易会出现超卖的现象,也就是典型的多线程并发安全问题。
解决方案:
-
悲观锁 :认为线程并发安全问题一定发生,因此在操作数据之前需要先获取锁,确保线程串行执行,常见的比如:synchronized、Lock都属于悲观锁。
-
乐观锁:认为线程安全问题不一定发生,不对它进行加锁,只是在更新数据时判断是否有其他线程进行修改,只有在没有其他线程修改时才可以更新数据
乐观锁的实现:
id | stock | version |
---|---|---|
12 | 1 | 2 |
update stock set stock = stock - 1 where id = 12 and version = 2;
如果在更新过程中,version字段发生改变的话,就直接拒绝更新。
悲观锁的性能一般、乐观锁性能好但是存在成功率低的问题。
一人一单
当我们的项目部署到单机情况下,可以通过加锁来解决并发条件下线程安全问题,但是当我们的项目部署到nginx,或者使用集群进行部署实现负载均衡,这样单挑单机的锁不能解决这个问题
所以我们需要解决用户下单时重复下单的问题或者单张优惠卷只能使用到一次。
分布式锁
分布式锁:需要满足分布式系统或集群多进程下可见并且互斥的锁。
需要满足多进程可见、互斥、高可用、安全性、高性能
分布式锁需要实现多进程之间互斥,常见的三种方式有以下三种:
Redis setnx实现分布式锁
在Redis中setnx命令是具有原子性的,我们可以通过setnx获取一把锁,设置过期时间从而可以释放锁。
上面的一个设计流程会有什么问题呢?
当多线程情况下,如果Thread1出现业务堵塞、超时,分布式锁会自动释放,然后Thread2可以获取到分布式锁,Thread1超时会释放掉锁,Thread2还没有完成执行,下一线程又会获得到这把分布式锁,也有可能出现线程安全问题。
解决方案:采用线程标识来识别是否可以是否分布式锁
- 在分布式锁超时释放前,需要判断当前这把锁是否是当前线程获取的,如果是已经超时释放掉,那就不允许释放当前这把锁。
这样就可以解决多线程下提前释放掉锁的问题,但是在Java代码中是很难保证释放锁操作的原子性的,所以我们需要引入lua脚本来编写释放分布式锁的代码。
-- 比较线程标示和锁的标识是否一致
if (redis.call('get', KEYS[1]) == ARGV[1]) then
-- 释放锁 del key
return redis.call('del', KEYS[1])
end
return 0
上述分布式锁存在不可重入、不可重试、超时释放、主从一致性,Redisson就实现了各种分布式锁,包括可重入锁、联锁、红锁、读写锁、信号量、闭锁等…