高性能分布式缓存的设计原理

article/2025/9/21 21:28:40

image

又是一个没有开工红包的公司!!!

问题分析

通过以上对话,各位是否能够猜到所有缓存穿透的原因呢?回答之前我们先来看一下缓存策略的具体代码

缓存服务器IP=hash(key)%服务器数量

这里还要多说一句,key的取值可以根据具体业务具体设计。比如,我想要做负载均衡,key可以为调用方的服务器IP;获取用户信息,key可以为用户ID;等等。

在服务器数量不变的情况下,以上设计没有问题。但是要知道,程序员的现实世界是悲惨的,唯一不变的就是业务一直在变。我本无奈,只能靠技术来改变这种状况。

假如我们现在服务器的数量为10,当我们请求key为6的时候,结果是4,现在我们增加一台服务器,服务器数量变为11,当再次请求key为6的服务器的时候,结果为5.不难发现,不光是key为6的请求,几乎大部分的请求结果都发生了变化,这就是我们要解决的问题, 这也是我们设计分布式缓存等类似场景时候主要需要注意的问题。

我们终极的设计目标是:在服务器数量变动的情况下

  1. 尽量提高缓存的命中率(转移的数据最少)
  2. 缓存数据尽量平均分配

解决方案

通过以上的分析我们明白了,造成大量缓存失效的根本原因是公式分母的变化,如果我们把分母保持不变,基本上可以减少大量数据被移动

分母不变方案

如果基于公式:缓存服务器IP=hash(key)%服务器数量 我们保持分母不变,基本上可以改善现有情况。我们选择缓存服务器的策略会变为:

缓存服务器IP=hash(key)%N (N为常数)
N的数值选择,可以根据具体业务选择一个满足情况的值。比如:我们可以肯定将来服务器数量不会超过100台,那N完全可以设定为100。那带来的问题呢?

目前的情况可以认为服务器编号是连续的,任何一个请求都会命中一个服务器,还是以上作为例子,我们服务器现在无论是10还是增加到11,key为6的请求总是能获取到一台服务器信息,但是现在我们的策略公式分母为100,如果服务器数量为11,key为20的请求结果为20,编号为20的服务器是不存在的。

以上就是简单哈希策略带来的问题(简单取余的哈希策略可以抽象为连续的数组元素,按照下标来访问的场景)

为了解决以上问题,业界早已有解决方案,那就是一致性哈希

一致性哈希算法在1997年由麻省理工学院的Karger等人在解决分布式Cache中提出的,设计目标是为了解决因特网中的热点(Hot spot)问题,初衷和CARP十分类似。一致性哈希修正了CARP使用的简单哈希算法带来的问题,使得DHT可以在P2P环境中真正得到应用。

一致性哈希具体的特点,请各位百度,这里不在详细介绍。至于解决问题的思路这里还要强调一下:

  1. 首先求出服务器(节点)的哈希值,并将其配置到环上,此环有2^32个节点。
  2. 采用同样的方法求出存储数据的键的哈希值,并映射到相同的圆上。
  3. 然后从数据映射到的位置开始顺时针查找,将数据保存到找到的第一个服务器上。如果超过2^32仍然找不到服务器,就会保存到第一台服务器上

当增加新的服务器的时候会发生什么情况呢?
image
通过上图我们可以发现发生变化的只有如黄色部分所示。删除服务器情况类似。

通过以上介绍,一致性哈希正是解决我们目前问题的一种方案。解决方案千万种,能解决问题即为好。

优化方案

到目前为止方案都看似完美,但现实是残酷的。以上方案虽好,但还存在瑕疵。假如我们有3台服务器,理想状态下服务器在哈希环上的分配如下图:
image
但是现实往往是这样:
image
这就是所谓的哈希环偏斜。分布不均匀在某些场景下会依次压垮服务器,实际生产环境一定要注意这个问题。为了解决这个问题,虚拟节点应运而生。
image
如上图,哈希环上不再是实际的服务器信息,而是服务器信息的映射信息,比如:ServerA-1,ServerA-2 都映射到服务器A,在环上是服务器A的一个复制品。这种解决方法是利用数量来达到均匀分布的目的,随之需要的内存可能会稍微大一点,算是空间换取设计的一种方案。

扩展阅读

  • 既然是哈希就会有哈希冲突,那多个服务器节点的哈希值相同该怎么办呢?我们可以采用散列表寻址的方案:从当前位置顺时针开始查找空位置,直到找到一个空位置。如果未找到,菜菜认为你的哈希环是不是该扩容了,或者你的分母参数是不是太小了呢。
  • 在实际的业务中,增加服务器或者减少服务器的操作要比查找服务器少的多,所以我们存储哈希环的数据结构的查找速度一定要快,具体说来本质是:自哈希环的某个值起,能快速查找第一个不为空的元素。
  • 如果你度娘过你就会发现,网上很多介绍虚拟哈希环节点个数为2^32(2的32次方),千篇一律。难道除了这个个数就不可以吗?在菜菜看来,这个数目完全必要这么大,只要符合我们的业务需求,满足业务数据即可。
  • 一致性哈希用到的哈希函数,不止要保证比较高的性能,还要保持哈希值的尽量平均分布,这也是一个工业级哈希函数的要求,一下代码实例的哈希函数其实不是最佳的,有兴趣的同学可以优化一下。
  • 有些语言自带的GetHashCode()方法应用于一致性哈希是有问题的,例如c#。程序重启之后同一个字符串的哈希值是变动的。所有需要一个更加稳定的字符串转int的哈希算法。

一致性哈希解决的本质问题是:相同的key通过相同的哈希函数,能正确路由到相同的目标。像我们平时用的数据库分表策略,分库策略,负载均衡,数据分片等都可以用一致性哈希来解决。

理论结合实际才是真谛(NetCore代码)

以下代码经过少许修改可直接应用于中小项目生产环境

 //真实节点的信息public abstract class NodeInfo{public abstract string NodeName { get; }}

测试程序所用节点信息:

    class Server : NodeInfo{public string IP { get; set; }public override string NodeName{get => IP;}}

以下为一致性哈希核心代码:

 /// <summary>/// 1.采用虚拟节点方式  2.节点总数可以自定义  3.每个物理节点的虚拟节点数可以自定义/// </summary>public class ConsistentHash{//哈希环的虚拟节点信息public class VirtualNode{public string VirtualNodeName { get; set; }public NodeInfo Node { get; set; }}//添加元素 删除元素时候的锁,来保证线程安全,或者采用读写锁也可以private readonly object objLock = new object();//虚拟环节点的总数量,默认为100int ringNodeCount;//每个物理节点对应的虚拟节点数量int virtualNodeNumber;//哈希环,这里用数组来存储public VirtualNode[] nodes = null;public ConsistentHash(int _ringNodeCount = 100, int _virtualNodeNumber = 3){if (_ringNodeCount <= 0 || _virtualNodeNumber <= 0){throw new Exception("_ringNodeCount和_virtualNodeNumber 必须大于0");}this.ringNodeCount = _ringNodeCount;this.virtualNodeNumber = _virtualNodeNumber;nodes = new VirtualNode[_ringNodeCount];}//根据一致性哈希key 获取node信息,查找操作请业务方自行处理超时问题,因为多线程环境下,环的node可能全被清除public NodeInfo GetNode(string key){var ringStartIndex = Math.Abs(GetKeyHashCode(key) % ringNodeCount);var vNode = FindNodeFromIndex(ringStartIndex);return vNode == null ? null : vNode.Node;}//虚拟环添加一个物理节点public void AddNode(NodeInfo newNode){var nodeName = newNode.NodeName;int virtualNodeIndex = 0;lock (objLock){//把物理节点转化为虚拟节点while (virtualNodeIndex < virtualNodeNumber){var vNodeName = $"{nodeName}#{virtualNodeIndex}";var findStartIndex = Math.Abs(GetKeyHashCode(vNodeName) % ringNodeCount);var emptyIndex = FindEmptyNodeFromIndex(findStartIndex);if (emptyIndex < 0){// 已经超出设置的最大节点数break;}nodes[emptyIndex] = new VirtualNode() { VirtualNodeName = vNodeName, Node = newNode };virtualNodeIndex++;}}}//删除一个虚拟节点public void RemoveNode(NodeInfo node){var nodeName = node.NodeName;int virtualNodeIndex = 0;List<string> lstRemoveNodeName = new List<string>();while (virtualNodeIndex < virtualNodeNumber){lstRemoveNodeName.Add($"{nodeName}#{virtualNodeIndex}");virtualNodeIndex++;}//从索引为0的位置循环一遍,把所有的虚拟节点都删除int startFindIndex = 0;lock (objLock){while (startFindIndex < nodes.Length){if (nodes[startFindIndex] != null && lstRemoveNodeName.Contains(nodes[startFindIndex].VirtualNodeName)){nodes[startFindIndex] = null;}startFindIndex++;}}}//哈希环获取哈希值的方法,因为系统自带的gethashcode,重启服务就变了protected virtual int GetKeyHashCode(string key){var sh = new SHA1Managed();byte[] data = sh.ComputeHash(Encoding.Unicode.GetBytes(key));return BitConverter.ToInt32(data, 0);}#region 私有方法//从虚拟环的某个位置查找第一个nodeprivate VirtualNode FindNodeFromIndex(int startIndex){if (nodes == null || nodes.Length <= 0){return null;}VirtualNode node = null;while (node == null){startIndex = GetNextIndex(startIndex);node = nodes[startIndex];}return node;}//从虚拟环的某个位置开始查找空位置private int FindEmptyNodeFromIndex(int startIndex){while (true){if (nodes[startIndex] == null){return startIndex;}var nextIndex = GetNextIndex(startIndex);//如果索引回到原地,说明找了一圈,虚拟环节点已经满了,不会添加if (nextIndex == startIndex){return -1;}startIndex = nextIndex;}}//获取一个位置的下一个位置索引private int GetNextIndex(int preIndex){int nextIndex = 0;//如果查找的位置到了环的末尾,则从0位置开始查找if (preIndex != nodes.Length - 1){nextIndex = preIndex + 1;}return nextIndex;}#endregion}

测试生成的节点

            ConsistentHash h = new ConsistentHash(200, 5);h.AddNode(new Server() { IP = "192.168.1.1" });h.AddNode(new Server() { IP = "192.168.1.2" });h.AddNode(new Server() { IP = "192.168.1.3" });h.AddNode(new Server() { IP = "192.168.1.4" });h.AddNode(new Server() { IP = "192.168.1.5" });for (int i = 0; i < h.nodes.Length; i++){if (h.nodes[i] != null){Console.WriteLine($"{i}===={h.nodes[i].VirtualNodeName}");}}

输出结果(还算比较均匀):

2====192.168.1.3#4
10====192.168.1.1#0
15====192.168.1.3#3
24====192.168.1.2#2
29====192.168.1.3#2
33====192.168.1.4#4
64====192.168.1.5#1
73====192.168.1.4#3
75====192.168.1.2#0
77====192.168.1.1#3
85====192.168.1.1#4
88====192.168.1.5#4
117====192.168.1.4#1
118====192.168.1.2#4
137====192.168.1.1#1
152====192.168.1.2#1
157====192.168.1.5#2
158====192.168.1.2#3
159====192.168.1.3#0
162====192.168.1.5#0
165====192.168.1.1#2
166====192.168.1.3#1
177====192.168.1.5#3
185====192.168.1.4#0
196====192.168.1.4#2

测试一下性能

            Stopwatch w = new Stopwatch();w.Start();for (int i = 0; i < 100000; i++){var aaa = h.GetNode("test1");}w.Stop();Console.WriteLine(w.ElapsedMilliseconds);

输出结果(调用10万次耗时657毫秒):

657

写在最后

以上代码实有优化空间

  1. 哈希函数
  2. 很多for循环的临时变量
    有兴趣优化的同学可以留言哦!!

添加关注,查看更精美版本,收获更多精彩

image


http://chatgpt.dhexx.cn/article/WEQBNwwb.shtml

相关文章

redis 分布式缓存 详解

1、Redis概述 1.1、NoSQL NoSQL(Not Only SQL)&#xff0c;意即不仅仅是SQL, 泛指非关系型的数据库。 1.2、Redis安装 首先需要从Redis官网上下载Redis的源码包&#xff0c;将下载的包上传到Linux&#xff0c;之后将gz文件进行解压。 # 解压gz文件 tar -zxvf redis-6.2.6…

本地缓存、分布式缓存以及多级缓存

像MySql等传统的关系型数据库已经不能适用于所有的业务场景&#xff0c;比如电商系统的秒杀场景&#xff0c;APP首页的访问流量高峰场景&#xff0c;很容易造成关系型数据库的瘫痪&#xff0c;随着缓存技术的出现很好的解决了这个问题。 一、缓存的概念&#xff08;什么是缓存…

分布式架构系列:缓存

一、缓存概述 缓存是分布式系统中的重要组件&#xff0c;主要解决高并发&#xff0c;大数据场景下&#xff0c;热点数据访问的性能问题。提供高性能的数据快速访问。 1.1缓存的原理 &#xff08;1&#xff09; 将数据写入/读取速度更快的存储&#xff08;设备&#xff09;&…

分布式缓存那些事儿

在前面的一些文章中&#xff0c;从实战的角度&#xff0c;讲解了有关memcached的应用、容灾、监控等等。但是缺乏对理论的讲解和原理性的剖析。本文将从理论的角度去介绍&#xff0c;让大家从宏观上对“分布式缓存、nosql”等技术有所了解&#xff0c;以便进一步学习和使用。在…

分布式缓存和本地缓存的区别

分布式缓存和本地缓存的区别 redis/memcached**分布式缓存**和map/guava**本地缓存**的区别什么是缓存一致性&#xff1f; redis/memcached分布式缓存和map/guava本地缓存的区别 缓存分为本地缓存和分布式缓存&#xff0c;使用map或guava的是本地缓存&#xff0c;轻量而快速&a…

分布式数据:缓存技术

分布式数据&#xff1a;缓存技术 前言什么是分布式缓存&#xff1f;Redis 分布缓存原理Memcached 分布式缓存原理对比分析知识扩展&#xff1a;除了分布式存储中的缓存&#xff0c;还有计算机体系结构和网络中的缓存&#xff0c; 它们又分别是什么呢&#xff1f;总结 前言 分布…

【分布式缓存】分布式缓存-缓存技术

目录 从数据的使用说起本地缓存远程缓存缓存策略缓存常见问题总结回顾与作业实践 1. 从数据的使用说起 我们把数据的使用频率和方式分个类 静态数据&#xff1a;一般不变&#xff0c;类似于字典表 准静态数据&#xff1a;变化频率很低&#xff0c;部门结构设置&#xff0c;…

分布式缓存详解

“ 今天无聊来撩一下分布式缓存&#xff0c;希望你们喜欢~ 编者荐语&#xff1a; 此篇文章对于分布式缓存讲解的非常透彻&#xff01; 目录 前言一. 常用的两种缓存技术的服务端特点1. Memcache服务端2. Redis服务端 二. 缓存结构化选型三. Redis构造大索引回源问题四. 一致性问…

分布式缓存的基本原理

随着互联网的发展&#xff0c;用户规模和数据规模越来越大&#xff0c;对系统的性能提出了更高的要求&#xff0c;缓存就是其中一个非常关键的组件&#xff0c;从简单的商品秒杀&#xff0c;到全民投入的双十一&#xff0c;我们都能见到它的身影。 分布式缓存首先也是缓存&…

分布式缓存

本文介绍关于缓存的常用设计模式。以及如何保证缓存的一致性进行分类讨论。 还会介绍关于缓存失效的常见问题&#xff0c;以及针对缓存失效的解决方法。 在高并发的环境下&#xff0c;比如春节抢票大战&#xff0c;一到放票的时间节点&#xff0c;分分钟大量用户以及黄牛的各种…

详解分布式系统的缓存设计

作者&#xff1a;vivo互联网服务器团队-Zhang Peng ​ 一、缓存简介 1.1 什么是缓存 缓存就是数据交换的缓冲区。缓存的本质是一个内存 Hash。缓存是一种利用空间换时间的设计&#xff0c;其目标就是更快、更近&#xff1a;极大的提高。 将数据写入/读取速度更快的存储&#xf…

今天带你了解-分布式缓存(一)

在网站架构的衍化历程中&#xff0c;当网站遇到性能瓶颈时&#xff0c;首先想到的解决方案就是使用缓存。 缓存指将数据存储在较高访问速度的存储介质中&#xff0c;以供系统处理。一方面缓存访问速度快&#xff0c;可以减少数据的访问时间&#xff0c;另一方面如果缓存的数据…

深入浅出分布式系统中的缓存架构

缓存&#xff0c;已经是一个老生常谈的技术了&#xff0c;在高并发读的情况下对于读服务来说可谓是抗流量的银弹。 高并发三大利器&#xff1a;缓存、限流、降级。 今天我们就来谈谈缓存。对于缓存&#xff0c;我的理解是让数据更接近于用户&#xff0c;目的是让用户的访问速…

分布式缓存灵魂十连,你能坚持几个?

点击上方蓝色“方志朋”&#xff0c;选择“设为星标” 回复“666”获取独家整理的学习资料&#xff01; 目录 前言 目前工作中用到的分布式缓存技术有redis和memcached两种&#xff0c;缓存的目的是为了在高并发系统中有效降低DB的压力&#xff0c;但是在使用的时候可能会因为缓…

Webform 常用控件

Webform 常用控件 一&#xff0c;简单控件 1&#xff0c;Lable——标签&#xff1a;在网页中呈现出来的时候会变成span标签 属性&#xff1a;Text——标签上的文字 BackColor&#xff0c;ForeColor——背景色&#xff0c;前景色 Font——字体 Bold-加粗 Italic-倾斜 Under…

Web窗体(WebForm)

一&#xff0e;简介 0. 页面的生命周期。 1. WebForm后台页面类继承于Page类&#xff0c;Page类实现了IHttpHandler接口。 2. 前台页面类继承于后台页面类。 3. 先调用PageLoad方法&#xff0c;再调用Render方法生成html代码。 二. 加密安全 互联网没有绝对的安全&#xff0c;登…

ASP.NET Web Form学习

ASP.NET Web Form学习 0.aspx与html 它如何工作&#xff1f; 从根本上讲&#xff0c;ASP.NET 页面与 HTML 完全相同。 HTML 页面的扩展名是 .htm 或 .html。假如浏览器从服务器请求某张 HTML 页面&#xff0c;服务器不进行任何修改&#xff0c;就会把该页面发往浏览器。 A…

forms.Form和forms.ModelForm

forms.ModelForm是forms.Form的升级版 forms.Form验证规则 2.1 forms.py 2.2 view.py 把我们写的UserResetForm导入到view.py 2.3 模板 forms.ModelForm验证规则 3.1 models.py 3.2 forms.py就用上面模型类里面的验证规则 3.3 view.py 3.4 模板看你实际的情况 forms.…

WebForm与MVC混用

在现有的WebForm项目中加入MVC&#xff0c;可以吗&#xff1f; 西蒙说&#xff0c;可以。 怎么加呢&#xff1f; 我的开发环境是&#xff1a;WIN7 IIS7.5 VS2012 一、WebForm项目添加引用&#xff1a; 我都是选了最高的版本。 二、将MVC项目的部分文件拷贝到WEBFORM项目 …

ASP.NET WebForm+Vue.js

QQ&#xff1a;285679784 欢迎加入博主CSDN资源QQ群799473954(附加信息&#xff1a;CSDN博客)一起学习 ! 参考原文&#xff1a;https://blog.csdn.net/myppbird/article/details/85598154 Vue.js教程&#xff1a;http://www.runoob.com/vue2/vue-tutorial.html Vue.js Ajax…