美文网首页
Redis实战

Redis实战

作者: c_gentle | 来源:发表于2022-08-08 15:36 被阅读0次

    一、缓存惊群

    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实战

          本文链接:https://www.haomeiwen.com/subject/gkuwwrtx.html