在传统单机业务系统中,咱们通常经过线程同步方法或同步代码块(Java)解决多线程并发场景资源竞争的问题,但当系统扩展到集群模式的分布式系统上时,须要实现不一样主机上多个进程间资源的竞争资源的协调,这时,单进程的锁失效,锁也要能支持分布式。数据库
分布式锁的特性
首先,类比单进程的锁,咱们来看下分布式锁要支持那些特性,才是一个可用的解决方案。
基本特性(能用)缓存
- 互斥性:须要保证锁只能被分布式系统中的某台服务器的某个线程获取。
- 不死锁:若是得到锁的线程发生崩溃而没有释放锁,须要保证锁能释放被其余线程获取。
高级特性(好用)服务器
- 可重入:获取锁的线程可屡次得到锁,避免死锁。
- 阻塞锁:线程阻塞的锁,简化客户端的实现。
- 高可用:提供得到锁和释放锁的HA。
- 锁性能:高效得到和释放锁。
分布式锁的实现方式
- 数据库:借助数据库实现分布式锁
- Redis:基于Redis的分布式锁
- Zookeeper:基于Zookeeper的分布式锁
借助数据库实现分布式锁
在分布式系统开始大规模应用前,因为数据库都是集中部署的,一些双机部署的系统要实现分布式锁,通常会想到借助数据库实现分布式锁。
好比在双机部署系统中,咱们要天天定时给客户发值班通知短信,因为两台服务器时间一致,会同时运行定时任务,但短信总不能发两条。多线程
针对这种状况,能够在数据库中设计一张表,记录某个日期短信是否发送了,表包括日期和短信发生状态两个字段,定时任务启动后,执行以下流程:并发
- 根据日期字段查询数据库,判断短信是否发送过了,若是没查询到记录,则插入一条数据,状态为todo。
- 尝试经过
select for update
语句,利用数据自带排他锁,锁定记录。若是执行成功则表示得到了锁,继续执行下一步;若是锁定不成功,则程序阻塞。
- 锁定成功后,先判断状态是否为todo,若是是,则执行短信发送任务;若是不是,说明短信已发送,rollback释放锁,直接返回。
- 更新记录状态为done,并经过commit释放锁。
以上是一个特殊的场景,从特殊推导出借助数据库实现锁的通常设计。分布式
-
数据库锁表(lock_table)结构性能
- ID:主键,自动生成
- lock_name:锁名称,锁的惟一标识
- lock_client:锁的客户端标识,用于重入时使用
- timestamp:锁最后一次更新时间(暂时无用)
- 初始化锁:向lock_table插入锁数据,若是已插入,则直接返回。
- 获取锁:利用数据库排他锁,经过
select * from lock_table where lock_name=xxx for update
获取锁。
- 更新锁:获取到锁后,更新lock_client为自身标识。
- 执行互斥业务:执行锁对应的业务逻辑。
- 释放锁:使用commit提交事务,释放锁。
以上步骤,实现了以下特性:线程
- 若是第三步获取锁失败,则系统会阻塞等待,实现了阻塞锁;
- select for update实现了互斥锁;
- lock_client字段实现可锁可重入;
- 若是客户端端口,事务自动rollback实现了不会死锁;
若是要实现高可用,则须要数据库自身支持HA;因为数据库锁开销比较大,锁的性能至关较差。设计
基于Redis的分布式锁
Redis分布式锁,主要经过Redis的setnx(SET IF NOT EXIST)结合缓存过时时间等特性来实现。code
以Spring的RedisTemplate为例,setnx对应发方法为:
Boolean setIfAbsent(K key, V value, Duration timeout)
若是Redis缓存中不存在此Key,则建立,并返回true;若是已经存在,则无动做,返回false。
相关设计点以下:
- 为了解决可重入问题,咱们把这里的value和数据库的lock_client作相同设计;
- 因为timeout的存在,能够解决死锁问题;
- 须要注意,若是互斥的业务逻辑,获取锁后,在timeout内未执行完,会致使锁被是否,因此获取锁以后,须要启动一个定时器,在业务执行完成前,按期去延长timeout,防止锁过时;
- 这是一把非阻塞锁,若是线程获取不到锁,须要自旋;
- 锁业务逻辑执行完后,经过删除Key来释放锁;
- 调用setIfAbsent,务必调用带timeout的重载方法,实现建立Key和设置timeout的原子操做,否则可能会出现建立Key后,客户端崩溃而为设置timeout,致使缓存永不过时。
基于Zookeeper的分布式锁
利用Zookeeper特性,有两种实现锁的方式。
- 建立节点的排他性:利用建立节点的排他性,多个进程竞争建立一个节点时,只有一个进程能成功得到锁;其余竞争此锁的进程,可经过监听节点的释放来获取锁。
- 临时有序节点:在同一个目录下,每一个进程建立本身的的节点,序号最小的节点得到锁,各进程排队得到锁。
原理都比较简单和直观,下面简要描述下临时有序节点方式的实现原理以下:
- 客户端要获取锁是,在Zookeeper指定目录下建立一个瞬时有序节点;
- 判断本身建立的有序节点是否目录下序号最小的,若是是最小的则得到锁,执行业务逻辑;
- 若是不是最小的,则监听目录下节点的删除事件,每次有节点删除后,判断自身是不是序号最小的,从而确认本身是否得到锁;
- 互斥业务逻辑执行完后,删除节点,释放锁。
相关设计点:
- 因为临时节点在客户端断开后自动删除,可解决死锁问题。
- 当自身节点的序号不是最小的时候,经过监听机制,一直等到自身节点序号为最小,可实现阻塞锁。
- 在建立节点是,客户端把自身信息写入节点,获取全部,经过节点信息判断,可实现锁重入。
- 因为ZK自己为集群部署,可解决单点问题,实现HA。