缓存架构中分布式一致性hash应用解析

前言

本篇文章会从什么是分布式一致性hash算法、hash算法在Memcached、Redis中的应用、以及Java本地缓存与分布式缓存绝佳组合、剖析从浏览器缓存到数据库缓存等;然后去解析一致性hash算法的应用。以及我们在项目应用中,并不是只应用redis等中间件去处理,也会结合本地缓存去处理实际的应用场景。

一致性Hash算法在缓存架构中应用

Hash+取模 在分布式架构下

分布式缓存集群

 

想要快速 ,算出key 所在节点,因为Hash所以高效。 因此在redis分片集群有个hash槽的概念,slots .

init长度有限,string长度无限,产生hash碰撞。在分布式系统下 缓存采用hash算法,hash碰撞。将链表进行扩长,然后使用hashtable进行存储查找数据 

说到高性能的缓存,例如redis、memcache 离不开c语言,然后快速查找一定离不开hash算法。

在下面示例上的应用

3个节点的集群,数据 World:888

假设: hash(World) = 200
则数据放到服务器2上
200%3 = 2
这里在集群中运行,不同场景下出现的问题
  • 高并发场景,集群临时扩容,加一台机器!
hash(World) = 200;200%4 = 0 再次读取,key对应的节点发生了变化
直接导致数据缓存命不中! 
  • 增加一个节点后,有多大比例的数据缓存命不中?
3台变4台,导致3/4的映射发生变动
99台服务器扩容到100台,导致99%的映射会发生变动

这样引出一致性hash算法

一致性Hash算法

什么是一致性hash算法

一致性hash能保证在分布式环境中,对key进行哈希的结果或者说key与节点之间的映射关系不会受节点的增加和删除而产生重大的变化。

一致性哈希是一种特殊的哈希算法,提供了这样的一个哈希表,当重新调整大小的时候,平均只有部分(k/n)key需要重新映射哈希槽,而不像传统哈希表那样几乎所有key需要需要重新映射哈希槽

  • hash值一个非负整数,把非负整数的值范围做成一个圆环;
  • 对集群的节点的某个属性求hash值(如节点名称), 根据hash值把节点放到环上;节点对应圆环上
  • 对数据的key求hash,一样的把数据也放到环上,按顺时针方向,找离它最近的节点,就存储到这个节点,永远取相邻的这个点

 这样就解决扩容,数据不命中。减少对原有的配置等修改。

增加一个节点影响   影响部分数据,(0~1)/3,取个中值 1/6 。  影响的是小部分数据

新增节点能均衡缓解并不能原有节点的压力,这是它会出现的问题,而且集群节点不会均匀的分布在圆环上,这是实践过程中会出现的问题,

 也是由于在圆环上添加节点过后,其他节点未动,会导致原有的请求,还是会到原来的节点上,不会影响其它节点,而且hash一定会有倾斜,就会导致看起来右下角一坨,不会均匀分布。

一致性Hash算法

利用虚拟节点去解决hash倾斜问题

 将 节点  进行 000-999  999-1000 .......等等虚拟节点上环,来分布数据

而且

虚拟节点越多,分布越均衡。
虚拟节点越多,新增节点对原有节点影响越均衡

 这里例如在 memached中 利用KetamaMemcachedSessionLocator 达到一致性hash算法的作用

	
    @Bean
    public MemcachedClient memcachedClient() throws IOException {
        String servers = clusterConfig;
        MemcachedClientBuilder builder = new XMemcachedClientBuilder(AddrUtil
                .getAddresses(servers));
        // 默认的客户端计算就是 key的哈希值模以连接数
        // KetamaMemcachedSessionLocator 一致性hash算法
        builder.setSessionLocator(new KetamaMemcachedSessionLocator());
        MemcachedClient client = builder.build();
        return client;
    }

redis的slot机制 是一个经典的实现。

使用的范围是0-16383,默认情况下slot是连续排列的

 假设数据redisserver3的数据量非常大,也会弄的这个服务器压力非常大,因此可以将槽点移动一些到redisserver2

对于memached的选择,也是有一致性hash算法,去解决问题 

 高性能体系之二级缓存

我们先理解一个概念,为提高效率,读多写少用缓存,读少写多用缓冲。

什么是二级缓存

L2Cache,即CPU的二级缓存。二级缓存是CPU性能表现的关键之一,在CPU核心不变化的情况下,增加二级缓存容量能使性能大幅度提高。而同一核心的CPU高低端之分往往也是在二级缓存上有差异,由此可见二级缓存对于CPU的重要性。

这是对于cpu中二级缓存的定义,而在缓存概念中多级缓存。根据效率和成本 现在比较流行的三级缓存。

对于我们在项目开发中,为提高效率,压榨性能,一般使用的方式

直接使用redis做为缓存

一个页面,每天800-1000万

从缓存读取数据50k

redis 一天要40-50g

 

 用这样的架构去处理,对于高峰期的时候,大于500m /秒 宽带是承受不住的,带宽是非常昂贵的,不足以解决问题

 而作为本地缓存 用作的工具有

Guava(工具比较强大)、 Ehcache(可以使用堆外内存的)、Caffeine(基于Java 8的高性能)

 为什么不采用hashmap作为一级缓存,自己写一个会少很多功能,优化不够。

两边缓存的过期时间,要错开,不一致,本地缓存存的时间为了不影响本地其他功能,肯定是需要设置软引用,并且过期时间需要短,因此没有时,才到分布式的。

两级缓存技术-J2Cache

项目和文档地址:J2Cache: Java 两级缓存框架,可以让应用支持两级缓存框架 ehcache(Caffeine) + redis 。避免完全使用独立缓存系统所带来的网络IO开销问题 (gitee.com)

解决的是领域问题,分布式缓存和本地缓存特点不一致。Redis针对特定的key 设置过期时间,本地缓存无法做到。领域:把本地设置 分组同步到redis上,key是相同得。

  •  客户端向服务端发起请求
  • 服务端先从本地缓存获取
  • 本地缓存未命中从分布式缓存redis获取
  • Redis缓存未命中从数据库中获取

解决的问题

  • 使用应用本地缓存时,一旦应用重启后,由于缓存数据丢失,缓存雪崩,给数据库造成巨大压力,导致应用阻塞
  • 使用应用本地缓存时,多个应用节点无法共享缓存数据
  • 使用分布式缓存 ,由于大量的数据通过缓存获取,导致缓存服务的数据吞吐量太大,带宽跑满,现象就是redis服务负载不高,但是由于机器网卡爬满,导致数据读取非常慢。

在代码中引用

这两个都可以使用,或者使用其中一个就可以 redis ,然后引入j2cache 依赖就可以

	<dependency>
	        <groupId>redis.clients</groupId>
	        <artifactId>jedis</artifactId>
	        <version>${jedis.version}</version>
	    </dependency>
	    <dependency>
	        <groupId>io.lettuce</groupId>
	        <artifactId>lettuce-core</artifactId>
	        <version>${lettuce.version}</version>
	    </dependency>

        <dependency>
		    <groupId>net.oschina.j2cache</groupId>
		    <artifactId>j2cache-core</artifactId>
		    <version>2.7.8-release</version>
		</dependency>

 在配置文件中配置好对应的 缓存

#########################################
# Cache Broadcast Method
# values:
# jgroups -> use jgroups's multicast
# redis -> use redis publish/subscribe mechanism (using jedis)
# lettuce -> use redis publish/subscribe mechanism (using lettuce, Recommend)
# rabbitmq -> use RabbitMQ publisher/consumer mechanism
# rocketmq -> use RocketMQ publisher/consumer mechanism
# none -> don't notify the other nodes in cluster
# xx.xxxx.xxxx.Xxxxx your own cache broadcast policy classname that implement net.oschina.j2cache.cluster.ClusterPolicy
#########################################

j2cache.broadcast = redis

#########################################
# Level 1&2 provider
# values:
# none -> disable this level cache
# ehcache -> use ehcache2 as level 1 cache
# ehcache3 -> use ehcache3 as level 1 cache
# caffeine -> use caffeine as level 1 cache(only in memory)
# redis -> use redis as level 2 cache (using jedis)
# lettuce -> use redis as level 2 cache (using lettuce)
# readonly-redis -> use redis as level 2 cache ,but never write data to it. if use this provider, you must uncomment `j2cache.L2.config_section` to make the redis configurations available.
# memcached -> use memcached as level 2 cache (xmemcached),
# [classname] -> use custom provider
#########################################

j2cache.L1.provider_class = caffeine
j2cache.L2.provider_class = redis

# When L2 provider isn't `redis`, using `L2.config_section = redis` to read redis configurations
# j2cache.L2.config_section = redis

# Enable/Disable ttl in redis cache data (if disabled, the object in redis will never expire, default:true)
# NOTICE: redis hash mode (redis.storage = hash) do not support this feature)
j2cache.sync_ttl_to_redis = true

# Whether to cache null objects by default (default false)
j2cache.default_cache_null_object = true

#########################################
# Cache Serialization Provider
# values:
# fst -> using fast-serialization (recommend)
# kyro -> using kyro serialization
# json -> using fst's json serialization (testing)
# fastjson -> using fastjson serialization (embed non-static class not support)
# java -> java standard
# [classname implements Serializer]
#########################################

j2cache.serialization = json

还有些配置,要参考着j2cache的官方文档

以及代码中使用

	public static void main(String[] args) throws IOException {
		CacheChannel cache = J2Cache.getChannel();
		// cache1表示region,region表示定义了缓存大小和过期时间的分组
		cache.set("myRegion","key","cacheValue");
		CacheObject cacheObj = cache.get("myRegion","key");
		System.out.println(cacheObj.getValue());
		cache.get("myRegion","key");
	}

在源码中表示

 然后使用过后,当它是一个缓存使用。

以及包括常见遇见的一些问题

  1. J2Cache 的使用场景是什么?
    首先你的应用是运行在集群环境,使用 J2Cache 可以有效降低节点间的数据传输量;其次单节点使用 J2Cache 可以避免应用重启后对后端业务系统的冲击

  2. 为什么不能在程序中设置缓存的有效期
    在程序中定义缓存数据的有效期会导致缓存不可控,一旦数据出问题无从查起,因此 J2Cache 的所有缓存的有效期都必须在 一级缓存 的配置中预设好再使用

  3. 如何使用 JGroups 组播方式(无法在云主机中使用)
    首先修改 j2cache.properties 中的 j2cache.broadcast 值为 jgroups,然后在 maven 中引入

    <dependency>
        <groupId>org.jgroups</groupId>
        <artifactId>jgroups</artifactId>
        <version>3.6.13.Final</version>
    </dependency>

  4. 如何使用 ehcache 作为一级缓存
    首先修改 j2cache.properties 中的 j2cache.L1.provider_class 为 ehcache 或者 ehcache3,然后拷贝 ehcache.xml 或者 ehcache3.xml 到类路径,并配置好缓存,需要在项目中引入对 ehcache 的支持:

    <dependency><!-- Ehcache 2.x //-->
        <groupId>net.sf.ehcache</groupId>
        <artifactId>ehcache</artifactId>
        <version>2.10.4</version>
    </dependency>
    
    <dependency><!-- Ehcache 3.x //-->
        <groupId>org.ehcache</groupId>
        <artifactId>ehcache</artifactId>
        <version>3.4.0</version>
    </dependency>

    spring cache 和 j2cache 继承示例代码,spring 配置类

/**
 * @author Chen
 */
@Configuration
@EnableCaching
public class MyCacheConfig extends CachingConfigurerSupport {
    @Override
    public CacheManager cacheManager() {
        // 引入配置
        J2CacheConfig config = J2CacheConfig.initFromConfig("/j2cache.properties");
        // 生成 J2CacheBuilder
        J2CacheBuilder j2CacheBuilder = J2CacheBuilder.init(config);
        // 构建适配器
        J2CacheSpringCacheManageAdapter j2CacheSpringCacheManageAdapter = new J2CacheSpringCacheManageAdapter(j2CacheBuilder, true);

        return j2CacheSpringCacheManageAdapter;
    }
}

高并发系统缓存架构方案

 包括代理层、应用层、数据库层、几层进行拆分开 每层都有一个缓存起来。

数据库缓存

在mysql官方文档中 MySQL :: MySQL Documentation

有部分是对缓存做描述的,包括怎么开启缓存,验证有没有缓存。

 

 直接使用 query_cache去查询缓存

缓存使用 key -vlaue

缓存性能指标:命中率

维护成本:数据库成本高

不建议使用,在mysql8.0就删除了

 应用层数据缓存

代理服务器缓存

 通过lua脚本直接去访问redis ,代理层面去。

Guess you like

Origin blog.csdn.net/qq_33373609/article/details/120903420