天天JAVA:缓存

本文探讨了Java缓存的重要性和问题,包括缓存穿透、击穿和雪崩解决方案。重点介绍了分布式缓存的一致性Hash算法,解释了如何在服务器数量变化时保持缓存均衡,以及如何实现一致性Hash算法,最后提供了相关资源链接。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

缓存

在这里插入图片描述

为什么需要缓存?

  • 高性能:mysql数据库查询耗时可以达到几十到几百毫秒,短时间内频繁的读取不变的数据是对性能极大的浪费,而缓存可以优化到几毫秒。(缓存有更高的性能

  • 高并发:mysql数据库单机支撑2000QPS可能就要报警了,所以高峰期每秒上万的请求量,会导致单机mysql数据库崩溃。而缓存单机可以支撑每秒几万到十几万的并发量,单机承载并发量是mysql单机的几十倍。(缓存支持更高的并发

缓存有什么问题?

  • 缓存穿透:不存在的或恶意的请求,量大,绕过缓存,打到数据库上。(恶意或无意导致

  • 缓存穿透解法:查询为空也进行缓存。时间可以短一些或者insert之后再清理。或者通过Bitmap存储所有可能存在的key,进而过滤掉一定不存在的key。(空,也缓存作为屏障/Bitmap布隆过滤器过滤作为屏障

  • 缓存击穿:热点数据过期,导致大量并发请求直接打到数据库上。(过期导致

  • 缓存击穿解法:缓存失效后,通过互斥分布式锁,使得只有一个线程去数据库加载数据。或者“手动”过期。(缓存失效后控制数据库访问并发度/不设置过期

  • 缓存雪崩:缓存全盘宕机,请求全部打到mysql数据库上。(宕机导致

  • 缓存雪崩解法:缓存失效后通过锁或者队列来控制读数据库写缓存的线程数量。或者让缓存失效时间更均衡。或者二级缓存,让拷贝缓存的时间多于原始缓存的时间。(缓存失效后控制数据库访问并发度/分布式缓存/缓存过期时间加上随机偏差

  • 缓存热key:比如店铺活动,导致热点Key所在的某台特定的Redis服务器压力过大。

  • 缓存热key解法:热Key后面加随机数,分散到不同的Redis服务器上。或者热点数据推送到JVM内存,内存有先在内存取。(备份冗余分流/多级缓存

  • 缓存一致性(缓存与Mysql数据一致性问题):先更新数据库,再更新缓存,metaQ解耦同步兜底。或者利用mysql binlog同步缓存。

  • 备注:缓存一致性设计方案,要具体情境具体分析,避免过度设计。

方案一:

在这里插入图片描述

  1. 如果数据库写入成功,则更新缓存。如果数据库写入失败,则整体更新过程失败。
  2. 如果更新缓存失败,则将更新数据操作发送到MetaQ,应用监听MetaQ通道,再更新Redis。
  3. 如果MetaQ集群重启,会导致丢失更新。
  4. MetaQ消息顺序的考虑,防止乱序更新缓存导致赃数据。
  5. 如果Redis没有设置过期时间,则需要手动更新失效数据。
  6. 可以设置时间戳作为Redis的过期时间,读缓存时,判别缓存是否在有效期内,如果在则直接读缓存,不在则读取数据库并更新缓存。
  7. 更新缓存操作如果变为删除缓存,可以使得:更新数据库100次,只删除缓存1次,然后下一次查询DB并回写缓存。
方案二:

在这里插入图片描述

  1. canal中间件读取binlog日志。
  2. canal中间件控制发送MQ消息频率。
  3. 应用监听MQ消息,更新缓存。

缓存有哪些类型?

  • 前端浏览器缓存(图片、数据)。

  • 负载均衡缓存(nginx反向代理服务器缓存)。

  • CDN缓存:静态页面缓存。

  • 本地缓存:业务本身服务器,一致性比较高。

  • 分布式缓存:redis单独搭建集群主从缓存,可以减少数据库压力。tair,可以划分应用场景。

  • 数据库缓存:数据库本身缓存,存储查询语句对应结构集。

分布式缓存服务器如何负载均衡?(一致性Hash算法)

  • 比如:一亿张图片,100台服务器,如何将一亿张图片均衡的分布到100台服务器上?
  • 实际上,这是一个数学问题,如何将一亿个不同的key,映射到100个不同的value上?
  • 最简单的,hash(图片名称)% N,N取100。
  • 真实场景中,缓存服务器是可能宕机的->100变为99或者98等。
  • 真实场景中,缓存服务器是可能不够用的->100变为101或者102等。
  • 也就是说,N是一个动态的值。如果使用value(映射到第几台服务器) = hash(图片名称)% N,N的变化会导致value的变化,极端情况就是value全部发生变化(缓存雪崩)。
  • 如何保证N的变化,不会造成缓存的失效?答:一致性Hash算法。
  • Hash环,分布2的32次幂个节点(节点既可以是服务器,也可以是value),value顺时针归属到距离其最近的一台服务器节点(value与服务器关系),当服务器减少,则部分缓存失效,并自动映射到下一个服务器节点(类比环形延伸),增多类似(类比环形截断)。
  • 如何保证value与服务器映射关系是均衡的?虚拟节点。
  • 缓存服务器的宕机或者扩容,实际上是服务器伸缩性的呈现,现实中不可避免。哈希取余的方式,如果遇到服务器伸缩变化,难点在于如何重新更换缓存(映射逻辑错乱导致缓存失效),一般采用的方式是在流量低谷时间段,增减服务器后进行缓存预热,让缓存失效后,重新加载缓存。不过和一致性Hash算法相比,显得不够智能。

如果是你,如何实现一致性Hash算法?

  • 常规hash取模,映射到服务器。

  • 但是当服务器的数量增减变化的时候,会导致set过的值(服务器A),get的时候映射的服务器发生变化(服务器B),导致逻辑错乱,无法兼容以往的数值。

  • 先来看看不带虚拟节点的一致性Hash算法的实现:

    package algorithmTest;
    
    import java.util.*;
    
    /**
     * 假设:ip+port 唯一确定一个客户端。
     *
     * @author:xtm
     */
    public class ConsistencyHashAlgorithmWithoutVirturalNode {
        /**
         * 服务器
         */
        private static final String[] SERVERS = {
                "1.6.3.1:106",
                "1.1.3.1:126",
    //            "1.6.1.1:801",
                "1.1.9.1:186"
        };
    
        /**
         * 比较器
         */
        private static final Comparator MAP_VALUE_COMPARATOR = new MapValueComparator();
    
        /**
         * TreeMap:基于红黑树实现的排序Map,增删改查和统计相关的操作的时间复杂度都为 O(logn)
         * k,v模型,k对应服务器,v对应服务器编号。
         * k可能会冲突。
         */
        private static final SortedMap<Integer, String> SORTED_MAP = new TreeMap<Integer, String>(MAP_VALUE_COMPARATOR);
    
        /**
         * 取模base
         */
        private static final int HUNDRED = 100;
    
        static {
            for (String eachServer : SERVERS) {
                // 构建 服务器编号 与 服务器关系
                long num = hashServer(eachServer) % HUNDRED;
                SORTED_MAP.put((int)num, eachServer);
            }
            System.out.println("SERVERS is : " + SORTED_MAP.toString());
        }
    
        /**
         * 使用的是 FNV1_32_HASH
         * 可能出现hash碰撞,导致服务器与服务器编号映射有问题。
         */
        public static long hashServer(String key) {
            final int p = 16777619;
            int hash = (int) 2166136261L;
            for (int i = 0; i < key.length(); i++) {
                hash = (hash ^ key.charAt(i)) * p;
            }
            hash += hash << 13;
            hash ^= hash >> 7;
            hash += hash << 3;
            hash ^= hash >> 17;
            hash += hash << 5;
            // 如果算出来的值为负数则取其绝对值
            if (hash < 0) {
                hash = Math.abs(hash);
            }
            return hash;
        }
    
        public static void main(String[] args) {
            List<String> clients = new ArrayList<>();
            clients.add("6.1.1.1:191");
            clients.add("6.111.2.1:191");
            clients.add("6.1.98.1:121");
            clients.add("7.14.7.1:121");
            clients.add("8.16.7.1:141");
            clients.add("9.1.7.1:121");
            clients.add("9.2.71.1:191");
            clients.add("9.42.7.1:121");
            clients.add("9.6.73.1:171");
    
            for (String eachClient : clients) {
                System.out.println("Client is : " + eachClient);
                System.out.println("Server is : " + getServer(eachClient));
                System.out.println("-----------------------------");
            }
        }
    
        /**
         * 获取客户端对应的服务器
         *
         * @param eachClient
         * @return
         */
        public static String getServer(String eachClient) {
            try {
                Integer hashValue = Math.abs(eachClient.hashCode()) % HUNDRED;
                // 得到大于该hash值的所有map 对应 顺时针找到最近的服务器编号 进而找到服务器。
                System.out.println("hashValue is : " + hashValue);
                SortedMap<Integer, String> nextRecentServer = SORTED_MAP.tailMap(hashValue);
                String server;
                if(nextRecentServer.size() == 0){
                    server = SORTED_MAP.get(SORTED_MAP.firstKey());
                }else{
                    server = SORTED_MAP.get(nextRecentServer.firstKey());
                }
    //            System.out.println("server is : " + server);
                return server;
            } catch (Exception e) {
                System.out.println("server Exception.");
                return null;
            }
        }
    }
    
    /**
     * 比较器类
     */
    class MapValueComparator implements Comparator<Integer> {
        public MapValueComparator() {
        }
    
        @Override
        public int compare(Integer valueA, Integer valueB) {
            return valueA.compareTo(valueB);
        }
    }
    
    • “1.6.1.1:801” 服务器被注释后,映射效果变动如下:
      在这里插入图片描述
    • 不带虚拟节点的情况,当一台服务器负载过高宕机,会导致流量顺时针向下一台服务器传导,进而形成多米诺骨牌效应,引发雪崩。带有虚拟节点的情况也是类似,但情况有所不同,要看虚拟服务器节点的分布而定。如果虚拟节点的分布是散乱的,当有服务器宕机时,流量可能会分散到不同的服务器上,相当于减轻了多米诺骨牌的效应。如果虚拟节点与真实服务器节点是对称的,则发生宕机后多米诺骨牌的效应没有减轻,但是在负载均衡方面倾向于更加合理。
  • 带虚拟节点的一致性hash算法如下:

    package algorithmTest;
    
    import java.util.*;
    
    /**
     * 假设:ip+port 唯一确定一个客户端。
     * 带有虚拟节点的形式可以考虑使用virtualNode+i的方式去添加"后缀",反解析的时候只需要剔除"后缀"即可。但是,此种方法是随机copy真实服务器进行散列的。
     * 接下来采用对称copy的方式进行实现。(此种方式hash冲突的比率会增大一倍)
     *
     * @author:xtm
     */
    public class ConsistencyHashAlgorithmWithVirturalNode {
        /**
         * 服务器
         */
        private static final String[] SERVERS = {
                "1.6.3.1:106",
                "1.1.3.1:126",
                "1.6.1.1:801",
                "1.1.9.1:186"
        };
    
        /**
         * 比较器
         */
        private static final Comparator MAP_VALUE_COMPARATOR = new MapValueComparator();
    
        /**
         * TreeMap:基于红黑树实现的排序Map,增删改查和统计相关的操作的时间复杂度都为 O(logn)
         * k,v模型,k对应服务器,v对应服务器编号。
         * k可能会冲突。
         */
        private static final SortedMap<Integer, String> SORTED_MAP = new TreeMap<Integer, String>(MAP_VALUE_COMPARATOR);
    
        /**
         * 虚拟节点倍数
         */
        private static final int VIRTUAL_NODE_RATIO = 2;
    
        /**
         * 让真实的服务器映射编号范围为0到49,让虚拟的服务器映射编号范围为50到99。
         * 虚拟节点编号+其对应真实服务器编号=99
         */
        private static final int SERVER_NODE_NUM = 50;
    
        /**
         * 虚拟服务器节点后缀
         */
        private static final String VIRTUAL_NODE_POSTFIX = "postfix_node";
    
        /**
         * 虚拟服务器分割符号:冒号。
         */
        private static final String COLON = ":";
    
        static {
            for (String eachServer : SERVERS) {
                // 构建 服务器编号 与 服务器关系
                long num = hashServer(eachServer) % SERVER_NODE_NUM;
                SORTED_MAP.put((int) num, eachServer);
                // 构建虚拟服务器节点,虚拟服务器节点编号为99-num
                String virtualServerNode = eachServer + COLON + VIRTUAL_NODE_POSTFIX;
                SORTED_MAP.put(SERVER_NODE_NUM*VIRTUAL_NODE_RATIO - 1 - (int) num, virtualServerNode);
            }
            System.out.println("SERVERS is : " + SORTED_MAP.toString());
        }
    
        /**
         * 使用的是 FNV1_32_HASH
         * 可能出现hash碰撞,导致服务器与服务器编号映射有问题。
         */
        public static long hashServer(String key) {
            final int p = 16777619;
            int hash = (int) 2166136261L;
            for (int i = 0; i < key.length(); i++) {
                hash = (hash ^ key.charAt(i)) * p;
            }
            hash += hash << 13;
            hash ^= hash >> 7;
            hash += hash << 3;
            hash ^= hash >> 17;
            hash += hash << 5;
            // 如果算出来的值为负数则取其绝对值
            if (hash < 0) {
                hash = Math.abs(hash);
            }
            return hash;
        }
    
        public static void main(String[] args) {
            List<String> clients = new ArrayList<>();
            clients.add("6.1.1.1:191");
            clients.add("6.111.2.1:191");
            clients.add("6.1.98.1:121");
            clients.add("7.14.7.1:121");
            clients.add("8.16.7.1:141");
            clients.add("9.1.7.1:121");
            clients.add("9.2.71.1:191");
            clients.add("9.42.7.1:121");
            clients.add("9.6.73.1:171");
    
            for (String eachClient : clients) {
                System.out.println("Client is : " + eachClient);
                System.out.println("Server is : " + getServer(eachClient));
                System.out.println("-----------------------------");
            }
        }
    
        /**
         * 获取客户端对应的服务器
         *
         * @param eachClient
         * @return
         */
        public static String getServer(String eachClient) {
            try {
                Integer hashValue = Math.abs(eachClient.hashCode()) % (SERVER_NODE_NUM*VIRTUAL_NODE_RATIO);
                // 得到大于该hash值的所有map 对应 顺时针找到最近的服务器编号 进而找到服务器。
                System.out.println("hashValue is : " + hashValue);
                SortedMap<Integer, String> nextRecentServer = SORTED_MAP.tailMap(hashValue);
                String server;
                if (nextRecentServer.size() == 0) {
                    server = SORTED_MAP.get(SORTED_MAP.firstKey());
                } else {
                    server = SORTED_MAP.get(nextRecentServer.firstKey());
                }
    //            System.out.println("server is : " + server);
                return server;
            } catch (Exception e) {
                System.out.println("server Exception.");
                return null;
            }
        }
    }
    
    /**
     * 比较器类
     */
    class MapValueComparator implements Comparator<Integer> {
        public MapValueComparator() {
        }
    
        @Override
        public int compare(Integer valueA, Integer valueB) {
            return valueA.compareTo(valueB);
        }
    }
    
    /**
     * SERVERS is : {12=1.6.3.1:106, 16=1.6.1.1:801, 25=1.1.3.1:126, 42=1.1.9.1:186, 57=1.1.9.1:186:postfix_node, 74=1.1.3.1:126:postfix_node, 83=1.6.1.1:801:postfix_node, 87=1.6.3.1:106:postfix_node}
     * Client is : 6.1.1.1:191
     * hashValue is : 30
     * Server is : 1.1.9.1:186
     * -----------------------------
     * Client is : 6.111.2.1:191
     * hashValue is : 73
     * Server is : 1.1.3.1:126:postfix_node
     * -----------------------------
     * Client is : 6.1.98.1:121
     * hashValue is : 75
     * Server is : 1.6.1.1:801:postfix_node
     * -----------------------------
     * Client is : 7.14.7.1:121
     * hashValue is : 28
     * Server is : 1.1.9.1:186
     * -----------------------------
     * Client is : 8.16.7.1:141
     * hashValue is : 77
     * Server is : 1.6.1.1:801:postfix_node
     * -----------------------------
     * Client is : 9.1.7.1:121
     * hashValue is : 38
     * Server is : 1.1.9.1:186
     * -----------------------------
     * Client is : 9.2.71.1:191
     * hashValue is : 51
     * Server is : 1.1.9.1:186:postfix_node
     * -----------------------------
     * Client is : 9.42.7.1:121
     * hashValue is : 21
     * Server is : 1.1.3.1:126
     * -----------------------------
     * Client is : 9.6.73.1:171
     * hashValue is : 71
     * Server is : 1.1.3.1:126:postfix_node
     * -----------------------------
     */
    
    • 一致性哈希算法有多种具体的实现,包括 Chord 算法,KAD 算法等,都比较复杂。

Reference

  • https://www.zsythink.net/archives/1182(白话解析:一致性哈希算法 consistent hashing)
  • https://blog.csdn.net/deng624796905/article/details/108354089(Java一致性Hash算法的实现)
  • https://www.cnblogs.com/xrq730/p/5186728.html(对一致性Hash算法,Java代码实现的深入研究)
  • https://www.cnblogs.com/xrq730/p/4954152.html(大型网站架构学习笔记)
  • https://blog.csdn.net/deng624796905/article/details/108354089(Java一致性Hash算法的实现)
  • https://www.jianshu.com/p/dd746074f390(集合13-TreeMap使用场景简析)
  • https://blog.csdn.net/kefengwang/article/details/81628977(一致性哈希算法的原理与实现)
  • https://www.jianshu.com/p/528ce5cd7e8f(一致性Hash原理与实现)
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值