本文是笔者对看到过和使用过相关分布式锁的整理和总结,分布式锁主要的实现有以下三种方式
- 基于数据库表的分布式锁实现
- 基于redis实现的分布式锁
- 基于zookeeper实现的分布式是锁
1、背景
在单机多线程环境下,为了实现锁,可以使用java提供的synchronized和java.util.concurrent.locks.Lock。随着大规模分布式系统的兴起,一个应用需要部署在多台机器上,多台机器之间怎么保证访问顺序性、同一时刻操作的幂等性,是分布式锁需要考虑的问题。下面举一个笔者在实际工作中遇到分布式锁的场景
背景是统计每5分钟的数据量,统计数据的工程部署在多台机器上,当统计数据到来时,随机选择在一台机器上进行统计,每台机器都可以对缓存数据进行累加更新,每5min钟对统计数据进行落库,缓存中的键为整点5min时间戳,值为这5min之内的累加的统计数据,当对缓存进行更新时,需要使用分布式锁进行更新。如果在更新的时候没有使用分布式锁,当两台机器同时进行更新,将会出现丢失一条信息的情况。
2、基于数据库分布式锁
数据库实现分布式锁有两种方式:
- 数据库事务
- 数据库表的唯一性约束
(1)数据库事务
数据库事务有一个特点是当一个事务更新数据库中一行操作,另一个事务更新同一行的时候会失败,利用这一特征,事务可以保证同一时间只有一个操作,数据库事务分布式锁实现流程如下图所示
(2)数据库表的唯一性约束
利用数据库唯一性约束,获取锁时insert一条数据,当插入成功时表示获取到了锁,执行程序逻辑操作,执行完成之后,删除对应的数据,表示释放了对应的锁,结束;当插入失败时,表示获取锁失败,结束。如下图所示,获取锁的操作流程
(3)优缺点
- 优点
严格互斥 - 缺点
依赖数据库的性能
3、基于redis实现的分布式锁
redis提供了一些原子性操作如SETNX、GET、GETSET、DEL等,由这些操作可以实现分布式锁,如下图所示分布式锁的实现流程图
redis中SETNX(key, value)原语操作提供了如果存在key则返回0,如果不存在则返回1,利用这一特性可以实现分布式锁,value为对应的超时时间和获取锁的业务方id(t | id),在SETNX失败后,调用GET原语操作,获取id和超时时间查看超时时间是否超时,如果未超时,则结束;如果超时,则调用GETSET原语操作重新设置t和id,同时判断获取的id和GET操作获取的id是否相等,如果相等则表示获取到了锁,进行相应的逻辑操作,不相等则结束。在删除锁时,需要判断,是否是当前正在使用的锁(id)和未超时,满足这两个条件,则可以删除锁。
4、基于Zookeeper实现的分布式锁
zk(zookeeper)是分布式集群处理技术的一种解决方案,其客户端curator提供了对zk集群的各种操作。其中包括分布式锁的相关操作,其中org.apache.curator.framework.recipes.locks.InterProcessMutex实现了分布式可重入锁的实现,它提供的两个方法acquire()和release()分别实现了获取锁和释放锁的操作。
InterProcessMutex封装的锁实现,当客户端断掉时,锁节点会自动删除。zk提供了watch功能,当节点删除时,会通知其他客户端去获取锁。
5、总结
数据库实现分布式锁方式有很大的局限性,受到数据库性能的影响,redis实现的分布式锁较为复杂,最好的实现分布式锁的方式是基于zk实现的分布式锁。