redis缓存的应用


title: redis缓存的应用
date: 2025-03-06 16:34:51
tags: redis
categories: redis教程

简单的缓存策略

在这里插入图片描述

@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public Result querygetById(Long id) {
        //1.从Redis内查询商品缓存
        String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
        if(StrUtil.isNotBlank(shopJson)){
            //手动反序列化
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }
        //2.不存在就根据id查询数据库
        Shop shop = getById(id);
        if(shop==null){
            return Result.fail("商户不存在!");
        }
        //3.数据库数据写入Redis
        //手动序列化
        String shopStr = JSONUtil.toJsonStr(shop);
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,shopStr,CACHE_SHOP_TTL, TimeUnit.MINUTES);
        return Result.ok(shop);
    }
}

缓存更新策略

在这里插入图片描述

缓存更新策略的最佳方案:

  1. 低一致性需求:使用Redis自带的内存淘汰机制
  2. 高一致性需求:主动更新,超时剔除的方式作为斗地方案
    • 读操作
      • 缓存命中就直接返回
      • 缓存未命中则查询数据库
    • 写操作:
      • 先写数据库,然后再删除缓存
      • 要确保数据库与缓存操作的原子性

操作缓存和数据库时需要考虑的三个问题

  1. 删除缓存还是更新缓存?
    • 每次更新数据库的同时更新缓存:若数据库更新了 100 次,期间没有任何查询请求,此时缓存的更新就是无效操作。
    • 数据库更新就删除缓存:数据库更新后缓存被删除,此时数据库无论更新多少次,缓存都不会做任何操作。直到有查询请求,缓存才会将数据库中的数据写入到缓存中。
  2. 如何保证缓存和数据库的操作同时成功或失败?
    • 单体系统:将缓存与数据库操作放在一个事务。
    • 分布式系统:利用 TCC 等分布式事务方案。
  3. 先操作缓存还是先操作数据库?
    • 先删除缓存,再操作数据库。假设缓存为 10,数据库为 10。(t1、t2、t3 代表三个时刻)
      • t1:线程 1 删除缓存,并更新数据库为 20。t2:线程 2 查询缓存未命中,从数据库中查询并写入缓存。✔️
      • t1:线程 1 删除缓存。t2:线程 2 查询缓存未命中,从数据库中查询并写入缓存。t3:线程 t1 更新数据库为 20。❌
    • **先操作数据库,再删除缓存。**假设缓存为 10,数据库为 10。(t1、t2、t3、t4 代表四个时刻)
      • t1:线程 1 更新数据库为 20,删除缓存。t2:线程 2 查询缓存未命中,从数据库中查询并写入缓存。✔️
      • t1:线程 1 查询缓存未命中,从数据库中查询。t2:线程 2 更新数据库为 20,删除缓存。t3:线程 1 写入缓存。❌(这种方式出现的概率很小,缓存写入的速度很快。更可能出现的情况是:线程 1 写入缓存后,线程 2 更新数据库然后将缓存删除)
@Override
public Result update(Shop shop) {
    if(shop.getId()==null){
        return Result.fail("店铺id不能为空!");
    }
    //1.更新数据库
    updateById(shop);
    //2.删除缓存
    String key = CACHE_SHOP_KEY + shop.getId();
    stringRedisTemplate.delete(key);
    return Result.ok();
}

缓存穿透问题

在这里插入图片描述

缓存空对象方案

客户端请求的数据在 Redis 和数据库中都不存在,为了防止不断的请求:将 空值 缓存到 Redis 中并且设置 TTL 时间后,返回给该请求。

  • 缓存中包含过多的 空值,会造成额外的内存消耗。(设置 TTL 可以缓解)

  • 可能造成短期的不一致:第一次请求的数据在 Redis 和数据库中都不存在,缓存空对象后,数据库中新增了该请求对应的数据

在这里插入图片描述

@Override
public CommonResult<Shop> getShopById(Long id) {
    ThrowUtils.throwIf(id == null, ErrorCode.PARAMS_ERROR);
    String shopKey = CACHE_SHOP_KEY + id;

    // 1. 先从 Redis 中查询数据,存在则将其转换为 Java 对象后返回
    String shopJsonInRedis = stringRedisTemplate.opsForValue().get(shopKey);
    if (StringUtils.isNotBlank(shopJsonInRedis)) {
        return CommonResult.success(JSONUtil.toBean(shopJsonInRedis, Shop.class));
    }
  	// 命中空值
		if (shopJsonInRedis != null) {
		    throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "该商铺不存在");
		}

    // 2. 从 Redis 中未查询到数据,则从数据库中查询
    Shop shop = this.getById(id);
    
    // 若数据中也查询不到,则缓存空值后返回提示信息
    if (shop == null) {
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", TTL_TWO, TimeUnit.MINUTES);
        throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "该商铺不存在");
    }

    // 3. 将从数据库中查询到的数据存入 Redis 后返回
    stringRedisTemplate.opsForValue().set(shopKey, JSONUtil.toJsonStr(shop), TTL_TWO, TimeUnit.HOURS);
    return CommonResult.success(shop);
}

布隆过滤器方案

布隆过滤器(Bloom Filter):一个很长的二进制数组(初始化值为 0),通过一系列的 Hash 函数判断该数据是否存在。 布隆过滤器的运行速度快、内存占用小,但是存在误判的可能。

  1. 存储数据时经过 n 个 hash 函数,计算出 n 个 hash 值,hash 值映射后得到 n 个索引,设置索引处的值为 1。(若当前索引处值已经为 1,则不需要任何操作)
  2. 查询数据时也会经过 n 个 hash 函数,计算出 n 个 hash 值,hash 值映射后得到 n 个索引,判断索引处的值是否为 1。
    • 查询 Anthony:经过 hash 算法得到的 hash 值映射后数组下标为 0、2、6,下标对应的值没有全为 1,数组中不存在该元素。
    • 查询 Coco:经过 hash 算法得到的 hash 值映射后数组下标为 0、2、6,下标对应的值都为 1,数组中可能存在该元素。

在这里插入图片描述

缓存雪崩问题:

在这里插入图片描述

解决方案:

  • 给不同的Key的TTL添加随机值
  • 利用Redis集群提高服务的可用性
  • 给缓存业务添加降级限流策略
  • 给业务添加多级缓存

缓存击穿问题

缓存击穿问题,也叫 热点 Key 问题;就是一个被 高并发访问 并且 缓存中业务较复杂的 Key 突然失效,大量的请求在极短的时间内一起请求这个 Key 并且都未命中,无数的请求访问在瞬间打到数据库上,给数据库带来巨大的冲击。

缓存击穿整体过程:

  1. 一个线程查询缓存,未命中,查询数据库并重建缓存(缓存重建业务比较复杂,时间长)。
  2. 在这个重建缓存的过程中,大量的请求穿过缓存直接请求数据库并重建缓存,导致性能下降。

解决方案:互斥锁(一致性)、逻辑过期(可用性)

在这里插入图片描述

互斥锁方案

synchronized

  1. 查询缓存,存在则直接返回。
  2. 不存在:执行 synchronized 代码块。
    1. 先查缓存,存在则直接返回。(若多个线程执行到同步代码块,某个线程拿到锁查询数据库并重建缓存后,其他拿到锁进来的线程直接查询缓存后返回,避免重复查询数据库并重建缓存)
    2. 查询数据库,重建缓存。
@SneakyThrows
@Override
public CommonResult<Shop> getShopById(Long id) {
    ThrowUtils.throwIf(id == null, ErrorCode.PARAMS_ERROR);
    String shopKey = CACHE_SHOP_KEY + id;

    // 1. 先从 Redis 中查询数据,存在则将其转换为 Java 对象后返回
    String shopJsonInRedis = stringRedisTemplate.opsForValue().get(shopKey);
    if (StringUtils.isNotBlank(shopJsonInRedis)) {
        return CommonResult.success(JSONUtil.toBean(shopJsonInRedis, Shop.class));
    }
  	// 命中空值
		if (shopJsonInRedis != null) {
		    throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "该商铺不存在");
		}

    // 2. 从 Redis 中未查询到数据,则从数据库中查询。(synchronized)
    Shop shop = new Shop();
    synchronized (ShopServiceImpl.class) {
        // 3. 再次查询 Redis:若多个线程执行到同步代码块,某个线程拿到锁查询数据库并重建缓存后,其他拿到锁进来的线程直接查询缓存后返回,避免重复查询数据库并重建缓存。
        shopJsonInRedis = stringRedisTemplate.opsForValue().get(shopKey);
        if (StringUtils.isNotBlank(shopJsonInRedis)) {
            return CommonResult.success(JSONUtil.toBean(shopJsonInRedis, Shop.class));
        }

        // 4. 查询数据库,缓存空值避免缓存穿透,重建缓存。
        shop = this.getById(id);
        if (shop == null) {
            stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", TTL_TWO, TimeUnit.MINUTES);
            throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "该商铺不存在");
        }
      	// 模拟缓存重建延迟
        Thread.sleep(100);
        stringRedisTemplate.opsForValue().set(shopKey, JSONUtil.toJsonStr(shop), TTL_TWO, TimeUnit.HOURS);
    }
    return CommonResult.success(shop);
}

用redis的setnx来充当分布式锁

/**
 * 获取互斥锁
 */
public boolean tryLock(String key) {
    Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", TTL_TWO, TimeUnit.SECONDS);
    return Boolean.TRUE.equals(result);
}

/**
 * 释放互斥锁
 */
public void unlock(String key) {
    stringRedisTemplate.delete(key);
}
@SneakyThrows
@Override
public CommonResult<Shop> getShopById(Long id) {
    ThrowUtils.throwIf(id == null, ErrorCode.PARAMS_ERROR);
    String shopKey = CACHE_SHOP_KEY + id;
  	String lockKey = LOCK_SHOP_KEY + id;

    // 1. 先从 Redis 中查询数据,存在则将其转换为 Java 对象后返回
    String shopJsonInRedis = stringRedisTemplate.opsForValue().get(shopKey);
    if (StringUtils.isNotBlank(shopJsonInRedis)) {
        return CommonResult.success(JSONUtil.toBean(shopJsonInRedis, Shop.class));
    }
  	// 命中空值
		if (shopJsonInRedis != null) {
		    throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "该商铺不存在");
		}

    // 2. 从 Redis 中未查询到数据,尝试获取锁后从数据库中查询。
    Shop shop = new Shop();
    boolean tryLock = tryLock(lockKey);
    try {
        // 2.1 未获取到锁则等待一段时间后重试(通过递归调用重试)
        if (BooleanUtil.isFalse(tryLock)) {
            Thread.sleep(50);
            this.getShopById(id);
        }

        // 2.2 获取到锁:查询数据库、缓存重建。
        if (tryLock) {
            // 3. 再次查询 Redis:若多个线程执行到获取锁处,某个线程拿到锁查询数据库并重建缓存后,其他拿到锁进来的线程直接查询缓存后返回,避免重复查询数据库并重建缓存。
            shopJsonInRedis = stringRedisTemplate.opsForValue().get(shopKey);
            if (StringUtils.isNotBlank(shopJsonInRedis)) {
                return CommonResult.success(JSONUtil.toBean(shopJsonInRedis, Shop.class));
            }

            // 4. 查询数据库,缓存空值避免缓存穿透,重建缓存。
            shop = this.getById(id);
            if (shop == null) {
                stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", TTL_TWO, TimeUnit.MINUTES);
                throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "该商铺不存在");
            }
          	// 模拟缓存重建延迟
            Thread.sleep(100);
            stringRedisTemplate.opsForValue().set(shopKey, JSONUtil.toJsonStr(shop), TTL_TWO, TimeUnit.HOURS);
        }
    } finally {
        // 5. 释放锁
        unlock(lockKey);
    }
    return CommonResult.success(shop);
}

逻辑过期方案

无需考虑缓存雪崩(Redis 宕机除外)、缓存穿透问题:缓存何时过期通过代码控制而非 TTL。需要进行数据预热,缓存未命中时直接返回空。

  1. 先查询缓存,未命中则直接返回。

  2. 命中则判断缓存是否过期,未过期则直接返回。

  3. 过期:获取锁。

    1. 未获取到锁:直接返回。
    2. 获取到锁:开启一个新的线程后直接返回,这个线程负责重建缓存后释放锁。

存储到 Redis 中的 Key 永久有效,过期时间通过代码控制而非 TTL。Redis 存储的数据需要带上一个逻辑过期时间,即 Shop 实体类中需要一个逻辑过期时间属性。新建一个 RedisData,该类包含两个属性 expireTime 和 Data,对原来的代码没有入侵性。

缓存预热(将热点数据提前存储到 Redis 中)

@Data
public class RedisData {
    private LocalDateTime expireTime;
    private Object data;
}

/**
 * 缓存预热(将热点数据提前存储到 Redis 中)
 */
public void saveHotDataIn2Redis(Long id, Long expireSeconds) {
    Shop shop = this.getById(id);
    ThrowUtils.throwIf(shop == null, ErrorCode.NOT_FOUND_ERROR, "该数据不存在");
    RedisData redisData = new RedisData();
    redisData.setData(shop);
    redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
    stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}

# Redis 中存储的数据会多一个 expireTime 的值
{
  "expireTime": 1681660099861,
  "data": {
    "id": 1,
    "name": "101茶餐厅",
    "typeId": 1,
    ...
  }
}

逻辑过期

/**
 * 缓存预热(将热点数据提前存储到 Redis 中)
 */
public void saveHotDataIn2Redis(Long id, Long expireSeconds) throws InterruptedException {
    Shop shop = this.getById(id);
    ThrowUtils.throwIf(shop == null, ErrorCode.NOT_FOUND_ERROR, "该数据不存在");
    // 模拟缓存重建延迟,让一部分线程先执行完毕,在此期间会短暂的不一致
    Thread.sleep(200);
    RedisData redisData = new RedisData();
    redisData.setData(shop);
    redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
    stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}

private static final ExecutorService ES = Executors.newFixedThreadPool(10);

@SneakyThrows
@Override
public CommonResult<Shop> getShopById(Long id) {
    ThrowUtils.throwIf(id == null, ErrorCode.PARAMS_ERROR);
    String shopKey = CACHE_SHOP_KEY + id;
    String lockKey = LOCK_SHOP_KEY + id;
    // 1. 先从 Redis 中查询数据,未命中则直接返回
    String redisDataJson = stringRedisTemplate.opsForValue().get(shopKey);
    if (StringUtils.isBlank(redisDataJson)) {
        return CommonResult.success(null);
    }
    // 2. 判断是否过期,未过期则直接返回
    RedisData redisData = JSONUtil.toBean(redisDataJson, RedisData.class);
    JSONObject jsonObject = (JSONObject) redisData.getData();
    Shop shop = JSONUtil.toBean(jsonObject, Shop.class);
    LocalDateTime expireTime = redisData.getExpireTime();
    if (expireTime.isAfter(LocalDateTime.now())) {
        return CommonResult.success(shop);
    }
    // 3. 未获取到锁直接返回
    boolean tryLock = tryLock(lockKey);
    if (BooleanUtil.isFalse(tryLock)) {
        return CommonResult.success(shop);
    }
    // 4. 获取到锁:开启一个新的线程后返回旧数据。(这个线程负责查询数据库、重建缓存)
    // 此处无需 DoubleCheck,因为未获取到锁直接返回旧数据,能保证只有一个线程执行到此处
    ES.submit(() -> {
        try {
            // 查询数据库、重建缓存
            this.saveHotDataIn2Redis(id, 3600 * 24L);
        } catch (Exception e) {
            log.error(e.getMessage());
        } finally {
            unlock(lockKey);
        }
    });
    return CommonResult.success(shop);
}

总结Redis Cache工具类:

@Component
@Slf4j
public class CacheClient {
    private static final ExecutorService ES = Executors.newFixedThreadPool(10);
  	
    private final StringRedisTemplate stringRedisTemplate;

    public CacheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * 获取锁
     */
    public boolean tryLock(String key) {
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", TTL_TWO, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(result);
    }

    /**
     * 释放锁
     */
    public void unlock(String key) {
        stringRedisTemplate.delete(key);
    }

    /**
     * 数据预热(将热点数据提前存储到 Redis 中)
     *
     * @param key        预热数据的 Key
     * @param value      预热数据的 Value
     * @param expireTime 逻辑过期时间
     * @param timeUnit   时间单位
     */
    public void dataWarmUp(String key, Object value, Long expireTime, TimeUnit timeUnit) {
        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(expireTime)));
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

    /**
     * 将 Java 对象序列化为 JSON 存储到 Redis 中并且设置 TTL 过期时间
     *
     * @param key      String 类型的键
     * @param value    序列化为 JSON 的值
     * @param time     TTL 过期时间
     * @param timeUnit 时间单位
     */
    public void set(String key, Object value, Long time, TimeUnit timeUnit) {
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, timeUnit);
    }

    /**
     * 解决缓存穿透问(缓存空值)
     *
     * @param keyPrefix Key 前缀
     * @param id        id
     * @param type      实体类型
     * @param function  有参有返回值的函数
     * @param time      TTL 过期时间
     * @param timeUnit  时间单位
     * @param <R>       实体类型
     * @param <ID>      id 类型
     * @return 设置某个实体类的缓存,并解决缓存穿透问题
     */
    public <R, ID> R setWithCachePenetration(String keyPrefix, ID id, Class<R> type, Function<ID, R> function, Long time, TimeUnit timeUnit) {
        String key = keyPrefix + id;

        // 1. 先从 Redis 中查询数据,存在则将其转换为 Java 对象后返回
        String jsonStr = stringRedisTemplate.opsForValue().get(key);
        if (StringUtils.isNotBlank(jsonStr)) {
            return JSONUtil.toBean(jsonStr, type);
        }
        // 命中空值
        if (jsonStr != null) {
            throw new BusinessException(ErrorCode.NOT_FOUND_ERROR);
        }

        // 2. 从 Redis 中未查询到数据,则从数据库中查询
        R result = function.apply(id);
        // 若数据中也查询不到,则缓存空值后返回提示信息
        if (result == null) {
            stringRedisTemplate.opsForValue().set(key, "", TTL_TWO, TimeUnit.MINUTES);
            throw new BusinessException(ErrorCode.NOT_FOUND_ERROR);
        }

        // 3. 将从数据库中查询到的数据存入 Redis 后返回
        this.set(key, result, time, timeUnit);
        return result;
    }

    /**
     * 解决缓存击穿问题(synchronized)
     */
    public <R, ID> R setWithCacheBreakdown4Synchronized(String keyPrefix, ID id, Class<R> type, Function<ID, R> function, Long time, TimeUnit timeUnit) {
        String key = keyPrefix + id;

        // 1. 先从 Redis 中查询数据,存在则将其转换为 Java 对象后返回
        String jsonStr = stringRedisTemplate.opsForValue().get(key);
        if (StringUtils.isNotBlank(jsonStr)) {
            return JSONUtil.toBean(jsonStr, type);
        }
        // 命中空值
        if (jsonStr != null) {
            throw new BusinessException(ErrorCode.NOT_FOUND_ERROR);
        }

        // 2. 从 Redis 中未查询到数据,则从数据库中查询。(synchronized)
        R result = null;
        synchronized (CacheClient.class) {
            // 3. 再次查询 Redis:若多个线程执行到同步代码块,某个线程拿到锁查询数据库并重建缓存后,其他拿到锁进来的线程直接查询缓存后返回,避免重复查询数据库并重建缓存。
            jsonStr = stringRedisTemplate.opsForValue().get(key);
            if (StringUtils.isNotBlank(jsonStr)) {
                return JSONUtil.toBean(jsonStr, type);
            }

            // 4. 查询数据库、缓存空值避免缓存穿透、重建缓存。
            result = function.apply(id);
            if (result == null) {
                stringRedisTemplate.opsForValue().set(key, "", TTL_TWO, TimeUnit.MINUTES);
                throw new BusinessException(ErrorCode.NOT_FOUND_ERROR);
            }
            this.set(key, result, time, timeUnit);
        }
        return result;
    }
  	
    /**
     * 解决缓存击穿问题(setnx)
     */
    public <R, ID> R setWithCacheBreakdown4SetNx(String keyPrefix, ID id, Class<R> type, Function<ID, R> function, Long time, TimeUnit timeUnit) {
        String key = keyPrefix + id;
        String lockKey = LOCK_SHOP_KEY + id;

        // 1. 先从 Redis 中查询数据,存在则将其转换为 Java 对象后返回
        String jsonStr = stringRedisTemplate.opsForValue().get(key);
        if (StringUtils.isNotBlank(jsonStr)) {
            return JSONUtil.toBean(jsonStr, type);
        }
        // 命中空值
        if (jsonStr != null) {
            throw new BusinessException(ErrorCode.NOT_FOUND_ERROR);
        }

        // 2. 从 Redis 中未查询到数据,尝试获取锁后从数据库中查询。
        R result = null;
        boolean tryLock = tryLock(lockKey);
        try {
            // 2.1 未获取到锁则等待一段时间后重试(通过递归调用重试)
            if (BooleanUtil.isFalse(tryLock)) {
                Thread.sleep(50);
                this.setWithCacheBreakdown4SetNx(keyPrefix, id, type, function, time, timeUnit);
            }
            // 2.2 获取到锁:查询数据库、缓存重建。
            if (tryLock) {
                // 3. 再次查询 Redis:若多个线程执行到同步代码块,某个线程拿到锁查询数据库并重建缓存后,其他拿到锁进来的线程直接查询缓存后返回,避免重复查询数据库并重建缓存。
                jsonStr = stringRedisTemplate.opsForValue().get(key);
                if (StringUtils.isNotBlank(jsonStr)) {
                    return JSONUtil.toBean(jsonStr, type);
                }

                // 4. 查询数据库、缓存空值避免缓存穿透、重建缓存。
                result = function.apply(id);
                if (result == null) {
                    stringRedisTemplate.opsForValue().set(key, "", TTL_TWO, TimeUnit.MINUTES);
                    throw new BusinessException(ErrorCode.NOT_FOUND_ERROR);
                }
                this.set(key, result, time, timeUnit);
            }
        } catch (Exception e) {
            log.error(e.getMessage());
        } finally {
            unlock(lockKey);
        }
        return result;
    }

    /**
     * 解决缓存击穿问题(逻辑过期时间)
     */
    public <R, ID> R setWithCacheBreakdown4LogicalExpiration(String keyPrefix, ID id, Class<R> type, Function<ID, R> function, Long time, TimeUnit timeUnit) {
        String key = keyPrefix + id;
        String lockKey = LOCK_SHOP_KEY + id;

        // 1. 先从 Redis 中查询数据,未命中则直接返回
        String jsonStr = stringRedisTemplate.opsForValue().get(key);
        if (StringUtils.isBlank(jsonStr)) {
            return null;
        }

        // 2. 判断是否过期,未过期则直接返回
        RedisData redisData = JSONUtil.toBean(jsonStr, RedisData.class);
        JSONObject jsonObject = JSONUtil.parseObj(redisData.getData());
        R result = JSONUtil.toBean(jsonObject, type);
        LocalDateTime expireTime = redisData.getExpireTime();
        if (expireTime.isAfter(LocalDateTime.now())) {
            return result;
        }

        // 3. 未获取到锁直接返回
        boolean tryLock = tryLock(lockKey);
        if (BooleanUtil.isFalse(tryLock)) {
            return result;
        }

        // 4. 获取到锁:开启一个新的线程后返回旧数据。(这个线程负责查询数据库、重建缓存)
        // 此处无需 DoubleCheck,因为未获取到锁直接返回旧数据,能保证只有一个线程执行到此处
        ES.submit(() -> {
            try {
                this.dataWarmUp(key, function.apply(id), time, timeUnit);
            } finally {
                unlock(lockKey);
            }
        });
        return result;
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值