Redis和Zookeeper实现分布式锁的原理对比

Redis实现分布式锁

获取锁

基于Redis内部是单线程执行指令,利用Redis命令支持的setNX EX原子操作指令给某个key赋值并设置过期时间,实现获取分布式锁,成功设置key的值并返回value值作为这把锁的拥有者,表示成功获取到锁。容易犯错的地方在于把setNX EX操作分解成两步setNXsetEX操作,破坏了原子性,有可能导致setEX失败后setNX设置的key永不过期,那么其他线程将永远无法获得这把锁。在获取分布式锁时,如果一次获取锁失败,就允许一定时间范围内的重试获取锁,这样能保证线程获取分布式锁的成功率。在每次获取锁失败后,要让当前线程睡一小会儿,防止短时间内过多的线程争夺分布式锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Override
public String acquireDistributeLock(String key) {
String keyOwner = UUID.randomUUID().toString();
long now = System.currentTimeMillis();
long end = now + 1000;
while (now<end){
boolean b = setDistributeLockValue(key, keyOwner);
if (b) {
break;
}else {
try {
Thread.sleep(20L);
} catch (InterruptedException e) {
log.error("",e);
}
}
}
return keyOwner;
}

private boolean setDistributeLockValue(final String key,final String keyOwner){
return stringRedisTemplate.execute(new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
JedisCommands jedisCommands = ((JedisCommands) connection.getNativeConnection());
String value = jedisCommands.set(key, keyOwner, "NX", "PX", 1000);
if ("OK".equals(value)) {
return true;
}else {
return false;
}
}
});
}

释放锁

Redis实现释放分布式锁,需要这把锁的拥有者才有权删除key达到释放锁的目的,为了保证释放锁过程指令执行的原子性,使用Redis(>=2.6)的eval指令执行Lua脚本的方式释放锁。redis.call('get', KEYS[1])得到key对应的value,ARGV[1]表示传入的keyOwner值,如果value==keyOwner,说明当前线程是分布式锁的拥有者,有权释放锁,则执行redis.call('del', KEYS[1]),执行成功返回1,实行失败返回0。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public Boolean releaseDistributeLock(String key,String keyOwner) {

return stringRedisTemplate.execute(new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
String unlockScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

Jedis jedis = ((Jedis) connection.getNativeConnection());
Object result = jedis.eval(unlockScript, Collections.singletonList(key), Collections.singletonList(keyOwner));
if (new Long(1L).equals(result)) {
return true;
}
return false;
}
});
}

Zookeeper实现分布式锁

获取锁

Zookeeper可以创建有生命周期的不同类型节点(持久节点、临时节点、持久顺序节点、临时顺序节点),实现高性能分布式协调和调度。通过Zookeeper同一个临时节点(CreateMode.EPHEMERAL)只能创建一次的原理可以实现分布式锁。跟Redis获取分布式锁的方式相似,在第一次获取分布式锁失败后,进行若干次重试失败后发送预警通知。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* 获取分布式锁
*/
public void acquireDistributedLock(String objectId){
try {
createNode(objectId);
log.info("=======分布式锁创建成功=========");
} catch (Exception e) {
//如果临时节点已经存在,则会抛出异常,临时创建失败(分布式锁没获取到),再延时尝试
int count = 0;
while(true){
if(count>100){
log.error("重试"+count+"次获取zookeeper分布式锁失败");
break;
//send msg 发送预警
}
try {
Thread.sleep(20);
createNode(objectId);
} catch (Exception e1) {
log.error("重试获取zookeeper分布式锁失败",e1);
count++;
continue;
}
log.info("经过"+count+"次尝试,获取到分布式锁["+objectId+"]");
break;
}
}
}

private void createNode(String objectId) throws KeeperException, InterruptedException {
zooKeeper.create("/"+objectId,"".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
}

释放锁

删除创建的临时节点,达到释放锁的目的。

1
2
3
4
5
6
7
8
9
10
/**
* 释放锁
*/
public void deleteDistribituedLock(String objectId){
try {
zooKeeper.delete("/"+objectId,-1);
} catch (Exception e) {
log.info("释放锁出错",e);
}
}

Redis与Zookeeper分布式锁实现对比

Redis实现分布式锁,难点在于key的原子性操作,需要借助与redis原生指令创建,需要Redis客户端支持执行原生命令。Redis分布式锁value可以保存锁的创建者,只有创建者才有权释放锁。

Zookeeper实现分布式锁,只需要编写创建和删除分布式锁的临时节点少量代码即可完成,要实现对分布式锁拥有者的管理,可以通过对节点的path格式进行规划,保存锁拥有者信息。

谢谢你请我吃糖果