WATCH命令的性能问题
WATCH命令在更新数据之前会监视要操作的键,如果提交数据前检测到目标键已经被更新过,那么会放弃更新数据,并且重新尝试重新执行上述的操作。如果在并发量大的情况下,系统完成一次更新操作尝试的数目会非常多,用户等待的时间也会变长,所以WATCH,MULTI,EXEC组成的事务不具备可扩展性,所以我们使锁来解决这一问题
redis锁
锁虽然可以解决上述的问题,但是构建正确的锁并不那么容易,大部分redis实现的锁只是基本正确,锁发生故障的时间和方通常难以预料。以下是一些导致锁出现不正确的原因,以及锁在不正确运行时的症状
- 持有锁的进程因为操作时间过长导致锁被自动释放,但是进程本身并不知晓这一点,甚至可能释放掉其它进程持有的锁
- 一个持有锁的进程崩溃,但其它想要获取锁的进程只能白白浪费等待时间等待锁被释放
- 在一个进程持有的锁过期后,其他多个进程同时尝试获取锁,并且都拿到了锁
- 一个持有锁线程运行时间过长,导致锁超时而被自动释放,但是线程并不知道,于此同时其它线程同时获取到了这个锁,并都认为自己是获得这个锁的唯一进程
简易锁
//加锁
public String acquireLock(Jedis conn, String lockName, long acquireTimeout){
String identifier = UUID.randomUUID().toString();
long end = System.currentTimeMillis() + acquireTimeout;
//如果在指定的时间内没有获取到锁则放弃获取锁
while (System.currentTimeMillis() < end){
if (conn.setnx("lock:" + lockName, identifier) == 1){
return identifier;
}
try {
Thread.sleep(1);
}catch(InterruptedException ie){
Thread.currentThread().interrupt();
}
}
return null;
}
//释放锁
public boolean releaseLock(Jedis conn, String lockName, String identifier) {
String lockKey = "lock:" + lockName;
while (true){
conn.watch(lockKey);
//释放锁前必须谨慎的判断加锁时的键值和现在的键值是否相同
if (identifier.equals(conn.get(lockKey))){
Transaction trans = conn.multi();
trans.del(lockKey);
List<Object> results = trans.exec();
if (results == null){
continue;
}
return true;
}
conn.unwatch();
break;
}
return false;
}
细粒度锁
简易锁与WATCH命令都存在一个问题,粒度太大,比如用一个sorted set代表一个市场,分值代表价格,值代表商品,如果要进行某个商品交易会锁住整个市场,实际上锁住一个商品会减小锁竞争同时提升程序的性能
带有超时限制的锁
之前所提到的锁在持有者崩溃的时候不会被释放,为了解决这一问题我们将为锁设置超时时间,为了确保锁在客户端已经崩溃的情况下能够释放,程序会保证在获取锁失败的情况下,检查锁的超时时间,并为未设置超时时间的锁设置超时时间
public String acquireLockWithTimeOut(Jedis conn,String lockName,long acquireTimeOut,long lockTimeOut){
String identifier=UUID.randomUUID().toString();
String lockKey="lock:"+lockName;
int lockExpire=(int)(lockTimeOut/1000);
long end=System.currentTimeMillis()+acquireTimeOut;
while (System.currentTimeMillis()<end) {
//如果成功的拿到锁,设置锁的有效时间,并返回锁
if (conn.setnx(lockKey, identifier)==1) {
conn.expire(lockKey, lockExpire);
return identifier;
}
//如果没有拿到锁,检查锁是否设置了超时时间,如果没有那么设置锁的时间
if (conn.ttl(lockKey)==-1) {
conn.expire(lockKey, lockExpire);
}
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return null;
}