空间换时间的redis分布式锁,高并发下怎么保证相同请求尽可能少回源查询

18,960 阅读3分钟

一:具体实现

 /**
     * 存在在高并发情况下,同一个key大量查询缓存,但是此时缓存过期,为了保证同一个key回源,其他请求不等待
     * 这个采用以空间换时间的概念,为了保证不阻塞,且业务数据能够正常返回,这边利用到了缓存副本,切保证缓存副本的过期时间
     * 和正式缓存错开
     * 如果没有获取到锁,切副本中也没有数据,这时候再回源查询
     * @param param
     * @return
     */
    private List<Integer> redisLock(int param) {
        if (param <= 0) {
            return Collections.emptyList();
        }
        List<Integer>  dataList = new ArrayList<>();
        Jedis jedis = jedisPool.getResource();
        String redisKey1 = "key" + param;
        var value = jedis.get(redisKey1);
        if (null == value) {
            final String identifier = UUID.randomUUID().toString();
            String lockKey = "lock" + param + identifier;
            String redisKeyCopy = "keyCopy" + param;
            if (RedisLockUtils.tryLock(redisStoreClient, lockKey, identifier, 30, true)) {
                try {
                    // db 回源
                    list = commonService.getDataList();
                    // 设置redis值
                    redisStoreClient.set(storeKey, list);
                    // 设置redis 副本值
                    redisStoreClient.set(storeKeyCopy, list);
                } finally {
                    // 解锁
                    RedisLockUtils.unLock(redisStoreClient, lockKey, identifier);
                }
            } else {
                list = redisStoreClient.get(storeKeyCopy);
                if (null == list) {
                    log.info("storeKey redisKeyCopy is null");
                    list = commonService.getDataList();
                }
            }
        }
        Optional.ofNullable(Lists.newArrayList()).ifPresent(dataList::retainAll);
        if (CollectionUtils.isNotEmpty(dataList) && dataList.size() > 10) {
            dataList = dataList.subList(0, 10);
        }
        return dataList;
    }

二:方法中RedisLockUtils 工具类

package com.maoyan.show.hk.ticketdata.service.cache;

import com.maoyan.myredis.clientV2.MYStoreKey;
import com.maoyan.myredis.clientV2.impl.redis.MYRedisStoreClient;
import io.netty.util.HashedWheelTimer;
import io.netty.util.Timeout;
import io.netty.util.TimerTask;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.TimeUnit;

public class RedisLockUtils {


    private static final Logger log = LoggerFactory.getLogger(RedisLockUtils.class);
    private static final HashedWheelTimer timer = new HashedWheelTimer();

    private RedisLockUtils() {
        throw new AssertionError("No util.RedisLockUtils instances for you!");
    }

    private static boolean isEmpty(CharSequence cs) {
        return cs == null || cs.length() == 0;
    }

    /**
     * 获取锁
     *
     * @param redisStoreClient redisClient
     * @param storeKey         要获取的锁
     * @param identifier       使用者唯一标识
     * @param lockTimeout      锁失效时间
     * @return {@code true} 获取锁成功, {@code false} 竞争锁失败
     */
    public static Boolean tryLock(MYRedisStoreClient redisStoreClient, MYStoreKey storeKey, String identifier, int lockTimeout,String logTitle) {
        return tryLock(redisStoreClient, storeKey, identifier, lockTimeout, 5, false,logTitle);
    }

    /**
     * 获取锁
     *
     * @param redisStoreClient redisClient
     * @param storeKey         要获取的锁
     * @param identifier       使用者唯一标识
     * @param lockTimeout      锁失效时间
     * @param isRenewal        锁续期是否开启
     * @return {@code true} 获取锁成功, {@code false} 竞争锁失败
     */
    public static Boolean tryLock(MYRedisStoreClient redisStoreClient, MYStoreKey storeKey, String identifier, int lockTimeout, boolean isRenewal,String logTitle) {
        return tryLock(redisStoreClient, storeKey, identifier, lockTimeout, 1, isRenewal,logTitle);
    }

    /**
     * 获取锁
     *
     * @param redisStoreClient redisClient
     * @param storeKey         要获取的锁
     * @param identifier       使用者唯一标识
     * @param lockTimeout      锁失效时间
     * @param acquireTimeout   锁竞争超时时间
     * @param isRenewal        锁续期是否开启
     * @return {@code true} 获取锁成功, {@code false} 竞争锁失败
     */
    public static Boolean tryLock(MYRedisStoreClient redisStoreClient, MYStoreKey storeKey, String identifier, int lockTimeout, int acquireTimeout, boolean isRenewal, String logTitle) {
        return tryLock(redisStoreClient, storeKey, identifier, lockTimeout, acquireTimeout, 1000L, isRenewal,logTitle);
    }

    /**
     * 获取锁
     *
     * @param redisStoreClient redisClient
     * @param storeKey         要获取的锁
     * @param identifier       使用者唯一标识
     * @param lockTimeout      锁失效时间
     * @param acquireTimeout   锁竞争超时时间
     * @param retryDuration    尝试竞争锁的间隔时间(每次等待retryDuration毫秒后,再次竞争锁)
     * @param isRenewal        锁续期是否开启
     * @return {@code true} 获取锁成功, {@code false} 竞争锁失败
     */
    public static Boolean tryLock(MYRedisStoreClient redisStoreClient, MYStoreKey storeKey, String identifier, int lockTimeout, int acquireTimeout, long retryDuration, boolean isRenewal,String logTitle) {
        if (storeKey != null && lockTimeout > 0) {
            Long endTime = System.currentTimeMillis() + acquireTimeout * 1000L;
            log.info("{} :Try to acquire the lock. lockKey={},acquireTimeout={}s,lockTimeout={}s", logTitle,storeKey, acquireTimeout, lockTimeout);
            do {
                boolean acquiredLock = redisStoreClient.setnx(storeKey, identifier, lockTimeout);
                if (acquiredLock) {
                    if (isRenewal) {
                        //lock renewal
                        scheduleExpirationRenewal(redisStoreClient, storeKey, identifier, lockTimeout, logTitle);
                    }
                    log.info("{} : acquired lock. lockKey={}", logTitle,storeKey);
                    return true;
                }
                log.info("{} : Retry to acquire the lock. lockKey={},acquireTimeout={}s,lockTimeout={}s",logTitle, storeKey, acquireTimeout, lockTimeout);
                try {
                    log.info("{} : wait 1000 milliseconds before retry. lockKey={}", logTitle, storeKey);
                    Thread.sleep(retryDuration);
                } catch (InterruptedException ignored) {
                    ;
                }
            } while (System.currentTimeMillis() < endTime);
        }
        return false;
    }

    /**
     * 释放锁
     *
     * @param redisStoreClient redisClient
     * @param storeKey         要获取的锁
     * @param userTag          使用者唯一标识
     */
    public static void unLock(MYRedisStoreClient redisStoreClient, final MYStoreKey storeKey, final String userTag) {
        if (storeKey != null && userTag != null) {
            //比较value并删除key采用原子操作,保证解除的是自己的锁
            redisStoreClient.compareAndDelete(storeKey, userTag);
        }
    }

    /**
     * 锁续期机制
     *
     * @param redisStoreClient redisClient
     * @param storeKey         要获取的锁
     * @param identifier       使用者唯一标识
     * @param lockTimeout      锁失效时间
     */
    private static void scheduleExpirationRenewal(MYRedisStoreClient redisStoreClient, MYStoreKey storeKey, String identifier, int lockTimeout, String logTitle) {
        Timeout task = timer.newTimeout(new TimerTask() {
            @Override
            public void run(Timeout timeout) throws Exception {
                try {
                    if (redisStoreClient.compareAndSet(storeKey, identifier, identifier, lockTimeout)) {
                        log.info("{} : lock time renewal success. lockKey={}", logTitle, storeKey);
                        scheduleExpirationRenewal(redisStoreClient, storeKey, identifier, lockTimeout, logTitle);
                    }
                } catch (Exception e) {
                    log.info("{} : lock time renewal failed. lockKey={}", logTitle, storeKey);
                }
            }
        }, lockTimeout * 1000L * 2 / 3, TimeUnit.MILLISECONDS);
    }
}