防重复下单与超卖问题
防止秒杀重复下单
在Redis中记录一个hash值,用户每次秒杀,值加1,是否大于1作为判断
使用分布式锁解决超卖的问题
分布式锁
为了保证一个方法 或属性在高并发情况下的同一时间只能被同一个线程执行,在传统单体应用单机部署的情况下,可以使用java并发处理相关的API(ReentrantLock或Synchronized)进行互斥控制,在单击环境汇总,java中提供了很多并发处理的API。但是,随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程。多进程并且分布在不同机器上,这使原单击部署情况下的并发控制锁策略失效,单纯的java Api并不能提供分布式的能力。为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问 ,这就是分布式锁要解决的问题
分布式锁应该具备的条件
在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行
高可用的获取锁与释放锁
高性能的获取锁与释放锁
具备可重入的特性
具备锁失效机制,防止死锁
具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败
分布式的实现的方式
1、基于数据库的实现方式
基于数据库的实现的核心思想是:在数据库中创建一个表,表中包含方法名等字段,并在方法名字端上创建唯一索引,想要执行某个方法,就使用这个方法名向表中插入数据,成功插入则获取锁,执行完成后删除对应的行数据释放锁。
数据库实现有以下几个问题:
这把锁强依赖数据库可用性,数据库是一个单点,一旦数据库挂掉,会导致业务不可用
这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得这把锁
这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作
这把锁是非重入的,同一个线程没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了
基于数据库排他锁
除了可以通过增删操作数据表中的记录以外,其实还可以借助数据库中自带的锁来实现分布式的锁。我们还用刚刚创建的那张数据库表。可以通过数据库的排它锁来实现分布式锁。
在查询语句后面增加 for update,数据库在查询过程中给数据表增加排他锁(InnoDb引擎在加锁的时候,只有通过索引进行检索的时候才会使用行级锁,否则会使用表级锁。我们希望使用行级锁,就要给method_name添加索引,这个索引一定要创建成唯一索引,否则会出现多个重载方法之间无法同时访问的问题)当某条记录被加上排他锁之后,其他线程无法再在该行记录上增加排他锁。
我们可以认为获得排他锁的线程即可获得分布式锁,当获取到锁之后,可以执行方法的业务逻辑,执行完方法之后,在通过以下方法解锁
通过connection.commit()操作来释放锁
这种方法可以有效的解决上面提到的无法释放锁和阻塞锁的问题。
阻塞锁? for update 语句会在执行成功后立即返回,在执行失败时一直处于阻塞状态,直到成功
锁定之后服务宕机,无法释放?使用这种方式,服务宕机之后数据库会把自己释放掉
但是还是⽆法直接解决数据库单点问题。
这⾥还可能存在另外⼀个问题,虽然我们对 method_name 使⽤了唯⼀索引,并且显示使⽤ for update 来使⽤⾏级锁。但是,MySql会对查询进⾏优化,即便在条件中使⽤了索引字段,但是否使⽤索
引来检索数据是由 MySQL 通过判断不同执⾏计划的代价来决定的,如果 MySQL 认为全表扫效率更⾼, ⽐如对⼀些很⼩的表,它就不会使⽤索引,这种情况下 InnoDB 将使⽤表锁,⽽不是⾏锁。如果发⽣这
种情况就悲剧了。。。
还有⼀个问题,就是我们要使⽤排他锁来进⾏分布式锁的lock,那么⼀个排他锁⻓时间不提交,就会占
⽤数据库连接。⼀旦类似的连接变得多了,就可能把数据库连接池撑爆
数据库实现分布式的缺点:
会有各种各样的问题,在解决问题的过程中会使整个方案变得越来越复杂
操作数据库需要一定的开销,性能问题需要考虑
使用数据库的行级锁并不一定靠谱,尤其是当我们的锁表并不大的时候(有可能锁住整张表)
基于Redis的实现方式
redis实现分布式的优点:
1、Redis有很高的性能
2、Redis命令对此支持较好,实现起来比较方便
命令介绍
1、SETNX
SETNX key val : 当且仅当key不存在时,set一个key为val的字符串,返回1,若key存在,则什么都不做,返回0
2、expire
expire key timeout : 为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁。
3、delete
delete key : 删除key
使用redis分布式的时候,主要就会使用到这三个命令
实现思想
1、获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间自动释放锁,所得value值为一个随机生成的UUID,通过此在释放锁的时候判断
2、获取锁的时候还设置一个获取超时时间,若超过这个时间则放弃获取锁
3、释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行所释放
总结
可以使用缓存来替代数据库来实现分布式锁,这个可以提供更好的性能,同时,很多缓存服务器都是集群部署的,可以避免单点问题。并且很多缓存服务都提供了可以用来实现分布式的方法,比如Tair的put ⽅法,redis的setnx⽅法等。并且,这些缓存服务也都提供了对数据的过期⾃动删除的⽀持,可以直接设置超时时间来控制锁的释放
使用缓存实现分布式锁的优点
性能好,实现起来较为方便
使用缓存实现分布式锁的缺点
使用缓存实现分布式锁的优点
通过超时时间来控制锁的失效时间并不是十分的靠谱
基于ZooKeeper的实现方式
实现分析
ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录数结构,规定同一个目录下文件名不能重复。基于ZooKeeper实现分布式锁的步骤如下:
创建一个目录 mylock
线程A想获取锁就在mylock目录下创建临时顺序节点
获取mylock目录下的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁
线程B获取锁节点,判断自己不是最小节点,设置监听比自己小的节点
线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁