Redis分布式锁的基础是Redis的单进程单线程,用来控制分布式系统之间同步访问共享资源。

实现的原理是CAP。

分布式锁的目的是对资源的保护,确保业务逻辑与预想的一致性、正确性。实现原理是每一个线程争夺对Redis的写操作的权限,从而获得操作业务代码的权限。

网上有非常多不同版本的实现,但是总觉得都有一定的问题,所以自己写了一个实现。

@Slf4j
public class RedisLockHelper {

    private RedisTemplate<String, Object> redisTemplate;


    public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    /**
     * 默认超时时间 100毫秒
     */
    private int defaultLockTimeout = 100;

    /**
     * 默认等待时间 50毫秒
     * 超时后重试获取锁时间
     */
    private int defaultLockTimeoutWait = 50;

    /**
     * 锁名
     */
    private String lockKeyName;

    /**
     * 锁状态
     */
    private volatile boolean locked = false;

    /**
     * 当前锁的超时时间
     */
    private String timeOutTime = "";

    public RedisLockHelper() {
    }

    /**
     * <p>@Description: <p>
     * <p>@param  lockName 锁名<p>
     **/
    public boolean lock(String lockName) throws InterruptedException {
        this.lockKeyName = lockName + "_LOCK";
        return getLock();
    }

    /**
     * <p>@Description: <p>
     * <p>@param  lockName 锁名<p>
     * <p>@param  timeOut  超时时间<p>
     **/
    public boolean lock(String lockName, int timeOut) throws InterruptedException {
        this.lockKeyName = lockName + "_LOCK";
        this.defaultLockTimeout = timeOut;
        return getLock();
    }

    /**
     * <p>@Description: <p>
     * <p>@param  lockName 锁名<p>
     * <p>@param  timeOut  超时时间<p>
     * <p>@param  timeWait 等待时间 <p>
     **/
    public boolean lock(String lockName, int timeOut, int timeWait) throws InterruptedException {
        this.lockKeyName = lockName + "_LOCK";
        this.defaultLockTimeout = timeOut;
        this.defaultLockTimeoutWait = timeWait;
        return getLock();
    }

    private boolean setNx(final String key, final String value) {
        try {
            Object obj = redisTemplate.execute(new RedisCallback<Object>() {
                @Override
                public Object doInRedis(RedisConnection connection) throws DataAccessException {
                    StringRedisSerializer serializer = new StringRedisSerializer();
                    Boolean success = connection.setNX(serializer.serialize(key), serializer.serialize(value));
                    connection.close();
                    return success;
                }
            });
            return obj != null ? (Boolean) obj : false;
        } catch (Exception var4) {
            log.error("普通缓存放入原子操作异常:{}", var4.getMessage(), var4);
            return false;
        }
    }

    private Object get(String key) {
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }

    private String getSet(String key, String value) {
        return (String) redisTemplate.opsForValue().getAndSet(key, value);
    }

    /**
     * 获取锁方法
     * <p>
     * 线程会循环直到获取到锁为止 业务代码是一定会执行一遍才能返回
     */
    private boolean getLock() throws InterruptedException {
        //锁名为空或超时时间为空时 防止死锁 就不能获取锁
        while (!StringUtils.isBlank(lockKeyName) && defaultLockTimeout > 0) {
            String newTimeOutTime = String.valueOf(System.currentTimeMillis() + defaultLockTimeout);
            //直接去获取锁 setNx (SET if Not exists)
            if (this.setNx(lockKeyName, newTimeOutTime)) {
                log.info("获取锁名为:{},超时时间为:{} 的锁成功", lockKeyName, defaultLockTimeout);
                locked = true;
                this.timeOutTime = newTimeOutTime;
                return true;
            }
            //获取锁内的时间 检查是否超时
            Object oldTimeOutTime = get(lockKeyName);
            //如果获取旧的超时时间是null 可能是其它线程在上锁
            //锁里的时间小于当前时间 则说明锁已过期 可以尝试获取
            if (oldTimeOutTime != null && Long.parseLong((String) oldTimeOutTime) < System.currentTimeMillis()) {
                String currentTimeOutTime = this.getSet(lockKeyName, newTimeOutTime);
                //保存后取出来的时间和之前获取的时间相等 则获取锁成功 否则锁已被其它线程获取
                if (StringUtils.equals(currentTimeOutTime, String.valueOf(oldTimeOutTime))) {
                    log.info("获取锁名为:{},超时时间为:{} 的锁成功2", lockKeyName, defaultLockTimeout);
                    this.locked = true;
                    this.timeOutTime = newTimeOutTime;
                    return true;
                }
            }
            Thread.sleep(defaultLockTimeoutWait);
        }
        log.info("获取锁名为:{},超时时间为:{} 的锁失败", lockKeyName, defaultLockTimeout);
        return false;
    }

    /**
     * 解锁方法
     * <p>
     * 正常情况下都需要主动的解锁 这样锁的效率才最高的
     * 如果经常性的等超时时间然后被替换 就需要优化代码 优化超时时间
     */
    public void unLock() {
        String oldTimeOutTime = String.valueOf(this.get(lockKeyName));
        //只有是自己的锁才去解锁 否则就不进行操作
        if (locked && timeOutTime.equals(oldTimeOutTime)) {
            redisTemplate.delete(lockKeyName);
            log.info("释放了锁名为:{}的锁", lockKeyName);
        }
    }
}

某晚突然想到集群部署时 每台机器的系统时间一定要保持一致,否则就会造成混乱