Skip to content

Redis实现分布式锁

分布式锁要求

  • 排他性:同一时间只能有一个客户端获取到锁,其他客户端无法同时获取到锁。
  • 避免死锁:这把锁在一段有限的时间之后,一定要被释放,否则产生死锁,这里包括正常释放和非正常释放;
  • 自己解锁:加锁和解锁都应该是同一客户端去完成,不能去解别人的锁。
  • 高可用:获取和释放的机制必须高可用且保证性能。

Redis分布式锁实现步骤

加锁

setnx命令加锁,并设置锁的有效时间和持有人标识。 setnx:只有在key不存在的情况下,将key的值设置位value。如果key存在,则setnx命令不做任何动作。 expore命令设置锁的过期时间。

解锁

del命令删除锁

加锁代码

java
@Autowired
private RedisTemplate<String, Object> redisTemplate;
 
//锁前缀
private static final String REDIS_LOCK_PRIFIX = "redis:lock:";
 
/**
 * 加锁
 * @param lockName - 锁名称,不同业务可以有不同的锁名称来区分
 * @param acquireLockTimeOut(毫秒) - 获取锁的超时时间,超过该时间不再获取
 * @param lockExpireTimeOut(毫秒) - 释放锁的超时时间,超过该时间释放锁
 * @return - 返回存入的value值
 */
public String acquireLock(String lockName,long acquireLockTimeOut,long lockExpireTimeOut){
    //key 和 value
    String redisLockKey = REDIS_LOCK_PRIFIX + lockName;
    String redisLockValue = UUID.randomUUID().toString();
 
    //获取redis连接,此处springboot配置好可略过
    //此处省略:redis.getConn
 
    //当前时间:20:00:00   获取锁超时时间:5000毫秒,那么20:00:05则超时
    long acquireLockEndTime = System.currentTimeMillis() + acquireLockTimeOut;
    while (System.currentTimeMillis() < acquireLockEndTime){
        //去redis获取锁 - (setnx + expire)这两个命令需要保证原子操作,此时需要使用lru脚本来实现
 
        //lua脚本,使setnx和expire为原子操作
        String scriptStr =  "if (redis.call('setnx', KEYS[1], ARGV[1]) == 1) then\n" +
                            "   return redis.call('expire', KEYS[1], ARGV[2]));\n" +
                            "else\n" +
                            "   return 0;\n" +
                            "end";
 
        //lua脚本字符串
        RedisScript script = RedisScript.of(scriptStr);
 
        //参数list
        List<String> redisParamList = new ArrayList<>();
        redisParamList.add(redisLockKey);
 
        //执行lua脚本 - 毫秒换算 /1000 = 秒
        Object result = redisTemplate.execute(script, new StringRedisSerializer(), new StringRedisSerializer(), redisParamList, redisLockValue, lockExpireTimeOut/1000);
 
        // 1代表执行成功,代表拿到了分布式锁
        if(result.toString().equals("1")){
            //将值直接return
            return redisLockValue;
        }
 
        //循环去redis获取锁,休眠一会再去获取
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
 
    //没拿到锁,返回null
    return null;
 
}

解锁代码

java
/**
 * 释放锁
 * @param lockName - 锁名称,不同业务可以有不同的锁名称来区分
 * @param redisLockValue - 锁对应的value值
 */
public void releaseLock(String lockName, String redisLockValue){
 
    String redisLockKey = REDIS_LOCK_PRIFIX + lockName;
 
    //删除key,需要判断,是不是当前锁的key(自己加锁,自己解锁。不能去解别人的锁) -> 先查询,再删除(需要保证原子性)
    String scriptStr =  "if (redis.call('get',KEYS[1]) == ARGV[1]) then\n" +
                        "   return redis.call('del',KEYS[1]);\n" +
                        "else\n" +
                        "   return 0;\n" +
                        "end";
 
    //lua脚本字符串
    RedisScript script = RedisScript.of(scriptStr);
 
    //参数list
    List<String> redisParamList = new ArrayList<>();
    redisParamList.add(redisLockKey);
 
    //执行lua脚本 - 释放锁
    Object result = redisTemplate.execute(script, new StringRedisSerializer(), new StringRedisSerializer(), redisParamList, redisLockValue);
 
}