现在的微服务系统,由于系统都是由多个微服务组成的,相比较于传统的应用系统,锁的实现就变的复杂了很多,单体应用的锁也不适用与分布式系统,下面就介绍几种分布式锁的实现方式,常见的分布式锁的实现方式有数据库分布式锁,redis分布式锁,zookeeper分布式锁;
一、数据库分布式锁:
1、利用数据库记录的唯一性实现分布式锁:
新建一张表locks,method_name字段代表要锁住的方法名,他是一个unique_key,当一个线程要获取方法上的锁时,就像数据库插入一条记录,如果是第一个获取锁时,因为数据库没有还没有记录,则返回插入记录成功,获取锁成功,此时如果第二个线程去获取锁,做插入操作,由于数据库里面已经有一条记录,所以插入失败,获取锁失败,此时便实现了分布式锁:
CREATE TABLE locks(
id INT NOT NULL AUTO_INCREMENT,
method_name VARCHAR(100),
`desc` VARCHAR(100),
create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
modify_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `udx_method_name`(`method_name`)
)
虽然用上面的数据库记录的唯一性能实现分布式锁,但是存在几个问题,我们还需要进行优化:
1.1、单节点的问题,单数据库如果挂了怎么办?
答:建立2个数据库,一主一从,从数据库同步主数据库的记录,如果主数据挂了,切换到从数据库;
1.2、非阻塞问题,现在是插入数据库失败(获取锁失败),直接报错,不是阻塞的;
答:程序里面做一个while循环,如果插入失败,sleep一段时间再重试,此时的问题是要将异常处理当作业务来做,个人觉得不是很合适;
1.3、没有失效时间;
答:数据库增加一个失效时间字段,启动一个线程单独清理已失效的锁记录;
1.4、非重入锁;
答:增加主机信息和线程信息、获取锁的次数(count),每次插入记录之前先按主机信息和线程信息查询表,如果有记录,直接将锁分配给他,同时将count加1;
2、利用mysql的排它锁实现分布式锁:
利用mysql的行锁 for update实现分布式锁;
select method_name from locks for update;
使用for update 行锁后,其它需要获取锁的线程就需要等待手动的connection.commit()行锁的释放,所以此时是一个阻塞锁,但是无法解决非冲入和没有失效时间的问题,同时还需要记住几点:第一只有method_name是唯一索引的时候才会使用行锁,否则会使用表锁,还有一种情况就是由于表里面数据较少,就算method_name是唯一索引,由于mysql底层的搜索引擎的优化,也会出现不使用索引而走全表扫描的情况,第二就是如果锁的数量特别大,大家都占着行锁不释放的时候,可能导致数据库连接池不够用,数据库宕机的情况;
二、缓存分布式锁:
这里基于redis为例:
redis有一个命令setnx命令,这里的意思是如果key存在,则什么都不操作返回0,如果key不存在,则插入key-value成功返回1,可以将method_name作为key,主机信息和线程等信息作为value,当插入成功返回1表示获取锁成功,当插入失败返回0则表示获取锁失败,由于现在的redis都是集群部署的,所以解决了单点登陆的问题,非阻塞问题也可以在程序里面用while循环解决,像数据库一样,用value存储主机信息和线程信息解决可重入问题,关于失效时间,redis可以为key设置失效时间,自动清理删除已经失效的key,释放锁。(这里有一个问题,失效时间设置为多少合适呢?设置短了业务还没执行完就释放锁会导致线程安全问题,设置时间长了会导致其他线程需要等待的时间过长,伤脑筋)但是缓存分布式锁的效率肯定要比数据库分布式锁高;
三、zookeeper实现分布式锁:
zookeeper实现分布式锁很简单,就是在该方法对应指定节点的目录下,创建一个临时有序节点,获取锁的方式:
第一步:在指定节点下面创建临时有序节点,并返回该节点下所有的节点信息;
第二步:如果自己是当前所有节点中最小的节点,则获取锁;
第三步:如果自己不是当前所有节点中最小的节点则查询创建节点前面的一个节点,并在前一个节点上创建监视点(watch),当前一个节点释放锁了,会删除节点并触发监视点,然后重新执行第二步;
由于zookeeper一般是集群部署的,所以不存在单点登陆的问题,zookeeper是一个可阻塞的锁的实现,通过添加监视点实现,重入问题同redis,失效时间问题:因为创建的是临时有序节点,所以当客户端与zookeeper断开时,那么这个临时节点就会自动删除(相当于释放锁),可能有人会问假入是网路抖动连接不上怎么办?这里zookeeper有各种重连的策略,只有重连失败后才会删除节点,这里就需要用户根据自己的需求设置合适的重连策略;
三种方案对比:
从理解的难度上:
zookeeper > redis >数据库(理解难度从难到易)
从分布式锁的可靠性:
zookeeper > redis > 数据库(可靠性从高到低)
从性能上排名:
redis > zook > 数据库(性能由高到低)