精华内容
下载资源
问答
  • Java项目的缓存方案

    2021-08-15 23:58:01
    做什么样的缓存方案能做到代码和性能的最佳平衡?

    性能不够,缓存来凑。

    作为一名开发人员,不管是面试还是平时工作都会被问到如何优化项目性能的问题,回答不外乎缓存、Redis,不行搞进程级别的缓存。可是你真的会做缓存吗?我看过很多的缓存方案,大多数与业务代码强耦合,代码侵入性极大,且缓存方案替代(如ehcache迁移到redis,redis的序列化方式由json替换为protobuf)需要修改大量代码。

    本人多年开发总结下来一套耦合度极小,且灵活高效的项目缓存方案,跟大家分享。

    样例程序背景,首先我的项目基于是SpringBoot的,程序里涉及学生模块和分数模块。

    一:项目总体结构

    config:配置

    db:数据库实体和repository

    lock:是一个基于注解的分布式锁方案

    proto:是protobuf相关的一些东西

    provider:是数据提供者

    dto和model:模型和传输对象

    service:是业务逻辑处理层

    controller:接口层

    注明:本人的开发习惯,使用四层结构:controller->service->provider->repository

    二:缓存操作的具体实现

     我的缓存一般放在数据提供层(provider),我的原则是尽量少用join的方式从数据库取数据,而是每个模块分别缓存,然后通过程序组装的方式输出最终的结果

    举例:现在有A B C三个模块,有几个接口,每个接口涉及到的数据可能包含三个模块中的任意组合,试想一下,如果我在业务处理地方做缓存,那么我的缓存种类超过三种,且每当A B C中的任意一个模块有数据变动,清理缓存也是一个麻烦,现在我做成了A B C三个模块分别缓存,各自在自己的Provider里做增删改查及对应的缓存操作,界限非常明确,任何一个模块的数据变动都只需要清理对应模块的缓存即可,接口层通过聚合的方式来组装数据并返回,可能会比原来多访问几次redis,但是换来的是程序逻辑的清晰和代码的严谨,可读性翻倍,我认为这样的方案换来程序的优雅是值得的,我一直认为程序的可读性也是一个非常重要的方面。

    StudentProvider代码如下:

    package chen.huai.jie.springboot.cache.provider;
    
    import chen.huai.jie.springboot.cache.db.entity.Student;
    import chen.huai.jie.springboot.cache.db.repository.StudentRepository;
    import chen.huai.jie.springboot.cache.lock.DistributedLock;
    import chen.huai.jie.springboot.cache.provider.access.StudentAccess;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.cache.annotation.CacheEvict;
    import org.springframework.cache.annotation.Cacheable;
    import org.springframework.cache.annotation.Caching;
    import org.springframework.stereotype.Service;
    import org.springframework.util.StringUtils;
    
    import static chen.huai.jie.springboot.cache.model.CacheConstants.STUDENT_BY_ID;
    import static chen.huai.jie.springboot.cache.model.CacheConstants.STUDENT_BY_NO;
    
    /**
     * @author chenhuaijie
     */
    @Slf4j
    @Service
    public class StudentProvider extends BaseProvider {
        @Autowired
        private StudentRepository studentRepository;
        @Autowired
        private StudentAccess studentAccess;
    
        /**
         * 根据ID查找
         * <p>
         * 只有当id!=null&&id>0时候才进入缓存,否则直接进入程序
         * 缓存null值
         * 缓存Key为id
         *
         * @param id
         * @return
         */
        @Cacheable(cacheNames = STUDENT_BY_ID, condition = "#id!=null and #id>0", key = "#id", sync = true)
        @DistributedLock(keyPrefix = "cache:lock:StudentProvider:findById", waitTime = 10, key = "#id")
        public Student findById(Long id) {
            if (id == null || id <= 0) {
                log.warn("id is null or less than 0,id:{}", id);
                return null;
            }
    
            Student student = studentAccess.findById(id);
            if (student != null) {
                return student;
            }
    
            log.info("从数据库加载数据:id:{}", id);
            return studentRepository.findById(id).orElse(null);
        }
    
        /**
         * 根据学号查找
         *
         * @param no
         * @return
         */
        @Cacheable(cacheNames = STUDENT_BY_NO, condition = "#no != null and !\"\".equals(#no)", key = "#no", sync = true)
        @DistributedLock(keyPrefix = "cache:lock:StudentProvider:findByNo", waitTime = 10, key = "#no")
        public Student findByNo(String no) {
            if (StringUtils.isEmpty(no)) {
                return null;
            }
    
            Student student = studentAccess.findByNo(no);
            if (student != null) {
                return student;
            }
    
            return studentRepository.findByNo(no);
        }
    
        /**
         * 添加
         *
         * @param student
         * @return
         */
        @Caching(evict = {
                @CacheEvict(cacheNames = STUDENT_BY_ID, condition = "#student!=null", key = "#student.id"),
                @CacheEvict(cacheNames = STUDENT_BY_NO, condition = "#student!=null", key = "#student.no")
        })
        public Student add(Student student) {
            if (student == null) {
                return null;
            }
    
            return studentRepository.save(student);
        }
    
        /**
         * 修改
         *
         * @param student
         * @return
         */
        @Caching(evict = {
                @CacheEvict(cacheNames = STUDENT_BY_ID, condition = "#student!=null", key = "#student.id"),
                @CacheEvict(cacheNames = STUDENT_BY_NO, condition = "#student!=null", key = "#student.no")
        })
        public Student update(Student student) {
            if (student == null) {
                return null;
            }
    
            return studentRepository.save(student);
        }
    
        /**
         * 删除
         *
         * @param student
         */
        @Caching(evict = {
                @CacheEvict(cacheNames = STUDENT_BY_ID, condition = "#student!=null", key = "#student.id"),
                @CacheEvict(cacheNames = STUDENT_BY_NO, condition = "#student!=null", key = "#student.no")
        })
        public void delete(Student student) {
            if (student == null) {
                return;
            }
    
            studentRepository.deleteById(student.getId());
        }
    }
    

    这个类一共有五个方法:

    public Student findById(Long id);
    public Student findByNo(String no);
    public Student add(Student student);
    public Student update(Student student);
    public void delete(Student student);

    两个获取的方法和三个修改的方法,在缓存操作层我利用的是Spring的Cacheable(写入缓存)和CacheEvict(清理缓存)。

    Cacheable注解有几个参数,解释如下:

    cacheNames(value):缓存的名字

    key:具体缓存的Key(支持SPEL表达式)

    cacheManager:指定的缓存管理器

    condition:条件,满足这个条件就进入缓存,否则直接跳过缓存进入方法

    unless:条件,满足这个条件就写入缓存,否则不写入缓存

    sync:是否同步操作

    接下来我们解析findById方法的注解:

        @Cacheable(cacheNames = STUDENT_BY_ID, condition = "#id!=null and #id>0", key = "#id", sync = true)
        @DistributedLock(keyPrefix = "cache:lock:StudentProvider:findById", waitTime = 10, key = "#id")

    第一个注解是缓存注解,

    这个模块的缓存名是:STUDENT_BY_ID,对应的值是 cache:student:id:;

    只有当id!=null&&id>0才进入缓存获取数据,否则跳过缓存直接进入方法;

    key是按照参数id的值来确定的,基于Spel表达式

    sync=true,同步写入缓存,意味着服务的一个程序实例里,缓存写入读取是一个同步的过程,避免出现大并发的情况下缓存击穿。(仅在单实例下有效,多实例下会出现每个实例访问一次数据库的情况)

    缓存null值,防止缓存穿透(缓存时间不要太大,防止攻击导致redis写满)。

    第二个注解是分布式锁:

    为了解决缓存击穿问题,光在Cacheable里家sync=true,并不能解决多实例下的问题,可能会出现每个实例访问一次数据库的情况,而不是希望的总共只访问一次数据库的情况,所以我们在这里增加一个分布式锁来协调不同实例直接的缓存读取存入操作。经过多次测试,明确用此方法可以保证高并发情况下只会有一次数据访问。

    studentAccess.findById其实就是从缓存中取一下数据,代码如下,不管任何情况都会进入缓存获取数据,且不缓存null值,这样程序返回的null不会写入缓存。

    /**
         * 从缓存获取学生信息
         * 直接进入缓存
         * 不缓存null值
         *
         * @param id
         * @return
         */
        @Cacheable(cacheNames = STUDENT_BY_ID, unless = "#result==null", key = "#id")
        public Student findById(Long id) {
            log.warn("缓存数据不存在:id:{}", id);
            return null;
        }

    我们都知道多个切面的执行顺序,如下图:

    在这里插入图片描述

    注解的执行顺序是有注解切面上的@Order的值决定的,我们这里DistributedLock的值要大于Cacheable的,所以Cacheable的代码先执行,如图上的外圈。

     通过测试,我们也确定Cacheable比DistributedLock先执行(缓存有数据执行返回,缓存没有数据进入分布式锁的切面执行代码)

    假设一共有200个线程分别进入A、B两个实例,各自100个线程。

    A实例的100个线程,当进入Cacheable注解时候会同步执行,此时只有一个线程会进入到DistributedLock的切面,如果此时获取到分布式锁,则进入方法执行,此时全局只有这一个线程进入到了方法内部,先判断参数是否有效,无效则返回null缓存起来,然后从缓存里取一下数据,如果不为空则返回,否则从数据库获取数据返回。

    当B实例的第一个线程现在正在等待获取锁,A实例的线程执行完了方法并释放了锁,此时如果没有执行完Cacheabe切面的代码,B实例会再次从数据库加载一次数据,如果此时有执行完Cacheable切面的额代码,B实例的线程会进入方法通过studentAccess.findById获取数据并返回。

    终极解决方案还是要在方法内部用分布式锁控制,这样就用不了分布式锁注解了。

    实际上经过我几次测试200个线程2个实例同时跑,几乎没有出现从数据库取2遍数据的情况,这个概率极低。 

    CacheEvict注解是缓存清除注解,当方法执行完毕后,会触发缓存的清理工作。

    一次增删改可能会触发多个缓存失效,如上。

    三、缓存的配置

    第二节的缓存操作只给我们提供了缓存操作的抽象实现,具体缓存是放到进程还是redis,是json序列化还是protobuf序列化,其实我们并不需要在provider层来指定,我们需要通过配置来指定。

    缓存配置我准备了三套:

    进程内缓存(主要采用的是ehcache):

    package chen.huai.jie.springboot.cache.config;
    
    import chen.huai.jie.springboot.cache.model.CacheConstants;
    import net.sf.ehcache.Cache;
    import net.sf.ehcache.config.CacheConfiguration;
    import net.sf.ehcache.store.MemoryStoreEvictionPolicy;
    import org.springframework.cache.CacheManager;
    import org.springframework.cache.ehcache.EhCacheCacheManager;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Profile;
    
    /**
     * @author chenhuaijie
     */
    @Profile("ehcache")
    @Configuration
    public class EhcacheCacheConfig {
    
        @Bean
        public CacheManager cacheManager() {
            return new EhCacheCacheManager(buildCacheManager());
        }
    
        private net.sf.ehcache.CacheManager buildCacheManager() {
            net.sf.ehcache.CacheManager cacheManager = new net.sf.ehcache.CacheManager();
            cacheManager.addCache(createCache(CacheConstants.STUDENT_BY_ID, 100, 600));
            cacheManager.addCache(createCache(CacheConstants.STUDENT_BY_NO, 100, 600));
            cacheManager.addCache(createCache(CacheConstants.SCORE_LIST_BY_STUDENT_NO, 100, 600));
            return cacheManager;
        }
    
        private Cache createCache(String cacheName, int maxEntries, long timeToLiveSeconds) {
            CacheConfiguration cacheConfiguration = new CacheConfiguration();
            cacheConfiguration.name(cacheName)
                    .maxEntriesLocalHeap(maxEntries)
                    .memoryStoreEvictionPolicy(MemoryStoreEvictionPolicy.LFU)
                    .timeToLiveSeconds(timeToLiveSeconds);
            return new Cache(cacheConfiguration);
        }
    }
    

    分布式缓存(主要是用redis):

    redis缓存里又分为

    json序列化:

    package chen.huai.jie.springboot.cache.config;
    
    import chen.huai.jie.springboot.cache.db.entity.Score;
    import chen.huai.jie.springboot.cache.db.entity.Student;
    import chen.huai.jie.springboot.cache.model.CacheConstants;
    import com.fasterxml.jackson.databind.JavaType;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.cache.CacheManager;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Profile;
    import org.springframework.data.redis.cache.RedisCacheConfiguration;
    import org.springframework.data.redis.cache.RedisCacheManager;
    import org.springframework.data.redis.cache.RedisCacheWriter;
    import org.springframework.data.redis.connection.RedisConnectionFactory;
    import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
    import org.springframework.data.redis.serializer.RedisSerializationContext;
    
    import java.time.Duration;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    
    /**
     * @author chenhuaijie
     */
    @Slf4j
    @Profile("json")
    @Configuration
    public class RedisJsonCacheConfig {
        @Autowired
        private ObjectMapper objectMapper;
    
        @Bean
        public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
            return RedisCacheManager
                    .builder(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory))
                    .withInitialCacheConfigurations(buildRedisCacheConfigurationMap())
                    .build();
        }
    
        private Map<String, RedisCacheConfiguration> buildRedisCacheConfigurationMap() {
            Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>(3);
            redisCacheConfigurationMap.put(CacheConstants.STUDENT_BY_ID, buildRedisCacheConfiguration4Class(Student.class));
            redisCacheConfigurationMap.put(CacheConstants.STUDENT_BY_NO, buildRedisCacheConfiguration4Class(Student.class));
            redisCacheConfigurationMap.put(CacheConstants.SCORE_LIST_BY_STUDENT_NO, buildRedisCacheConfiguration4JavaType(List.class, Score.class));
            return redisCacheConfigurationMap;
        }
    
        private RedisCacheConfiguration buildRedisCacheConfiguration4Class(Class clazz) {
            return buildRedisCacheConfiguration(new Jackson2JsonRedisSerializer(clazz));
        }
    
        private RedisCacheConfiguration buildRedisCacheConfiguration4JavaType(Class collectionClazz, Class clazz) {
            JavaType javaType = objectMapper.getTypeFactory().constructParametricType(collectionClazz, clazz);
            return buildRedisCacheConfiguration(new Jackson2JsonRedisSerializer(javaType));
        }
    
        private RedisCacheConfiguration buildRedisCacheConfiguration(Jackson2JsonRedisSerializer jackson2JsonRedisSerializer) {
            jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
    
            return RedisCacheConfiguration.defaultCacheConfig()
                    .entryTtl(Duration.ofSeconds(600))
                    .computePrefixWith(name -> name)
                    .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer));
        }
    }
    

    protobuf序列化:

    package chen.huai.jie.springboot.cache.config;
    
    import chen.huai.jie.springboot.cache.model.CacheConstants;
    import chen.huai.jie.springboot.cache.proto.codec.ScoreListRedisSerializer;
    import chen.huai.jie.springboot.cache.proto.codec.StudentRedisSerializer;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.cache.CacheManager;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Profile;
    import org.springframework.data.redis.cache.RedisCacheConfiguration;
    import org.springframework.data.redis.cache.RedisCacheManager;
    import org.springframework.data.redis.cache.RedisCacheWriter;
    import org.springframework.data.redis.connection.RedisConnectionFactory;
    import org.springframework.data.redis.serializer.RedisSerializationContext;
    import org.springframework.data.redis.serializer.RedisSerializer;
    
    import java.time.Duration;
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * @author chenhuaijie
     */
    @Slf4j
    @Profile("proto")
    @Configuration
    public class RedisProtoCacheConfig {
    
        @Bean
        public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
            return RedisCacheManager
                    .builder(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory))
                    .withInitialCacheConfigurations(buildRedisCacheConfigurationMap())
                    .build();
        }
    
        private Map<String, RedisCacheConfiguration> buildRedisCacheConfigurationMap() {
            Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>(3);
            redisCacheConfigurationMap.put(CacheConstants.STUDENT_BY_ID, buildRedisCacheConfiguration(new StudentRedisSerializer()));
            redisCacheConfigurationMap.put(CacheConstants.STUDENT_BY_NO, buildRedisCacheConfiguration(new StudentRedisSerializer()));
            redisCacheConfigurationMap.put(CacheConstants.SCORE_LIST_BY_STUDENT_NO, buildRedisCacheConfiguration(new ScoreListRedisSerializer()));
            return redisCacheConfigurationMap;
        }
    
        private RedisCacheConfiguration buildRedisCacheConfiguration(RedisSerializer redisSerializer) {
            return RedisCacheConfiguration.defaultCacheConfig()
                    .entryTtl(Duration.ofSeconds(600))
                    .computePrefixWith(name -> name)
                    .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer));
        }
    }
    

    补充一点:开发中遇到一个问题,就是清缓存的时候,事务还没有结束,缓存就清理掉了,导致数据不一致。很好解决设置transactionAware=true。

    四、压测及数据

    接下来我们用压测工具测试下性能和吞吐量:

    单机的情况下:jvm内存给到2048m

    200个线程跑500遍,一共10万个请求,每个场景取三次

    redis-json的情况下:吞吐量大概在1500左右

    redis-protobuf情况下:吞吐量在2000左右,在同等硬件资源及其他资源的情况下,protobuf凭借着出色的序列化性能,可以使我们的性能和吞吐量再提升33%,且占用更少的redis内存,同时我们需要付出的代价是:需要手工去实现protobuf每个类的序列化和反序列化,写入redis的数据也不像json一样具有可读性,因为都是二进制数组。如果有极致性能需要,这个也不失为一个精益求精的方案。

    ehcache情况下:吞吐量在4800左右,进程级缓存省去了序列化反序列化和网络开销,速度可谓是一骑绝尘,但是要付出很大的代价,占用了过多的jvm内存,同时多实例的情况下,还需要借助第三方工作来同步各个实例的缓存数据,除非有非常严苛的要求,一般可以不考虑这种缓存方案。

    谈谈我的这个方案是怎么解决常见的缓存问题的:

    缓存穿透:采用了缓存null值得方式,此时缓存的时间应该尽量设置的小。

    缓存击穿:为了防止大量线程同时涌入数据库查询,采用了分布式锁和Cacheable的sync=true结合的方式,保证了当大量线程查询同一个key的情况下,最终只有一个线程会查询到数据库。

    缓存雪崩:我们的缓存配置某个缓存是定死的,无法做到每个key的缓存时间不同,如果是程序启动的时候加载了所有数据,大概率会在同一时刻全部失效,方案是在失效前进行一次reload.

    大Key:将一个大的Key按照条件拆分为几个小的Key

    热Key:进程级缓存+定期reload

    总结:

    性能优化是我们开发过程中非常重要的一项技能,

    我们的终极目标是优化性能,但是在优化的过程中,我们也不能增加程序的复杂度,代码的复杂程度,我认为在缓存方案实施的过程中,除了追求我们要的极致性能体验外,程序的设计,代码的侵入性等方面我们也需要做进一步的思考,灵活多变。

    展开全文
  • H5 页面列表缓存方案

    2021-05-01 00:46:12
    大家好,我是若川(点这里加我微信ruochuan12,长期交流学习)。今天给大家介绍一下关于h5页面的列表缓存方案。感谢屏幕前的你一直关注着我。点击下方卡片关注我、加个星标,或者查看源码...

    大家好,我是若川(点这里加我微信 ruochuan12,长期交流学习)。今天给大家介绍一下关于h5页面的列表缓存方案。感谢屏幕前的你一直关注着我。

    点击下方卡片关注我、加个星标,或者查看源码等系列文章。学习源码整体架构系列年度总结JS基础系列


    前言

    在 H5 日常开发中,会经常遇到列表点击进入详情页面然后返回列表的情况,对于电商类平台尤为常见,像我们平常用的淘宝、京东等电商平台都是做了缓存,而且不只是列表,很多地方都用到了缓存。但刚才说的都是 App,在原生 App 中,页面是一层层的 View,盖在 LastPage 上,天然就能够保存上一个页面的状态,而 H5 不同,从详情返回到列表后,状态会被清除掉,重新走一遍生命周期,会重新发起请求,会有新的状态写入,对于分页接口,列表很长,当用户翻了好几页后,点击详情看看商品详情后再返回列表,此时页面回到第一页,这样用户体验很差,如果在进入详情的时候将列表数据缓存起来,返回列表的时候用缓存数据,而不是重新请求数据,停留在离开列表页时的浏览位置;或者是能够像 App 那样,将页面一层层堆叠在 LastPage 上,返回的时候展示对应的页面,这样用户体验会好很多,本文简单介绍一下在自己在做列表缓存的时候考虑的几点,后附简单实现。

    思考

    状态丢失的原因

    通常在页面开发中,我们是通过路由去管理不同的页面,常用的路由库也有很多,譬如:React-Router (https://react-guide.github.io/react-router-cn/),Dva-router (https://dvajs.com/api/#dva-router)... 当我们切换路由时,没有被匹配到的 Component 也会被整体替换掉,原有的状态也丢失了。因此,当用户从详情页退回到列表页时,会重新加载列表页面组件,重新走一遍生命周期,获取的就是第一页的数据,从而回到了列表顶部,下面是常用的路由匹配代码段。

    function RouterConfig({ history, app }) {
      const routerData = getRouterData(app);
      return (
        <ConnectedRouter history={history}>
          <Route
            path="/"
            render={(props) => <Layouts routerData={routerData} {...props} />}
            redirectPath="/exception/403"
          />
        </ConnectedRouter>
      );
    }
    
    // 路由配置说明(你不用加载整个配置,
    // 只需加载一个你想要的根路由,
    // 也可以延迟加载这个配置)。
    React.render((
      <Router>
        <Route path="/" component={App}>
          <Route path="about" component={About}/>
          <Route path="users" component={Users}>
            <Route path="/user/:userId" component={User}/>
          </Route>
          <Route path="*" component={NoMatch}/>
        </Route>
      </Router>
    ), document.body)
    

    如何解决

    原因找到了,那么我们怎么去缓存页面或者数据呢?一般有两种解决方式:1. 路由切换时自动保存状态。2. 手动保存状态。在 Vue 中,可以直接使用 keep-alive 来实现组件缓存,只要使用了 keep-alive 标签包裹的组件,在页面切换的时候会自动缓存 失活 的组件,使用起来非常方便,简单例子如下。

    <!-- 失活的组件将会被缓存!-->
    <keep-alive>
      <component v-bind:is="currentTabComponent"></component>
    </keep-alive>
    

    但是,React 中并没有 keep-alive 这种类似的标签或功能,官方认为这个功能容易造成内存泄漏,暂不考虑支持 (https://github.com/facebook/react/issues/12039)。

    所以只能是在路由层做手脚,在路由切换时做对应的缓存操作,之前有开发者提出了一种方案:通过样式来控制组件的显示/隐藏 (https://github.com/facebook/react/issues/12039),但是这可能会有问题,例如切换组件的时候无法使用动画,或者使用 ReduxMobx 这样的数据流管理工具,还有开发者通过 React.createPortal API 实现了 React 版本的 React Keep Alive (https://github.com/Sam618/react-keep-alive),并且使用起来也比较方便。第二种解决方案就是手动保存状态,即在页面卸载时手动将页面的状态收集存储起来,在页面挂载的时候进行数据恢复,个人采用的就是简单粗暴的后者,实现上比较简单。缓存缓存,无外乎就是两件事,存和取,那么在存、取的过程中需要注意哪些问题呢?

    个人认为需要注意的有以下几点:

    存什么?何时存?存在哪?何时取?在哪取?

    存什么

    首先我们需要关心的是:存什么?既然要缓存,那么我们要存的是什么?是缓存整个 Component、列表数据还是滚动容器的 scrollTop。举个例子,微信公众号里的文章就做了缓存,任意点击一篇文章浏览,浏览到一半后关闭退出,再一次打开该文章时会停留在之前的位置,而且大家可以自行测试一下,再次打开的时候文章数据是重新获取的,在这种场景下,是缓存了文章详情滚动容器的滚动高度,在离开页面的时候存起来,再次进入的时候拿到数据后跳转到之前的高度,除此之外,还有很多别的缓存的方式,可以缓存整个页面,缓存 state 的数据等等,这些都可以达到我们想要的效果,具体用哪一种要看具体的业务场景。

    何时存

    其次,我们需要考虑的是什么时候存,页面跳转时会有多种 action 导航操作,比如:POPPUSHREPLACE 等,当我们结合一些比较通用的路由库时,action 会区分的更加细致,对于不同的 action 在不同的业务场景下处理的方式也不尽相同。还是拿微信公众号举例,文章详情页面就是无脑存,无论是 PUSHPOP 都会存高度数据,所以我们无论跳转多少次页面,再次打开总能跳转到之前离开时的位置,对于商品列表的场景时,就不能无脑存了,因为从 List -> Detail -> List 需要缓存没问题,但是用户从 List 返回到其他页面后再次进入 List 时,是进入一个新的页面,从逻辑上来说就不应该在用之前缓存的数据,而是重新获取数据。正确的方式应该是进行 PUSH 操作的时候存,POP 的时候取。

    存在哪

    1. 持久化缓存。如果是数据持久化可存到 URL 或 localStorage 中,放到 URL 上有一个很好点在于确定性,易于传播。但 URL 可以先 pass 掉,因为在复杂列表的情况下,需要存的数据比较多,全部放到 URL 是不现实的,即使可以,也会让 URL 显得极其冗长,显然不妥。localStorage 是一种方式,提供的 getItemsetItem 等 api 也足够支持存取操作,最大支持 5M,容量也够,通过序列化 Serialize 整合也可以满足需求,另外 IndexDB 也不失为一种好的方式,WebSQL 已废弃,就不考虑了,详细可点击张鑫旭的这篇文章《HTML5 indexedDB前端本地存储数据库实例教程》(https://www.zhangxinxu.com/wordpress/2017/07/html5-indexeddb-js-example/)查看对比。

    2. 内存。对于不需要做持久化的列表或数据来说,放内存可能是一个更好的方式,如果进行频繁的读写操作,放内存中操作 I/O 速度快,方便。因此,可以放到 Redux 或 Rematch 等状态管理工具中,封装一些通用的存取方法,很方便,对于一般的单页应用来说,还可以放到全局的 window 中。

    何时取

    在进入缓存页面的时候取,取的时候又有几种情况

    1. 当导航操作为 POP 时取,因为每当 PUSH 时,都算是进入一个新的页面,这种情况是不应该用缓存数据。

    2. 无论哪种导航操作都进行取数据,这种情况需要和何时存一起看待。

    3. 看具体的业务场景,来判断取的时机。

    在哪取

    这个问题很简单,存在哪就从哪里取。

    CacheHoc 的方案

    • 存什么:列表数据 + 滚动容器的滚动高度

    • 何时存:页面离开且导航操作为 PUSH

    • 存在哪:window

    • 何时取:页面初始化阶段且导航操作为 POP 的时候

    • 在哪取:window

    CacheHoc 是一个高阶组件,缓存数据统一存到 window 内,通过 CACHE_STORAGE 收敛,外部仅需要传入 CACHE_NAMEscrollElRefs 即可,CACHE_NAME 相当于缓存数据的 key,而 scrollElRefs 则是一个包含滚动容器的数组,为啥用数组呢,是考虑到页面多个滚动容器的情况,在 componentWillUnmount 生命周期函数中记录对应滚动容器的 scrollTopstate,在 constructor 内初始化 state,在 componentDidMount 中更新 scrollTop

    简单使用

    import React from 'react'
    import { connect } from 'react-redux'
    import cacheHoc from 'utils/cache_hoc'
    
    @connect(mapStateToProps, mapDispatch)
    @cacheHoc
    export default class extends React.Component {
      constructor (...props) {
        super(...props)
        this.props.withRef(this)
      }
    
      // 设置 CACHE_NAME
      CACHE_NAME = `customerList${this.props.index}`;
      
      scrollDom = null
    
      state = {
        orderBy: '2',
        loading: false,
        num: 1,
        dataSource: [],
        keyWord: undefined
      }
    
      componentDidMount () {
        // 设置滚动容器list
        this.scrollElRefs = [this.scrollDom]
        // 请求数据,更新 state
      }
    
      render () {
        const { history } = this.props
        const { dataSource, orderBy, loading } = this.state
    
        return (
          <div className={gcmc('wrapper')}>
            <MeScroll
              className={gcmc('wrapper')}
              getMs={ref => (this.scrollDom = ref)}
              loadMore={this.fetchData}
              refresh={this.refresh}
              up={{
                page: {
                  num: 1, // 当前页码,默认0,回调之前会加1,即callback(page)会从1开始
                  size: 15 // 每页数据的数量
                  // time: null // 加载第一页数据服务器返回的时间; 防止用户翻页时,后台新增了数据从而导致下一页数据重复;
                }
              }}
              down={{ auto: false }}
            >
              {loading ? (
                <div className={gcmc('loading-wrapper')}>
                  <Loading />
                </div>
              ) : (
                dataSource.map(item => (
                  <Card
                    key={item.clienteleId}
                    data={item}
                    {...this.props}
                    onClick={() =>
                      history.push('/detail/id')
                    }
                  />
                ))
              )}
            </MeScroll>
            <div className={styles['sort']}>
              <div className={styles['sort-wrapper']} onClick={this._toSort}>
                <span style={{ marginRight: 3 }}>最近下单时间</span>
                <img
                  src={orderBy === '2' ? SORT_UP : SORT_DOWN}
                  alt='sort'
                  style={{ width: 10, height: 16 }}
                />
              </div>
            </div>
          </div>
        )
      }
    }
    
    

    效果如下:

    缓存的数据:

    代码

    const storeName = 'CACHE_STORAGE'
    window[storeName] = {}
    
    export default Comp => {
      return class CacheWrapper extends Comp {
        constructor (props) {
          super(props)
          // 初始化
          if (!window[storeName][this.CACHE_NAME]) {
            window[storeName][this.CACHE_NAME] = {}
          }
          const { history: { action } = {} } = props
          // 取 state
          if (action === 'POP') {
            const { state = {} } = window[storeName][this.CACHE_NAME]
            this.state = {
              ...state,
            }
          }
        }
    
        async componentDidMount () {
          if (super.componentDidMount) {
            await super.componentDidMount()
          }
          const { history: { action } = {} } = this.props
          if (action !== 'POP') return
          const { scrollTops = [] } = window[storeName][this.CACHE_NAME]
          const { scrollElRefs = [] } = this
          // 取 scrollTop
          scrollElRefs.forEach((el, index) => {
            if (el && el.scrollTop !== undefined) {
              el.scrollTop = scrollTops[index]
            }
          })
        }
    
        componentWillUnmount () {
          const { history: { action } = {} } = this.props
          if (super.componentWillUnmount) {
            super.componentWillUnmount()
          }
          if (action === 'PUSH') {
            const scrollTops = []
            const { scrollElRefs = [] } = this
            scrollElRefs.forEach(ref => {
              if (ref && ref.scrollTop !== undefined) {
                scrollTops.push(ref.scrollTop)
              }
            })
            window[storeName][this.CACHE_NAME] = {
              state: {
                ...this.state
              },
              scrollTops
            }
          }
          if (action === 'POP') {
            window[storeName][this.CACHE_NAME] = {}
          }
        }
      }
    }
    
    

    总结

    以上的 CacheHoc 只是最简单的一种实现,还有很多可以改进的地方,譬如:直接存在 window 中有点粗暴,多页应用下存到 window 会丢失数据,可以考虑存到 IndexDB 或者 localStorage 中,另外这种方案若不配合上 mescroll 需要在 componentDidMount 判断 state 内的数据,若有值就不初始化数据,这算是一个 bug

    缓存方案纵有多种,但需要考虑的问题就以上几点。另外在讲述需要注意的五个点的时候,着重介绍了存什么和存在哪,其实存在哪不太重要,也不需要太关心,找个合适的地方存着就行,比较重要的是存什么、何时存,需要结合实际的应用场景,来选择合适的方式,可能不同的页面采用的方式都不同,没有固定的方案,重要的是分析存取的时机和位置。


    最近组建了一个江西人的前端交流群,如果你也是江西人可以加我微信 ruochuan12 拉你进群。


    ················· 若川出品 ·················

    今日话题

    还有最后一天上班就放五一小长假啦(努力让内心喜悦不被发现),虽然扣除2天补班, 2天周末,实际天只有1天假期~哈哈,但是能连着休息5天,也还是很不错哦。趁着小长假可以好好放休息休息,整理一下之前没及时整理的东西,大家五一都有什么计划呢?欢迎在下方留言~  欢迎分享、收藏、点赞、在看我的公众号文章~

    一个愿景是帮助5年内前端人走向前列的公众号

    可加我个人微信 ruochuan12,长期交流学习

    推荐阅读

    我在阿里招前端,我该怎么帮你?(现在还能加我进模拟面试群)

    若川知乎问答:2年前端经验,做的项目没什么技术含量,怎么办?

    点击方卡片关注我、加个星标,或者查看源码等系列文章。
    学习源码整体架构系列年度总结JS基础系列

    展开全文
  • 前言:前端缓存目的是减少性能...方案一:数据缓存 简单的数据缓存,第一次请求的时候获取数据,之后便使用数据,不再请求后端api,代码如下: // 方案一:数据缓存 const dataCache = new Map(); async funct

    前言:前端缓存目的是减少性能损耗,提升web性能,以客户端而言,我们有很多缓存数据和资源的方法,例如标准的浏览器缓存以及目前火热的Service worker。但是他们更适合静态内容的缓存,例如html,js,css以及图片等文件,而缓存系统数据我们采用其他方案!

    方案一:数据缓存

    简单的数据缓存,第一次请求的时候获取数据,之后便使用数据,不再请求后端api,代码如下:

    // 方案一:数据缓存
        const dataCache = new Map();
        
        async function getData() {  
          let key = 'todos';
    
          // 从data缓存中获取数据
          let data = dataCache.get(key);
          if(!data){
            // 缓存中无数据从服务器请求数据
            const res = await fetch('https://jsonplaceholder.typicode.com/todos')
            data = await res.json();
            
            // 设置数据缓存
            dataCache.set(key, data);
          }
    
          return data;
        }
    
        // 调用函数,获取数据
        getData().then(res => console.log(res));

    方案二:promise缓存

    方案一本身是不足的。因为如果考虑同时两个以上的调用此 api,会因为请求未返回而进行第二次请求api。当然,如果你在系统中添加类似于 vuex、redux这样的单一数据源框架,这样的问题不太会遇到,但是有时候我们想在各个复杂组件分别调用api,而不想对组件进行组件通信数据时候,便会遇到此场景。

    const promiseCache = new Map()
    
    getWares() {
        const key = 'wares'
        let promise = promiseCache.get(key);
        // 当前promise缓存中没有 该promise
        if (!promise) {
            promise = request.get('/getWares').then(res => {
                // 对res 进行操作
                ...
            }).catch(error => {
                // 在请求回来后,如果出现问题,把promise从cache中删除 以避免第二次请求继续出错S
                promiseCache.delete(key)
                return Promise.reject(error)
            })
        }
        // 返回promise
        return promise
    }
    

    该代码避免了方案一的同一时间多次请求的问题。同时也在后端出错的情况下对promise进行了删除,不会出现缓存了错误的promise就一直出错的问题。

    调用方式:

    getWares().then( ... )
    // 第二次调用 取得先前的promise
    getWares().then( ... )

     

    展开全文
  • 下面会介绍缓存使用技巧和设计方案,包含如下内容:缓存的收益和成本分析、缓存更新策略的选择和使用场景、缓存粒度控制方法、穿透问题优化、无底洞问题优化、雪崩问题优化、热点key重建优化。 1)缓存的收益和成本...
    
     

    今日推荐

    
     
    
     
    这 9 个 Java 开源项目 yyds,你知道几个?
    
    阿里技术专家推荐的20本书,免费送!
    
    K8S 部署 SpringBoot 项目(一篇够用)
    
    妙用Java 8中的 Function接口 消灭if...else(非常新颖的写法)
    Nginx 入门到实战,新手必懂。

    缓存能够有效地加速应用的读写速度,同时也可以降低后端负载,对日常应用的开发至关重要。下面会介绍缓存使用技巧和设计方案,包含如下内容:缓存的收益和成本分析、缓存更新策略的选择和使用场景、缓存粒度控制方法、穿透问题优化、无底洞问题优化、雪崩问题优化、热点key重建优化。

    1)缓存的收益和成本分析

    下图左侧为客户端直接调用存储层的架构,右侧为比较典型的缓存层+存储层架构。

    6e8a2b7d2a8216e8bd85f0f005b40d6e.png

    下面分析一下缓存加入后带来的收益和成本。

    收益:

    ①加速读写:因为缓存通常都是全内存的,而存储层通常读写性能不够强悍(例如MySQL),通过缓存的使用可以有效地加速读写,优化用户体验。

    ②降低后端负载:帮助后端减少访问量和复杂计算(例如很复杂的SQL语句),在很大程度降低了后端的负载。

    成本:

    ①数据不一致性:缓存层和存储层的数据存在着一定时间窗口的不一致性,时间窗口跟更新策略有关。

    ②代码维护成本:加入缓存后,需要同时处理缓存层和存储层的逻辑,增大了开发者维护代码的成本。

    ③运维成本:以Redis Cluster为例,加入后无形中增加了运维成本。

    缓存的使用场景基本包含如下两种:

    ①开销大的复杂计算:以MySQL为例子,一些复杂的操作或者计算(例如大量联表操作、一些分组计算),如果不加缓存,不但无法满足高并发量,同时也会给MySQL带来巨大的负担。

    ②加速请求响应:即使查询单条后端数据足够快(例如select*from table where id=),那么依然可以使用缓存,以Redis为例子,每秒可以完成数万次读写,并且提供的批量操作可以优化整个IO链的响应时间。

    2)缓存更新策略

    缓存中的数据会和数据源中的真实数据有一段时间窗口的不一致,需要利用某些策略进行更新,下面会介绍几种主要的缓存更新策略。

    ①LRU/LFU/FIFO算法剔除:剔除算法通常用于缓存使用量超过了预设的最大值时候,如何对现有的数据进行剔除。例如Redis使用maxmemory-policy这个配置作为内存最大值后对于数据的剔除策略。

    ②超时剔除:通过给缓存数据设置过期时间,让其在过期时间后自动删除,例如Redis提供的expire命令。如果业务可以容忍一段时间内,缓存层数据和存储层数据不一致,那么可以为其设置过期时间。在数据过期后,再从真实数据源获取数据,重新放到缓存并设置过期时间。例如一个视频的描述信息,可以容忍几分钟内数据不一致,但是涉及交易方面的业务,后果可想而知。

    ③主动更新:应用方对于数据的一致性要求高,需要在真实数据更新后,立即更新缓存数据。例如可以利用消息系统或者其他方式通知缓存更新。

    三种常见更新策略的对比:

    1612bd38ab4214892ff855feca5bd7f1.png

    有两个建议:

    ①低一致性业务建议配置最大内存和淘汰策略的方式使用。

    ②高一致性业务可以结合使用超时剔除和主动更新,这样即使主动更新出了问题,也能保证数据过期时间后删除脏数据。

    3)缓存粒度控制

    缓存粒度问题是一个容易被忽视的问题,如果使用不当,可能会造成很多无用空间的浪费,网络带宽的浪费,代码通用性较差等情况,需要综合数据通用性、空间占用比、代码维护性三点进行取舍。

    缓存比较常用的选型,缓存层选用Redis,存储层选用MySQL。

    89465b1f62aa234aa4a4abe56d377475.png

    4)穿透优化

    缓存穿透是指查询一个根本不存在的数据,缓存层和存储层都不会命中,通常出于容错的考虑,如果从存储层查不到数据则不写入缓存层。

    通常可以在程序中分别统计总调用数、缓存层命中数、存储层命中数,如果发现大量存储层空命中,可能就是出现了缓存穿透问题。造成缓存穿透的基本原因有两个。第一,自身业务代码或者数据出现问题,第二,一些恶意攻击、爬虫等造成大量空命中。下面我们来看一下如何解决缓存穿透问题。

    ①缓存空对象:如图下所示,当第2步存储层不命中后,仍然将空对象保留到缓存层中,之后再访问这个数据将会从缓存中获取,这样就保护了后端数据源。

    ac0b77da0fc0f13eecaf8eda2aab8111.png

    缓存空对象会有两个问题:第一,空值做了缓存,意味着缓存层中存了更多的键,需要更多的内存空间(如果是攻击,问题更严重),比较有效的方法是针对这类数据设置一个较短的过期时间,让其自动剔除。第二,缓存层和存储层的数据会有一段时间窗口的不一致,可能会对业务有一定影响。例如过期时间设置为5分钟,如果此时存储层添加了这个数据,那此段时间就会出现缓存层和存储层数据的不一致,此时可以利用消息系统或者其他方式清除掉缓存层中的空对象。

    ②布隆过滤器拦截

    如下图所示,在访问缓存层和存储层之前,将存在的key用布隆过滤器提前保存起来,做第一层拦截。例如:一个推荐系统有4亿个用户id,每个小时算法工程师会根据每个用户之前历史行为计算出推荐数据放到存储层中,但是最新的用户由于没有历史行为,就会发生缓存穿透的行为,为此可以将所有推荐数据的用户做成布隆过滤器。如果布隆过滤器认为该用户id不存在,那么就不会访问存储层,在一定程度保护了存储层。

    1cc14b7345bc6dfcff7e1da2dca89cda.png

    缓存空对象和布隆过滤器方案对比

    e7e6f4c6e5679356e1ce8ce5ad1065ac.png

    另:布隆过滤器简单说明:

    如果想判断一个元素是不是在一个集合里,一般想到的是将集合中所有元素保存起来,然后通过比较确定。链表、树、散列表(又叫哈希表,Hash table)等等数据结构都是这种思路。但是随着集合中元素的增加,我们需要的存储空间越来越大。同时检索速度也越来越慢。

    Bloom Filter 是一种空间效率很高的随机数据结构,Bloom filter 可以看做是对 bit-map 的扩展, 它的原理是:

    当一个元素被加入集合时,通过 K 个 Hash 函数将这个元素映射成一个位阵列(Bit array)中的 K 个点,把它们置为 1。检索时,我们只要看看这些点是不是都是 1 就(大约)知道集合中有没有它了:

    如果这些点有任何一个 0,则被检索元素一定不在;如果都是 1,则被检索元素很可能在。

    5)无底洞优化

    为了满足业务需要可能会添加大量新的缓存节点,但是发现性能不但没有好转反而下降了。用一句通俗的话解释就是,更多的节点不代表更高的性能,所谓“无底洞”就是说投入越多不一定产出越多。但是分布式又是不可以避免的,因为访问量和数据量越来越大,一个节点根本抗不住,所以如何高效地在分布式缓存中批量操作是一个难点。

    无底洞问题分析:

    ①客户端一次批量操作会涉及多次网络操作,也就意味着批量操作会随着节点的增多,耗时会不断增大。

    ②网络连接数变多,对节点的性能也有一定影响。

    如何在分布式条件下优化批量操作?我们来看一下常见的IO优化思路:

    • 命令本身的优化,例如优化SQL语句等。

    • 减少网络通信次数。

    • 降低接入成本,例如客户端使用长连/连接池、NIO等。

    这里我们假设命令、客户端连接已经为最优,重点讨论减少网络操作次数。下面我们将结合Redis Cluster的一些特性对四种分布式的批量操作方式进行说明。

    ①串行命令:由于n个key是比较均匀地分布在Redis Cluster的各个节点上,因此无法使用mget命令一次性获取,所以通常来讲要获取n个key的值,最简单的方法就是逐次执行n个get命令,这种操作时间复杂度较高,它的操作时间=n次网络时间+n次命令时间,网络次数是n。很显然这种方案不是最优的,但是实现起来比较简单。

    ②串行IO:Redis Cluster使用CRC16算法计算出散列值,再取对16383的余数就可以算出slot值,同时Smart客户端会保存slot和节点的对应关系,有了这两个数据就可以将属于同一个节点的key进行归档,得到每个节点的key子列表,之后对每个节点执行mget或者Pipeline操作,它的操作时间=node次网络时间+n次命令时间,网络次数是node的个数,整个过程如下图所示,很明显这种方案比第一种要好很多,但是如果节点数太多,还是有一定的性能问题。

    e698261e7c72a5aa471f21e13165df00.png

    ③并行IO:此方案是将方案2中的最后一步改为多线程执行,网络次数虽然还是节点个数,但由于使用多线程网络时间变为O(1),这种方案会增加编程的复杂度。

    745747959bb09310127b44fe2cdb8d1a.png

    ④hash_tag实现:Redis Cluster的hash_tag功能,它可以将多个key强制分配到一个节点上,它的操作时间=1次网络时间+n次命令时间。

    四种批量操作解决方案对比

    1fa6c0527ac70080fe7b28e69ad108e5.png

    6)雪崩优化

    缓存雪崩:由于缓存层承载着大量请求,有效地保护了存储层,但是如果缓存层由于某些原因不能提供服务,于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会级联宕机的情况。

    预防和解决缓存雪崩问题,可以从以下三个方面进行着手:

    ①保证缓存层服务高可用性。如果缓存层设计成高可用的,即使个别节点、个别机器、甚至是机房宕掉,依然可以提供服务,例如前面介绍过的Redis Sentinel和Redis Cluster都实现了高可用。

    ②依赖隔离组件为后端限流并降级。在实际项目中,我们需要对重要的资源(例如Redis、MySQL、HBase、外部接口)都进行隔离,让每种资源都单独运行在自己的线程池中,即使个别资源出现了问题,对其他服务没有影响。但是线程池如何管理,比如如何关闭资源池、开启资源池、资源池阀值管理,这些做起来还是相当复杂的。

    ③提前演练。在项目上线前,演练缓存层宕掉后,应用以及后端的负载情况以及可能出现的问题,在此基础上做一些预案设定。

    7)热点key重建优化

    开发人员使用“缓存+过期时间”的策略既可以加速数据读写,又保证数据的定期更新,这种模式基本能够满足绝大部分需求。但是有两个问题如果同时出现,可能就会对应用造成致命的危害:

    • 当前key是一个热点key(例如一个热门的娱乐新闻),并发量非常大。

    • 重建缓存不能在短时间完成,可能是一个复杂计算,例如复杂的SQL、多次IO、多个依赖等。在缓存失效的瞬间,有大量线程来重建缓存,造成后端负载加大,甚至可能会让应用崩溃。

    要解决这个问题也不是很复杂,但是不能为了解决这个问题给系统带来更多的麻烦,所以需要制定如下目标:

    • 减少重建缓存的次数

    • 数据尽可能一致。

    • 较少的潜在危险

    ①互斥锁:此方法只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可,整个过程如图所示。

    e2d735ed1cc4c548fb2368dcbb68d841.png

    下面代码使用Redis的setnx命令实现上述功能:

    d805ba830e1599030a8a692e4a7e3f6c.png

    1)从Redis获取数据,如果值不为空,则直接返回值;否则执行下面的2.1)和2.2)步骤。

    2.1)如果set(nx和ex)结果为true,说明此时没有其他线程重建缓存,那么当前线程执行缓存构建逻辑。

    2.2)如果set(nx和ex)结果为false,说明此时已经有其他线程正在执行构建缓存的工作,那么当前线程将休息指定时间(例如这里是50毫秒,取决于构建缓存的速度)后,重新执行函数,直到获取到数据。

    ②永远不过期

    “永远不过期”包含两层意思:

    • 从缓存层面来看,确实没有设置过期时间,所以不会出现热点key过期后产生的问题,也就是“物理”不过期。

    • 从功能层面来看,为每个value设置一个逻辑过期时间,当发现超过逻辑过期时间后,会使用单独的线程去构建缓存。

    从实战看,此方法有效杜绝了热点key产生的问题,但唯一不足的就是重构缓存期间,会出现数据不一致的情况,这取决于应用方是否容忍这种不一致。

    38801e2131646ff52d894de729bba524.png

    两种热点key的解决方法

    e64230894c4340557e212bd78ef0f195.png

    来源:blog.csdn.net/ym123456677/article/details/80063491

    
     
    推荐文章
    
     
    1、一款高颜值的 SpringBoot+JPA 博客项目
    
    2、超优 Vue+Element+Spring 中后端解决方案
    
    3、推荐几个支付项目!
    
    4、推荐一个 Java 企业信息化系统
    
    5、一款基于 Spring Boot 的现代化社区(论坛/问答/社交网络/博客)
    展开全文
  • 本文的主要内容 ...现在市场上各种各样的应用,充满了多媒体信息,而声音和视频又是体积最大的文件,如果直接使用 URL 通过 AVPlayer 播放,系统并不会做缓存处理,等下次再播又要重新下载,对...
  • 本文总结了一些使用缓存时常见的问题及解决方案,以后在遇到这类问题时可以作为参考,在设计缓存系统的时候也应该考虑这些常见的情况。为了表述方便,本文以数据库查询缓存为例,使用缓存可以减小对数据库的压力。...
  • 缓存空数据方案2:验证拦截方案3:使用布隆过滤器二、缓存击穿解决方案方案1:设置热点数据永不过期方案2:应用级别锁控制并发方案3:使用分布式锁三、缓存雪崩解决方案方案1:高可用缓存方案2:缓存降级(临时支持...
  • 那我现在就对我应用到项目中的各种 api 请求缓存方案,从简单到复杂依次介绍一下。 方案一 数据缓存 简单的 数据 缓存,第一次请求时候获取数据,之后便使用数据,不再请求后端api。代码如下: const dataCache =...
  • 文章目录先删除缓存,再更新数据库延时双删先更新数据库,再删除缓存修改缓存过期时间消息队列 Redis 缓存常见问题 :缓存雪崩,缓存击穿,缓存穿透,缓存预热 在之前的博客中,我介绍了Redis缓存的一些常见问题,...
  • 缓存一致性解决方案

    2021-06-07 13:40:49
    1)先删除缓存 2)再写数据库 3)休眠500毫秒(根据具体的业务时间来定) 4)再次删除缓存。 那么,这个500毫秒怎么确定的,具体该休眠多久呢? 需要评估自己的项目的读数据业务逻辑的耗时。这么做的目的,就是...
  • 文章目录前言1、缓存技术介绍2、缓存工作原理2.1 缓存如何加速读取操作2.2 缓存如何加速写入操作3、常用术语...因此,有些情况下我们希望结合两者优点,于是SSD缓存技术方案应运而生,SSD缓存方案最终性能接近SSD的性
  • 8种方案,保证缓存和数据库的最终一致性

    千次阅读 多人点赞 2021-11-23 21:48:47
    由于对数据库以及缓存的整体操作,并不是原子性的,再加上读写并发,究竟什么样的方案可以保证数据库与缓存的一致性呢? 下面介绍8种方案,配合读写时序图,希望你能从其中了解到保证一致性的设计要点。
  • 前言由于H5具备 开发周期短、灵活性好 的特点,所以现在 Android App大多嵌入了 Android ... 消耗流量今天,我将针对 Android Webview 的性能问题,提出一些有效解决方案。目录示意图1. Android WebView 存在什...
  • 在业务环境中,频繁访问数据库获取数据的做法是不可取的,为了提升数据请求的效率,目前比较流行的做法就是使用 Redis 缓存服务,将频繁被请求的数据缓存起来,在下一次数据被请求时,根据设定的 key 返回相应的数据...
  • 如果对数据不一致容忍度比较低,那么建议是采用先更新DB,后淘汰缓存方案。 这里只是简单方案分析,如果复杂点的场景,还需要考虑DB读写分离时,主从数据同步延时导致的缓存不一致。 1)Redis引起的缓存不一致问题...
  • 今天又学到了很多,感觉雪崩和穿透很有意思理解起来也比较清晰,然后我搜索了一些资料,给自己做一个普及我们通常使用 缓存 + 过期时间的策略来帮助我们加速接口的访问速度,减少了后端负载,同时保证功能的更新缓存...
  • 下面会详细介绍:1.1.1 渲染速度慢前端H5页面渲染的速度取决于 两个方面:Js 解析效率 Js 本身的解析过程复杂、解析速度不快 & 前端页面涉及较多 JS 代码文件,所以叠加起来会导致 Js 解析效率非常低手机硬件...
  • 分布式缓存的话,使用的比较多的是Memcached和Redis。 不过现在基本没有用Memcached来做缓存,都是直接用Redis。...Redis支持更丰富的数据类型(支持更复杂的应用场景)。Redis不仅仅支持简单的k/v类型的数
  • 随着互联网行业不断的演进与变更,体量与复杂性的变化催生出一个又一个难题,从而衍生出一系列方便开发者解决问题的中间件,比如Redis,我们为什么要使用redis,有两个重要的原因,一个是为了减轻服务器数据库的压力...
  • ###redis的缓存击穿? 缓存穿透是指查询一个根本不存在的数据,缓存层和存储层都不会命中,但是出于容错的考虑,如果从存储层查不到数据则不写入缓存层,如图 11-3 所示整个过程分为如下 3 步: 缓存层不命中 存储...
  • 缓存穿透详解及解决方案

    千次阅读 热门讨论 2021-06-12 15:35:23
    缓存虽然能够大大提升整个系统的性能,但同时也引入了更多复杂性。 如果没有针对缓存进行比较好的处理,某些场景下甚至会导致整个系统崩溃。 这次我们要聊的就是:缓存穿透。 缓存穿透 缓存穿透是指在查询缓存数据时...
  • Java面试少不了一些技术类型的笔试题,面试过程中很多小伙伴可能表达很好,结果Java基础不是很牢固,所以今天针对Java笔试题,小编汇总来一下Java的面试题缓存三大问题及解决方案。1. Java面试题缓存三大问题及解决...
  • 后台开启另外一个守护线程,让其定时去更新缓存,但是这种实现相对复杂,难以把握。 缓存穿透(二) 触发条件: ​ 查询缓存和数据库中都不存在的数据,这样每次请求直接打到数据库,就像缓存不存在一样,失去了
  • (1)缓存失效一致性问题一般缓存的使用方式是:先读取缓存,若不存在则从DB中读取,并将结果写入到缓存中;下次数据读取时便可以直接从缓存中获取数据。数据的修改是直接失效缓存数据,再修改DB内容,避免DB修改成功...
  • 在生产环境中,会因为很多的原因造成访问请求绕过了缓存,都需要访问数据库持久层,虽然对Redsi缓存服务器不会造成影响,但是数据库的负载就会增大,使缓存的作用降低 一、缓存穿透 缓存穿透是指查询一个根本不...
  • java缓存一致性问题及解决方案:使用缓存,肯定会存在一致性问题; 读取缓存步骤一般没有什么问题,但是一旦涉及到数据更新:数据库和缓存更新,就容 易出现缓存(Redis)和数据库(MySQL)间的数据一致性问题。 ...
  • 点击上方蓝色小字,关注“涛哥聊Python”重磅干货,第一时间送达来源:艾小仙你应该从网上看过太多的文章说缓存穿透怎么解决?无非就是布隆过滤器,缓存空值什么的。但是,更深入的一个问题,缓...
  • 今天分享 java基于Spring注解的缓存 解决方案; 上篇以代码的方式分享缓存解决方案;今天分享以注解的方式解决缓存问题: Spring 3.1 起,提供了基于注解的对 Cache 的支持。 一、使用 Spring Cache 的好处: 1...
  • 解决方案: 1、布隆过滤器拦截 将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。 2、简单粗暴的方法(推荐) 如果一个查询返回...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 225,386
精华内容 90,154
关键字:

复杂缓存方案