缓存穿透、缓存击穿、缓存雪崩相关概念及代码落地

发布时间 2023-06-26 22:47:40作者: DuX1ao

1.概念

缓存穿透:请求的数据在缓存中不存在,同时也不在数据库中,导致每次请求都要访问数据库,增加了数据库的负载

缓存击穿:某个热点数据对应缓存不存在(缓存过期/被清除/突然产生的热点数据还未建立缓存),大量请求涌入数据库,造成数据库负载激增,可能导致数据库崩溃

缓存雪崩:缓存集中失效,大量请求涌入数据库,可能导致数据库崩溃

2.准备工作

通过两个接口对相关概念进行代码实现

1)初始代码

通过id查询用户信息和修改用户信息

    @Override
    public ResponseResult getUserById(Integer userId) {
        ApUser apUser = userMapper.selectById(userId);
        return ResponseResult.okResult(apUser);
    }

    @Override
    public void updateUserById(ApUser user) {
        userMapper.updateById(user);
    }

2)所需maven依赖

        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.10.1</version>
        </dependency>
        <!--spring data redis & cache-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

需要配置Redis连接信息

3)工具类

key定义常量类

public class RedisKeyConstants {

    /**
     * 用户信息锁修改前缀
     */
    public static final String USER_UPDATE_LOCK_PREFIX= "user_update_lock:" ;
    /**
     * 用户锁信息锁前缀
     */
    public static final String USER_LOCK_PREFIX="user_info_lock:";
    /**
     * 用户详情前缀
     */
    public static final String USER_INFO_PREFIX="user_info_prefix:";

}

随机数生成

public class RandomUtil {

    /**
     * 生成指定长度的随机数
     *
     * @param length
     * @return
     */
    public static String genRandomNumber(int length) {

        String sources = "0123456789";
        Random random = new Random();
        StringBuilder sb = new StringBuilder();
        for (int j = 0; j < length; j++) {
            sb.append(sources.charAt(random.nextInt(9)));
        }
        return sb.toString();
    }


    private static final String ALL_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";

    /**
     * 生成指定长度的随机字符串
     *
     * @param length
     * @return
     */
    public static String genRandomNumberStr(int length) {
        Random random = new Random();
        StringBuilder saltString = new StringBuilder(length);
        for (int i = 1; i <= length; ++i) {
            saltString.append(ALL_CHARS.charAt(random.nextInt(ALL_CHARS.length())));
        }
        return saltString.toString();
    }


    /**
     * 生成随机数
     *
     * @param bound
     * @return
     */
    public static int genRandomInt(int bound) {
        return new Random().nextInt(bound);
    }


    /**
     * 生成区间范围内的随机数
     *
     * @param min
     * @param max
     * @return
     */
    public static int genRandomInt(int min, int max) {
        return genRandomInt(max-min)+min;
    }

}

缓存时间生成

public interface CacheSupport {

    /**
     * 缓存空数据
     */
    String EMPTY_CACHE = "{}";

    Integer TWO_DAYS_SECONDS = 2 * 24 * 60 * 60;

    /**
     * 生成缓存穿透过期时间,单位 秒
     *
     * @return
     */
    static Integer generateCachePenetrationExpireSecond() {
        return RandomUtil.genRandomInt(30, 100);
    }


    /**
     * 生成缓存过期时间
     * 2天加上随机几小时
     *
     * @return
     */
    static Integer generateCacheExpireSecond() {
        return TWO_DAYS_SECONDS + RandomUtil.genRandomInt(0, 10) * 60 * 60;
    }

}

RedisTemplate二次封装

public class RedisCache {

    private RedisTemplate redisTemplate;

    public RedisCache(RedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    /**
     * 缓存存储
     * @param key
     * @param value
     * @param seconds
     * @return void
     * @author zhonghuashishan
     */
    public void set(String key, String value, int seconds){
        ValueOperations<String,String> vo = redisTemplate.opsForValue();
        if(seconds > 0){
            vo.set(key, value, seconds, TimeUnit.SECONDS);
        }else{
            vo.set(key, value);
        }
    }

    /**
     * 缓存获取
     *
     * @param key
     * @return java.lang.String
     * @author zhonghuashishan
     */
    public String get(String key){
        ValueOperations<String,String> vo = redisTemplate.opsForValue();
        return vo.get(key);
    }



    /**
     * 缓存手动失效
     *
     * @param key
     * @return boolean
     * @author zhonghuashishan
     */
    public boolean delete(String key){
        return redisTemplate.delete(key);
    }

    /**
     * 缓存存储并设置过期时间
     *
     * @param key
     * @param value
     * @param time
     * @return void
     * @author zhonghuashishan
     */
    public void setex(String key, String value, long time) {
        redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
    }

    /**
     * 缓存批量获取
     *
     * @param keyList
     * @return java.util.List
     * @author zhonghuashishan
     */
    public List mget(List<String> keyList) {
        return redisTemplate.opsForValue().multiGet(keyList);
    }

    /**
     * 删除key下的多个值
     *
     * @param key
     * @param values
     * @return void
     * @author zhonghuashishan
     */
    public void srem(String key, String[] values) {
        redisTemplate.opsForSet().remove(key, values);
    }

    /**
     * 删除key下的多个值
     *
     * @param key
     * @param values
     * @return void
     * @author zhonghuashishan
     */
    public void sadd(String key, String[] values) {
        redisTemplate.opsForSet().add(key, values);
    }

    /**
     * 缓存成员获取
     *
     * @param key
     * @return java.util.Set
     * @author zhonghuashishan
     */
    public Set smembers(String key) {
        return redisTemplate.opsForSet().members(key);
    }

    /**
     * 缓存成员是否存在
     *
     * @param key
     * @param member
     * @return java.lang.Boolean
     * @author zhonghuashishan
     */
    public Boolean sismember(String key, String member) {
        return redisTemplate.opsForSet().isMember(key, member);
    }

    /**
     * 缓存有序区间值
     *
     * @param key
     * @param min
     * @param max
     * @param offset
     * @param count
     * @return java.util.Set
     * @author zhonghuashishan
     */
    public Set zrangeByScore(String key, double min, double max, long offset, long count) {
        return redisTemplate.opsForZSet().rangeByScore(key, min, max, offset, count);
    }

    /**
     * 缓存有序区间值
     *
     * @param key
     * @param min
     * @param max
     * @return java.util.Set
     * @author zhonghuashishan
     */
    public Set zrangeByScore2(String key, double min, double max) {
        return redisTemplate.opsForZSet().rangeByScore(key, min, max);
    }

    /**
     * 倒序返回zset区间值
     * @param key
     * @param start
     * @param end
     * @return
     */
    public Set zrevrange(String key, long start, long end) {
        return redisTemplate.opsForZSet().reverseRange(key, start, end);
    }

    /**
     * 缓存倒序排列指定区间值
     *
     * @param key
     * @param min
     * @param max
     * @param offset
     * @param count
     * @return java.util.Set
     * @author zhonghuashishan
     */
    public Set zrevrangeByScore(String key, double min, double max, long offset, long count) {
        return redisTemplate.opsForZSet().reverseRangeByScore(key, min, max, offset, count);
    }

    /**
     * 缓存有序存储
     *
     * @param key
     * @param member
     * @param score
     * @return java.lang.Boolean
     * @author zhonghuashishan
     */
    public Boolean zadd(String key, String member, double score) {
        return redisTemplate.opsForZSet().add(key, member, score);
    }

    /**
     * 缓存有序存储
     *
     * @param key
     * @param values
     * @return java.lang.Long
     * @author zhonghuashishan
     */
    public Long zremove(String key, String... values) {
        return redisTemplate.opsForZSet().remove(key, values);
    }

    /**
     * 缓存有序数量
     *
     * @param key
     * @return java.lang.Long
     * @author zhonghuashishan
     */
    public Long zcard(String key) {
        return redisTemplate.opsForZSet().zCard(key);
    }


    /**
     * 判断hash key是否存在
     *
     * @param key
     * @return
     */
    public boolean hExists(String key) {
        return hGetAll(key).isEmpty();
    }

    /**
     * 判断hash field是否存在
     * @return
     */
    public boolean hFieldExists(String key, String field) {
        return redisTemplate.opsForHash().hasKey(key, field);
    }

    /**
     * 获取hash变量中的键值对
     * 对应redis hgetall 命令
     *
     * @param key
     * @return
     */
    public Map<String, String> hGetAll(String key) {
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * 获取hash变量中的
     * @return
     */
    public Object hGet(String key, String field) {
        return redisTemplate.opsForHash().get(key, field);
    }


    /**
     * hash变量中field对应的value自增
     * @param key
     * @param field
     * @param increment
     * @return
     */
    public Long hIncr(String key, String field, Integer increment) {
        return redisTemplate.opsForHash().increment(key, field, increment);
    }

    /**
     * hash变量field对应的value自增
     * @param key
     * @param field
     * @return
     */
    public Long hIncr(String key, String field) {
        return hIncr(key, field, 1);
    }

    /**
     * 获取hash变量中的field数量
     * 对应redis hlen 命令
     *
     * @param key
     * @return
     */
    public Long hLen(String key) {
        return redisTemplate.opsForHash().size(key);
    }

    /**
     * 添加hash的value
     * @param key
     * @param field
     * @param value
     */
    public void hPut(String key, String field, Object value) {
        redisTemplate.opsForHash().put(key, field, value);
    }

    /**
     * 以map集合的形式添加hash键值对
     *
     * @param key
     * @param map
     */
    public void hPutAll(String key, Map<String, String> map) {
        redisTemplate.opsForHash().putAll(key, map);
    }

    /**
     * 删除某个field
     * @param key
     * @param field
     */
    public long hDel(String key, String field) {
        return redisTemplate.opsForHash().delete(key, field);
    }

    /**
     * 设置key过期时间
     * @param key
     * @param time
     * @param unit
     */
    public void expire(String key, long time, TimeUnit unit) {
        redisTemplate.expire(key, time, unit);
    }

    /**
     * 以list集合的形式添加数据
     *
     * @param key
     * @return
     */
    public Boolean hasKey(String key) {
        return redisTemplate.hasKey(key);
    }

    /**
     * 以list集合的形式添加数据
     *
     * @param key
     * @param values
     * @return
     */
    public Long lPushAll(String key, String... values) {
        return redisTemplate.opsForList().leftPushAll(key, values);
    }

    /**
     * 以list集合的形式添加数据
     *
     * @param key
     * @param values
     * @return
     */
    public Long lPushAll(String key, List<String> values) {
        return redisTemplate.opsForList().leftPushAll(key, values);
    }

    /**
     * 以list集合的形式添加数据
     *
     * @param key
     * @param values
     * @return
     */
    public Long rPushAll(String key, String... values) {
        return redisTemplate.opsForList().rightPushAll(key, values);
    }

    /**
     * 以list集合的形式添加数据
     *
     * @param key
     * @param values
     * @return
     */
    public Long rPushAll(String key, List<String> values) {
        return redisTemplate.opsForList().rightPushAll(key, values);
    }

    /**
     * 返回list集合下表区间的元素
     *
     * @param key
     * @param start
     * @param end
     * @return
     */
    public List<String> lRange(String key, long start, long end) {
        return redisTemplate.opsForList().range(key, start, end);
    }

    /**
     * 返回list集合的大小
     *
     * @param key
     * @return
     */
    public Long lsize(String key) {
        return redisTemplate.opsForList().size(key);
    }

    /**
     * 设置缓存过期时间
     *
     * @param key
     * @return
     */
    public void expire(String key, long time) {
        this.expire(key, time, TimeUnit.SECONDS);
    }

    /**
     * 执行lua脚本
     *
     * @param script
     * @param keys
     * @param args
     * @param <T>
     * @return
     */
    public <T> T execute(RedisScript<T> script, List<String> keys, String... args) {
        return (T) redisTemplate.execute(script, keys, args);
    }

    /**
     * 缓存存储并设置过期时间
     *
     * @param key
     * @param value
     * @param time
     * @param timeUnit
     * @return void
     */
    public void setex(String key, String value, long time, TimeUnit timeUnit) {
        redisTemplate.opsForValue().set(key, value, time, timeUnit);
    }

    /**
     * 缓存增量
     *
     * @param key
     * @param increment
     * @return java.lang.Long
     */
    public Long increment(String key, long increment) {
        return redisTemplate.opsForValue().increment(key, increment);
    }

    /**
     * 缓存失效时间
     *
     * @param key
     * @param timeUnit
     * @return java.lang.Long
     */
    public Long getExpire(String key, TimeUnit timeUnit) {
        return redisTemplate.getExpire(key, timeUnit);
    }

    /**
     * 指定缓存失效时间
     *
     * @param key
     * @param date
     * @return java.lang.Boolean
     */
    public Boolean expireAt(String key, Date date) {
        return redisTemplate.expireAt(key, date);
    }


}

RedissonClient二次封装

public class RedisLock {

    RedissonClient redissonClient;

    public RedisLock(RedissonClient redissonClient) {
        this.redissonClient = redissonClient;
    }

    /**
     * 互斥锁,seconds秒后自动失效
     * @param key
     * @param seconds
     */
    public boolean lock(String key, int seconds) {
        RLock rLock = redissonClient.getLock(key);
        if (rLock.isLocked()) {
            return false;
        }
        rLock.lock(seconds, TimeUnit.SECONDS);
        return true;
    }

    /**
     * 互斥锁,自动续期
     * @param key
     */
    public boolean lock(String key) {
        RLock rLock = redissonClient.getLock(key);
        // 抢失败就再见
        return rLock.tryLock();
    }

    public boolean blockedLock(String key) {
        // 无脑续期抢
        RLock rLock = redissonClient.getLock(key);
        rLock.lock();
        return true;
    }

    public boolean tryLock(String key, long timeout) throws InterruptedException {
        RLock rLock = redissonClient.getLock(key);
        // 大家一起抢锁,然后抢了指定时间之后,如果不成功,大家别抢了
        return rLock.tryLock(timeout, TimeUnit.MILLISECONDS);
    }





    /**
     * 手动释放锁
     *
     * @param key
     */
    public void unlock(String key) {
        RLock rLock = redissonClient.getLock(key);
        if (rLock.isLocked()) {
            rLock.unlock();
        }
    }

}

redis配置类

@Data
@Configuration
@ConditionalOnClass(RedisConnectionFactory.class)
public class RedisConfig {

    @Bean
    @ConditionalOnClass(RedisConnectionFactory.class)
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.setDefaultSerializer(new StringRedisSerializer());
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    @Bean
    @ConditionalOnClass(RedissonClient.class)
    public RedissonClient redissonClient(){
        Config config = new Config();
        //控制台redis报相关错误就需要.setPassword("leadnews")
        config.useSingleServer().setAddress("redis://192.168.200.130:6379").setPassword("leadnews");
        return Redisson.create(config);
    }

    @Bean
    @ConditionalOnClass(RedisConnectionFactory.class)
    public RedisCache redisCache(RedisTemplate redisTemplate) {
        return new RedisCache(redisTemplate);
    }

    @Bean
    @ConditionalOnClass(RedissonClient.class)
    public RedisLock redisLock(RedissonClient redissonClient) {
        return new RedisLock(redissonClient);
    }
}

3.代码实现

1)查询方法加入缓存

先从缓存里边拿,如果缓存里边有,则直接把缓存中的数据,返回,如果缓存里边没有,再从数据库中查询,如果有的话,放入缓存中

public ResponseResult getUserById(Integer userId) {
	String redisKey = RedisKeyConstants.USER_INFO_PREFIX + userId;
    String userJson = redisCache.get(redisKey);
    if (userJson != null) {
        ApUser apUser = JSON.parseObject(userJson, ApUser.class);
        return ResponseResult.okResult(apUser);
    }
    
    ApUser apUser = userMapper.selectById(userId);
    if (apUser != null) {
        //-1 :表示该缓存永不过期
    	redisCache.set(redisKey, JSON.toJSONString(apUser), -1);
        return ResponseResult.okResult(apUser);
    }
    //缓存和数据库都没查到
    return ResponseResult.errorResult(AppHttpCodeEnum.AP_USER_DATA_NOT_EXIST);
    
}

2)缓存雪崩问题解决

缓存雪崩 :是指内存中的数据同一时刻,大面积的失效,万一大量请求访问数据库,数据库也可能被打死

我们为了避免缓存雪崩:让key不要在同一时间过期,给key设置一个固定时间+随机时间

时间上如何处理:除非是特别热的数据,否则都要带上过期时间 设置固定时间+随机时间

//在设置缓存时加上随机生成的缓存时间
redisCache.set(redisKey, JSON.toJSONString(apUser), CacheSupport.generateCacheExpireSecond());

3)修改方法使用双写

当修改用户信息后如果不对缓存信息进行处理会产生缓存和数据库的数据不一致性问题

为了解决数据不一致性问题:更新用户信息后重新设置对应的缓存,即双写(写完数据库,再写缓存)


    @Override
    public void updateUserById(ApUser user) {
        String redisKey = RedisKeyConstants.USER_INFO_PREFIX + user.getId();
        userMapper.updateById(user);
        //TODO 此处缓存中的用户信息并不完整
        redisCache.set(redisKey, JSON.toJSONString(user), CacheSupport.generateCacheExpireSecond());
    }

双写在并发请求时产生了缓存和数据库的数据一致性问题

4)修改方法保证幂等性

假如说用户信息存在一个字段在更新时 set num = num - 1

并且出现了网络卡顿,就可能导致一个请求发起了多次,出现了幂等性问题

如何解决幂等性问题:借助于Redisson分布式锁加锁,保证同一时刻只能有一个请求可以修改用户信息

@Override
public void updateUserById(ApUser user) {
    String redisKey = RedisKeyConstants.USER_INFO_PREFIX + user.getId();
    String redisLockKey = RedisKeyConstants.USER_UPDATE_LOCK_PREFIX + user.getId();

    boolean lock = redisLock.lock(redisLockKey);
    if (!lock) {
        // 抢锁失败要进来
        throw new RuntimeException("为了保证幂等,修改失败");
    }
    try {
        userMapper.updateById(user);
        redisCache.set(redisKey, JSON.toJSONString(user), CacheSupport.generateCacheExpireSecond());
    } finally {
        redisLock.unlock(redisLockKey);
    }

}

这种解决幂等问题的方案有一点瑕疵:假如请求不是同一时刻来的,两次修改请求先后到来,就无法保证幂等

5)冷热数据处理

冷数据和热数据

redis中存在的应该是热数据 =======> 如果这个数据从redis中查询出来之后,我们给数据 续期

结果是:热数据会一直存在,冷数据一段时间后会自动过期,从redis中消失

最热的数据不设置过期时间,普通数据设置过期时间 固定时间+随机时间

public ResponseResult getUserById(Integer userId) {
	String redisKey = RedisKeyConstants.USER_INFO_PREFIX + userId;
    String userJson = redisCache.get(redisKey);
    if (userJson != null) {
        ApUser apUser = JSON.parseObject(userJson, ApUser.class);
        //针对查询到普通数据进行续期
        redisCache.expire(redisKey, CacheSupport.generateCacheExpireSecond());
        return ResponseResult.okResult(apUser);
    }
    
    ApUser apUser = userMapper.selectById(userId);
    if (apUser != null) {
        //-1 :表示该缓存永不过期
    	redisCache.set(redisKey, JSON.toJSONString(apUser), -1);
        return ResponseResult.okResult(apUser);
    }
    //缓存和数据库都没查到
    return ResponseResult.errorResult(AppHttpCodeEnum.AP_USER_DATA_NOT_EXIST);
    
}

6)缓存穿透问题解决

解决缓存穿透有两种方案:

数据库查不到数据也在缓存中设置一个值,减轻数据库压力

缺点是如果每次请求的资源标识都不一样,缓存中的值相当于没有生效,仍然有缓存穿透问题

使用布隆过滤器对请求进行过滤

实际开发多使用第一种方案

public ResponseResult getUserById(Integer userId) {
	String redisKey = RedisKeyConstants.USER_INFO_PREFIX + userId;
    String userJson = redisCache.get(redisKey);
    
    if (userJson != null && !userJson.isEmpty()) {
        //判断是不是{}
        if (Objects.equals(CacheSupport.EMPTY_CACHE, userJson)) {
            //此处处理可以和前端约定
            return ResponseResult.okResult(new ApUser());
        }

        ApUser apUser = JSON.parseObject(userJson, ApUser.class);
        //针对查询到普通数据进行续期
        redisCache.expire(redisKey, CacheSupport.generateCacheExpireSecond());
        return ResponseResult.okResult(apUser);;
    }
    
    ApUser apUser = userMapper.selectById(userId);
    if (apUser != null) {
        //-1 :表示该缓存永不过期
    	redisCache.set(redisKey, JSON.toJSONString(apUser), -1);
        return ResponseResult.okResult(apUser);
    }
    //如果是缓存穿透,放一个{}
    redisCache.set(redisKey, CacheSupport.EMPTY_CACHE, CacheSupport.generateCacheExpireSecond());
    //缓存和数据库都没查到
    return ResponseResult.errorResult(AppHttpCodeEnum.AP_USER_DATA_NOT_EXIST);
    
}

7)查询方法代码优化

@Override
public ResponseResult getUserById(Integer userId) {

    ApUser apUser = readDataFromRedis(userId);

    if (apUser != null) {
        //有数据
        return ResponseResult.okResult(apUser);
    }

    //缓存里边没有
    return readDataFromDB(userId);

}

private ResponseResult readDataFromDB(Integer userId) {
    String redisKey = RedisKeyConstants.USER_INFO_PREFIX + userId; //user_info_prefix:9

    ApUser apUser = userMapper.selectById(userId);

    if (apUser != null) {
        redisCache.set(redisKey,JSON.toJSONString(apUser),CacheSupport.generateCacheExpireSecond());
        return ResponseResult.okResult(apUser);
    }

    //如果是缓存穿透,放一个{}
    redisCache.set(redisKey, CacheSupport.EMPTY_CACHE, CacheSupport.generateCacheExpireSecond());

    return ResponseResult.errorResult(AppHttpCodeEnum.REDIS_CACHE_PENETRATION);

}

private ApUser readDataFromRedis(Integer userId) {
    String redisKey = RedisKeyConstants.USER_INFO_PREFIX + userId; //user_info_prefix:9

    String userJson = redisCache.get(redisKey);

    if (userJson != null && !userJson.isEmpty()) {
        //判断是不是{}
        if (Objects.equals(CacheSupport.EMPTY_CACHE, userJson)) {
            //此处处理可以和前端约定
            return new ApUser();
        }

        ApUser apUser = JSON.parseObject(userJson, ApUser.class);
        //针对查询到普通数据进行续期
        redisCache.expire(redisKey, CacheSupport.generateCacheExpireSecond());
        return apUser;
    }
    return null;
}

8)缓存击穿问题解决

缓存击穿:热点key突然过期导致大量请求打到数据库

热点key不是会自动续期吗,怎么会过期?换个说法,一个key突然成为热点

解决方案:加互斥锁(blockedLock)

private ResponseResult readDataFromDB(Integer userId) {
    String redisKey = RedisKeyConstants.USER_INFO_PREFIX + userId; //user_info_prefix:9
    //解决击穿加锁
    String redisLockKey = RedisKeyConstants.USER_LOCK_PREFIX + userId;
    boolean lock = redisLock.blockedLock(redisLockKey);
    if (!lock) {
        throw new RuntimeException("查询失败");
    }
    try {
        ApUser apUser = userMapper.selectById(userId);

        if (apUser != null) {
            redisCache.set(redisKey,JSON.toJSONString(apUser),CacheSupport.generateCacheExpireSecond());
            return ResponseResult.okResult(apUser);
        }

        //如果是缓存穿透,放一个{}
        redisCache.set(redisKey, CacheSupport.EMPTY_CACHE, CacheSupport.generateCacheExpireSecond());

        return ResponseResult.errorResult(AppHttpCodeEnum.REDIS_CACHE_PENETRATION);
    } finally {
        redisLock.unlock(redisLockKey);
    }
}

有阻塞问题

9)双写数据库缓存一致性问题

在并发请求查询和修改同时执行可能会导致缓存和数据库数据不一致

解决:让查询和修改使用同一把锁

private ResponseResult readDataFromDB(Integer userId) {
    String redisKey = RedisKeyConstants.USER_INFO_PREFIX + userId; //user_info_prefix:9
    //解决击穿加锁
    //修改锁类型和修改时类型一致解决一致性问题
    String redisLockKey = RedisKeyConstants.USER_UPDATE_LOCK_PREFIX + userId;
    
    boolean lock = redisLock.blockedLock(redisLockKey);
    if (!lock) {
        throw new RuntimeException("查询失败");
    }
    try {
        ApUser apUser = userMapper.selectById(userId);

        if (apUser != null) {
            redisCache.set(redisKey,JSON.toJSONString(apUser),CacheSupport.generateCacheExpireSecond());
            return ResponseResult.okResult(apUser);
        }

        //如果是缓存穿透,放一个{}
        redisCache.set(redisKey, CacheSupport.EMPTY_CACHE, CacheSupport.generateCacheExpireSecond());

        return ResponseResult.errorResult(AppHttpCodeEnum.REDIS_CACHE_PENETRATION);
    } finally {
        redisLock.unlock(redisLockKey);
    }
}

10)分布式锁串行转并行

在缓存击穿问题解决中加了自旋锁,带来了阻塞问题,效率低

解决:double check + tryLock(带时间的锁)

先读缓存然后check,然后抢锁,抢锁失败后再读缓存,check

private ResponseResult readDataFromDB(Integer userId) {
    String redisKey = RedisKeyConstants.USER_INFO_PREFIX + userId; //user_info_prefix:9
    //解决击穿加锁
    String redisLockKey = RedisKeyConstants.USER_UPDATE_LOCK_PREFIX + userId;

    //解决缓存击穿加锁后自旋效率低的问题
    //tryLock 大家一起抢锁,时间到后就全部停止抢锁
    boolean lock = false;
    try {
        lock = redisLock.tryLock(redisLockKey, 1);
    } catch (InterruptedException e) {
        
        ApUser apUser = readDataFromRedis(userId);
		
        if (apUser != null) {
            //有数据
            return ResponseResult.okResult(apUser);
        }
        e.printStackTrace();
    }

    if (!lock) {
        ApUser apUser = readDataFromRedis(userId);

        if (apUser != null) {
            //有数据
            return ResponseResult.okResult(apUser);
        }
        throw new RuntimeException("查询失败");
    }

    try {
        ApUser apUser = userMapper.selectById(userId);

        if (apUser != null) {
            redisCache.set(redisKey,JSON.toJSONString(apUser),CacheSupport.generateCacheExpireSecond());
            return ResponseResult.okResult(apUser);
        }

        //如果是缓存穿透,放一个{}
        redisCache.set(redisKey, CacheSupport.EMPTY_CACHE, CacheSupport.generateCacheExpireSecond());

        return ResponseResult.errorResult(AppHttpCodeEnum.REDIS_CACHE_PENETRATION);
    } finally {
        redisLock.unlock(redisLockKey);
    }
}