需要注意的点:
- 抢占:redis setnx,只有一个线程可以成功,成功即意味着抢锁成功
- 防死锁:设置过期时间 expire,并且可重入(ReentrantLock),同个线程每次加锁state加1,解锁state减1,当为0的时候释放锁
- 防止误删:设置唯一标识 (uuid+线程id),在删除前进行判断是否是自己的锁
- 原子性: lua脚本,reids EVAL 或者redisTemplate.execute()
- 自动续期:Timer定时器
示例及详细解释如下代码:
package self.jason.distribute.lock;
import java.util.Arrays;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
public class MyDistributedRedisLock implements Lock {
private StringRedisTemplate redisTemplate;
private String lockName;
private String lockId;
private long expire = 30;
// 使用工厂模式来创建该对象实例,所以没有在本类上添加@Component和@Autowired注解
public MyDistributedRedisLock(StringRedisTemplate redisTemplate, String lockName, String uuid) {
this.redisTemplate = redisTemplate;
this.lockName = lockName;
// lockId使用uuid和当前线程id进行拼接,以确保排他(uuid)和可重入(thread id)
this.lockId = uuid + ":" + Thread.currentThread().getId();
}
@Override
public void lock() {
tryLock();
}
@Override
public void lockInterruptibly() throws InterruptedException {
// TODO Auto-generated method stub
}
@Override
public boolean tryLock() {
try {
return tryLock(-1, TimeUnit.SECONDS);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
if (time != -1) {
expire = unit.toSeconds(time);
}
// lua脚本进行判断,如果当前没有锁或者是自己线程对应的锁(重入),则将锁的value加1(类似ReentantLock中的state加1),并设置(或重置)过期时间
// 否则返回0,利用while循环进行重试
String script = "if redis.call('exists',KEYS[1])==0 or redis.call('hexists',KEYS[1],ARGV[1])==1 " //
+ "then " //
+ " redis.call('hincrby',KEYS[1],ARGV[1],1) " //
+ " redis.call('expire',KEYS[1],ARGV[2]) " //
+ " return 1 " //
+ "else " //
+ " return 0 " //
+ "end";
while (!redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), lockId,
String.valueOf(expire))) {
// sleep一定时间以防止频繁争抢锁
Thread.sleep(50);
}
// 给锁续期
renewLock();
return true;
}
@Override
public void unlock() {
// lua脚本进行解锁:
// 如果锁不存在或者不是自己的锁,则抛异常
// 否则对锁的value减1(类似state减1),当为0时表示应该释放锁,则通过删除来进行释放
// 否则直接返回即可(比如可重入退出的情况)
String script = "if redis.call('hexists',KEYS[1],ARGV[1])==0 " //
+ "then " //
+ " return nil " //
+ "elseif redis.call('hincrby',KEYS[1],ARGV[1],-1)==0 " //
+ "then " //
+ " return redis.call('del',KEYS[1]) " //
+ "else " //
+ " return 0 " //
+ "end";
Long flag = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName),
lockId);
if (flag == null) {
throw new IllegalMonitorStateException("try to release others' lock");
}
}
@Override
public Condition newCondition() {
// TODO Auto-generated method stub
return null;
}
private void renewLock() {
// lua脚本进行续期,需要判断是自己的锁才可以进行续期
String script = "if redis.call('hexists',KEYS[1],ARGV[1]) " //
+ "then " //
+ " return redis.call('expire',KEYS[1],ARGV[2]) " //
+ "else "//
+ " return 0 "//
+ "end";
// 当过期时间的1/3时进行判断续期,如果脚本返回0,则表示该锁已经不存,不需要在重新启timer,否则调用本方法进行reschedule
new Timer().schedule(new TimerTask() {
@Override
public void run() {
if (redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName),
lockId, String.valueOf(expire))) {
renewLock();
}
}
}, expire * 1000 / 3);
}
}
所有实现基本都在上面的类中,使用时和JDK的锁类似,redisLock.lock(), finally里redisLock.unlock()即可。
但是本类仅可以在单redis中使用,对于redis集群不适用。