good article
需求
- 互斥性:一个时刻只有一个进程拿到锁
- 可释放:加锁者挂掉时
- 身份识别:谁加的锁,谁解锁
- 阻塞性
- 重入(解铃还须系铃人和这点本质是一样的需求)
数据库方式
- 方式1:基于增删
- 加锁:插入记录
- 解锁:删除记录
- 问题
- 数据库单点:主备
- 无失效:时间戳 + 定时任务清理超时锁
- 非阻塞:while循环插入
- 不可重入:加个字段保存主机信息和线程信息,插入之前先查询
- 方式2:基于排他锁
- 加锁:for update
- 解锁:commit
- 优于增删的地方
- 阻塞:for update是阻塞的
- 失效:加锁者挂掉时,server会自行释放
- 新问题:
- 表小的时候,如果被认为走全表扫描效率更高,会锁全表(不知道是不是可以强制)
- 长时间不commit,会导致连接太多,撑爆数据库连接池
- 优点
- 缺点
缓存(redis,memcached,Tair)
Tair
// 加锁
public boolean trylock(String key) {
ResultCode code = ldbTairManager.put(NAMESPACE, key, "This is a Lock.", 2, 0);
if (ResultCode.SUCCESS.equals(code))
return true;
else
return false;
}
//解锁
public boolean unlock(String key) {
ldbTairManager.invalid(NAMESPACE, key);
}
- 问题
- 无失效:put方法传入失效时间
- 非阻塞:while循环
- 不可重入:在value里保存主机信息和线程信息
- 失效时间长短问题
- 优点
- 缺点
redis正确姿势
// set key value [ex seconds]|[px milliseconds] [nx|xx]
public class RedisTool {
// NX:不存在创建,返回OK,否则返回null
// XX:存在返回OK,否则返回null
// EX:秒,PX:毫秒;只能设置一个
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
if ("OK".equals(result)) {
return true;
}
return false;
}
}
public class RedisTool {
private static final Long RELEASE_SUCCESS = 1L;
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
redis错误加锁姿势1
- 进程在设置过期时间前突破崩溃,那么这个锁就无法失效了
public static void wrongGetLock1(Jedis jedis, String lockKey, String requestId, int expireTime) {
Long result = jedis.setnx(lockKey, requestId);
if (result == 1) {
jedis.expire(lockKey, expireTime);
}
}
redis错误加锁姿势2
- 虽然可以实现单一加锁的功能,但是超时时间可能已经被别人覆盖了
public static boolean wrongGetLock2(Jedis jedis, String lockKey, int expireTime) {
long expires = System.currentTimeMillis() + expireTime;
String expiresStr = String.valueOf(expires);
// 获得锁
if (jedis.setnx(lockKey, expiresStr) == 1) {
return true;
}
// 锁已存在,获取锁过期时间
String currentValueStr = jedis.get(lockKey);
if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
// 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间
String oldValueStr = jedis.getSet(lockKey, expiresStr);
if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
// 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才有权利加锁
return true;
}
}
// 其他情况,一律返回加锁失败
return false;
}
redis错误解锁姿势1
public static void wrongReleaseLock1(Jedis jedis, String lockKey) {
jedis.del(lockKey);
}
redis错误解锁姿势2
public static void wrongReleaseLock2(Jedis jedis, String lockKey, String requestId) {
if (requestId.equals(jedis.get(lockKey))) {
jedis.del(lockKey);
}
}
ZK