含义:程序首先需要通过获取锁来对数据进行排他性访问的能力,然后才能对数据执行一系列操作,最后还要将锁释放给其他程序。根据获取锁的方式:可分为乐观锁和悲观锁。
乐观锁:每次拿数据的时候都认为别人不会修改数据,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。一句话概括“直接对数据进行操作,执行完后,判断是否能执行成功”。redis的watch命令就是使用了乐观锁。
悲观锁:每次去拿数据的时候都认为别人会修改,所以每次在拿这个数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。一句话概括就是“首先获取锁,然后执行操作,最后释放锁”。
锁出现不正确的行为原因,以及锁不正确运行时的症状:
1.持有锁的进程因为操作时间过长而导致锁被自动释放,但进程本身并不知晓这一点,甚至还可能错误地释放掉了其他进程持有的锁。
2.一个持有锁并打算执行长时间操作的进程已经崩溃,但其他想要获取锁的进程不知道哪个进程持有锁,也无法检测出持有锁的进程已经崩溃,只能白白地浪费时间等待锁被释放。
3.在一个进程持有的锁过期之后,其他多个进程同时尝试去获取锁,并且都获得了锁。
4.上面提到的第1点和第3点情况同时出现,导致多个进程获得了锁,而每个进程都以为自己是唯一一个获得锁的进程。
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;
}
redis释放锁的实例:因为程序持有锁期间,其他程序可能会擅自对锁进行修改;所以函数首先使用watch命令监视代表锁的键,接着检查键目前是否和加锁时设置的值相同,并在确认值没有变化之后删除该键。
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;
}
上诉操作的加锁,持有者在崩溃的时候不会自动释放锁,这将导致锁一直处于已被获得状态。所以需要为锁添加超时功能。
给锁设置超时的好处:为了确保锁在客户端已经崩溃的情况下仍然能够自动被释放,客户端会在尝试获取锁失败之后,检查锁的超时时间,并为未设置超时时间的设置超时时间。因此锁总会带有超时时间,并最终因为超时而自动被释放,使得其他客户端可以继续尝试获取已经被释放的锁。
代码实例:
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); //确保传给expire的都是整数
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 {
Thread.sleep(1);
}catch(InterruptedException ie){
Thread.currentThread().interrupt();
}
}
return null;
}