Use redis based StringRedisTemplate achieve Distributed Lock

In order to prevent mutual interference between multiple processes in a distributed system, we need a distributed coordination technique to schedule these processes. And this is the core technology of distributed coordination to achieve this distributed lock . Specific definition, implementation and usage scenarios, see details of what is distributed lock , strongly recommend the article, written very thorough.

Under emphasized, distributed lock specific conditions and important implementation logic.

  • In a distributed system environment, a method can only be executed by one thread of a machine at the same time

Common scenarios, such as a scheduled task to run multiple services. Repeat the same operation on the same user of the commodity.

  • Highly available acquiring the lock and release the lock

When using redis achieve, redis to achieve the main sentry mode from mode to avoid a single point of call.

  • With lock failure mechanisms to prevent deadlock

Redis set using the mode atomic operation key, value and time to failure, and the type of operation. Implementation of setnx atoms and redis expire operations operate simultaneously. Specific code as follows:

Boolean set(byte[] key, byte[] value, Expiration expiration, SetOption option);
  • Use ScheduledExecutorService regular tasks for re-lock about to expire expiration time set

Define a timeout acquire and release locks methods, interfaces are defined as follows:

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

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

 

In the implementation class RedisLock, set the lua script to release the lock, set the timer to inherit Runnable task to re-set the expiration time for the lock expiring logic. For the operation of the lock timeout acquired, according to the time set by the user setting, the lock is acquired within the timeout period is valid. Detailed code is as follows:

@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;
    }

When the lock is released, using lua script implementation, implementation logic is based on value is compared with the value obtained for the incoming key, and if the same is deleted, do not delete the operation fails. Detailed code is as follows:

   /**
     * 解锁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;
    }

For lock concurrency scenarios, using ScheduledExecutorService timed execution of tasks to reset the expiration time, avoid different processes to acquire the lock. Detailed code is as follows:

  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;
        });
    }

Complete example code, please refer https://github.com/alldays/spring-boot-mall/tree/master/src/main/java/com/kuqi/mall/demo/conmon/lock

Released eight original articles · won praise 0 · Views 3845

Guess you like

Origin blog.csdn.net/new_com/article/details/104045501