一个啤酒肚,穿着格子衬衫,发际线严重后退的中年男人,手里拿着保温瓶,下着MacBook,正向你走来手臂。师级别。采访开始,开门见山。采访者:你参与过秒杀系统的设计吗?我:没有,我一般开发后台管理系统、OA办公系统、内部管理系统。我从来没有开发过秒杀系统。采访者:嗯……,小伙子很老实。今天先过来,等有消息我再联系你。后面有消息吗?你什么时候主动联系我的?说真话的被拒,背八股作文的反而被录取了。好吧,让我看看一登是怎么总结秒杀系统的八股文的。我:参与过秒杀系统,独立负责秒杀系统的架构设计(【狗头】是的,都是我设计的)。面试官:对,这样我就可以继续提问了。你们在设计秒杀系统的时候,是怎么防止产品超卖的?比如活动只有一部iPhone,最后卖了100部,肯定不行,平台亏本。我:肯定是加锁的,但是由于秒杀系统请求量大,一般都是用分布式集群。Java自带的Synchronized和ReentrantLock锁只能在单机系统中使用,这时候就需要分布式锁了。面试官:您提到了分布式锁,分布式锁的作用是什么?八足作文就这样开始了。我:我觉得分布式锁主要有两个作用:保证数据的正确性:比如:防止闪购时商品超卖,表单重复提交,接口幂等。避免重复处理数据:比如调度任务在多台机器上重复执行,当缓存过期时所有请求加载到数据库。总结八股文,不得不说是一盏灯。采访者:小伙子总结的很全面。你知道设计分布式锁需要哪些特性吗?我:我觉得分布式锁应该具备以下特点:互斥:一次只能有一个线程获取锁。可重入:线程获得锁后,可以再次获得锁,避免死锁。高可用:当少数节点挂掉时,仍然可以对外提供服务。高性能:实现高并发和低延迟。支持阻塞和非阻塞:Synchronized是阻塞的,ReentrantLock.tryLock()是非阻塞的支持公平锁和非公平锁:Synchronized是非公平锁,ReentrantLock(booleanfair)可以创建公平锁面试官:小子,有东西。如何设计分布式锁?我:有几个常用的工具可以实现分布式锁。例如:关系型数据库(例如:MySQL)、分布式数据库(例如:Redis)、分布式协同服务框架(例如:zookeeper)使用MySQL实现分布式锁比较简单。创建表:CREATETABLE`distributed_lock`(`id`bigintunsignedNOTNULLAUTO_INCREMENTCOMMENT'主键ID',`resource_name`varchar(200)NOTNULLDEFAULT''COMMENT'资源名(唯一索引)',PRIMARYKEY(`id`),UNIQUEKEY`uk_resource_name`(`resource_name`))ENGINE=InnoDBDEFAULTCHARSET=utf8COMMENT='分布式锁';获取锁时,插入一条记录。插入成功表示获取到锁,插入失败表示获取锁失败。INSERTINTOdistributed_lock(`resource_name`)VALUES('resource1');当锁被释放时,删除这条记录。DELETEFROMdistributed_lockWHEREresource_name='resource1';实现比较简单,但不能用于实际生产。有几个问题没有解决:这个锁不支持阻塞,插入失败会立即返回。当然可以使用while循环直到插入成功,但是自旋也会占用CPU。这个锁是不可重入的,已经获得锁的线程再次插入会失败。我们可以添加两列,一列记录获取锁的节点和线程,另一列记录锁的数量。获取锁,次数加1,释放锁,次数减1,次数为0时删除锁。这个锁没有过期时间。如果业务流程失败或者机器宕机,锁没有释放,锁会一直存在,其他线程将无法获取到锁。我们可以添加一个锁过期时间的列表,然后启动一个异步任务扫描并删除过期时间大于当前时间的锁。就是这么麻烦,看看优化后的锁长啥样:CREATETABLE`distributed_lock`(`id`bigintunsignedNOTNULLAUTO_INCREMENTCOMMENT'主键ID',`resource_name`varchar(200)NOTNULLDEFAULT''COMMENT'Resourcename(uniqueindex)',`owner`varchar(200)NOTNULLDEFAULT''COMMENT'Lockholder(machinecode+threadname)',`lock_count`intNOTNULLDEFAULT'0'COMMENT'LockTimes',`expire_time`datetimeNOTNULLDEFAULTCURRENT_TIMESTAMPCOMMENT'Lockexpirationtime',PRIMARYKEY(`id`),UNIQUEKEY`uk_resource_name`(`resource_name`))ENGINE=InnoDBDEFAULTCHARSET=utf8COMMENT='分布式锁';这应该是完美的,对吧?这不,还有一个问题:业务逻辑还没处理完,锁过期了怎么办?如果我们将锁过期时间设置为6秒,正常情况下业务逻辑可以在6秒内处理完,但是当JVM发生FullGC或者调用第三方服务出现网络延迟时,业务逻辑一直没有处理完还没处理完,锁就过期了,被删除掉了,然后锁被其他线程获取了,不就出问题了吗?这里引入另一个知识点“锁更新”:在获取锁的同时,启动一个异步任务,每次业务执行到三分之一的时间,即6秒中的第2秒,会自动延??长锁过期时间将继续延长至6秒,以保证在业务逻辑处理完成之前锁不会过期。面试官:小伙子,分布式锁你懂的。我还想继续问一下,MySQL在生产中很少使用分布式锁,因为MySQL的并发性能跟不上。刚刚提到Redis也可以实现分布式锁,你知道怎么实现吗?我当然知道整套八卦文我得背下来。我:用Redis实现分布式锁和用MySQL类似。它还需要解决实施过程中遇到的各种问题,但解决方案略有不同。最简单的获取锁的方法://1.获取锁redis.setnx('resource_name1','owner1')//2.释放锁redis.del('resource_name1')当“resource_name1”不存在时,set成功,即获取锁成功。但是需要加上一个过期时间,防止锁被释放。//1.获取锁redis.setnx('resource_name1','owner1')//2.增加锁过期时间redis.exprire('resource_name1',6,TimeUnit.SECONDS)引入了一个新问题,两条命令不是Atomic的,在设置过期时间前获取锁后可能会down,怎么办?好办,Redis2.6.12之后,提供了复合命令:redis.set('resource_name1','owner1',"NX""EX",6)还有一个问题,当释放锁的时候,锁不判断线程的持有者可能会释放其他线程持有的锁。这不可能。你可以这样做://释放锁if('owner1'.equals(redis.get('resource_name1'))){redis.del('resource_name1')}可以这样做吗?还没有,因为get和del这两个命令不是原子操作,需要引入Lua脚本将这两个命令打包成一个发给Redis执行:Stringscript="ifredis.call('get',KEYS[1])==ARGV[1]thenreturnredis.call('del',KEYS[1])elsereturn0end";redis.eval(script,Collections.singletonList('resource_name1'),Collections.singletonList('owner1'))这样的总公司行吗?还没有,还有一个“锁更新”的问题没有解决。更简单的,Redis客户端Redisson已经为我们实现了续约功能,叫做“看门狗”(watchdog),当我们调用lock时自动唤醒“看门狗”。面试官:小伙子,你真好。能谈谈如何使用zookeeper实现分布式锁吗?我:zookeeper使用的是树形节点,类似linux的目录文件结构,同一目录下的节点名不能重复。节点有四种类型:持久节点:一旦创建,除非手动删除,否则它们将永久存储在服务器上。临时节点:生命周期与客户端绑定。如果客户端断开连接,该节点将被自动删除。持久顺序节点:特性与持久节点相同,只是在节点名称后附加了一个自增序号。临时顺序节点:特性与临时节点相同,只是在节点名称后附加了一个自增序号。zookeeper还有一个监听-通知机制,客户端可以在资源节点上创建监听事件。当节点发生变化时,会通知客户端,客户端可以根据变化进行相应的业务处理。我们可以利用临时时序节点的特性来创建分布式锁,分为以下三个步骤:在resource/resource1目录下创建一个临时时序节点节点,获取/resource1目录下的所有节点。如果当前节点序号最小,则说明加锁成功。如果不是,即watch监控序号最小的节点实现逻辑很简单。下面分析一下zookeeper实现分布式锁的优势:由于创建的临时节点在断开连接后会自动删除,所以不需要设置锁超时时间,也不需要考虑释放和更新锁,因为保存了创建者信息在节点上,锁也支持重入。既然节点可以被监控,那么它也可以被屏蔽。面试官:小伙子,升职加薪的机会是留给你这种人的。工资翻倍,明天来上班。总结:虽然关于分布式锁的知识点很多,但是都在这张图里做了总结。欢迎点赞收藏转发评论。
