从性能角度(从高到低)来看:“缓存方式>Zookeeper方式>=数据库方式”。
说起分布式的概念,首当其冲就是
CAP理论
,即满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)。但是CAP理论告诉我们,任何系统只能满足其中两个。
1. 悲观锁
利用select … where … for update 排他锁。阻塞的,容易锁表。
2. 乐观锁
update version通过增加递增的版本号字段实现乐观锁。
3、 基于数据库表获取
获取锁时,只要执行insert语句
insert into lock_table(“method_name”,“time”);
释放锁,delete
当然还有其他许多问题需要解决,可重入、定时扫描过期时间等
1、简化版setnx
命令:
if (jedis.setnx(lockKey, val) == 1) {
jedis.expire(lockKey, timeout);
}
很显然,加锁操作和后面的设置超时时间是分开的,并非原子操作。假如加锁成功,但是设置超时时间失败了,该lockKey就变成永不失效。
2、set(lockKey, requestId, "NX", "PX", expireTime)
命令,该命令可以指定多个参数。
String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
if ("OK".equals(result)) {
return true;
}
return false;
其中:
lockKey:锁的标识
requestId:请求id
NX:只在键不存在时,才对键进行设置操作。
PX:设置键的过期时间为 millisecond 毫秒。
expireTime:过期时间
满足了原子性,但是每次都要达到了超时时间才释放锁,显然也不是很合理,那么如何手工释放锁?
3、finally中释放锁+全局唯一标识
try{
String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
if ("OK".equals(result)) {
return true;
}
return false;
} finally {
if (jedis.get(lockKey).equals(requestId)) {
jedis.del(lockKey);
return true;
}
return false;
}
无论代码执行成功或失败了,都需要释放锁。如果有异常了,到达超时时间,锁还是会被redis自动释放。
而requestId是全局唯一的,保证了自己只能释放自己加的锁,不存在加锁和释放别人锁的情况。
4、Redis+Lua
Redis+Lua,可以说是专门为解决原子问题而生。Lua专门整合原子操作。有了 Lua 的特性,Redis 才真正在分布式锁、秒杀等场景,有了用武之地。
为什么要用Lua脚本呢?因为一段复杂的业务逻辑,可以通过封装在Lua脚本中发送给Redis,保证这段复杂业务逻辑执行的原子性。
5、看门狗
如果锁达到了超时时间,但业务代码还没执行完怎么办?
看门狗可以通过定时任务不断刷新锁的获取事件,从而在用户获取锁到释放锁期间保持一直持有锁。
eg:我们可以使用TimerTask类,来实现自动续期的功能:
获取锁之后,自动开启一个定时任务,每隔10秒钟,自动刷新一次过期时间。
这种机制就是redisson框架中的watch dog
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Overridepublicvoidrun(Timeout timeout)throws Exception {
//自动续期逻辑
}
}, 10000, TimeUnit.MILLISECONDS);
ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构
,规定同一个目录下只能有一个唯一文件名。基于ZooKeeper实现分布式锁的步骤如下:
(1)创建一个目录mylock;
(2)线程A想获取锁就在mylock目录下创建临时顺序节点;
(3)获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁;
(4)线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点;
(5)线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。
通常使用ZooKeeper的一个客户端Curator,Curator提供的InterProcessMutex是分布式锁的实现,acquire方法用于获取锁,release方法用于释放锁。
数据库锁:
redis分布式锁:
基于Zookeeper的分布式锁:
小结:
- 如果你的实际业务场景,更需要的是保证数据一致性。那么请使用CP类型的分布式锁,比如:zookeeper
- 如果你的实际业务场景,更需要的是保证数据高可用性。那么请使用AP类型的分布式锁,比如:redis;
- 通常redis足够用了,通过最终一致性大部分即可满足需求了,强一致性实时数据还是要依赖数据库的事务。