redis一致性hash实践

本文介绍了Redis中使用一致性哈希解决分布式缓存扩展性问题,详细解析了一致性哈希算法原理,包括增加和删除节点的影响。在实际项目中,面临Redis迁移时,通过一致性哈希和Sentinel配合,确保迁移后缓存key能路由到相同数据节点。文中还详述了迁移步骤,包括业务配置更新、避免数据丢失风险等。

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

之前在网上看过一致性hash原理,但是看过就忘记了,根本原因是没有理解透彻也没有实践,最近我负责了公司的会员业务,redis是开发自己搭建的,没有被DBA管理,最近要求迁移,这个工作就落在了我头上,为了确保迁移后缓存的key还能路由到原来的数据上,我把架构和源码研究了一番,发现竟然用了一致性hash,顿时眼前一亮,毕竟之前的系统没有这么用过,简单的就是主从+Sentinel,稍微复杂点用集群将数据分slot存储

一、一致性hash要解决什么问题

一致性hash算法主要应用于分布式缓存系统中,可以有效地解决分布式存储结构下普通余数Hash算法伸缩性差的问题,可以保证在动态增加和删除节点的情况下尽量有多的请求命中原来路由到的节点。

对于分布式缓存,不同的节点存放不同的数据,先看一下普通余数算法的方式,假设有节点数n,缓存的健key,那么健为key的数据该存储到的节点序号index = hash(key) mod n ,但是当增加和删除节点时n会发生变化,造成命中率下降。

二、一致性hash算法原理

一致性hash算法将全量的缓存空间当作一个环型数据结构,总共有2^32个缓存区,环的起点是0,终点是2^32 - 1,并且起点与终点相连,整数按照顺时针分布,范围是[0, 2^32-1]。如下图所示:

 那么如何将key和节点对应起来呢?

1)节点和key必须通过同一种hash算法映射到环上,即通过某种hash算法转换成一个32位的整数

      将节点hash到环上,index = hash(node),一般会用节点的ip或者name

      将key也hash到环上,index = hash(key)

2)找到顺时针方向离key哈希值最近的节点,即大于等于key哈希值的第一个节点作为存储节点

那么我们来看一下增加、删除节点后对缓存系统的影响。

1.增加节点

如下图,增加node4节点,受到影响的仅仅是node1到node4之间的key,不再归node2管,归node4管理

2.删除节点

比如这张图,我们删除了node3,那么key将会存储到node1,所以受到影响的仅仅是node2到node3之间的key,即前一个节点到该节点之间的key。

如果仅仅是这样增加删除节点的命中率问题解决了,但是如果节点在环上分布不均匀,就会导致各个节点的负载不均衡,比如下图,所有的key都将存储到node2。

为了优化因为节点太少分布不均匀而导致的负载不均衡问题,一致性hash算法引入了虚拟节点的概念。
所谓虚拟节点,就是基于原来的物理节点映射出多个节点再映射到环形空间上,虚拟节点保存的机器信息是相同的,只不过hash到不不同的位置上。

 三、jedis包的一致性hash实现源码分析

package redis.clients.util;

import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import redis.clients.util.Hashing;
import redis.clients.util.SafeEncoder;
import redis.clients.util.ShardInfo;

public class Sharded<R, S extends ShardInfo<R>> {
    public static final int DEFAULT_WEIGHT = 1;
    private TreeMap<Long, S> nodes;//hash环,保存所有节点,key就是hash后的值,value是节点的信息
    private final Hashing algo;//hash算法类
    private final Map<ShardInfo<R>, R> resources;
    private Pattern tagPattern;
    public static final Pattern DEFAULT_KEY_TAG_PATTERN = Pattern.compile("\\{(.+?)\\}");

    public Sharded(List<S> shards) {
        this(shards, Hashing.MURMUR_HASH);
    }

    public Sharded(List<S> shards, Hashing algo) {
        this.resources = new LinkedHashMap();
        this.tagPattern = null;
        this.algo = algo;
        this.initialize(shards);
    }

    public Sharded(List<S> shards, Pattern tagPattern) {
        this(shards, Hashing.MURMUR_HASH, tagPattern);
    }

    public Sharded(List<S> shards, Hashing algo, Pattern tagPattern) {
        this.resources = new LinkedHashMap();
        this.tagPattern = null;
        this.algo = algo;
        this.tagPattern = tagPattern;
        this.initialize(shards);
    }

    private void initialize(List<S> shards) {//节点的初始化 最终多个物理节点的“虚拟节点”将会在环上交错布局,不一定分布均匀。
        this.nodes = new TreeMap();

        for(int i = 0; i != shards.size(); ++i) {
            ShardInfo shardInfo = (ShardInfo)shards.get(i);
            int n;
            if(shardInfo.getName() == null) {//判断节点的名称为空 就给默认名称计算hash值
                for(n = 0; n < 160 * shardInfo.getWeight(); ++n) {//默认权重为1,每个物理节点有160个虚拟节点
                    this.nodes.put(Long.valueOf(this.algo.hash("SHARD-" + i + "-NODE-" + n)), shardInfo);
                }
            } else {//判断节点的名称不为空 就用名称计算hash值
                for(n = 0; n < 160 * shardInfo.getWeight(); ++n) {
                    this.nodes.put(Long.valueOf(this.algo.hash(shardInfo.getName() + "*" + shardInfo.getWeight() + n)), shardInfo);
                }
            }

            this.resources.put(shardInfo, shardInfo.createResource());
        }

    }

    public R getShard(byte[] key) {
        return this.resources.get(this.getShardInfo(key));
    }

    public R getShard(String key) {
        return this.resources.get(this.getShardInfo(key));
    }

    public S getShardInfo(byte[] key) {//获取key对应的存储节点
        SortedMap tail = this.nodes.tailMap(Long.valueOf(this.algo.hash(key)));//获取到大于等于key哈希值的子map
        return tail.isEmpty()?(ShardInfo)this.nodes.get(this.nodes.firstKey()):(ShardInfo)tail.get(tail.firstKey());//如果子map为空就取map的第一个节点,否则取子map的第一个节点
    }

    public S getShardInfo(String key) {
        return this.getShardInfo(SafeEncoder.encode(this.getKeyTag(key)));
    }

    public String getKeyTag(String key) {
        if(this.tagPattern != null) {
            Matcher m = this.tagPattern.matcher(key);
            if(m.find()) {
                return m.group(1);
            }
        }

        return key;
    }

    public Collection<S> getAllShardInfo() {
        return Collections.unmodifiableCollection(this.nodes.values());
    }

    public Collection<R> getAllShards() {
        return Collections.unmodifiableCollection(this.resources.values());
    }
}

再来看看hash算法的实现源码

package redis.clients.util;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import redis.clients.util.MurmurHash;
import redis.clients.util.SafeEncoder;
/**
* 采用MD5加密算法的一种哈希算法实现,安全性高,但是速度慢。
*/
public interface Hashing {
    Hashing MURMUR_HASH = new MurmurHash();//这个变量放在这里感觉没用啊
    ThreadLocal<MessageDigest> md5Holder = new ThreadLocal();
    Hashing MD5 = new Hashing() {
        public long hash(String key) {
            return this.hash(SafeEncoder.encode(key));
        }

        public long hash(byte[] key) {
            try {
                if(md5Holder.get() == null) {
                    md5Holder.set(MessageDigest.getInstance("MD5"));
                }
            } catch (NoSuchAlgorithmException var6) {
                throw new IllegalStateException("++++ no md5 algorythm found");
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值