使用redis基于StringRedisTemplate实现分布式锁

为了防止分布式系统中的多个进程之间相互干扰,我们需要一种分布式协调技术来对这些进程进行调度。而这个分布式协调技术的核心就是来实现这个分布式锁。具体定义,实现和使用场景,请参详什么是分布式锁,该篇文章强力推荐,写得很透彻。

着重强调下,分布式锁具体的条件和重要的实现逻辑。

  • 在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行

常见的情景,例如定时任务在多个服务运行。同一个用户对同一个商品的重复操作的。

  • 高可用的获取锁与释放锁

使用redis实现时,redis要实现哨兵模式的主从模式,避免单点调用。

  • 具备锁失效机制,防止死锁

使用redis的set方式,原子性的操作key,value和失效时间,以及操作类型。实现redis的setnx和expire操作同时操作的原子性。具体代码如下:

Boolean set(byte[] key, byte[] value, Expiration expiration, SetOption option);
  • 使用ScheduledExecutorService定时任务,对于快要过期的锁重新设置失效时间

定义超时获取锁和释放锁的方法,接口定义如下:

    /**
     * 超时获取锁
     *
     * @param condition 获取锁条件
     */
    boolean tryLock(TryLockPrams condition);

    /**
     * 释放锁
     *
     * @param key   key
     * @param owner 锁的owner
     */
    boolean unlock(String key, String owner);

在实现类RedisLock中,设置释放锁的lua脚本,继承Runnable设置定时任务中对于即将失效的锁重新设置失效时间的逻辑。对于超时获取锁的操作,根据用户设置的时间设置,在超时时间内获取锁都有效。详细代码如下:

@Override
    public boolean tryLock(TryLockPrams prams) {

        String key;
        String owner;
        long expireTime;
        TimeUnit expireTimeUnit;
        if (StringUtils.isBlank(key = prams.getKey())
                || StringUtils.isBlank(owner = prams.getOwner())
                || (expireTime = prams.getExpireTime()) <= 0
                || Objects.isNull(expireTimeUnit = prams.getExpireTimeUnit())) {

            log.warn("prams [{}] is invalid!", JSON.toJSONString(prams));
            return false;
        }
        long startTime = System.currentTimeMillis();
        long tryLockMaxTime = prams.getTryLockMaxTime();
        long executeTime;
        do {
            try {
                RedisSerializer<String> keySerializer = (RedisSerializer<String>) redisTemplate.getKeySerializer();
                RedisSerializer<String> valueSerializer = (RedisSerializer<String>) redisTemplate.getValueSerializer();
                boolean success = redisTemplate.execute((RedisCallback<Boolean>) connection -> connection.set(
                        keySerializer.serialize(key),
                        valueSerializer.serialize(owner),
                        Expiration.from(expireTime, expireTimeUnit),
                        RedisStringCommands.SetOption.ifAbsent()));
                // 设置成功
                if (success) {
                    // 守护线程监控
                    prams.setDeadLine(System.currentTimeMillis() + expireTimeUnit.toMillis(expireTime));
                    extendExpiredTimeMap.putIfAbsent(key, prams);
                    return success;
                }
            } catch (Exception e) {

                log.warn("set if absent error!key {},value {},expireTime {}", key, owner, expireTime);
            }
        }
        while ((executeTime = System.currentTimeMillis() - startTime) < expireTimeUnit.toMillis(tryLockMaxTime));

        log.info("try lock error!prams {},executeTime {},expireTimeUnit {}",
                JSON.toJSONString(prams), executeTime, expireTimeUnit.name());
        return false;
    }

释放锁时,使用lua脚本实现,实现逻辑是,根据key获取的值与传入的对于的value值进行比较,如果相同则删除,不删除则操作失败。详细代码如下:

   /**
     * 解锁lua脚本
     */
    private static final String LUA_UNLOCK = "if redis.pcall('get', KEYS[1]) == ARGV[1] then return redis.pcall('del', KEYS[1]) else return 0 end";

@Override
    public boolean unlock(String key, String owner) {

        Long result = redisTemplate.execute((RedisCallback<Long>) connection ->
                connection.eval(LUA_UNLOCK.getBytes(), ReturnType.INTEGER, 1, key.getBytes(), owner.getBytes()));
        boolean success = Objects.nonNull(result) && result > 0;
        if (success) {
            // 移除延长失效时间数据
            extendExpiredTimeMap.remove(key);
        } else {
            log.warn("unlock error!key {},owner {}", key, owner);
        }
        return success;
    }

对于锁并发的场景,使用ScheduledExecutorService定时执行重新设置失效时间的任务,避免不同的进程获取到锁。详细代码如下:

  public RedisLock() {
        scheduledExecutorService = new ScheduledThreadPoolExecutor(1);
        extendExpiredTimeMap = new ConcurrentHashMap<>();
        init();
    }

    private void init() {
        scheduledExecutorService.scheduleAtFixedRate(this, 0, executingTimePeriod, TimeUnit.SECONDS);
    }


/**
     * 定时任务,对即将过期的key再次设置失效时间
     */
    @Override
    public void run() {

        if (log.isDebugEnabled()) {
            log.debug("execute extend expired time task!extend expired time map size {}", extendExpiredTimeMap.size());
        }
        if (extendExpiredTimeMap.size() <= 0) {
            return;
        }
        List<TryLockPrams> tryLockPrams = new LinkedList<>();
        for (ConcurrentHashMap.Entry<String, TryLockPrams> entry : extendExpiredTimeMap.entrySet()) {
            // 校验参数
            if (!isValid(entry) || !isExpiringTime(entry.getValue())) {
                continue;
            }
            tryLockPrams.add(entry.getValue());
            if (tryLockPrams.size() > 0 && 0 == tryLockPrams.size() % PIPELINE_LIMIT) {

                setIfPresentPipeline(tryLockPrams);
                tryLockPrams.clear();
            }
        }
        if (tryLockPrams.size() > 0) {

            setIfPresentPipeline(tryLockPrams);
            tryLockPrams.clear();
        }
    }

/**
     * 在缓存中即将过期数据才会重新设置失效时间,降低操作redis的频率
     *
     * @param tryLockPrams
     * @return
     */
    private boolean isExpiringTime(TryLockPrams tryLockPrams) {

        long deadLine = tryLockPrams.getDeadLine();
        long current = System.currentTimeMillis();
        long validTime = deadLine - current;
        if (BigDecimal.valueOf(validTime).divide(BigDecimal.valueOf(TimeUnit.SECONDS.toMillis(executingTimePeriod))).intValue() <= 2) {

            if (log.isDebugEnabled()) {
                log.debug("tryLockPrams [{}] is expiring!current is {}", JSON.toJSONString(tryLockPrams), current);
            }
            tryLockPrams.setDeadLine(current + tryLockPrams.getExpireTimeUnit().toMillis(tryLockPrams.getExpireTime()));
            return true;
        } else {
            return false;
        }
    }

    private boolean isValid(ConcurrentHashMap.Entry<String, TryLockPrams> entry) {

        TryLockPrams prams;
        if (Objects.isNull(prams = entry.getValue())
                || StringUtils.isBlank(prams.getKey())
                || StringUtils.isBlank(prams.getOwner())
                || (prams.getExpireTime()) <= 0
                || Objects.isNull(prams.getExpireTimeUnit())) {

            log.warn("prams [{}] is invalid!", JSON.toJSONString(prams));
            return false;
        }
        return true;
    }

    private void setIfPresentPipeline(List<TryLockPrams> tryLockPrams) {
        redisTemplate.execute((RedisCallback<Object>) connection -> {

            // 开启Pipeline
            connection.openPipeline();
            for (TryLockPrams tryLockPram : tryLockPrams) {
                redisTemplate.opsForValue().setIfPresent(tryLockPram.getKey(), tryLockPram.getOwner(),
                        tryLockPram.getExpireTime(), tryLockPram.getExpireTimeUnit());
            }
            // 关闭Pipeline
            connection.closePipeline();
            return null;
        });
    }

完整示例代码,请参考https://github.com/alldays/spring-boot-mall/tree/master/src/main/java/com/kuqi/mall/demo/conmon/lock

发布了8 篇原创文章 · 获赞 0 · 访问量 3845

猜你喜欢

转载自blog.csdn.net/new_com/article/details/104045501