Redis实战

一、缓存惊群

1、场景描述

用户数据写入和查询,缓存设计,写入的时候,库+缓存,双写,缓存默认 2 天多随机时间
的过期,读的时候,读延期,频繁读,缓存会不停的延期,没有人查呢,过期,避免占用缓
存的空间,缓存没查到,从数据库里提出来,放到缓存里去
每次写入缓存的时候,为什么一定要设置 2 天+随机几个小时的时间呢?
答案,缓存惊群的问题,缓存一批数据,突然之间一起都没了,过期时间设置的都是一样的,
缓存集群都惊了,数据库也惊了,大量缓存同时过期->惊群 ->瞬时大量请求走到 mysql 去
造成压力
大量的缓存数据,过期时间都是随机,不要集中在某个时间点一起过期
惊群,技术里典型的术语,突然在某个时间点,出了一个故障,一大片范围线程/进程/机器,
都同时被惊动了,惊群效应

2、解决方案

每次写入缓存的时候,设置 2 天+随机几个小时的时间,使得缓存数据不会一起失效
代码实现

 redisCache.set(RedisKeyConstants.USER_INFO_PREFIX + cookbookUserDO.getId(),
                    JsonUtil.object2Json(cookbookUserDTO), CacheSupport.generateCacheExpireSecond());


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

二、缓存穿透

1、场景描述

缓存穿透的一个问题,穿透->读取缓存没读到->从 db 里读->也没读到->bug->高并发的读一个数据,缓存和 db 都没有->每次高并发读取,缓存都会被穿透过去,每次都要读一下db,导致高并发空请求都针对 db 再走,压力

2、解决方案

先从缓存中获取数据,如果缓存中不存在,则从数据库中查出来,如果数据库中查出来的数据为空,则赋值为"{}",并且设置失效时间。

 @Override
    public CookbookUserDTO getUserInfo(CookbookUserQueryRequest request) {
        Long userId = request.getUserId();

        CookbookUserDTO user = getUserFromCache(userId);
        if(user != null) {
            return user;
        }

        return getUserInfoFromDB(userId);
    }

    private CookbookUserDTO getUserFromCache(Long userId) {
        String userInfoKey = RedisKeyConstants.USER_INFO_PREFIX + userId;
        String userInfoJsonString = redisCache.get(userInfoKey);
        log.info("从缓存中获取作者信息,userId:{},value:{}", userId, userInfoJsonString);

        if (StringUtils.hasLength(userInfoJsonString)){
            // 防止缓存穿透
            if (Objects.equals(CacheSupport.EMPTY_CACHE, userInfoJsonString)) {
                return new CookbookUserDTO();
            }
            redisCache.expire(RedisKeyConstants.USER_INFO_PREFIX + userId,
                    CacheSupport.generateCacheExpireSecond());
            CookbookUserDTO dto = JsonUtil.json2Object(userInfoJsonString, CookbookUserDTO.class);
            return dto;
        }

        return null;
    }


private CookbookUserDTO getUserInfoFromDB(Long userId) {
        // 有两个选择,load from db + write redis,加两把锁,user_lock,user_update_lock
        // 基于redisson,加多锁,multi lock
        // 共用一把锁,multi lock加锁,不同的锁应对的是不同的并发场景

//        String userLockKey = RedisKeyConstants.USER_LOCK_PREFIX + userId;

        // 有大量的线程突然读一个冷门的用户数据,都囤积在这里,在上面大家都没读到
        // 都在这个地方在排队等待获取锁,然后去尝试load db + write redis
        // 非常严重的锁竞争的问题,线程,串行化,一个一个的排队,一个人先拿锁,load一次db,写缓存
        // 下一个人拿到锁了,通过double check,直接读缓存,下一个人,短时间内突然有一个严重串行化,虽然每次读缓存,时间不多

        // 其实只要有第一个人,能够拿到锁,进去,laod db + wreite redis,redis里就已经有数据了
        // 后续的线程就不需要通过锁排队,串行化,一个一个load redis里的数据
        // 只要有一个人能够成功,其他的人,其实可以突然之间全部转换为上面的操作,无锁的情况下,大量的一起并发的读redis就可以了

        String userLockKey = RedisKeyConstants.USER_UPDATE_LOCK_PREFIX + userId;
        boolean lock = false;
        try {
            lock = redisLock.tryLock(userLockKey, USER_UPDATE_LOCK_TIMEOUT);
        } catch(InterruptedException e) {
            CookbookUserDTO user = getUserFromCache(userId);
            if(user != null) {
                return user;
            }
            log.error(e.getMessage(), e);
            throw new BaseBizException("查询失败");
        }

        if (!lock) {
            CookbookUserDTO user = getUserFromCache(userId);
            if(user != null) {
                return user;
            }
            log.info("缓存数据为空,从数据库查询作者信息时获取锁失败,userId:{}", userId);
            throw new BaseBizException("查询失败");
        }

        try {
            CookbookUserDTO user = getUserFromCache(userId);
            if(user != null) {
                return user;
            }

            log.info("缓存数据为空,从数据库中获取数据,userId:{}", userId);

            String userInfoKey = RedisKeyConstants.USER_INFO_PREFIX + userId;

            // 在这里先读到了db里的用户信息的旧数据
            // 这个线程刚刚读到,还没有来得及把旧数据写入缓存里去
            CookbookUserDO cookbookUserDO = cookbookUserDAO.getById(userId);
            if (Objects.isNull(cookbookUserDO)) {
                redisCache.set(userInfoKey, CacheSupport.EMPTY_CACHE, CacheSupport.generateCachePenetrationExpireSecond());
                return null;
            }

            CookbookUserDTO dto = cookbookUserConverter.convertCookbookUserDTO(cookbookUserDO);

            // 此时这个线程,在上面的那个线程都已经把新数据写入缓存里去了,缓存里已经是最新数据了
            // 把旧数据库,写入了缓存做了一个覆盖操作,典型的,数据库+缓存双写的时候,写和读,并发的时候
            // db里是新数据,缓存里是旧数据,旧数据是覆盖了新数据的
            // db和缓存,数据是不一致的
            redisCache.set(userInfoKey, JsonUtil.object2Json(dto), CacheSupport.generateCacheExpireSecond());
            return dto;
        } finally {
            redisLock.unlock(userLockKey);
        }
    }

三、缓存一致性

1、场景描述

如果数据库中的某条数据,放入缓存之后,又立马被更新了,那么该如何更新缓存呢?不更新缓存行不行?
答:当然不行,如果不更新缓存,在很长的一段时间内(决定于缓存的过期时间),用户请求从缓存中获取到的都可能是旧值,而非数据库的最新值。这不是有数据不一致的问题?

2、解决方案
image.png

你可能感兴趣的:(Redis实战)