热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

了解一致性哈希算法

一、背景1.1使用场景一致性哈希算法一般用于解决分布式系统当中的热点问题,用于提升分布式系统的可扩展性与健壮性。1.2解决的问题一般用于分布式缓存系统当中的缓存击穿问题,简单哈希在

一、背景

1.1 使用场景

一致性哈希算法一般用于解决分布式系统当中的热点问题,用于提升分布式系统的可扩展性与健壮性。

1.2 解决的问题

一般用于分布式缓存系统当中的缓存击穿问题,简单哈希在服务节点数量产生变化的时候,其缓存命中率很低,从而导致大量接口直接请求数据库,造成缓存击穿的情况。

例如我们有 10 台缓存服务器,我们可以对请求关键字 (key) 进行哈希操作,将其值对 10 取模,得到的结果即服务器的索引。

服务器信息=hash(key) % 10

但是一旦增加/减少了一台服务器,其所有缓存数据的位置都会发生相应的改变。例如原本 Id 为 2 的用户信息,取模的结果为 hash(2)%10=4 ,当增加了一台服务器之后 hash(2)%11=? ,这里的缓存位置就被改变了,这个时候就需要一致性哈希来解决问题了。

一致性哈希可以解决的问题:

  1. 提高缓存命中率。
  2. 缓存数据应该均匀地分配在各个服务器中。

二、原理

注意:

由于粗心,将服务器 C 的 IP 地址也设置成了 192.168.100.102,其 IP 应该是 192.168.100.103。

  1. 创建一个环,这个哈希环有 2^32 个节点。

  1. 求出服务器的哈希值,并将其与哈希环的节点数量取模,根据得到的位置,把服务分配在一个哈希环当中。

  1. 根据存储数据的键,求出其哈希值,也将其映射在哈希环上。

  1. 根据键在哈希环的位置,顺时针查找第一个服务节点,将数据存储到对应的服务器当中。

  1. 如果增加了一台新的服务器 D,仅会影响 D 之前的区间数据。

  1. 当我们需要获得某个缓存数据在哪个服务器的时候,仅需要对这个数据的关键字取其 Hash 值,并对 2^32 取模,找到它下一个节点即是数据所对应的服务器节点。

2.1 新的问题

使用上述方案的确可以解决由于服务 节点增加/减少 造成的缓存击穿,这是节点分布位置均衡的情况。如果节点的在哈希环上 分布位置不均匀 ,就会造成下图这种极端情况。

上图中,黄色的小点代表缓存的数据,A、B、C 并没有均匀地分布在哈希环上。可以看到 C -> A 区间是最大的,这就会造成大部分数据都会存放到 A 节点当中,导致 服务器雪崩 的情况发生。

2.2 使用虚拟节点解决问题

由于服务器节点可能存在分布不均的问题,我们可以将一些虚拟节点放在哈希环上,这些虚拟节点其实是 映射 的真实服务器的地址。

从下图来看,黑底白字的服务器节点 A、B、C 只是真实节点的三个副本,虚拟节点 B -> C 区间的内容,实际上是存放到真实 C 节点的。我们就可以通过增加虚拟节点来解决服务器节点分布不均的问题。

三、实现

这里的 DEMO 我们使用的是 C# + .NET Core 进行实现,在这个 DEMO 当中演示了基于一致性哈希算法的缓存的

using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;

/*
 * 一致性哈希算法的 DEMO,主要用于演示一致性哈希算法的实现与实际应用。
 */

namespace ConsistentHashing.Startup
{
    public class NodeInfo
    {
        public string IPAddress { get; set; }
    }

    public class VirtualNodeInfo
    {
        public string NodeName { get; set; }

        public NodeInfo RealNodeInfo { get; set; }
    }

    public class ConsistentHashing
    {
        // 哈希环的大小
        private readonly int _ringCount;
        // 每个物理节点对应的虚拟节点数量
        private readonly int _virtualNodeCount;

        // 哈希环
        public readonly VirtualNodeInfo[] HashRing;

        public ConsistentHashing(int ringCount = int.MaxValue, int virtualNodeCount = 3)
        {
            _ringCount = ringCount;
            _virtualNodeCount = virtualNodeCount;
            HashRing = new VirtualNodeInfo[_ringCount];
        }

        public NodeInfo GetNode(string key)
        {
            var pos = Math.Abs(GetStandardHashCode(key) % _ringCount);
            // 顺时针查找最近的节点
            return GetFirstNodeInfo(pos).RealNodeInfo;
        }

        /// 
        /// 向哈希环上添加虚拟节点。
        /// 
        public void AddNode(NodeInfo info)
        {
            for (int index = 0; index <_virtualNodeCount; index++)
            {
                // 哈希环上没有物理节点,只有虚拟节点
                var virtualNodeName = $"{info.IPAddress}#{index}";
                var hashIndex = Math.Abs(GetStandardHashCode(virtualNodeName) % _ringCount);
                
                // 搜索空的哈希环位置
                var emptyIndex = GetEmptyNodeIndex(hashIndex);

                if (emptyIndex == -1)
                {
                    break;
                }
                
                HashRing[emptyIndex] = new VirtualNodeInfo{NodeName = virtualNodeName,RealNodeInfo = info};
            }
        }

        public void RemoveNode(NodeInfo info)
        {
            // 构建虚拟节点的 KEY
            var virtualNames = new List();
            for (int i = 0; i <_virtualNodeCount; i++)
            {
                virtualNames.Add($"{info.IPAddress}#{i}");
            }

            for (int i = 0; i 
        /// 计算指定 KEY 的 HASH 值
        /// 
        private int GetStandardHashCode(string key)
        {
            var sha1 = SHA256.Create();
            var hashValue = sha1.ComputeHash(Encoding.UTF8.GetBytes(key));
            return BitConverter.ToInt32(hashValue);
        }

        /// 
        /// 循环遍历哈希环,寻找空节点的索引,防止覆盖存在的节点信息。
        /// 
        private int GetEmptyNodeIndex(int startFindIndex)
        {
            while (true)
            {
                if (HashRing[startFindIndex] == null)
                {
                    return startFindIndex;
                }

                var nextIndex = GetNextNodeIndex(startFindIndex);
                // 说明已经遍历了整个哈希环,说明没有空的节点了。
                if (startFindIndex == nextIndex)
                {
                    return -1;
                }

                startFindIndex = nextIndex;
            }
        }

        /// 
        /// 根据指定的索引,获得哈希环的下一个索引。这里需要注意的是,因为哈希环是一个环形,当
        /// 当前位置为环的末尾时,应该从 0 开始查找。
        /// 
        private int GetNextNodeIndex(int preIndex)
        {
            if (preIndex == HashRing.Length - 1) return 0;

            return ++preIndex;
        }

        private VirtualNodeInfo GetFirstNodeInfo(int currentIndex)
        {
            VirtualNodeInfo nodeInfo = null;
            int nextIndex = currentIndex;
            while (nodeInfo == null)
            {
                nodeInfo = HashRing[GetNextNodeIndex(nextIndex)];
                nextIndex += 1;
            }
            
            return nodeInfo;
        }
    }

    internal class Program
    {
        private static void Main(string[] args)
        {
            var cOnsistentHashing= new ConsistentHashing(400,10);
            consistentHashing.AddNode(new NodeInfo {IPAddress = "192.168.1.101"});
            consistentHashing.AddNode(new NodeInfo {IPAddress = "192.168.1.102"});
            consistentHashing.AddNode(new NodeInfo {IPAddress = "192.168.1.103"});
            consistentHashing.AddNode(new NodeInfo {IPAddress = "192.168.1.104"});

            foreach (var node in consistentHashing.HashRing)
            {
                Console.WriteLine(node?.NodeName ?? "NULL");
            }

            // 存放 Id 为 15 的缓存服务器
            var nodeInfo = consistentHashing.GetNode("15");
            
            // 删除节点测试
            consistentHashing.RemoveNode(new NodeInfo {IPAddress = "192.168.1.103"});
        }
    }
}

推荐阅读
  • 秒建一个后台管理系统?用这5个开源免费的Java项目就够了
    秒建一个后台管理系统?用这5个开源免费的Java项目就够了 ... [详细]
  • 深入理解Redis中的字典实现
    本文详细介绍了Redis中字典的实现机制,包括其底层数据结构、哈希表与哈希节点的关系、元素添加方法及rehash操作的具体流程。 ... [详细]
  • 2020年9月15日,Oracle正式发布了最新的JDK 15版本。本次更新带来了许多新特性,包括隐藏类、EdDSA签名算法、模式匹配、记录类、封闭类和文本块等。 ... [详细]
  • 本文详细介绍了Java代码分层的基本概念和常见分层模式,特别是MVC模式。同时探讨了不同项目需求下的分层策略,帮助读者更好地理解和应用Java分层思想。 ... [详细]
  • 浅析python实现布隆过滤器及Redis中的缓存穿透原理_python
    本文带你了解了位图的实现,布隆过滤器的原理及Python中的使用,以及布隆过滤器如何应对Redis中的缓存穿透,相信你对布隆过滤 ... [详细]
  • 在C#编程中,数值结果的格式化展示是提高代码可读性和用户体验的重要手段。本文探讨了多种格式化方法和技巧,如使用格式说明符、自定义格式字符串等,以实现对数值结果的精确控制。通过实例演示,展示了如何灵活运用这些技术来满足不同的展示需求。 ... [详细]
  • B站服务器故障影响豆瓣评分?别担心,阿里巴巴架构师分享预防策略与技术方案
    13日晚上,在视频观看高峰时段,B站出现了服务器故障,引发网友在各大平台上的广泛吐槽。这一事件导致了连锁反应,大量用户纷纷涌入A站、豆瓣和晋江等平台,给这些网站带来了突如其来的流量压力。为了防止类似问题的发生,阿里巴巴架构师分享了一系列预防策略和技术方案,包括负载均衡、弹性伸缩和容灾备份等措施,以确保系统的稳定性和可靠性。 ... [详细]
  • 本文介绍了Memcached分布式集群中的取模算法和一致性哈希算法的原理及其对缓存命中率的影响。通过详细分析,探讨了如何优化这些算法以提高系统的稳定性和性能。 ... [详细]
  • Ihavetwomethodsofgeneratingmdistinctrandomnumbersintherange[0..n-1]我有两种方法在范围[0.n-1]中生 ... [详细]
  • 本文回顾了作者初次接触Unicode编码时的经历,并详细探讨了ASCII、ANSI、GB2312、UNICODE以及UTF-8和UTF-16编码的区别和应用场景。通过实例分析,帮助读者更好地理解和使用这些编码。 ... [详细]
  • 本文详细介绍了如何解决DNS服务器配置转发无法解析的问题,包括编辑主配置文件和重启域名服务的具体步骤。 ... [详细]
  • 网站访问全流程解析
    本文详细介绍了从用户在浏览器中输入一个域名(如www.yy.com)到页面完全展示的整个过程,包括DNS解析、TCP连接、请求响应等多个步骤。 ... [详细]
  • C# 实现可浮动工具栏功能
    本文介绍如何在 C# 中实现一个可浮动的工具栏,即工具栏可以从其初始位置拖出,并且可以重新拖回原位。通过创建一个新的窗口作为工具栏的容器,并利用 .NET Framework 提供的 ToolStrip 控件,可以轻松实现这一功能。 ... [详细]
  • 解决Bootstrap DataTable Ajax请求重复问题
    在最近的一个项目中,我们使用了JQuery DataTable进行数据展示,虽然使用起来非常方便,但在测试过程中发现了一个问题:当查询条件改变时,有时查询结果的数据不正确。通过FireBug调试发现,点击搜索按钮时,会发送两次Ajax请求,一次是原条件的请求,一次是新条件的请求。 ... [详细]
  • 该大学网站采用PHP和MySQL技术,在校内可免费访问某些外部收费资料数据库。为了方便学生校外访问,建议通过学校账号登录实现免费访问。具体方案可包括利用学校服务器作为代理,结合身份验证机制,确保合法用户在校外也能享受免费资源。 ... [详细]
author-avatar
mobiledu2502900917
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有