分布式锁
前情提要
基于分布式的 CAP (一致性、可用性、容错性)原理,系统在设计之初必须要考虑取舍,在互联网领域的绝大多数场景中,一般会牺牲强一致性来换取高可用性,只需要保证最终一致性即可,而这个最终一致的时间是用户可接受的范围即可。
为了保证最终一致性,一般会使用分布式事务、分布式锁等,分布式锁也有多种实现方案:
- 基于数据库
- 基于缓存
- 基于 zookeeper
分布式锁的实现目标:
- 可以保证在分布式部署的应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行;
- 必须是可重入锁,以避免死锁
- 可以根据业务决定是不是一个阻塞锁
- 有高可用的加锁解锁功能
- 加锁解锁操作的性能要高
基于数据库
基于表
在数据库中创建表,要锁住某个方法或资源时,在表中增加一条记录,想要释放时删除该记录。
创建表:
CREATE TABLE `methodLock` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`method_name` varchar(64) NOT NULL DEFAULT '' COMMENT '锁定的方法名',
`desc` varchar(1024) NOT NULL DEFAULT '备注信息',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存数据时间,自动生成',
PRIMARY KEY (`id`),
UNIQUE KEY `uidx_method_name` (`method_name `) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';
加锁操作:
insert into methodLock(method_name,desc) values (‘method_name’,‘desc’);
解锁操作:
delete from methodLock where method_name ='method_name';
缺点:
- 这把锁强依赖于数据库的高可用性;
- 锁没有失效时间,一旦解锁操作失败,将导致其他线程无法再获得锁;
- 这把锁只能是非阻塞的,因为数据的插入操作一旦失败就会返回报错。没有获得锁的线程并不会排队进入队列,想要再次获得锁就要再次出发获得锁操作;
- 这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为表中数据已经存在了。
解决方法:
- 使用分布式数据库;
- 使用定时任务,按时清理超时的锁;
- 通过 while 循环直到插入成功;
- 添加字段,记录当前获得锁的主机的信息和线程信息,然后在下次获取锁时先查询,如果信息匹配则分配锁。
基于数据库排它锁
基于 MySQL 中 InnoDB 自带的排它锁实现加锁:
public boolean lock(){
connection.setAutoCommit(false)
while(true){
try{
result = select * from methodLock where method_name=xxx for update;
if(result==null){
return true;
}
}catch(Exception e){
}
sleep(1000);
}
return false;
}
在查询语句后面增加for update
,数据库会在查询过程中给数据库表增加排它锁,当某条记录被加上排它锁之后,其他线程则无法再在该记录上增加排它锁。
InnoDB 引擎在加锁的时候,只有通过索引进行检索的时候才会使用行级锁,否则会使用表级锁。这里我们希望使用行级锁,就要给
method_name
添加索引,同时一定要创建成唯一索引,否则会出现多个重载方法之间无法同时被访问的问题,重载方法的话建议同时保存参数类型。
这时可以认为获得排他锁的线程即获得了分布式锁,当获取到锁之后,可以执行方法的业务逻辑,最后再进行解锁操作:
public void unlock(){
connection.commit();
}
这种方式可以有效解决上面提到的无法解锁和阻塞锁的问题。
- 阻塞锁:
for update
语句会在执行成功之后立即返回,在执行失败时一直处于阻塞状态,直到成功; - 锁定之后服务宕机造成的无法释放:这种方式下,服务宕机时数据库会自动释放锁。
但是仍然无法解决单点问题和可重入问题。
另一个问题:虽然对method_name
使用了唯一索引,并且显式使用for update
来使用行级别锁。但是,MySQL 会对查询进行优化,即便在条件中使用了索引字段,但是否使用索引来检索数据仍然是由 MySQL 通过判断不同执行计划的代价来决定的,如果 MySQL 自认为扫表效率更高,比如对一些很小的表,则不会使用索引,这是将会使用表锁而不是行锁。。。
同时,我们要使用排它锁来进行分布式锁的 lock,那么一个排它锁长时间不提交就会占用数据库连接,连接过多则造成数据库不可用。
基于缓存
基于类似 Redis、Memcached、Tair等缓存时,性能会表现更好。
比如在 Tair 中使用TairManager.put
进行加锁和解锁操作:
public boolean trylock(String key) {
ResultCode code = ldbTairManager.put(NAMESPACE, key, "This is a Lock.", 2, 0);
if (ResultCode.SUCCESS.equals(code))
return true;
else
return false;
}
public boolean unlock(String key) {
ldbTairManager.invalid(NAMESPACE, key);
}
同样会存在的问题:
- 锁没有失效时间;
- 只能是非阻塞的,无论成功失败都会直接返回;
- 是非重入的,一个线程获得锁之后,在释放之前,都无法再获得锁,因为使用的 KEY 在 缓存中已存在,无法再次进行 put 操作。
解决方式:
- put 时设置过期时间;
- while 重复执行知道成功;
- 保存主机和线程信息。
但是失效时间设计为多久合适呢?太短的话还没执行完就自动释放了,太久则浪费时间。
基于 ZK
基于 ZK 临时有序节点来实现分布式锁。
每个客户端对某个方法加锁时,在 ZK 中的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。当释放锁的时候,只需要将这个节点删除即可。同时可以避免因宕机而造成的死锁。
是否能解决其他实现遇到的问题:
- 可以有效解决无法释放问题,因为在创建锁的时候,客户端会在 ZK 中创建一个临时节点,一点客户端宕机则 session 连接断开,这时临时节点会自动删除,其他客户端会再次获得锁。
- 阻塞锁:客户端通过在 ZK 中创建顺序节点,并且在节点中绑定监听器,一旦节点有变化,ZK 会通知客户端,客户端可以检查自己创建的节点是不是当前所有节点中需要最小的,如果是,那么自己就获得锁。
- 可重入:客户端在创建节点时同时保存主机与线程信息,下次想要获取时和当前最小节点中的信息进行对比即可,相同则获得锁,不同在创建一个新的节点来进行排队。
- 单点:ZK 本身就作为高可用集群部署。
同时可以直接使用 ZK 的三方库 Curator 客户端,这个客户端中已经封装了一个可重入锁服务:
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException{
try{
return interProcessMutex.acquire(timeout, unit);
} catch (Exception e){
e.printStackTrace();
}
return true;
}
public boolean unlock(){
try{
interProcessMutex.release();
}catch(Throwable e){
log.error(e.getMessage(), e)
} finally{
executorService.schedule(new Cleaner(client, path), delayTimeForClean, TimeUnit.MILLISECONDS);
}
return true;
}
只是基于 ZK 的实现在性能上达不到基于缓存的实现,这是由 ZK 的分布式机制决定的(Leader 执行再同步到所有 follower)。
同时可能带来并发问题,只是不常见而已。比如,由于网络抖动,客户端与 ZK 集群的 session 断开了,那么 ZK 会以为客户端挂了,这时删除临时节点,这时其他节点则获得了锁,则可能产生并发问题。不常见是因为 ZK 有重试机制,一旦 ZK 集群检测不到客户端心跳就会进行重试,Curator 客户端支持多种尝试策略,多次重试仍然不行则删除临时节点。
总结
从理解的难以程度(由低到高):数据库 - 缓存 - ZK
从实现复杂度(由低到高):ZK -= 缓存 - 数据库
性能角度(由高到低):缓存 - ZK -= 数据库
可靠性(由高到低):ZK - 缓存 - 数据库
Feedback
Was this page helpful?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.