前言

缓存就是数据交换的缓冲区,是存贮数据的临时地方,一般读写性能较高,作用:降低后端负载、提高读写效率,降低响应时间。本文主要是记录Redis更新策略、缓存穿透、缓存雪崩、缓存击穿,下面进入正题。

更新策略

原理

缓存更新策略一般分为以下三种

redis 数据更新时间 redis数据更新策略_缓存


对高一致性需求的场景,主动更新缓存时,采用先操作数据库,再删除缓存

原因:如果每次操作数据库都更新缓存,如这期间,没有查询操作,会导致很多的无效写操作,浪费性能,最优方式就是删除缓存,而不是更新缓存。至于为什么需要先操作数据库,再删除缓存,分析见下图:

redis 数据更新时间 redis数据更新策略_学习_02


先删除缓存,再操作数据库:线程1,删除完缓存后,更新数据库(删除缓存很快,更新数据库教慢),在更新数据库的过程中,线程2进行查询,查询为缓存未命中,查询数据库,此时线程1的更新数据还未完成,线程2查到的数据库数据为旧值,查到旧值后,线程2将数据写如缓存,此时线程1更新完成,导致数据库是新值,缓存是旧值。

先操作数据库,再删除缓存:线程1查询,缓存未命中,查询数据库,此时线程2,更新数据库,再删除缓存,因线程1还未写入缓存,所以此时删除无效,线程1,会把查到的旧值更新到缓存中,数据库是新增,导致数据不一致,但是,写入缓存的时间非常快,更新数据库的时间比较慢,这种情况几乎不会发生。

代码实现

查询

@Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public Result queryShopById(Long id) {
        String key = RedisConstants.CACHE_SHOP_KEY + id;
        // 1.从redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //2.判断值是否存在
        if (StrUtil.isNotBlank(shopJson)){
            // 3.存在直接返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }
        // 4.不存在根据id查数据库
        Shop byId = getById(id);
        // 5.不存在返回错误
        if (byId== null){
            return Result.fail("店铺不存在!");
        }
        // 6.存在写入redis
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(byId),RedisConstants.CACHE_SHOP_TTL,TimeUnit.MINUTES);
        // 7.返回
        return Result.ok(byId);
    }

更新

// @Transactional 只适用于单体系统采用将缓存与数据库操作放在一个事务
    // 如分布式系统,利用TCC等分布式事务方案
    @Override
    @Transactional
    public Result updateShop(Shop shop) {
        Long id = shop.getId();
        if (id == null) {
            return Result.fail("店铺id不能为空");
        }
        //更新数据库
        updateById(shop);
        //删除缓存
        stringRedisTemplate.delete(RedisConstants.CACHE_SHOP_KEY + id);
        return Result.ok();
    }

缓存穿透

redis 数据更新时间 redis数据更新策略_缓存_03

代码实现

public Result queryShopById(Long id) {
        String key = RedisConstants.CACHE_SHOP_KEY + id;
        // 1.从redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //2.判断值是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            // 3.存在直接返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }
        // ①.《解决缓存穿透》 判断命中是的是否为空值,因2中的isNotBlank判断shopJson为空字符串的时候
        //返回的也是false,不会进2中的判断条件,此时shopJson有两种情况,一种是空字符串,一种是null,空字符串
        //是为解决缓存击穿设置的,不能放行查数据库,故直接返回报错。
        if (shopJson != null) {
            return Result.fail("店铺信息不存在!");
        }
        // 4.不存在根据id查数据库
        Shop byId = getById(id);
        // 5.不存在返回错误
        if (byId == null) {
            // ②.《解决缓存穿透》,将空值写入redis,过期时间设置2分钟
            stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
            return Result.fail("店铺不存在!");
        }
        // 6.存在写入redis,过期时间设置30分钟
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(byId), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
        // 7.返回
        return Result.ok(byId);
    }

缓存雪崩

redis 数据更新时间 redis数据更新策略_学习_04

缓存击穿

redis 数据更新时间 redis数据更新策略_缓存_05

redis 数据更新时间 redis数据更新策略_redis_06


redis 数据更新时间 redis数据更新策略_数据库_07


**互斥锁方式:**适应于数据一致性要求较高的场景,通过加锁的方式,来解决热点key失效,大量请求都查询数据库,重建设缓存造成数据库压力。

**逻辑过期方式:**适应于性能要求较高的场景,通过逻辑过期,但实际redis中一直都有数据,永久存在,每次通过逻辑过期时间来判断重建缓存。

代码实现(互斥锁)

/**
     * 获取锁
     * @param key
     * @return
     */
    private boolean trylock(String key) {
        //setIfAbsent方法和命令行的setnx命令是一样的,并发执行setIfAbsent,只有第一个线程能成功,其余线程都会失败
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.MINUTES);
        // 此处使用hutool包的工具,防止直接返回flag,造成拆箱时空指针问题。
        return BooleanUtil.isTrue(flag);
    }

    /**
     * 释放锁
     * @param key
     * @return
     */
    private void unlock(String key) {
        stringRedisTemplate.delete(key);
    }
 	/**
     * 击穿(互斥锁实现方式)
     * @param id
     * @return
     */
    public Shop queryWithMutex(Long id) {
        String key = RedisConstants.CACHE_SHOP_KEY + id;
        // 1.从redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //2.判断值是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            // 3.存在直接返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return shop;
        }
        // ①.《解决缓存穿透》 判断命中是的是否为空值,因2中的isNotBlank判断shopJson为空字符串的时候
        //返回的也是false,不会进2中的判断条件,此时shopJson有两种情况,一种是空字符串,一种是null,空字符串
        //是为解决缓存击穿设置的,不能放行查数据库,故直接返回报错。
        if (shopJson != null) {
            return null;
        }
        // 4.《解决缓存击穿》实现缓存重建
        // 4.1 《解决缓存击穿》获取互斥锁
        String lockkey = RedisConstants.LOCK_SHOP_KEY + id;
        Shop byId = null;
        try {
            boolean islock = trylock(lockkey);
            // 4.2《解决缓存击穿》判断是否获取成功
            if (!islock) {
                // 4.3《解决缓存击穿》失败,则休眠重试
                Thread.sleep(50);
                return queryWithMutex(id);
            }
            // 4.4 成功,不存在根据id查数据库
            byId = getById(id);
            // 5.不存在返回错误
            if (byId == null) {
                // ②.《解决缓存穿透》,将空值写入redis,过期时间设置2分钟
                stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
                return null;
            }
            // 6.存在写入redis,过期时间设置30分钟
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(byId), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);

        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            // 7.《解决缓存击穿》释放互斥锁
            unlock(lockkey);
        }
        // 8.返回
        return byId;
    }
    @Override
    public Result queryShopById(Long id) {
        Shop shop = queryWithMutex(id);
        if (shop == null) {
            Result.fail("店铺信息不存在!");
        }
        return Result.ok(shop);
    }

代码实现(逻辑过期)

定义通用对象,增加逻辑过期时间

import lombok.Data;

import java.time.LocalDateTime;

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

大致流程图

redis 数据更新时间 redis数据更新策略_缓存_08

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisConstants;
import com.hmdp.utils.RedisData;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
     * 重建缓存,存储逻辑过期时间的方法
     * @param id
     * @param expireSeconds
     */
    private void saveShopToRedis(Long id, Long expireSeconds) {
        // 1.查询数据库数据
        Shop shop = getById(id);
        // 2.封装逻辑过期时间
        RedisData redisData = new RedisData();
        redisData.setData(shop);
        // 3.获取当前时间并且增加expireSeconds
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
        // 4.写入redis
        stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
    }

    /**
     * 创建线程池
     */
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
    /**
     * 击穿(逻辑过期实现方式)
     * @param id
     * @return
     */
    public Shop queryWithLogiCalExpire(Long id) {
        String key = RedisConstants.CACHE_SHOP_KEY + id;
        // 1.从redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //2.判断值是否存在
        if (StrUtil.isBlank(shopJson)) {
            //3.为空直接返回null
            return null;
        }
        // 4.命中把json反序列化成对象
        RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
        Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        // 5.判断是否过期,过期时间是否在当前时间之后
        if (expireTime.isAfter(LocalDateTime.now())) {
            // 5.1 未过期返回店铺信息
            return shop;
        }
        // 5.2 过期,需要重建缓存
        // 6.1获取锁
        String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
        // 此处获取锁方式和互斥锁实现方式一致
        boolean isLock = trylock(lockKey);
        // 6.2 判断获取锁是否成功
        if (isLock) {
            // 7.开启一个独立的线程来执行重建
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    // 7.1 重建缓存
                    this.saveShopToRedis(id, 30L);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    //7.2 释放锁, 此处释放锁方式和互斥锁实现方式一致
                    unlock(lockKey);
                }
            });
        }
        //先返回旧的数据
        return shop;
    }
    
     @Override
    public Result queryShopById(Long id) {
        // 互斥锁实现方式
		//Shop shop = queryWithMutex(id);
        // 逻辑过期实现方式
        Shop shop = queryWithLogiCalExpire(id);
        if (shop == null) {
            Result.fail("店铺信息不存在!");
        }
        return Result.ok(shop);
    }