- 基于数据库的
- 基于redis
- 基于zookeeper
基于数据库
基于redis
先来看第一种
public static void demo(Jedis jedis, String lockKey, String requestId, int expireTime) {
// setnx 是set if not exist,r如果不存在,则插入,返回1 否则返回0
//lockkey就是需要获取锁的名称或者id value是该线程id
Long result = jedis.setnx(lockKey, requestId);
if (result == 1) {
// 若在这里程序突然崩溃,则无法设置过期时间,将发生死锁
//也就是无法保证和上一个操作的原子性
jedis.expire(lockKey, expireTime);
}
}
改进版 redis 2.6.12
/**
* 获取分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @expireTime 过期时间
*/
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
// 这个方法就可以保证过期时间和获取锁操作的原子性
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
上面value用当前线程的id目的:
- 如果线程在过期时间结束之前完成任务,要执行del操作,释放锁。
- 如果线程A设置过期时间30s,30s之后该线程还没有执行完,其他线程B获取到锁
- 这时候A执行完了,开始执行del,这时候就会删除掉B获取的锁,所以要用requestId做判断。
删除操作
if(threadId.equals(jedis.get(lockkey))){
del(lockkey)
}
同样的,判断和删除操作无法保证原子性!
这时候解决办法就是用lua脚本
public class RedisTool {
private static final Long RELEASE_SUCCESS = 1L;
/**
* 释放分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @return 是否释放成功
*/
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
//这里用到lua脚本
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
//eval 第一个是lua脚本 第二个是参数个数 从第三个开始是变量 KEY[1] KEY[2]等 后面是附加
Object result = jedis.eval(script, 2,,Collections.singletonList(lockKey), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
/**
* @param script 要执行的脚本
* @param numbers 指定键名参数的个数
* @param key 脚本中的redis的key 从第三个参数开始 KEY[1] KEY[2]等等
* @return arg 附加参数 ARGV[1] ARGV[2]等等
*/
EVAL script numbers key[key ...] arg[arg ...]
为什么用脚本就能保证原子性呢?
补充
上面的情况。线程A没有执行完,线程B获取锁,怎么解决呢?
用守护线程。 线程A在最后一秒还没有执行完的话,就继续增加过期时间。