经过以上对话,各位是否可以猜到全部缓存穿透的缘由呢?回答以前咱们先来看一下缓存策略的具体代码node
缓存服务器IP=hash(key)%服务器数量程序员
这里还要多说一句,key的取值能够根据具体业务具体设计。好比,我想要作负载均衡,key能够为调用方的服务器IP;获取用户信息,key能够为用户ID;等等。算法
在服务器数量不变的状况下,以上设计没有问题。可是要知道,程序员的现实世界是悲惨的,惟一不变的就是业务一直在变。我本无奈,只能靠技术来改变这种情况。数据库
假如咱们如今服务器的数量为10,当咱们请求key为6的时候,结果是4,如今咱们增长一台服务器,服务器数量变为11,当再次请求key为6的服务器的时候,结果为5.不难发现,不光是key为6的请求,几乎大部分的请求结果都发生了变化,这就是咱们要解决的问题, 这也是咱们设计分布式缓存等相似场景时候主要须要注意的问题。c#
咱们终极的设计目标是:在服务器数量变更的状况下数组
经过以上的分析咱们明白了,形成大量缓存失效的根本缘由是公式分母的变化,若是咱们把分母保持不变,基本上能够减小大量数据被移动缓存
若是基于公式:缓存服务器IP=hash(key)%服务器数量 咱们保持分母不变,基本上能够改善现有状况。咱们选择缓存服务器的策略会变为:安全
缓存服务器IP=hash(key)%N (N为常数) N的数值选择,能够根据具体业务选择一个知足状况的值。好比:咱们能够确定未来服务器数量不会超过100台,那N彻底能够设定为100。那带来的问题呢?bash
目前的状况能够认为服务器编号是连续的,任何一个请求都会命中一个服务器,仍是以上做为例子,咱们服务器如今不管是10仍是增长到11,key为6的请求老是能获取到一台服务器信息,可是如今咱们的策略公式分母为100,若是服务器数量为11,key为20的请求结果为20,编号为20的服务器是不存在的。服务器
以上就是简单哈希策略带来的问题(简单取余的哈希策略能够抽象为连续的数组元素,按照下标来访问的场景)
为了解决以上问题,业界早已有解决方案,那就是一致性哈希。
一致性哈希算法在1997年由麻省理工学院的Karger等人在解决分布式Cache中提出的,设计目标是为了解决因特网中的热点(Hot spot)问题,初衷和CARP十分相似。一致性哈希修正了CARP使用的简单哈希算法带来的问题,使得DHT能够在P2P环境中真正获得应用。
一致性哈希具体的特色,请各位百度,这里不在详细介绍。至于解决问题的思路这里还要强调一下:
当增长新的服务器的时候会发生什么状况呢?
经过以上介绍,一致性哈希正是解决咱们目前问题的一种方案。解决方案千万种,能解决问题即为好。
到目前为止方案都看似完美,但现实是残酷的。以上方案虽好,但还存在瑕疵。假如咱们有3台服务器,理想状态下服务器在哈希环上的分配以下图:
一致性哈希解决的本质问题是:相同的key经过相同的哈希函数,能正确路由到相同的目标。像咱们平时用的数据库分表策略,分库策略,负载均衡,数据分片等均可以用一致性哈希来解决。
如下代码通过少量修改可直接应用于中小项目生产环境
//真实节点的信息
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();
//虚拟环节点的总数量,默认为100
int 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 私有方法
//从虚拟环的某个位置查找第一个node
private 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
复制代码
以上代码实有优化空间
添加关注,查看更精美版本,收获更多精彩