关于锁
- 锁存在的意义就是避免多个线程同时操作一个变量或数据资源时,无序和同时读写造成的数据结果异常,不可控与期望结果不一致的问题。
- synchronized关键字,lock类等, 可以保证此锁在单个JVM中唯一,但在多个JVM(集群/分布式)的架构下就无法使用JAVA自带的解决方案来保证锁唯一。
- 此时就需要引入一个跨JVM的共用唯一锁,也就是分布式锁。
分布式锁具备的条件
- 在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行
- 高可用的获取锁与释放锁
- 高性能的获取锁与释放锁
- 具备可重入特性(可理解为重新进入,由多于一个任务并发使用,而不必担心数据错误)
- 具备锁失效机制,防止死锁
- 具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败
实现
- 分布式锁的实现也有很多种. 基于redis,zookeeper等等, 也可以基于数据库.
- 本文主要说一下 使用redis做分布式锁的简单实现。
获取锁
1.使用setnx 和 expire 两条命令 来实现加锁和设置锁有效时间,
setnx 和 expire 本身是原子性的操作,但是叠加使用就是非原子操作,根据redis官网的说明可以使用set命令来实现
redis官网说明 SETNX
redis官网说明 SET
可以看到 set命令已经完全可以覆盖setnx的全部功能,同时可以设置key的有效期 秒/毫秒 可选。
- EX seconds – Set the specified expire time, in seconds.
- PX milliseconds – Set the specified expire time, in milliseconds.
- NX – Only set the key if it does not already exist.
- XX – Only set the key if it already exist.
- EX seconds – 设置键key的过期时间,单位时秒
- PX milliseconds – 设置键key的过期时间,单位时毫秒
- NX – 只有键key不存在的时候才会设置key的值
- XX – 只有键key存在的时候才会设置key的值
在SET命令页面同时也给出了释放锁的例子,使用lua脚本来实现原子性操作同时 get和del。
简单实例代码如下:
//NX – 只有键key不存在的时候才会设置key的值
//EX seconds – 设置键key的过期时间,单位时秒
String result = jedis.set(redisKey, String.valueOf("testvalue", "NX", "EX", 2);
if (result == null || !result.equalsIgnoreCase("OK")) {
}
//用Lua 脚本 保证操作(get+del)原子性 删除可能的垃圾key
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object jret = jedis.eval(script, Collections.singletonList(redisKey), Collections.singletonList(redisValue));
//jret返回值 成功是1L 未成功是0L
//"if redis.call('get', KEYS[1])==false or tonumber(redis.call('get', KEYS[1])) < tonumber(ARGV[1]) then return redis.call('del', KEYS[1]) else return 0 end"
网上找了个比较清晰的demo,供参考:
redis-lock