缓存
为什么需要缓存?
-
高性能:mysql数据库查询耗时可以达到几十到几百毫秒,短时间内频繁的读取不变的数据是对性能极大的浪费,而缓存可以优化到几毫秒。(缓存有更高的性能)
-
高并发:mysql数据库单机支撑2000QPS可能就要报警了,所以高峰期每秒上万的请求量,会导致单机mysql数据库崩溃。而缓存单机可以支撑每秒几万到十几万的并发量,单机承载并发量是mysql单机的几十倍。(缓存支持更高的并发)
缓存有什么问题?
-
缓存穿透:不存在的或恶意的请求,量大,绕过缓存,打到数据库上。(恶意或无意导致)
-
缓存穿透解法:查询为空也进行缓存。时间可以短一些或者insert之后再清理。或者通过Bitmap存储所有可能存在的key,进而过滤掉一定不存在的key。(空,也缓存作为屏障/Bitmap布隆过滤器过滤作为屏障)
-
缓存击穿:热点数据过期,导致大量并发请求直接打到数据库上。(过期导致)
-
缓存击穿解法:缓存失效后,通过互斥分布式锁,使得只有一个线程去数据库加载数据。或者“手动”过期。(缓存失效后控制数据库访问并发度/不设置过期)
-
缓存雪崩:缓存全盘宕机,请求全部打到mysql数据库上。(宕机导致)
-
缓存雪崩解法:缓存失效后通过锁或者队列来控制读数据库写缓存的线程数量。或者让缓存失效时间更均衡。或者二级缓存,让拷贝缓存的时间多于原始缓存的时间。(缓存失效后控制数据库访问并发度/分布式缓存/缓存过期时间加上随机偏差)
-
缓存热key:比如店铺活动,导致热点Key所在的某台特定的Redis服务器压力过大。
-
缓存热key解法:热Key后面加随机数,分散到不同的Redis服务器上。或者热点数据推送到JVM内存,内存有先在内存取。(备份冗余分流/多级缓存)
-
缓存一致性(缓存与Mysql数据一致性问题):先更新数据库,再更新缓存,metaQ解耦同步兜底。或者利用mysql binlog同步缓存。
-
备注:缓存一致性设计方案,要具体情境具体分析,避免过度设计。
方案一:
- 如果数据库写入成功,则更新缓存。如果数据库写入失败,则整体更新过程失败。
- 如果更新缓存失败,则将更新数据操作发送到MetaQ,应用监听MetaQ通道,再更新Redis。
- 如果MetaQ集群重启,会导致丢失更新。
- MetaQ消息顺序的考虑,防止乱序更新缓存导致赃数据。
- 如果Redis没有设置过期时间,则需要手动更新失效数据。
- 可以设置时间戳作为Redis的过期时间,读缓存时,判别缓存是否在有效期内,如果在则直接读缓存,不在则读取数据库并更新缓存。
- 更新缓存操作如果变为删除缓存,可以使得:更新数据库100次,只删除缓存1次,然后下一次查询DB并回写缓存。
方案二:
- canal中间件读取binlog日志。
- canal中间件控制发送MQ消息频率。
- 应用监听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” 服务器被注释后,映射效果变动如下:
- 不带虚拟节点的情况,当一台服务器负载过高宕机,会导致流量顺时针向下一台服务器传导,进而形成多米诺骨牌效应,引发雪崩。带有虚拟节点的情况也是类似,但情况有所不同,要看虚拟服务器节点的分布而定。如果虚拟节点的分布是散乱的,当有服务器宕机时,流量可能会分散到不同的服务器上,相当于减轻了多米诺骨牌的效应。如果虚拟节点与真实服务器节点是对称的,则发生宕机后多米诺骨牌的效应没有减轻,但是在负载均衡方面倾向于更加合理。
- “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原理与实现)