之前已经写过一篇关于分布式锁的小总结了,最近结合工作中实际遇到的情况进行了一些总结和思考,因此这里更多谈的是为什么要用分布式锁(我将给出更多实际的案例),其次才是怎么设计分布式锁。
回顾我们使用线程锁的原因,当多线程并发工作时,我们需要保证对共享变量正确的操作,以i++为例,假设有两个线程A、B,分别对共享i做++操作,使用锁的我们实际期望的是,假设当前i值为5,则A和B不会同时对i=5这个值进行++操作然后再重新赋给i。注意,这里我们可以换一种说法,把一次++操作看做是一个job,我们用锁的根本原因是希望不要重复的执行已经执行过的job,因为这意味着资源的浪费以及潜在的错误。用这个视角来看锁的问题,会发现去理解使用锁的场景,以及用哪种锁(乐观锁、悲观锁)都更为直观和清晰了。
比如我们现在的要用一个分布式的任务,对英语词典进行统计计数,记录每A-Z开头的单词分别有多少个,我们采用的是遍历的方式,将最终结果存到数据库里,当多机任务并发时,我们就可以设计一个这样的分布式锁,DB锁:
create table scan_lock(
first varchar(10),
second varchar(10),
ip varchar(30),
locked int,
gmt_create timestamp,
gmt_updated timestamp,
PRIMARY KEY (first , second,ip)
)
其中first是指首字母 , second 是 第二个字母,每次去对词典扫描的时候,首先尝试获取分布式锁,获取锁的逻辑过程为,按gmt_updated排序,找到当前所有任务已经扫描的到的最后的位置(假设为fa打头的字母,locked状态为锁住且锁owner不是本机),则尝试写入一个fb开头的锁 (insert操作),若写入成功,在select for update 获取这个锁。这里我们完全不必觉得采用db做操作会影响性能,因为我们使用的锁相对于整个扫描词典的扫描和统计逻辑西能影响是微乎其微的。另外为什么这里要加入ip这一项呢,假设这个扫描任务分成了多段逻辑,其中每一段都需要校验是否获取了该分布式锁,那么需要保证该分布式锁是可重入的,ip保证了只有锁的owner机能够重入该锁。
另外注意我们这里锁设计,我们并不保证一台获取锁的机器宕机后重启后能够将它之前未完成的扫描任务给完成掉,比如,获取fa锁的机器重启了,后来扫描任务已经到了fm,那么重启后的机器获取到的锁时fn,之前的fa的扫描任务完成与否我们并不能通过锁来给予保证,若要达到这一点,我们需要再加入字段比如 full_scaned 字段,每次获取锁时检查之前的锁有没有full_scaned字段为false的,若有则进行重扫来完成。
锁我们通常会设置失效时间,失效时间可以直接加在db的字段里,也可以在代码中控制,我这里对字典扫描的锁没有加入失效时间,可以通过gmt_updated 配合代码进行判断锁超时的处理。
总结:
这里给出的例子是词典扫描,其实核心在于让分布式的机器完成的任务进行隔离,比如分布式缓存更新,或者是一些库存交易处理等问题,我们都可以采用类似这样的分布式锁。这里我使用的是db锁,没有使用redis或者zookeeper锁,因为db是每个项目必备的组件,实际工程中为了尽量减少依赖我们不会加入那么多的组件,而是尽可能的对现有能力进行复用,比如你有memcache或者别的缓存中间件,你也可以使用他们来完成锁服务,不一定是db或者redis,比如你已经有了一个分布式的配置中心,你也可以采用当中的一些特性来完成锁服务。