精华内容
下载资源
问答
  • 后端缓存原理及常见问题

    千次阅读 2019-04-25 20:19:42
    文章目录1 缓存的基本实现2 缓存穿透2.1 原理2.2 解决方案3 缓存击穿4 缓存雪崩5 热点数据集中失效问题6 参考资料 常见的缓存有redis等内存性缓存服务器。对于自己维护数据库而言,所有的请求都...

    作为 server端经常会用到缓存技术来提供降低数据库压力和提高服务性能,因为一些数据我们没有必要每次查询的时候都去查询到数据库。今天读到一篇文章加深了自己对缓存的认识,整理一篇文档以作记录。


    常见的缓存有redis等内存性缓存服务器。对于自己维护数据库而言,所有的请求都查询数据库会对数据库造成极大的压力。而对于正在使用的AWS dynamodb 这类以SaaS服务形式提供数据库就更加直接了,按次数收费或按可用QPS 最大值收费,性能直接就是财富。

    1 缓存的基本实现

    通常不用缓存的情况,每次业务请求,都需要去数据库查询。
    在这里插入图片描述
    使用缓存后,业务查询数据时,会先查询缓存,如果缓存中存在该数据则直接返回,如果没有则继续查询数据库,然后返回。并更新缓存。
    在这里插入图片描述
    原理上说起来是比较简单的情况。但实际使用上会出现比较多的异常情况需要考虑。比如下面的情况:

    • 缓存穿透
    • 缓存击穿
    • 缓存雪崩
    • 热点数据集中失效问题

    2 缓存穿透

    2.1 原理

    正常情况下,需要业务系统查询的数据都是存在的。就会走我们上面图中的缓存流程。

    但如果业务去请求数据库中不存在的数据,就会造成缓存和数据库都查询不到该数据,直接返回。这样每次查询这条数据都会造成缓存和数据库的查询并每次均会直接对数据库造成查询压力。这种查询找不存在数据的现象称为缓存穿透。

    缓存穿透如果一旦被人恶意利用就会导致,利用一条不存在的id,产生大量的请求对数据库进行查询,这种情况很可能就会造成数据库压力过大引起宕机。(相当于缓存系统没有工作,而数据库性能是缓存系统工作情况下设计的)

    2.2 解决方案

    而缓存穿透一般情况下有两种解决方案:
    1 缓存空值
    发生穿透的原因是因为缓存中没有存储这些空数据的key,从而导致每次查询均会在缓存中拿不到值,每次都需要查询数据库。而我们可以将这些实际上不存在的key对应的值设置为null缓存到数据库中。以后在查询时,即可在缓存中查到值(null)并返回。这样就避开了每次对数据库的直接查询。这些缓存需要合理设置过期时间,防止对缓存服务器造成大大容量消耗。

    2 BloomFilter
    我们可以在缓存之前再加一道屏障,里面存储目前数据库中存在的所有key。BloomFilter 类似于一个hbase set 用来判断某个元素(key)是否存在于某个集合中。(PS:但想想这里面的key非常多的话,也要谨慎处理)实际行为如下图所示:
    在这里插入图片描述
    使用BloomFilter后,会存储数据库中所有key(不存值),查询时会先去 BloomFilter 去查询 key 是否存在,如果不存在就直接返回,存在再走查缓存 ,查 DB。这样就可以避免第一种缓存空值解决方案带来的缓存大量不存在的key对缓存服务器造成的压力。

    对于恶意攻击的情况,空值key异常多、请求重复率比较低的数据,就没有必要进行缓存,使用第二种方案直接过滤掉。
    而对于空数据的key有限的,重复率比较高的,则可以采用第一种方式进行缓存。

    3 缓存击穿

    在高并发情况下,大量请求查询同一个key时,而该key在缓存过期机制(有缓存则必须要有过期机制)的情况下正好失效了,就会导致大量请求发现缓存中该key不存在,直接向数据库发起查询请求导致数据库压力激增。

    这种情况,我们可以在查询数据的请求上使用一个互斥锁,其他请求查询数据是发现拿不到锁,就进行等待,等待第一个线程查询到数据后,将数据缓存。后面的线程再从缓存中读取该数据。即缓存不存在时,发现要进行数据读时就应该对该数据的读取加锁了。

    4 缓存雪崩

    缓存雪崩是指,当某一时刻发生大规模的缓存失效的情况,如缓存服务宕机,这样就会有大量的请求直接查询数据库,导致数据库压力剧增,甚至引起宕机。

    对于这种突发问题,首先从缓存设计上就要进行考虑,尽可能让缓存不挂。比如使用缓存集群,提供服务可用性。如redis的使用 主从+哨兵 ,Redis Cluster 来避免 Redis 全盘崩溃的情况。

    如果缓存挂掉了,我们应该采取一定方案,避免过多数据库查询请求直接导致数据库挂掉,导致整个系统异常。比如引入限流降级机制。比如使用防雪崩工具。 Hystrix是一款开源的“防雪崩工具”,它通过 熔断、降级、限流三个手段来降低雪崩发生后的损失。

    Hystrix一旦发现当前服务的请求失败率达到预设的值,Hystrix将会拒绝随后该服务的所有请求,直接返回一个预设的结果。这就是所谓的“熔断”。当经过一段时间后,Hystrix会放行该服务的一部分请求,再次统计它的请求失败率。如果此时请求失败率符合预设值,则完全打开限流开关;如果请求失败率仍然很高,那么继续拒绝该服务的所有请求。这就是所谓的“限流”。而Hystrix向那些被拒绝的请求直接返回一个预设结果,被称为“降级”。
    Hystrix 介绍:
    https://segmentfault.com/a/1190000005988895

    在维持住服务后,我们应该尽快恢复缓存数据并提供服务。使用Redis时应该开启缓存持久化,重启后能够从磁盘加载数据从而恢复缓存功能。

    5 热点数据集中失效问题

    有缓存就必须要有过期机制和管理机制。一般而言。在设置缓存是,我们都会设置一个过期时间。而对于某些热点数据。一点缓存失效就会存在大量请求,导致数据库压力激增。

    而这种情况,我们对于过期机制而言,应该为这些热点数据过期时间添加随机值,使它们的过期时间错开,防止造成较多的热点数据过期,造成数据库查询峰值。第二个方面,对于热点数据过期造成的问题也可以通过3中加读写锁的方式进行处理,但这样会阻塞住其他线程造成系统吞吐率下降。所以对于热点数据的过期机制一定要谨慎设计。

    6 参考资料

    https://blog.csdn.net/u010425776/article/details/79555894
    https://mp.weixin.qq.com/s/7gbJCeBKklTlAxU_vsrIxg

    展开全文
  • 一.什么是缓存缓存,简单说就是为了节约对原始资源重复获取的...缓存的命中率: 把一批数据获取中,通过缓存获得数据的次数,除以总的次数,得到的结果,叫做缓存的命中率。 3. 缓存是为了解决“开销”的问题,...

    一.什么是缓存:

    缓存,简单说就是为了节约对原始资源重复获取的开销,而将结果数据副本存放起来以供获取的方式。

    二.什么时候使用缓存

    1.以 幂等和安全的方式对资源的获取操作

    2.缓存数据必须是重复获取的:

         缓存能生效的本质是空间换时间

    缓存的命中率: 把一批数据获取中,通过缓存获得数据的次数,除以总的次数,得到的结果,叫做缓存的命中率。

    3. 缓存是为了解决“开销”的问题,开销包括:时间的开销、CPU、网络、I/O等资源的开销。

    4. 缓存的存取其实不一定是“更快”的

    5.缓存使用最常用的动机:(1) 一个是 latency,延迟(2) 另一个使用动机,是 throughput,吞吐量

    三.缓存应用模式:

    1. Cache-Aside(最常用的缓存模式)

      (1)获取数据: 

            应用先去查看缓存是否有所需数据;

            如果有,应用直接将缓存数据返回给请求方;

            如果没有,应用执行原始逻辑,例如查询数据库得到结果数据;

            应用将结果数据写入缓存。

      (2)更新数据:

            关键点一:必须先更新数据库-再令缓存失效:

    (原因在于,如果先令缓存失效,那么在数据库更新成功前,如果有另外一个请求访问了缓存,发现缓存数据库已经失效,于是就会按照数据获取策略,从数据库中使用这个已经陈旧的数值去更新缓存中的数据,这就导致这个过期的数据会长期存在于缓存中,最终导致数据不一致的严重问题。)

            关键点二:数据库更新以后,需要令缓存失效,而不是更新缓存为数据库的最新值

    (为什么呢?你想一下,如果两个几乎同时发出的请求分别要更新数据库中的值为 A 和 B,如果结果是 B 的更新晚于 A,那么数据库中的最终值是 B。但是,如果在数据库更新后去更新缓存,而不是令缓存失效,那么缓存中的数据就有可能是 A,而不是 B。因为数据库虽然是“更新为 A”在“更新为 B”之前发生,但如果不做特殊的跨存储系统的事务控制,缓存的更新顺序就未必会遵从“A 先于 B”这个规则,这就会导致这个缓存中的数据会是一个长期错误的值 A。)

      2. Read-Through

       (1)获取数据:

                应用向缓存要求数据;

                如果缓存中有数据,返回给应用,应用再将数据返回;

                如果没有,缓存查询数据库,并将结果写入自己;

                缓存将数据返回给应用。

    这种情况下缓存系统彻底变成了它身后数据库的代理,二者成为了一个整体,应用的请求访问只能看到缓存的返回数据,而数据库系统对它是透明的。

    虽然read-through和cache-aside非常相似,但至少有两个关键区别:

    1. 在cache-aside中,应用程序负责从数据库中获取数据并填充缓存。在read-through中,此逻辑通常由库或独立缓存提供程序支持。

    2. 与cache-aside不同,read-through cache中的数据模型不能与数据库中的数据模型不同

    3. Write-Through

         主要用于处理数据更新的场景。

        (1)获取数据:

                应用要求缓存更新数据;

                如果缓存中有对应数据,先更新该数据;

                缓存再更新数据库中的数据;缓存告知应用更新完成。

        这里的一个关键点是,缓存系统需要自己内部保证并发场景下,缓存更新的顺序要和数据库更新的顺序一致。

      (数据更新的异常情形:如果缓存更新失败,直接返回失败,没有数据不一致的情况发生;如果缓存更新成功,数据库更新失败,这种情况下需要回滚缓存中的更新,或者干脆从缓存中删除该数据。)

        4. Write-Back

       数据写入缓存和数据库更新是异步完成的,即数据写入缓存后立即返回,数据库更新异步完成。

    (这种方式带来的最大好处是拥有最大的请求吞吐量,并且操作非常迅速,数据库的更新甚至可以批量进行,因而拥有杰出的更新效率以及稳定的速率,这个缓存就像是一个写入的缓冲,可以平滑访问尖峰。另外,对于存在数据库短时间无法访问的问题,它也能够很好地处理。但是它的弊端也很明显,异步更新一定会存在着不可避免的一致性问题,并且也存在着数据丢失的风险(数据写入缓存但还未入库时,如果宕机了,那么这些数据就丢失了)

    总结:缓存在实际项目中应用非常多,而最常用的缓存模式就是 Cache-Aside,所以深入理解 Cache-Aside是关键。

     

     

     

     

     

     

     

     

     

     

     

     

    展开全文
  • 后端系统缓存技术分析

    千次阅读 2020-08-07 20:54:24
    本文主要针对后端系统的缓存,也就是将程序或系统经常要使用的对象存在内存中,以便在使用时可以快速调用,避免加载数据或者创建重复的实例,以达到减少系统开销,提高系统效率的目的。 为什么需要缓存? 我觉得...

    缓存有很多种,如CPU 缓存、磁盘缓存、浏览器缓存等;本文主要针对后端系统的缓存,也就是将程序或系统经常要使用的对象存在内存中,以便在使用时可以快速调用,避免加载数据或者创建重复的实例,以达到减少系统开销,提高系统效率的目的。

    为什么需要缓存?

    我觉得操作系统里面讲高速缓存(CPU Cache)的一句话特别好:

    为什么要使用CPU Cache?

    因为CPU 和内存访问性能的差距非常大,为了弥补两者之间的性能差异,充分利用 CPU,现代 CPU 中引入了高速缓存(CPU Cache)。

    因为我们一般都会把数据存放在关系型数据库中,不管数据库的性能有多么好,受限于磁盘IO性能和远程网络的原因,一个简单的查询也要消耗毫秒级的时间,这样我们的 QPS 就会被数据库的性能所限制。

    而内存的性能高于磁盘,也没有远程网络的花费。如果恰好这些数据的数据量不大,不经常变动,而且访问频繁。那么就非常适合引入缓存。

    总结一下,缓存使用场景:数据不常变更且查询比较频繁是最好的场景,如果查询量不够大或者数据变动太频繁,缓存使用的意义不大,甚至可能适得其反。

    缓存的类型

    常用的缓存可以分为内部缓存和外部缓存。

    内部缓存是指存放在运行实例内部并使用实例内存的缓存,这种缓存可以使用代码直接访问。

    外部缓存是指存放在运行实例外部的缓存,通常是通过网络获取,反序列化后进行访问。

    一般来说对于不需要实例间同步的,都更加推荐内部缓存,因为内部缓存有访问方便,性能好的特点;需要实例间同步的数据可以使用外部缓存。

    内部缓存

    内部缓存有容量的限制,毕竟还是在 JVM 里的,存放的数据总量不能超出内存容量。

    本地缓存的优点:

    • 直接使用内存,速度快,通常存取的性能可以达到每秒千万级
    • 可以直接使用 Java 对象存取

    本地缓存的缺点:

    • 数据保存在当前实例中,无法共享
    • 重启应用会丢失

    最简单内部缓存-Map

    对于字典型的数据,在项目启动的时候加载到 Map 中,程序就可以使用了,也很容易更新。

        Map<String, String> configs = Maps.newHashMap();
    
        public void reloadConfigs(){
            configs =  loadConfigFromDB();
        }
    
        public String getConfigs(String key){
            return configs.getOrDefault(key, "null");
        }

    但是JDK自带的Map做缓存存在一定的问题:

    1、hashmap的容量理论上是无上限的(内存无限的情况下),如果缓存太大会严重的占用内存,甚至导致OutOfMemory异常,因此应该限制缓存的容量,容量达到上限后采取合适的淘汰策略。

    有准备面试的童鞋应该知道,这就是面试中常见的实现LRU缓存。

    实现方式一:可以通过继承LinkedHashMap重写removeEldestEntry()方法实现。

    class LRUCache extends LinkedHashMap<Integer, Integer>{
        private int capacity;
        
        public LRUCache(int capacity) {
            super(capacity, 0.75F, true);
            this.capacity = capacity;
        }
    
        public int get(int key) {
            return super.getOrDefault(key, -1);
        }
    
        public void put(int key, int value) {
            super.put(key, value);
        }
    
        @Override
        protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
            return size() > capacity; 
        }
    }

    实现方式二,通过HashMap加双向链表的方式实现。

    public class LRUCache {
        class DLinkedNode {
            int key;
            int value;
            DLinkedNode prev;
            DLinkedNode next;
            public DLinkedNode() {}
            public DLinkedNode(int key, int value) {key = key; value = value;}
        }
    
        private Map<Integer, DLinkedNode> cache = new HashMap<Integer, DLinkedNode>();
        private int size;
        private int capacity;
        private DLinkedNode head, tail;
    
        public LRUCache(int capacity) {
            this.size = 0;
            this.capacity = capacity;
            // 使用伪头部和伪尾部节点
            head = new DLinkedNode();
            tail = new DLinkedNode();
            head.next = tail;
            tail.prev = head;
        }
    
        public int get(int key) {
            DLinkedNode node = cache.get(key);
            if (node == null) {
                return -1;
            }
            // 如果 key 存在,先通过哈希表定位,再移到头部
            moveToHead(node);
            return node.value;
        }
    
        public void put(int key, int value) {
            DLinkedNode node = cache.get(key);
            if (node == null) {
                // 如果 key 不存在,创建一个新的节点
                DLinkedNode newNode = new DLinkedNode(key, value);
                // 添加进哈希表
                cache.put(key, newNode);
                // 添加至双向链表的头部
                addToHead(newNode);
                ++size;
                if (size > capacity) {
                    // 如果超出容量,删除双向链表的尾部节点
                    DLinkedNode tail = removeTail();
                    // 删除哈希表中对应的项
                    cache.remove(tail.key);
                    --size;
                }
            }
            else {
                // 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部
                node.value = value;
                moveToHead(node);
            }
        }
    
        private void addToHead(DLinkedNode node) {
            node.prev = head;
            node.next = head.next;
            head.next.prev = node;
            head.next = node;
        }
    
        private void removeNode(DLinkedNode node) {
            node.prev.next = node.next;
            node.next.prev = node.prev;
        }
    
        private void moveToHead(DLinkedNode node) {
            removeNode(node);
            addToHead(node);
        }
    
        private DLinkedNode removeTail() {
            DLinkedNode res = tail.prev;
            removeNode(res);
            return res;
        }
    }

    2、无法设置过期时间。这一点是非常致命的,我们在大多数场景都希望缓存可以在合理的时间后自动失效。

    3、并发安全。HashMap、LinkedHashMap都是非并发安全的集合类型。所以应该使用ConcurrentHashMap。

    使用ConcurrentHashMap、ReentrantLock 和ScheduledThreadPoolExecutor可以实现带过期时间线程安全的缓存设计。

    4、清除数据时的回调通知。

    由于代码篇幅过长,见附录1

    功能强大的内部缓存-Guava Cache

    如果需要缓存有强大的性能,或者对缓存有更多的控制,可以使用 Guava 里的 Cache 组件。

    它是 Guava 中的缓存工具包,是非常简单易用且功能强大的 JVM 内缓存。

    1、支持多种缓存过期策略:基于容量回收、定时回收、基于引用回收

    2、自动加载:

    3、显示清除缓存:个别、批量、全量清除

    4、移除监听器

    5、统计功能:缓存命中率,加载新值的平均时间,缓存项被回收的总数

        LoadingCache<String, String> configs = CacheBuilder.newBuilder()
            .maximumSize(1000) // 设置最大大小
            .expireAfterWrite(10, TimeUnit.MINUTES) // 设置过期时间, 10分钟
            .build(
                new CacheLoader<String, String>() {
                // 加载缓存内容
                    public String load(String key) throws Exception {
                        return getConfigFromDB(key);
                    }
                    public Map<String, String> loadAll() throws Exception {
                    return loadConfigFromDB();
                }
            });
            //CacheLoader.loadAll// 获取某个key的值try {
        return configs.get(key);} catch (ExecutionException e) {
            throw new OtherException(e.getCause());
        }
        // 显式的放入缓存
        configs.put(key, value)
        // 个别清除缓存
        configs.invalidate(key)
        // 批量清除缓存
        configs.invalidateAll(keys)
        // 清除所有缓存项
        configs.invalidateAll()

    Guava Cache的使用

    1、创建(加载)cache

    两种方法 CacheLoader和Callable。

    LoadingCache userCache = CacheBuilder.newBuilder().maximumSize(100).
                    expireAfterAccess(30, TimeUnit.MINUTES)
                    .build(new CacheLoader() {
                        @Override
                        public Object load(Object name) throws Exception {
                             return getIdFromDBByName(name);
                        }
                    });
    Cache<String, String> cache = CacheBuilder.newBuilder().maximumSize(100).
        expireAfterAccess(30, TimeUnit.MINUTES)
        .build();
    String resultVal = cache.get("hello", new Callable<String>() {
        @Override
        public String call() throws Exception {
            return "hello" + "heitao";
        }
    });

    以上两种,都实现了一种逻辑:先取缓存——娶不到再执行load 或者call方法来娶。

    简单比较,CacheLoader 是按key统一加载,所有娶不到的统一执行一种load逻辑;而callable方法允许在get的时候根据指定key值执行对应的call方法。

    在使用缓存前,拍拍自己的四两胸肌,问自己一个问题:有没有【默认方法】来加载或计算与键关联的值?如果有的话,你应当使用CacheLoader。如果没有,或者想要覆盖默认的加载运算。你应该在调用get时传入一个Callable实例。

    2、添加、插入key

    get : 要么返回已经缓存的值,要么使用CacheLoader向缓存原子地加载新值;

    getUnchecked:CacheLoader 会抛异常,定义的CacheLoader没有声明任何检查型异常,则可以 getUnchecked 查找缓存;反之不能;

    getAll :方法用来执行批量查询;

    put : 向缓存显式插入值,Cache.asMap()也能修改值,但不具原子性;

    getIfPresent :该方法只是简单的把Guava Cache当作Map的替代品,不执行load方法;

    3、清除key

    guava cache 自带 清除机制,但仍旧可以手动清除:

    个别清除:Cache.invalidate(key)

    批量清除:Cache.invalidateAll(keys)

    清除所有缓存项:Cache.invalidateAll()

    4 refresh和expire刷新机制

    expireAfterAccess(30, TimeUnit.MINUTES ) // 30min内没有被读或写就会被回收 ;

    expireAfterWrite(30, TimeUnit.MINUTES ) // 30min内没有没有更新就会被回收 :

    refreshAfterAccess(30, TimeUnit.MINUTES) //上一次更新操作30min后再刷新;

    注意:和redis的惰性回收机制一样

    30min内没有被读或写就会被回收” 不等于 “30min内会被回收” ,因为真正的过期/刷新操作是在key被读或写时发生的。

    只有发生“取值”操作,才会执行load,然而为了防止“缓存穿透”,在多线程的环境下,任何时刻只允许一个线程操作执行load操作 。

    但在执行load操作这个步骤,expire 与 refresh 的线程机制不同

    expire 在load 阶段——同步机制:当前线程load未完成,其他线程呈阻塞状态,待当前线程load完成,其他线程均需进行”获得锁--获得值--释放锁“的过程。这种方法会让性能有一定的损耗。

    refresh 在load阶段——异步机制 :当前线程load未完成,其他线程仍可以取原来的值,等当前线程load完成后,下次某线程再取值时,会判断系统时间间隔是否时超过设定refresh时间,来决定是否设定新值。所以,refresh机制的特点是,设定30分钟刷新,30min后并不一定就是立马就能保证取到新值

    expire与refresh 都能控制key得回收,究竟如何选择?

    答案是,两个一起来!

    只要refresh得时间小于expire时间,就能保证多线程在load取值时不阻塞,也能保证refresh时间到期后,取旧值向新值得平滑过渡,当然,仍旧不能解决取到旧值得问题。

    5 监听

    在guava cache中移除key可以设置相应得监听操作,以便key被移除时做一些额外操作。缓存项被移除时,RemovalListener会获取移除通知[RemovalNotification],其中包含移除原因[RemovalCause]、键和值。监听有同步监听和异步监听两种 :

    同步监听:

    默认情况下,监听器方法是被同步调用的(在移除缓存的那个线程中执行),执行清理key的操作与执行监听是单线程模式,当然监听器中抛出的任何异常都不会影响其正常使用,顶多把异常写到日记了。

        @Test
        public void testCacheRemovedNotification(){
            CacheLoader<String, String> loader = CacheLoader.from(String::toUpperCase);
            RemovalListener<String, String> listener = notification -> {
                if (notification.wasEvicted()){
                    RemovalCause cause = notification.getCause();
                    System.out.println("remove case is : " + cause.toString());
                    System.out.println("key: "+notification.getKey()+" value:" + notification.getValue());
                }
            };
            LoadingCache<String, String> cache = CacheBuilder.newBuilder().maximumSize(4)
                    .removalListener(listener).build(loader);
            cache.getUnchecked("maomao");
            cache.getUnchecked("wangwang");
            cache.getUnchecked("guava");
            cache.getUnchecked("cache");
            cache.getUnchecked("hello");
        }

    异步监听:

    假如在同步监听模式下,监听方法中的逻辑特别复杂,执行效率慢,那此时如果有大量的key进行清理,会使整个缓存性能变得很低下,所以此时适合用异步监听,移除key与监听key的移除分属2个线程。

        @Test
        public void testCacheRemovedNotification(){
            CacheLoader<String, String> loader = CacheLoader.from(String::toUpperCase);
            RemovalListener<String, String> listener = notification -> {
                if (notification.wasEvicted()){
                    RemovalCause cause = notification.getCause();
                    System.out.println("remove case is : " + cause.toString());
                    System.out.println("key: "+notification.getKey()+" value:" + notification.getValue());
                }
            };
            LoadingCache<String, String> cache = CacheBuilder.newBuilder().maximumSize(4)
                    .removalListener(RemovalListeners.asynchronous(listener, Executors.newSingleThreadExecutor())).build(loader);
            cache.getUnchecked("maomao");
            cache.getUnchecked("wangwang");
            cache.getUnchecked("guava");
            cache.getUnchecked("cache");
            cache.getUnchecked("hello");
        }

    6 统计

    guava cache还有一些其他特性,比如weight 按权重回收资源,统计等,这里列出统计。CacheBuilder.recordStats()用来开启Guava Cache的统计功能。统计打开后Cache.stats()方法返回如下统计信息:

    • hitRate():缓存命中率;
    • hitMiss(): 缓存失误率;
    • loadcount() ; 加载次数;
    • averageLoadPenalty():加载新值的平均时间,单位为纳秒;
    • evictionCount():缓存项被回收的总数,不包括显式清除。

    Guava Cache 的替代者 Caffeine

     

    外部缓存

    最著名的外部缓存 - Redis / Memcached

    Redis / Memcached 都是使用内存作为存储,所以性能上要比数据库要好很多,再加上Redis 还支持很多种数据结构,使用起来也挺方便,所以作为很多人的首选。

    Redis 确实不错,不过即便是使用内存,也还是需要通过网络来访问,所以网络的性能决定了 Reids 的性能;

    在一些性能测试中,在万兆网卡的情况下,对于 Key 和 Value 都是长度为 20 Byte 的字符串的 get 和 set 是每秒10w左右的,如果 Key 或者 Value 的长度更大或者使用数据结构,这个会更慢一些;

    作为一般的系统来使用已经绰绰有余了,从目前来看,Redis 确实很适合来做系统中的缓存。

    如果考虑多实例或者分布式,可以考虑下面的方式:

    • Jedis 的 ShardedJedis( 调用端自己实现分片 )
    • twemproxy / codis( 第三方组件实现代理 )
    • Redis Cluster( 3.0 之后官方提供的集群方案 )

    缓存的异常情况

    缓存穿透

    指的是对某个一定不存在的数据进行请求,该请求将会穿透缓存到达数据库。

    解决方案:

    • 对这些不存在的数据缓存一个空数据;
    • 对这类请求进行过滤。

    缓存击穿

    指的是由于数据没有被加载到缓存中,或者缓存数据在同一时间大面积失效(过期),又或者缓存服务器宕机,导致大量的请求都到达数据库。

    在有缓存的系统中,系统非常依赖于缓存,缓存分担了很大一部分的数据请求。当发生缓存雪崩时,数据库无法处理这么大的请求,导致数据库崩溃。

    解决方案:

    • 为了防止缓存在同一时间大面积过期导致的缓存雪崩,可以通过观察用户行为,合理设置缓存过期时间来实现;
    • 为了防止缓存服务器宕机出现的缓存雪崩,可以使用分布式缓存,分布式缓存中每一个节点只缓存部分的数据,当某个节点宕机时可以保证其它节点的缓存仍然可用。
    • 也可以进行缓存预热,避免在系统刚启动不久由于还未将大量数据进行缓存而导致缓存雪崩。

    缓存一致性

    缓存一致性要求数据更新的同时缓存数据也能够实时更新。

    解决方案:

    • 在数据更新的同时立即去更新缓存;
    • 在读缓存之前先判断缓存是否是最新的,如果不是最新的先进行更新。

    要保证缓存一致性需要付出很大的代价,缓存数据最好是那些对一致性要求不高的数据,允许缓存数据存在一些脏数据。

    缓存“无底洞”现象

    指的是为了满足业务要求添加了大量缓存节点,但是性能不但没有好转反而下降了的现象。

    产生原因:缓存系统通常采用 hash 函数将 key 映射到对应的缓存节点,随着缓存节点数目的增加,键值分布到更多的节点上,导致客户端一次批量操作会涉及多次网络操作,这意味着批量操作的耗时会随着节点数目的增加而不断增大。此外,网络连接数变多,对节点的性能也有一定影响。

    解决方案:

    • 优化批量数据操作命令;
    • 减少网络通信次数;
    • 降低接入成本,使用长连接 / 连接池,NIO 等。

    附录

    附录1   使用ConcurrentHashMap实现带过期时间的缓存

     private static ScheduledExecutorService swapExpiredPool = new ScheduledThreadPoolExecutor(10);
      
        private ReentrantLock lock = new ReentrantLock();
      
        private ConcurrentHashMap<String, Node> cache = new ConcurrentHashMap<>(1024);
        /**
         * 让过期时间最小的数据排在队列前,在清除过期数据时
         * ,只需查看缓存最近的过期数据,而不用扫描全部缓存
         *
         * @see Node#compareTo(Node)
         * @see SwapExpiredNodeWork#run()
         */
        private PriorityQueue<Node> expireQueue = new PriorityQueue<>(1024);
      
        public LocalCache() {
      
            //使用默认的线程池,每5秒清除一次过期数据
            //线程池和调用频率 最好是交给调用者去设置。
            swapExpiredPool.scheduleWithFixedDelay(
                    new SwapExpiredNodeWork(), 5, 5, TimeUnit.SECONDS);
        }
      
        public Object set(String key, Object value, long ttl) {
      
            Assert.isTrue(StringUtils.hasLength(key), "key can't be empty");
            Assert.isTrue(ttl > 0, "ttl must greater than 0");
      
            long expireTime = System.currentTimeMillis() + ttl;
            Node newNode = new Node(key, value, expireTime);
            lock.lock();
            try {
                Node old = cache.put(key, newNode);
                expireQueue.add(newNode);
                //如果该key存在数据,还要从过期时间队列删除
                if (old != null) {
                    expireQueue.remove(old);
                    return old.value;
                }
                return null;
            } finally {
                lock.unlock();
            }
      
        }
      
        /**
         * 拿到的数据可能是已经过期的数据,可以再次判断一下
         * if(n.expireTime<System.currentTimeMillis()){
         * return null;
         * }
         * 也可以直接返回整个节点Node ,交给调用者去取舍
         * <p>
         * <p>
         * 无法判断不存在该key,还是该key存的是一个null值,如果需要区分这两种情况
         * 可以定义一个全局标识,标识key不存在
         * public static final NOT_EXIST = new Object();
         * 返回值时
         * return n==null?NOT_EXIST:n.value;
         */
        public Object get(String key) {
            Node n = cache.get(key);
            return n == null ? null : n.value;
        }
      
        /**
         * 删除KEY,并返回该key对应的数据
         */
        public Object remove(String key) {
            lock.lock();
            try {
                Node n = cache.remove(key);
                if (n == null) {
                    return null;
                } else {
                    expireQueue.remove(n);
                    return n.value;
                }
            } finally {
                lock.unlock();
            }
        }
      
        /**
         * 删除已经过期的数据
         */
        private class SwapExpiredNodeWork implements Runnable {
      
            @Override
            public void run() {
      
                long now = System.currentTimeMillis();
                while (true) {
                    lock.lock();
                    try {
                        Node node = expireQueue.peek();
                        //没有数据了,或者数据都是没有过期的了
                        if (node == null || node.expireTime > now) {
                            return;
                        }
                        cache.remove(node.key);
                        expireQueue.poll();
      
                    } finally {
                        lock.unlock();
                    }
                }
            }
        }
      
      
        private static class Node implements Comparable<Node> {
            private String key;
            private Object value;
            private long expireTime;
      
            public Node(String key, Object value, long expireTime) {
                this.value = value;
                this.key = key;
                this.expireTime = expireTime;
            }
      
      
            /**
             * @see SwapExpiredNodeWork
             */
            @Override
            public int compareTo(Node o) {
                long r = this.expireTime - o.expireTime;
                if (r > 0) {
                    return 1;
                }
                if (r < 0) {
                    return -1;
                }
                return 0;
            }
      
        }

     

    展开全文
  • Java 后端国际化设计方案设计需求 设计需求 国际化配置集中到数据库中进行管理,包含前端部分国际化 最好可动态添加国际化的语种 好用易用 高效

    前言

    代码就不放全了,还在公司上跑着呢,就放一点非核心代码,工具类封装之类的

    设计需求

    • 国际化配置集中到数据库中进行管理,包含前端部分国际化
    • 最好可动态添加国际化的语种
    • 好用易用
    • 高效

    设计思路

    • 利用自定义注解来启用国际化,拦截所有返回请求进行处理
    • 大数据量处理使用多线程并行处理
    • 国际化数据保存在 Redis 中视为热点数据
    • 使用手动刷新方式,保证无缝刷新缓存
    • 国际化部分数据以 Json 形式来保存,保证扩展性
    • 语种以配置的形式保存,必要可添加语种
    • 需要多语言切换的数据全部以占位符代替,通过自定义注解统一替换
    • 当前语言环境通过前端带在请求头里给后端,后端默认为英文

    数据库设计

    • type: 类型,非空
    • module: 模块,可为空
    • label: 标签,非空
    • langs: 国际化 Json String,非空
    • to_web: 是否返回前端,不返回的就只是后端使用,将数据切成两半

    其中 type.module.label 的组合为唯一标识
    在这里插入图片描述

    需要国际化翻译的数据保存形式
    由于后端部分为自动生成的,因此 label 使用 UUID()
    在这里插入图片描述

    后端返回数据部分,比如:异常提示这部分的翻译
    在这里插入图片描述

    功能设计

    用到的工具类

    JsonUtils.java

    自定义注解

    个人认为只需要一个启动开关即可,没必要做成那种一个个接口去加

    /**
     * 开启国际化注解
     */
    @Import(TranslationAspect.class)
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface EnableTranslation {
    }
    

    使用
    在这里插入图片描述

    切面开发

    TranslationAspect

    /**
     * 国际化实现
     * @Author: linjinp
     * @Date: 2021/1/18 15:47
     */
    @Slf4j
    @Aspect
    public class TranslationAspect {
    
        // 默认语种
        private final static String DEFAULT_LANGUAGE = "en";
    
        // 切点
        // 指定拦截的包路径
        @Pointcut("execution(* com.xxx..*.*(..))")
        private void pointcut() {}
    
        @Around("pointcut()")
        private Object around(ProceedingJoinPoint pjp) throws Throwable {
    		// ...
    		// TODO
    		// ...
    		return ...
    	}
    

    从请求头获取当前语言环境

    在这里插入图片描述

    获取当前返回值的类型

    在这里插入图片描述

    /**
     * 获取列表中数据类型
     * @param obj
     * @return
     */
    private static Class getArrayListClass(Object obj) {
        // 判断空,判断类型是否为列表,判断是否有数据(无数据无法获取类型)
        if (obj != null && ArrayList.class.equals(obj.getClass()) && ((List) obj).size() > 0) {
            return ((List) obj).get(0).getClass();
        }
        return null;
    }
    
    /**
     * 获取数据类型
     * @param obj
     * @return
     */
    private static Class getClass(Object obj) {
        // 判断空,判断类型是否为列表,判断是否有数据(无数据无法获取类型)
        if (obj != null && !ArrayList.class.equals(obj.getClass())) {
            return obj.getClass();
        }
        return null;
    }
    

    将返回值转为 Json String 后,统一获取其中的占位符

    使用 StringBuilder 保存,防止大量的对象被创建
    在这里插入图片描述

    /**
    * 获取字符串中所有的变量参数
     * @param str 字符串/对象Json
     * @return
     */
    private static List<String> findParams(String str) {
        List<String> params = new ArrayList<>();
        // 转化为二进制
        char[] chars = str.toCharArray();
        // 找到标志的索引
        int findIndex = -1;
        for (int i = 0; i < chars.length; i ++) {
            // 判断 ${ 组合
            // i <= chars.length - 3 防越界,${A,假如以此结尾,$ 在 length - 3 位置
            if (i <= chars.length - 3 && chars[i] == '$' && chars[i + 1] == '{') {
                // 获取首个变量的下标索引
                findIndex = i + 2;
            }
            // 判断 } 且,已经存在索引下标,防止前面单独出现 } 的情况
            if (chars[i] == '}' && findIndex != -1) {
                // 添加变量
                params.add(new String(Arrays.copyOfRange(chars, findIndex, i)));
                // 重置标识
                findIndex = -1;
            }
        }
        return params;
    }
    

    替换返回值中所有的占位符为对应语言

    /**
     * 数据处理
     * @param lang 语言环境
     * @param data 返回数据
     * @param languages 语言包
     * @param params 需要替换的参数列表
     * @return
     */
    private static StringBuilder dataProcess(String lang, StringBuilder data, List<MultiLanguage> languages, List<String> params) {
        // 循环数据
        for (MultiLanguage language : languages) {
            // 有配置语言,非空对象,为后端使用的标签
            if (StringUtils.isNotBlank(language.getLangs()) && !"{}".equals(language.getLangs())) {
                for (String param : params) {
                    // 如果标签组合匹配
                    if (language.equalsCombination(param)) {
                        // 假如当前环境非默认语种,判断当前语种是否已经配置,如果没配置或为空,使用默认语种数据
                        if (!DEFAULT_LANGUAGE.equals(lang) && JsonUtils.toMap(language.getLangs()).containsKey(lang) && StringUtils.isNotBlank((String) JsonUtils.toMap(language.getLangs()).get(lang))) {
                            data.replace(0, data.length(), replaceRegex(data.toString(), param, StrUtil.nullToEmpty((String) JsonUtils.toMap(language.getLangs()).get(lang))));
                        } else {
                            data.replace(0, data.length(), replaceRegex(data.toString(), param, StrUtil.nullToEmpty((String) JsonUtils.toMap(language.getLangs()).get(DEFAULT_LANGUAGE))));
                        }
                    }
                }
            }
        }
        return data;
    }
    
    /**
     * 正则内容替换
     * @param source 数据
     * @param key 国际化标签
     * @param value 国际化对应值
     * @return
     */
    private static String replaceRegex(String source, String key, String value) {
        String regex = "\\$\\{"+key+"\\}";
        return source.replaceAll(regex, value);
    }
    

    最后要保证返回值的类型正确

    也是为了保证旧代码的兼容,比如你后来才加的国际化,这也是之前获取数据类型的原因
    在这里插入图片描述

    数据缓存

    构建线程池

    <!-- hutool -->
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.3.10</version>
    </dependency>
    
    /**
     * 线程池配置
     *
     * @Author: linjinp
     * @Date: 2020/9/29 10:54
     */
    @Slf4j
    @Component
    public class ExecutorConfig {
    
        public static ExecutorService executor;
    
        // 初始线程数量
        private final static int DEFAULT_NUM = 5;
    
        // 最大线程数
        private final static int MAX_NUM = 10;
    
        // 最大等待线程数
        private final static int MAX_WAITING = 100;
    
        @Bean
        public ExecutorService createExecutor() {
            this.executor = ExecutorBuilder.create()
                    // 默认初始化 5 个线程
                    .setCorePoolSize(DEFAULT_NUM)
                    // 最大线程数 10
                    .setMaxPoolSize(MAX_NUM)
                    // 最大等待线程数 100
                    .setWorkQueue(new LinkedBlockingQueue<>(MAX_WAITING))
                    .build();
            log.info("\n初始化线程池\n默认初始线程数:{}\n最大线程数:{}\n最大等待线程数:{}", DEFAULT_NUM, MAX_NUM, MAX_WAITING);
            return this.executor;
        }
    }
    

    数据缓存到 Redis

    刷新时将数据保存到 Redis 中

    这里使用 多线程 + 闭锁 的方式同步进行两方的处理,闭锁保证两个线程都完成后才继续执行,返回前端成功

    /**
     * 刷新国际化配置缓存
     * @return
     */
    @ApiOperation("刷新国际化配置缓存")
    @GetMapping(value = "/refresh")
    public ErrorMsg<Map<String, Object>> refresh() throws InterruptedException {
        // 利用闭锁保证两个线程都执行完毕后返回
        final CountDownLatch countDownLatch = new CountDownLatch(2);
    
        // 前端国际化数据,多线程处理
        ExecutorConfig.executor.execute(new Runnable(() -> {
            try {
            	// 构建前端所需的数据格式
                Map<String, Object> languageMap = buildLangToWeb();
                // 保存前端国际化部分数据 Map
                redisTemplate.opsForValue().set(RedisKeyConfig.LANGUAGE_ZONE, languageMap);
            } finally {
                countDownLatch.countDown();
            }
        }));
    
        // 后端国际化数据,多线程处理
        ExecutorConfig.executor.execute(new Runnable(() -> {
            try {
            	// 获取后端所需的数据列表
                List<MultiLanguage> languageList = buildLangToJava();
                // 保存后端国际化部分数据 List
                redisTemplate.opsForValue().set(RedisKeyConfig.LANGUAGE_JAVA, languageList);
            } finally {
                countDownLatch.countDown();
            }
        }));
        // 闭锁阻塞
        countDownLatch.await();
        return ErrorMsg.SUCCESS;
    }
    

    项目启动初始化国际化数据

    // 开启语言翻译
    @EnableTranslation
    public class AdminApplication {
        public static void main(String[] args) {
            SpringApplication.run(AdminApplication.class, args);
        }
    
        @Autowired
        private MultiLanguageController multiLanguageController;
    
        @Bean
        public CommandLineRunner runner() {
            return args -> {
                log.info("开始初始化国际化数据:{}", new Date());
                multiLanguageController.refresh();
                log.info("国际化初始化完成:{}", new Date());
            };
        }
    }
    

    效果展示

    中文返回
    在这里插入图片描述
    在这里插入图片描述
    英文返回
    在这里插入图片描述
    在这里插入图片描述

    展开全文
  • 后端系统的缓存使用浅谈

    千次阅读 2017-12-14 00:00:00
    1. 什么是缓存缓存有很多种,从 CPU 缓存、磁盘缓存到浏览器缓存等,本文所说的缓存,主要针对后端系统的缓存。也就是将程序或系统经常要使用的对象存在内存中,以便在使用时可以快速调用,也可以避免加载数据或者...
  • Vue SSR服务端渲染之数据缓存

    千次阅读 2018-07-20 18:02:00
    当我们在做vue的服务器端渲染时,可能会碰到各种各样的坑,内存泄露就是其中的一种。...1. 安装缓存依赖: lru-cache npm install lru-cache --dev 2. api 配置文件 config-server.js var LRU = req...
  • Nginx默认会对上传的文件先在本地进行缓存,再转发到应用服务器。请问怎么禁止掉这个缓存,让Nginx只转发而不缓存文件?本文给大家详细介绍Nginx上传文件全部缓存解决方案,有需要的朋友来参考下
  • App 数据缓存方案

    千次阅读 2020-07-02 17:24:12
    策略1:规则缓存(单纯App本地缓存) App根据接口数据特性,本地规定缓存以及更新数据策略(例如:登录后更新数据)。即特定场景下指定特定数据源(local,remote)。有些数据不会经常变更,例如用户权限,可以规定...
  • Github语言应用 早期应用程序的扩展,以显示基于Github API的用户语言配置文件。构建后端解决方案以存储返回的API数据,以减少API流量并允许更快的加载。 UI是使用TypeScript在React中构建的。
  • Web前后端缓存技术

    千次阅读 2018-08-26 15:49:55
    Web缓存技术 一、缓存概述 缓存原本是一个硬件的概念:缓存就是数据交换的缓冲区(称作Cache),当某一硬件要读取数据时,会首先从缓存中查找需要的数据,如果找到了则直接执行,找不到的话则从内存中找。由于缓存...
  • 缓存系统主要消耗的是服务器的内存,因此,在使用缓存时必须先对应用需要缓存的数据大小进行评估,包括缓存的数据结构、缓存大小、缓存数量、缓存的失效时间。在业务方面,作者建议将使用缓存的业务进行分离,核心...
  • 点击上方Java后端,选择设为星标优质文章,及时送达作者:叶不闻juejin.im/post/5d5c99b66fb9a06ae072060d背景缓存是软件开发中一个非常有用的概念...
  • App 后端架构设计方案 设计思想与最佳实践 标签: App后台架构设计用户验证方案后台架构的演进架构 2017-01-08 16:10 10957人阅读 评论(3) 收藏 举报 版权声明:本文为博主原创文章,未经博主允许...
  • 后续每次请求都会将此token放在请求头中传递到后端服务,后端服务会有一个过滤器对token进行拦截校验,校验token的合法性以及token是否过期,如果token过期则会让前端跳转到登录页面重新登录。 因为token中一般会...
  • 后端优化的六种方法: 1.硬件升级 硬件问题对性能的影响不容忽视。 举一个例子:一个DB集群经常有慢SQL报警,业务排查下来发现SQL都很简单,该做的索引优化也都做了。后来DBA同学帮忙定位到问题是硬件过旧导致...
  • 后端开发的同学,想必对缓存都不会陌生了,平时我们可能会使用Redis,MemCache这类缓存组件,或者是本地缓存,来实现一些后端的应用。 那么,严格来说,到底什么才是缓存呢,先来看看百度百科的定义。 缓存...
  • 分布式缓存平台方案

    万次阅读 2019-01-03 11:09:08
    2、多协议支持方案 3、高可用(HA)方案 3.1、基础设施 3.2、故障检测 3.3、故障切换 4、分布式方案 4.1、垂直扩展 4.2、水平扩展 5、容量管理 6、安全 6.1、配置安全 6.2、访问安全 6.3、内置安全机制 ...
  • 解决前端localStorage不是能实时与后端同步方案
  • 前端 api 请求缓存方案

    千次阅读 2019-04-27 10:45:56
    在开发 web 应用程序时,性能都是必不可少的话题。对于webpack打包的单页面应用程序而言,我们可以采用很多方式来对性能进行优化,比方说 tree-shaking...而事实上,缓存一定是提升web应用程序有效方法之一,尤其是...
  • 前言:前端缓存目的是减少性能损耗,提升web性能,...简单的数据缓存,第一次请求的时候获取数据,之后便使用数据,不再请求后端api,代码如下: // 方案一:数据缓存 const dataCache = new Map(); async funct
  • 缓存穿透,缓存击穿,缓存雪崩解决方案分析

    万次阅读 多人点赞 2017-01-06 11:12:50
    前言 设计一个缓存系统,不得不要考虑的问题就是:缓存穿透、缓存击穿与失效时的雪崩效应。 缓存穿透 缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层...解决方案
  • 缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。...
  • nginx缓存cache的5种方案  1、传统缓存之一(404)  这个办法是把nginx的404错误定向到后端,然后用proxy_store把后端返回的页面保存。  配置:  location / {  root /home/html/;#主目录  expires 1d;#网页...
  • 【宏观】缓存的应用场景

    千次阅读 热门讨论 2017-01-22 20:46:20
    本文主要跟大家来探讨下缓存的应用场景有哪些?缓存给我们带来了哪些便利?同时又给我们带来了哪些问题?还有,简单介绍一些常用的缓存工具。JUST DO IT! Go! 缓存的应用场景 CPU缓存 是位于CPU与内存之间的...
  • 通用数据缓存设计方案

    千次阅读 2016-11-24 22:44:35
    随着redis等内存数据库的流行,大多数应用及系统均开始采用redis集群作为系统的缓存方案,用来减少数据库的压力,增加应用并发及响应速度。前端页面的缓存暂不做探讨,主要是针对后端服务的数据缓存。 数据缓存...
  • 10年后端开发程序员详解数据库缓存方案到底有多少名堂。丨Linux服务器开发丨后端开发丨中间件丨web服务器丨数据库丨C/C++丨web服务器丨分布式 C/C++Linux服务器开发精彩内容包括:C/C++,Linux,Nginx,ZeroMQ,...
  • 后端服务HTML转图片实现方案

    千次阅读 2021-10-24 16:50:40
    后端(开发语言JAVA、Python等)实现HTML转图片有三大思路,一、通过第三方JAR包实现,二、借助工具的CLI实现。 以下面html为例: 图形是Echarts实现,页面中有很多CSS3语法,用的html2java、cssbox、...
  • 前端页面向后端发起请求时,出现跨域错误,mark一下,以防再次遇到这类问题 通过XMLHttpRequest发起Post请求: var data = { "enterName":enterName, "linkName" :linkName, "...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 86,785
精华内容 34,714
关键字:

后端缓存方案