当前位置: 首页 > article >正文

Redis学习(hmdp-缓存优化模块)

缓存

添加redis缓存

无论有没有查询前都先查询redis
无论是redis还是数据库,查询出来的结果都需要进行判断
redis放置的都是value为jsonstr的数据
判断缓存是否命中:StrUtil.isNotBlank()、Objects.isNull()
双检

image.png
1、从Redis中查询店铺数据
2、判断缓存是否命中(不为空就是命中)
2.1 缓存命中,直接返回店铺数据
2.2 缓存未命中,从数据库中查询店铺数据 (数据库中可能也没有这个数据)
3、判断数据库是否存在店铺数据
3.1 数据库中不存在,返回失败信息
3.2 数据库中存在,写入Redis,并返回店铺数据

    /**
     * 根据id查询商铺数据
     *
     * @param id
     * @return
     */
    @Override
    public Result queryById(Long id) {
        String key = CACHE_SHOP_KEY + id;
        // 1、从Redis中查询店铺数据
	    String shopJson = stringRedisTemplate.opsForValue().get(key);
	    Shop shop = null;
        // 2、判断缓存是否命中
        if(StrUtil.isNotBlank(shopJson)){
            // 2.1 缓存命中,直接返回店铺数据
	        shop = JSONUtil.toBean(shopJson,shop.class);
	        return Result.ok(shop);
        }
        // 2.2 缓存未命中,从数据库中查询店铺数据
        shop = this.getById(id);
        // 3、判断数据库是否存在店铺数据
        if(Objects.isNull(shop)){
            // 3.1 数据库中不存在,返回失败信息
	        return Result.fail("店铺不存在");
        }
        // 3.2 数据库中存在,写入Redis,并返回店铺数据
        stringRedisTemplate.opsValueFor().set(key,JSONUtil.toJsonStr(shop));             return Result.ok(shop);
    }

细节

  1. shopJson(json)使用JSONUtil转化为shop(java对象)
  2. 数据库和缓存中可能都没有要查找的数据,都没有就返回失败
    image.png
    shoptype
    /**
     * 查询店铺的类型
     *
     * @return
     */
    @Override
    public Result queryTypeList() {
        // 1、从Redis中查询店铺类型
        String key = CACHE_SHOP_TYPE_KEY + UUID.randomUUID().toString(true);
        String shopTypeJSON = stringRedisTemplate.opsForValue().get(key);

        List<ShopType> typeList = null;
        // 2、判断缓存是否命中
        if(StrUtil.isNotBlank(typeList)){
            // 2.1 缓存命中,直接返回缓存数据
            return Result.ok(typeList);
		}
        // 2.2 缓存未命中,查询数据库
		typeList = this.list(this.list(new LambdaQueryWrapper<ShopType>() .orderByAsc(ShopType::getSort));
        // 3、判断数据库中是否存在该数据
		if (Objects.isNull(typeList)) { 
			// 3.1 数据库中不存在该数据,返回失败信息 
			return Result.fail("店铺类型不存在"); 
		}
        // 3.2 店铺数据存在,写入Redis,并返回查询的数据
		stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(typeList), CACHE_SHOP_TYPE_TTL, TimeUnit.MINUTES);
        return Result.ok(typeList);
    }

缓存更新策略

image.png

修改ShopController中的业务逻辑,满足下面的需求:

  • 根据id查询店铺时,如果缓存未命中,则查询数据库,将数据库结果写入缓存,并设置超时时间(同上)
  • 根据id修改店铺时,先修改数据库,再删除缓存

采用TTL过期+内存淘汰机制作为兜底方案,同时将缓存和数据库的操作放到同一个事务来保障操作的原子性

@Transactional

	/**
     * 更新商铺数据(更新时,更新数据库,删除缓存)
     *
     * @param shop
     * @return
     */
    @Transactional
    @Override
    public Result updateShop(Shop shop) {
        // 参数校验, 略

        // 1、更新数据库中的店铺数据
        boolean f = this.updateById(shop);
        if (!f){
            // 缓存更新失败,抛出异常,事务回滚
            throw new RuntimeException("数据库更新失败");
        }
        // 2、删除缓存
        f = stringRedisTemplate.delete(CACHE_SHOP_KEY + shop.getId());
        if (!f){
            // 缓存删除失败,抛出异常,事务回滚
            throw new RuntimeException("缓存删除失败");
        }
        return Result.ok();
    }

缓存穿透

参考!

image.png

方案一:设置缓存空对象,缓解压力,一定要设置过期时间,否则数据库更新了查到的还是空值。
image.png

/**
     * 根据id查询商铺数据
     *
     * @param id
     * @return
     */
    @Override
    public Result queryById(Long id) {
        String key = CACHE_SHOP_KEY + id;
        // 1、从Redis中查询店铺数据
	    String shopJson = stringRedisTemplate.opsForValue().get(key);
	    Shop shop = null;
        // 2、判断缓存是否命中
        if(StrUtil.isNotBlank(shopJson)){
            // 2.1 缓存命中,判断是不是空值
            // 是不是空值,都直接返回
	        shop = JSONUtil.toBean(shopJson,shop.class);
	        return Result.ok(shop);
        }
        // 2.2 缓存未命中,从数据库中查询店铺数据
        shop = this.getById(id);
        // 3、判断数据库是否存在店铺数据
        if(Objects.isNull(shop)){
            // 3.1 数据库中不存在,redis写入空值,并设置过期时间
            stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.SECONDS);
	        
        }
        // 3.2 数据库中存在,写入Redis,并返回店铺数据
        stringRedisTemplate.opsValueFor().set(key,JSONUtil.toJsonStr(shop));             return Result.ok(shop);
    }

方案二:布隆过滤器

不存在
存在
命中
未命中
存在
不存在
开始
提交商铺id
布隆过滤器判断id是否存在
返回店铺不存在
从Redis查询商铺缓存
判断缓存是否命中
返回商铺信息
根据id查询数据库
判断商铺是否存在
将商铺数据写入Redis
返回商铺信息
返回店铺不存在
结束
/**
 * 根据id查询商铺数据
 * 使用布隆过滤器防止缓存穿透
 *
 * @param id 商铺id
 * @return Result
 */
@Override
public Result queryById(Long id) {
    String key = CACHE_SHOP_KEY + id;
    
    // 1、通过布隆过滤器判断id是否存在
    if (!bloomFilter.mightContain(id)) {
        // 1.1 布隆过滤器显示id不存在,直接拒绝
        return Result.fail("店铺不存在");
    }
    
    // 2、通过布隆过滤器,从Redis中查询店铺缓存
    String shopJson = stringRedisTemplate.opsForValue().get(key);
    Shop shop = null;
    
    // 3、判断缓存是否命中
    if (StrUtil.isNotBlank(shopJson)) {
        // 3.1 缓存命中,直接返回店铺数据
        shop = JSONUtil.toBean(shopJson, Shop.class);
        return Result.ok(shop);
    }
    
    // 3.2 缓存未命中,判断是否是缓存的空值(之前的缓存穿透方案可以移除,因为有布隆过滤器兜底)
    
    // 4、从数据库中查询店铺数据
    shop = this.getById(id);
    
    // 5、判断数据库中是否存在该店铺
    if (Objects.isNull(shop)) {
        // 5.1 数据库不存在,说明布隆过滤器出现了误判,返回错误信息
        return Result.fail("店铺不存在");
    }
    
    // 5.2 数据库中存在,写入Redis缓存
    stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), 
                                          CACHE_SHOP_TTL, TimeUnit.MINUTES);
    
    return Result.ok(shop);
}

/**
 * 初始化布隆过滤器
 * 这个方法应该在服务启动时调用,将所有店铺ID加载到布隆过滤器中
 */
@PostConstruct
public void initBloomFilter() {
    // 创建布隆过滤器,设置预期元素数量和误判率
    bloomFilter = BloomFilter.create(
        Funnels.longFunnel(),
        1000000, // 预期店铺数量
        0.01     // 期望的误判率
    );
    
    // 查询所有店铺ID
    List<Long> shopIds = this.listObjs(wrapper -> wrapper.select("id"), Object::toString);
    
    // 将所有店铺ID添加到布隆过滤器
    for (Long shopId : shopIds) {
        bloomFilter.put(shopId);
    }
}

缓存击穿

缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。

image.png

image.png

需要把从redis查询商铺数据单独抽出来
新建两个方法,pv操作
在缓存重建前后加锁和释放锁
判断是否获取锁后还需要判断是否需要重建缓存,因为其他线程也会重构,流程图少了这部分

    /**
     * 根据id查询商铺数据
     *
     * @param id
     * @return
     */
    @Override
    public Result queryById(Long id) {
		String key = CACHE_SHOP_KEY + id;
		// 如果命中
		Result result = getShopFromCache(key);
		if(Objects.nonNull(result)){
			return result;
		}
        try{
	        // 缓存未命中,需要重建,判断能否能够获取互斥锁
	        String lockKey = 
	        boolean isLock = tryLock();
	        if(!isLock){
		        Thread.sleep(50);
		        return queryById(id);
	        }
	        result = getShopFromCache(key);
	        // 其他线程已经重构完了
	        if(Objects.nonNull(result)){
				return result;
			}
			Shop shop = this.getById(id);
			if(Objects.isNull(shop)){
				stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.SECONDS); return Result.fail("店铺不存在");
			}
			stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop),
                    CACHE_SHOP_TTL, TimeUnit.MINUTES);
            return Result.ok(shop);
        }catch(Exception e){
	        throw new RuntimeException("发生异常");
        }finally{
	        unlock(key);
        }
        return Result.ok(shop);
    }

	private Result getShopFromCache(String key){
        // 1、从Redis中查询店铺数据
	    String shopJson = stringRedisTemplate.opsForValue().get(key);
	    Shop shop = null;
        // 2、判断缓存是否命中
        if(StrUtil.isNotBlank(shopJson)){
            // 2.1 缓存命中,直接返回店铺数据
	        shop = JSONUtil.toBean(shopJson,shop.class);
	        return Result.ok(shop);
        }
        // 2.2 缓存未命中,从数据库中查询店铺数据
        shop = this.getById(id);
        // 3、判断数据库是否存在店铺数据
        if(Objects.isNull(shop)){
            // 3.1 数据库中不存在,返回失败信息
	        return Result.fail("店铺不存在");
        }

	}

	    /**
     * 获取锁
     *
     * @param key
     * @return
     */
    private boolean tryLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        // 拆箱要判空,防止NPE
        return BooleanUtil.isTrue(flag);
    }

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

  1. 这里使用Redis中的setnx指令实现互斥锁,只有当值不存在时才能进行set操作
  2. 锁的有效期更具体业务有关,需要灵活变动,一般锁的有效期是业务处理时长10~20倍
  3. 线程获取锁后,还需要查询缓存(也就是所谓的双检),这样才能够真正有效保障缓存不被击穿
    image.png
@Data
public class RedisData {
    /**
     * 过期时间
     */
    private LocalDateTime expireTime;
    /**
     * 缓存数据
     */
    private Object data;
}

    /**
     * 缓存重建线程池
     */
    public static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    /**
     * 根据id查询商铺数据
     *
     * @param id
     * @return
     */
    @Override
    public Result queryById(Long id) {
        String key = CACHE_SHOP_KEY + id;
        // 1、从Redis中查询店铺数据,并判断缓存是否命中
        
            // 1.1 缓存未命中,直接返回失败信息
           
        // 1.2 缓存命中,将JSON字符串反序列化未对象,并判断缓存数据是否逻辑过期
        
        // 这里需要先转成JSONObject再转成反序列化,否则可能无法正确映射Shop的字段
        
            // 当前缓存数据未过期,直接返回


        // 2、缓存数据已过期,获取互斥锁,并且重建缓存
        
            // 获取锁成功,开启一个子线程去重建缓存
            
     

        // 3、获取锁失败,再次查询缓存,判断缓存是否重建(这里双检是有必要的)
       
            // 3.1 缓存未命中,直接返回失败信息
            
        // 3.2 缓存命中,将JSON字符串反序列化未对象,并判断缓存数据是否逻辑过期
        
        // 这里需要先转成JSONObject再转成反序列化,否则可能无法正确映射Shop的字段
        
            // 当前缓存数据未过期,直接返回
            

        // 4、返回过期数据
        return Result.ok(shop);
    }

    /**
     * 将数据保存到缓存中
     *
     * @param id            商铺id
     * @param expireSeconds 逻辑过期时间
     */
    public void saveShopToCache(Long id, Long expireSeconds) {
        // 从数据库中查询店铺数据
        Shop shop = this.getById(id);
        // 封装逻辑过期数据
        RedisData redisData = new RedisData();
        redisData.setData(shop);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
        // 将逻辑过期数据存入Redis中
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
    }

    /**
     * 获取锁
     *
     * @param key
     * @return
     */
    private boolean tryLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        // 拆箱要判空,防止NPE
        return BooleanUtil.isTrue(flag);
    }

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


    /**
     * 缓存重建线程池
     */
    public static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    /**
     * 根据id查询商铺数据
     *
     * @param id
     * @return
     */
    @Override
    public Result queryById(Long id) {
        String key = CACHE_SHOP_KEY + id;
        // 1、从Redis中查询店铺数据,并判断缓存是否命中
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isBlank(shopJson)) {
            // 1.1 缓存未命中,直接返回失败信息
            return Result.fail("店铺数据不存在");
        }
        // 1.2 缓存命中,将JSON字符串反序列化对象,并判断缓存数据是否逻辑过期
        RedisData redisData = JSONUtil.toBean(shopJson,RedisData.class);
        // 这里需要先转成JSONObject再转成反序列化,否则可能无法正确映射Shop的字段
        JSONObject data = (JSONObject) redisData.getData();
        LocalDataTime expireTime = redisData.getExpireTime();
        if(expireTime.isAfter(LocalDataTime.now())){
            // 当前缓存数据未过期,直接返回
			return Result.ok(shop);
		}
		
        // 2、缓存数据已过期,获取互斥锁,并且重建缓存
        String lockKey = LOCK_SHOP_KEY + id;
        
        if(isLock){
            // 获取锁成功,开启一个子线程去重建缓存
	        CACHE_REBUILD_EXECUTOR.submit(()->{
		        try{
			        this.saveShopToCache(id,)
		        }finally{
			        unlock(lockKey);
		        }
	        }); 
	    }

        // 3、获取锁失败,再次查询缓存,判断缓存是否重建(这里双检是有必要的)
		shopJson = stringRedisTemplate.opsForValue().get(key);
        if(StrUtil.isBlank(shopJson)){
	        // 3.1 缓存未命中,直接返回失败信息
	        return Result.fail("");
        }
        // 3.2 缓存命中,将JSON字符串反序列化为对象,并判断缓存数据是否逻辑过期
        redisData = JSONUtil.toBean(shopJson, RedisData.class);
        // 这里需要先转成JSONObject再转成反序列化,否则可能无法正确映射Shop的字段
        data = (JSONObject) redisData.getData();
        shop = JSONUtil.toBean(data, Shop.class);
        expireTime = redisData.getExpireTime();
        if (expireTime.isAfter(LocalDateTime.now())) {
            // 当前缓存数据未过期,直接返回
            return Result.ok(shop);
        }
        // 4、返回过期数据
        return Result.ok(shop);
    }

    /**
     * 将数据保存到缓存中
     *
     * @param id            商铺id
     * @param expireSeconds 逻辑过期时间
     */
    public void saveShopToCache(Long id, Long expireSeconds) {
        // 从数据库中查询店铺数据
        Shop shop = this.getById(id);
        // 封装逻辑过期数据
        RedisData redisData = new RedisData();
        redisData.setData(shop);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
        // 将逻辑过期数据存入Redis中
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
    }

    /**
     * 获取锁
     *
     * @param key
     * @return
     */
    private boolean tryLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        // 拆箱要判空,防止NPE
        return BooleanUtil.isTrue(flag);
    }

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

使用逻辑过期要先进行数据预热

image.png

@Component
@Slf4j
public class CacheClient {

    private final StringRedisTemplate stringRedisTemplate;

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

    /**
     * 将数据加入Redis,并设置有效期
     *
     * @param key
     * @param value
     * @param timeout
     * @param unit
     */
    public void set(String key, Object value, Long timeout, TimeUnit unit) {
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), timeout, unit);
    }

    /**
     * 将数据加入Redis,并设置逻辑过期时间
     *
     * @param key
     * @param value
     * @param timeout
     * @param unit
     */
    public void setWithLogicalExpire(String key, Object value, Long timeout, TimeUnit unit) {
        RedisData redisData = new RedisData();
        redisData.setData(value);
        // unit.toSeconds()是为了确保计时单位是秒
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(timeout)));
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), timeout, unit);
    }

    /**
     * 根据id查询数据(处理缓存穿透)
     *
     * @param keyPrefix  key前缀
     * @param id         查询id
     * @param type       查询的数据类型
     * @param dbFallback 根据id查询数据的函数
     * @param timeout    有效期
     * @param unit       有效期的时间单位
     * @param <T>
     * @param <ID>
     * @return
     */
    public <T, ID> T handleCachePenetration(String keyPrefix, ID id, Class<T> type,
                                            Function<ID, T> dbFallback, Long timeout, TimeUnit unit) {
        String key = keyPrefix + id;
        // 1、从Redis中查询店铺数据
        String jsonStr = stringRedisTemplate.opsForValue().get(key);

        T t = null;
        // 2、判断缓存是否命中
        if (StrUtil.isNotBlank(jsonStr)) {
            // 2.1 缓存命中,直接返回店铺数据
            t = JSONUtil.toBean(jsonStr, type);
            return t;
        }

        // 2.2 缓存未命中,判断缓存中查询的数据是否是空字符串(isNotBlank把null和空字符串给排除了)
        if (Objects.nonNull(jsonStr)) {
            // 2.2.1 当前数据是空字符串(说明该数据是之前缓存的空对象),直接返回失败信息
            return null;
        }
        // 2.2.2 当前数据是null,则从数据库中查询店铺数据
        t = dbFallback.apply(id);

        // 4、判断数据库是否存在店铺数据
        if (Objects.isNull(t)) {
            // 4.1 数据库中不存在,缓存空对象(解决缓存穿透),返回失败信息
            this.set(key, "", CACHE_NULL_TTL, TimeUnit.SECONDS);
            return null;
        }
        // 4.2 数据库中存在,重建缓存,并返回店铺数据
        this.set(key, t, timeout, unit);
        return t;
    }

    /**
     * 缓存重建线程池
     */
    public static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    /**
     * 根据id查询数据(处理缓存击穿)
     *
     * @param keyPrefix  key前缀
     * @param id         查询id
     * @param type       查询的数据类型
     * @param dbFallback 根据id查询数据的函数
     * @param timeout    有效期
     * @param unit       有效期的时间单位
     * @param <T>
     * @param <ID>
     * @return
     */
    public <T, ID> T handleCacheBreakdown(String keyPrefix, ID id, Class<T> type,
                                          Function<ID, T> dbFallback, Long timeout, TimeUnit unit) {
        String key = keyPrefix + id;
        // 1、从Redis中查询店铺数据,并判断缓存是否命中
        String jsonStr = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isBlank(jsonStr)) {
            // 1.1 缓存未命中,直接返回失败信息
            return null;
        }
        // 1.2 缓存命中,将JSON字符串反序列化未对象,并判断缓存数据是否逻辑过期
        RedisData redisData = JSONUtil.toBean(jsonStr, RedisData.class);
        // 这里需要先转成JSONObject再转成反序列化,否则可能无法正确映射Shop的字段
        JSONObject data = (JSONObject) redisData.getData();
        T t = JSONUtil.toBean(data, type);
        LocalDateTime expireTime = redisData.getExpireTime();
        if (expireTime.isAfter(LocalDateTime.now())) {
            // 当前缓存数据未过期,直接返回
            return t;
        }

        // 2、缓存数据已过期,获取互斥锁,并且重建缓存
        String lockKey = LOCK_SHOP_KEY + id;
        boolean isLock = tryLock(lockKey);
        if (isLock) {
            // 获取锁成功,开启一个子线程去重建缓存
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    // 查询数据库
                    T t1 = dbFallback.apply(id);
                    // 将查询到的数据保存到Redis
                    this.setWithLogicalExpire(key, t1, timeout, unit);
                } finally {
                    unlock(lockKey);
                }
            });
        }

        // 3、获取锁失败,再次查询缓存,判断缓存是否重建(这里双检是有必要的)
        jsonStr = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isBlank(jsonStr)) {
            // 3.1 缓存未命中,直接返回失败信息
            return null;
        }
        // 3.2 缓存命中,将JSON字符串反序列化未对象,并判断缓存数据是否逻辑过期
        redisData = JSONUtil.toBean(jsonStr, RedisData.class);
        // 这里需要先转成JSONObject再转成反序列化,否则可能无法正确映射Shop的字段
        data = (JSONObject) redisData.getData();
        t = JSONUtil.toBean(data, type);
        expireTime = redisData.getExpireTime();
        if (expireTime.isAfter(LocalDateTime.now())) {
            // 当前缓存数据未过期,直接返回
            return t;
        }

        // 4、返回过期数据
        return t;

    }

    /**
     * 获取锁
     *
     * @param key
     * @return
     */
    private boolean tryLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        // 拆箱要判空,防止NPE
        return BooleanUtil.isTrue(flag);
    }

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

总结

  1. 从Redis中查询店铺类型
  2. 判断缓存是否命中
    • 缓存命中,直接返回缓存数据
    • 缓存未命中,查询数据库
  3. 判断数据库中是否存在该数据
    • 数据库中不存在该数据,返回失败信息
    • 店铺数据存在,写入Redis,并返回查询的数据

queryById 方法:

  1. 从Redis中查询店铺数据
  2. 判断缓存是否命中
    • 缓存命中,直接返回店铺数据
    • 缓存未命中,从数据库中查询店铺数据
  3. 判断数据库是否存在店铺数据
    • 数据库中不存在,返回失败信息
    • 数据库中存在,重建缓存,并返回店铺数据
      updateShop 方法:
  4. 更新数据库中的店铺数据
    • 如果数据库更新失败,抛出异常,事务回滚
  5. 删除缓存
    • 如果缓存删除失败,抛出异常,事务回滚

  1. 从Redis中查询店铺数据
  2. 判断缓存是否命中
    • 缓存命中,直接返回店铺数据
    • 缓存未命中,判断缓存中查询的数据是否是空字符串(isNotBlanknull 和空字符串排除)
  3. 当前数据是空字符串
    • 当前数据是空字符串(说明该数据是之前缓存的空对象),直接返回失败信息
  4. 当前数据是 null
    • 当前数据是 null,则从数据库中查询店铺数据
  5. 判断数据库是否存在店铺数据
    • 数据库中不存在,缓存空对象(解决缓存穿透),返回失败信息
    • 数据库中存在,重建缓存,并返回店铺数据

queryById 方法:

  1. 从Redis中查询店铺数据,并判断缓存是否命中

    • 缓存命中,直接返回
  2. 缓存未命中,需要重建缓存,判断能否获取互斥锁

    • 获取锁失败,已有线程在重建缓存,则休眠重试
    • 获取锁成功,判断缓存是否重建,防止堆积的线程全部请求数据库
  3. 从数据库中查询店铺数据,并判断数据库是否存在店铺数据

    • 数据库中不存在,缓存空对象(解决缓存穿透),返回失败信息
    • 数据库中存在,重建缓存,响应数据
  4. 释放锁(释放锁一定要记得放在finally中,防止死锁)
    getShopFromCache 方法:

  5. 判断缓存是否命中

    • 缓存命中,直接返回店铺数据
  6. 判断缓存中查询的数据是否是空字符串

    • 当前数据是空字符串,说明缓存也命中了(该数据是之前缓存的空对象),直接返回失败信息
  7. 缓存未命中(缓存数据既没有值,又不是空字符串)
    tryLock 方法:

  8. 获取锁
    unlock 方法:

  9. *释放锁


CACHE_REBUILD_EXECUTOR:

  1. 缓存重建线程池
    queryById 方法:
  2. 从Redis中查询店铺数据,并判断缓存是否命中
    • 缓存未命中,直接返回失败信息
    • 缓存命中,将JSON字符串反序列化为对象,并判断缓存数据是否逻辑过期
  3. 缓存数据已过期,获取互斥锁,并且重建缓存
    • 获取锁成功,开启一个子线程去重建缓存
  4. 获取锁失败,再次查询缓存,判断缓存是否重建(双检)
    • 缓存未命中,直接返回失败信息
    • 缓存命中,将JSON字符串反序列化为对象,并判断缓存数据是否逻辑过期
  5. 返回过期数据
    saveShopToCache 方法:
  6. 将数据保存到缓存中
    • 从数据库中查询店铺数据
    • 封装逻辑过期数据
    • 将逻辑过期数据存入Redis中
      tryLock 方法:
  7. 获取锁
    unlock 方法:
  8. 释放锁
    引用

为了解决数据一致性问题,我们可以选择适当的缓存更新策略:
以缓存主动更新(双写方案+删除缓存模式+先操作数据库后操作缓存+事务)为主,超时剔除为辅

  1. 查询时,先查询缓存,缓存命中直接返回,缓存未命中查询数据库并重建缓存,返回查询结果
  2. 更新时,先修改数据删除缓存,使用事务保证缓存和数据操作两者的原子性
    除了会遇到数据一致性问题意外,我们还会遇到缓存穿透、缓存雪崩、缓存击穿等问题
  3. 对于缓存穿透,我们采用了缓存空对象解决
  4. 对于缓存击穿,我们分别演示了互斥锁(setnx实现方式)和逻辑过期两种方式解决
    最后我们通过抽取出一个工具类,并且利用泛型编写几个通用方法,形成最终的形式

附录

双写方案(Cache Aside Pattern):人工编码方式,缓存调用者在更新完数据库后再去更新缓存。使用困难,灵活度高。
1)读取(Read):当需要读取数据时,首先检查缓存是否存在该数据。如果缓存中存在,直接返回缓存中的数据。如果缓存中不存在,则从底层数据存储(如数据库)中获取数据,并将数据存储到缓存中,以便以后的读取操作可以更快地访问该数据。
2)写入(Write):当进行数据写入操作时,首先更新底层数据存储中的数据。然后,根据具体情况,可以选择直接更新缓存中的数据(使缓存与底层数据存储保持同步),或者是简单地将缓存中与修改数据相关的条目标记为无效状态(缓存失效),以便下一次读取时重新加载最新数据


1. 什么是数据预热?

数据预热(Cache Warming)是指在缓存失效或系统启动前,主动将高频访问的数据预先加载到缓存中的过程。它的核心目标是:

  • 避免冷启动问题:当系统重启或缓存完全失效时,如果用户请求直接穿透到数据库,可能导致瞬时高负载,甚至引发宕机。
  • 提升用户体验:通过预先加载数据,用户请求可以直接命中缓存,减少延迟。
  • 平滑流量峰值:在业务高峰期前预热缓存,避免突发流量压垮数据库。

常见预热场景

  • 系统启动时(如每日凌晨重启服务)。
  • 缓存批量失效时(如促销活动开始前)。
  • 周期性热点数据更新时。

2. 什么是逻辑过期?

逻辑过期(Logical Expiration)是一种缓存管理策略,其核心思想是:

  • 物理缓存不过期:缓存的键值(Key-Value)在Redis等缓存服务中永不过期(或设置较长的TTL)。
  • 业务层控制有效期:在缓存值的元数据中嵌入过期时间字段,由业务代码判断数据是否逻辑失效。

示例

{
  "data": "实际数据",
  "expire_time": "2023-10-01 12:00:00"
}

当业务代码读取缓存时,先检查expire_time

  • 若未过期:直接使用缓存数据。
  • 若已过期:触发异步更新(如通过消息队列重建缓存),并短暂允许旧数据继续服务

3. 为什么逻辑过期需要数据预热?

逻辑过期机制必须依赖数据预热,原因如下:

(1) 避免缓存空洞
  • 问题:如果逻辑过期后未预热,缓存中将长期存在已过期的脏数据。此时新请求可能触发异步更新,但异步更新需要时间,期间用户可能读到旧数据。
  • 解决:通过预热,确保缓存中始终有逻辑未过期的数据,即使旧数据过期,也能立即用预热的新数据替换。
(2) 降低并发更新风险
  • 问题:逻辑过期后,多个请求可能同时触发缓存更新,导致大量请求穿透到数据库(缓存击穿)。
  • 解决:预热可以提前加载数据,减少缓存击穿的概率。
(3) 保证一致性
  • 问题:逻辑过期依赖业务代码判断时间,如果缓存中完全没有数据,异步更新可能因系统延迟导致数据不一致。
  • 解决:预热确保缓存中始终有基准数据,即使过期也能作为兜底。

4. 数据预热与逻辑过期的工作流程

  1. 预热阶段

    • 定时任务或事件触发,提前加载高频数据到缓存。
    • 设置较长的TTL(如24小时),但元数据中嵌入较短的逻辑过期时间(如5分钟)。
  2. 逻辑过期阶段

    • 用户请求命中缓存后,校验逻辑过期时间。
    • 若数据已过期:
      • 触发异步更新(如发送消息到MQ)。
      • 仍返回旧数据,直到异步任务完成更新。
  3. 缓存更新阶段

    • 异步任务从数据库读取最新数据,更新缓存。
    • 更新时需保证原子性(如用Redis Lua脚本)。

5. 总结

  • 数据预热是主动填充缓存的过程,解决冷启动和缓存击穿问题。
  • 逻辑过期是数据不过期,由业务代码控制有效期的策略。
  • 二者结合:逻辑过期需要预热来保证缓存的连续性,避免因异步更新延迟导致的数据不一致或缓存空洞。

通过这种设计,可以在高并发场景下显著提升系统的可用性和性能。


http://www.kler.cn/a/534868.html

相关文章:

  • uniapp访问django目录中的图片和视频,2025[最新]中间件访问方式
  • 深入理解和使用定时线程池ScheduledThreadPoolExecutor
  • 什么叫DeepSeek-V3,以及与GPT-4o的区别
  • dl学习笔记(8):fashion-mnist
  • [c语言日寄]赋值操作对内存的影响
  • 最大矩阵的和
  • 自定义多功能输入对话框:基于 Qt 打造灵活交互界面
  • 蓝桥杯之c++入门(六)【string】
  • PAT甲级1052、Linked LIst Sorting
  • TongSearch3.0.4.0安装和使用指引(by lqw)
  • python处理json文件
  • 人工智能丨PyTorch 计算机视觉
  • [创业之路-286]:《产品开发管理-方法.流程.工具 》-1- IPD两个跨职能团队的组织
  • (安全防御)防火墙安全策略部署
  • 玩转Amazon Bedrock基础模型:解锁图像风格混搭的无限可能
  • 【论文复现】基于适应度-距离平衡的自适应引导差分进化算法用于考虑可再生能源的安全约束最优潮流问题
  • 【Go语言快速上手】第一部分:Go 语言基础
  • Angular-hello world
  • 青少年编程与数学 02-008 Pyhon语言编程基础 22课题、类的定义和使用
  • C++【深入 STL--list 之 迭代器与反向迭代器】
  • 【鸿蒙HarmonyOS Next实战开发】视频压缩库VideoCompressor
  • Vue混入(Mixins)与插件开发深度解析
  • 常用抓包工具tcpdump、Fiddler、Charles、Wireshark 和 Sniffmaster 下载地址
  • 使用 CMake 自动管理 C/C++ 项目
  • C语言程序设计P6-5【应用指针进行程序设计 | 第五节】——指针与函数
  • OCR--光学字符识别