通常,为了提高网站响应速度,总是把热点数据保存在内存中而不是直接从后端数据库中读取。Redis是一个很好的Cache工具。大型网站应用,热点数据量往往巨大,几十G上百G是很正常的事儿,在这种情况下,如何正确架构Redis呢?
首先,无论我们是使用自己的物理主机,还是使用云服务主机,内存资源往往是有限制的,scale up不是一个好办法,我们需要scale out横向可伸缩扩展,这需要由多台主机协同提供服务,即分布式多个Redis实例协同运行。
其次,目前硬件资源成本降低,多核CPU,几十G内存的主机很普遍,对于主进程是单线程工作的Redis,只运行一个实例就显得有些浪费。同时,管理一个巨大内存不如管理相对较小的内存高效。因此,实际使用中,通常一台机器上同时跑多个Redis实例。
Redis 3正式推出了官方集群技术,解决了多Redis实例协同服务问题。Redis Cluster可以说是服务端Sharding分片技术的体现,即将键值按照一定算法合理分配到各个实例分片上,同时各个实例节点协调沟通,共同对外承担一致服务。
多Redis实例服务,比单Redis实例要复杂的多,这涉及到定位、协同、容错、扩容等技术难题。这里,我们介绍一种轻量级的客户端Redis Sharding技术。
Redis Sharding可以说是Redis Cluster出来之前,业界普遍使用的多Redis实例集群方法。其主要思想是采用哈希算法将Redis数据的key进行散列,通过hash函数,特定的key会映射到特定的Redis节点上。这样,客户端就知道该向哪个Redis节点操作数据。Sharding架构如图:
庆幸的是,java redis客户端驱动jedis,已支持Redis Sharding功能,即ShardedJedis以及结合缓存池的ShardedJedisPool。
Jedis的Redis Sharding实现具有如下特点:
1. 采用一致性哈希算法(consistent hashing),将key和节点name同时hashing,然后进行映射匹配,采用的算法是MURMUR_HASH。采用一致性哈希而不是采用简单类似哈希求模映射的主要原因是当增加或减少节点时,不会产生由于重新匹配造成的rehashing。一致性哈希只影响相邻节点key分配,影响量小。
2.为了避免一致性哈希只影响相邻节点造成节点分配压力,ShardedJedis会对每个Redis节点根据名字(没有,Jedis会赋予缺省名字)会虚拟化出160个虚拟节点进行散列。根据权重weight,也可虚拟化出160倍数的虚拟节点。用虚拟节点做映射匹配,可以在增加或减少Redis节点时,key在各Redis节点移动再分配更均匀,而不是只有相邻节点受影响。
3.ShardedJedis支持keyTagPattern模式,即抽取key的一部分keyTag做sharding,这样通过合理命名key,可以将一组相关联的key放入同一个Redis节点,这在避免跨节点访问相关数据时很重要。
下面我们用Jedis实际操作下:
1.pom.xml中配置jedis jar包
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.7.2</version>
</dependency>
2.spring配置文件中配置ShardedJedisPool
<bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
<property name="maxTotal" value="4096"/>
<property name="maxIdle" value="200"/>
<property name="maxWaitMillis" value="3000"/>
<property name="testOnBorrow" value="true" />
<property name="testOnReturn" value="true" />
</bean>
<bean id = "shardedJedisPool" class = "redis.clients.jedis.ShardedJedisPool">
<constructor-arg index="0" ref="poolConfig"/>
<constructor-arg index="1">
<list>
<bean class="redis.clients.jedis.JedisShardInfo">
<constructor-arg index="0" value="192.168.1.119" type="String"/>
<!-- shard name -->
<constructor-arg index="1" value="Shard-1" type="String"/>
<constructor-arg index="2" value="6379" type="int"/>
<!-- timeout,default is 2 sec -->
<constructor-arg index="3" value="2000" type="int"/>
<!-- weight,default is 1 -->
<constructor-arg index="4" value="1" type="int"/>
</bean>
<bean class="redis.clients.jedis.JedisShardInfo">
<constructor-arg index="0" value="192.168.1.119" type="String"/>
<constructor-arg index="1" value="Shard-2" type="String"/>
<constructor-arg index="2" value="6479" type="int"/>
<constructor-arg index="3" value="2000" type="int"/>
<constructor-arg index="4" value="1" type="int"/>
</bean>
<bean class="redis.clients.jedis.JedisShardInfo">
<constructor-arg index="0" value="192.168.1.119" type="String"/>
<constructor-arg index="1" value="Shard-3" type="String"/>
<constructor-arg index="2" value="6579" type="int"/>
<constructor-arg index="3" value="2000" type="int"/>
<constructor-arg index="4" value="1" type="int"/>
</bean>
<bean class="redis.clients.jedis.JedisShardInfo">
<constructor-arg index="0" value="192.168.1.119" type="String"/>
<constructor-arg index="1" value="Shard-4" type="String"/>
<constructor-arg index="2" value="6679" type="int"/>
<constructor-arg index="3" value="2000" type="int"/>
<constructor-arg index="4" value="2" type="int"/>
</bean>
</list>
</constructor-arg>
</bean>
3.编写测试代码
@Test
public void basicOpTestForSharded(){
ShardedJedis jedis = shardedJedisPool.getResource();
long begin = System.currentTimeMillis();
for(int i=0;i<10000; i++){
jedis.set("person." + i + ".name", "frank");
jedis.set("person." + i + ".city", "beijing");
String name = jedis.get("person." + i + ".name");
String city = jedis.get("person." + i + ".city");
assertEquals("frank",name);
assertEquals("beijing",city);
jedis.del("person." + i + ".name");
Boolean result = jedis.exists("person." + i + ".name");
assertEquals(false,result);
result = jedis.exists("person." + i + ".city");
assertEquals(true,result);
}
long end = System.currentTimeMillis();
for(Jedis myJedis: jedis.getAllShards()){
System.out.println("redis shard: " +
myJedis.getClient().getHost() + ":" + myJedis.getClient().getPort());
System.out.println("redis shard size: " + myJedis.dbSize());
}
System.out.println("total time: " + (end-begin)/1000);
jedis.close();
}
4.运行代码
可以看到,最终的10000个键值,被合理分配到四个Redis实例中,由于Shard-4的weight权重是其它三个的1倍,我们看到,分配给Shard-4节点的键值数也大致是其它三个的1倍,整个键值数比例基本符合1:1:1:2。
扩容问题R
Redis Sharding采用客户端Sharding方式,服务端Redis还是一个个相对独立的Redis实例节点,没有做任何变动。同时,我们也不需要增加额外的中间处理组件,这是一种非常轻量、灵活的Redis多实例集群方法。
当然,Redis Sharding这种轻量灵活方式必然在集群其它能力方面做出妥协。比如扩容,当想要增加Redis节点时,尽管采用一致性哈希,毕竟还是会有key匹配不到而丢失,这时需要键值迁移。
作为轻量级客户端sharding,处理Redis键值迁移是不现实的,这就要求应用层面允许Redis中数据丢失或从后端数据库重新加载数据。但有些时候,击穿缓存层,直接访问数据库层,会对系统访问造成很大压力。有没有其它手段改善这种情况?
Redis作者给出了一个比较讨巧的办法--presharding,即预先根据系统规模尽量部署好多个Redis实例,这些实例占用系统资源很小,一台物理机可部署多个,让他们都参与sharding,当需要扩容时,选中一个实例作为主节点,新加入的Redis节点作为从节点进行数据复制。数据同步后,修改sharding配置,让指向原实例的Shard指向新机器上扩容后的Redis节点,同时调整新Redis节点为主节点,原实例可不再使用。
presharding是预先分配好足够的分片,扩容时只是将属于某一分片的原Redis实例替换成新的容量更大的Redis实例。参与sharding的分片没有改变,所以也就不存在key值从一个区转移到另一个分片区的现象,只是将属于同分片区的键值从原Redis实例同步到新Redis实例。
节点故障问题R
并不是只有增删Redis节点引起键值丢失问题,更大的障碍来自Redis节点突然宕机。在《Redis持久化》一文中已提到,为不影响Redis性能,尽量不开启AOF和RDB文件保存功能,可架构Redis主备模式,主Redis宕机,数据不会丢失,备Redis留有备份。
这样,我们的架构模式变成一个Redis节点切片包含一个主Redis和一个备Redis。在主Redis宕机时,备Redis接管过来,上升为主Redis,继续提供服务。主备共同组成一个Redis节点,通过自动故障转移,保证了节点的高可用性。则Sharding架构演变成:
Redis Sentinel提供了主备模式下Redis监控、故障转移功能达到系统的高可用性。下面我们搭建一主一从并利用Sentinel进行监控。
1.搭建主从架构,一主一从
主端口号是6379,从端口号是6479,此步略,参看redis持久性一文。
2.构建Sentinel系统
Redis Sentinel其实也是Redis,只不过是以Sentinel模式启动。在Sentinel模式下,Redis只接受有限的几个命令,主要是监控Redis实例是否发生故障,在主Redis发生故障的前提下,进行故障转移,在可用从Redis实例中挑选一个上升为主Redis,同时其它从Redis的主Redis重定向到这个新的主Redis。原有故障的主Redis如重新上线,也会降级为从Redis,指向新的主Redis。
这样看来,Sentinel又成为一个关键的节点,如果Sentinel节点发生故障,那整个HA高可用将变成不可用,故通常情况下,Sentinel本身是处于集群状态的。
多个Sentinel实例集群,那由谁执行故障转移呢?这需要选举一个Sentinel作为主Sentinel,如果一半以上同意,这个Sentinel将选举为主Sentinel负责执行故障转移操作。故一般Sentinel集群为单数,如3个,2个Sentinel集群是无效的。
在本例中,我们只搭建一个Sentinel实例作为监控节点。
Sentinel启动需要指定配置文件,我们来看下sentinel.conf中几个主要参数:
port 26379 监控系统端口号
sentinel monitor Shard-1 192.168.1.146 6379 1 监控名为Shard-1的节点,且其主Redis的IP是192.168.1.146,端口号为6379,同时有1个Sentinel认为其下线,那此主Redis就认为是有效的客观下线状态,需要执行故障转移。
sentinel down-after-milliseconds Shard-1 30000 sentinel实时监控主Redis,如发现30秒没反应,则主观认为其已下线。
sentinel parallel-syncs Shard-1 1 故障转移,从Redis重新同时指向新主Redis的个数。
sentinel failover-timeout Shard-1 180000 故障转移超时判定,缺省3分钟
如下命令启动sentinel:
redis-sentinel /etc/sentinel.conf >> /var/log/sentinel.log &
查看sentinel.log,日志如图:
3.故障转移测试
我们试着shutdown掉主Redis,看看sentinel和从redis反应:
我们看到,Shard-1的主Redis从6379这个实例转到了6479这个实例
可以看到,6479这个Redis实例由从升级到主,系统完成了自动故障转移。
我们再重新启动6379原主Redis, 查看日志:
此时,这个Redis已降级为从Redis,同步6479主Redis。
Jedis提供了JedisSentinelPool类可以访问Sentinel监控的主从Redis组成的节点。在我们的架构方案中,它只是作为一个分片节点如Shard-1存在。Jedis并没有提供分片节点是主从模式下的驱动,好在Jedis是开源产品,我们可以根据JedisSentinelPool主逻辑方式得到各分片的最新主Redis信息,这就组成了ShardedJedisPool所需要的JedisShardInfo列表参数,然后按照JedisSentinelPool重新初始化pool的方式重新初始化ShardedJedisPool中的pool。
读写分离R
高访问量下,即使采用Sharding分片,一个单独节点还是承担了很大的访问压力,这时我们还需要进一步分解。通常情况下,应用访问Redis读操作量和写操作量差异很大,读常常是写的数倍,这时我们可以将读写分离,而且读提供更多的实例数。
可以利用主从模式实现读写分离,主负责写,从负责只读,同时一主挂多个从。在Sentinel监控下,还可以保障节点故障的自动监测。这时,上述sharding架构下每个单节点进一步演化为一主多从。如下:
同样,Jedis没有提供Sharding状态下一主多从节点的访问驱动,我们还是根据ShardedJedisPool和JedisSentinelPool源码实现机理做相应改造,从sentinel那里得到可用从redis实例信息,并将读相关操作按照一定算法合理分配到这些可用从Redis节点,分担主节点压力。
相关推荐
然而,在Redis 3.0版本之前,其仅支持单实例模式,尽管引入了主从模式及哨兵模式以解决单点故障问题,但在面对大规模数据量需求时,这些解决方案显得力不足。因此,从3.0版本开始,Redis引入了集群模式。 Redis集群...
这本书详细阐述了构建和优化大规模互联网应用的技术要点,旨在帮助读者理解和掌握构建高性能、高可用性、可扩展性的网站架构的关键知识。 在书中,作者首先介绍了大型网站面临的挑战,如高并发访问、海量数据处理、...
本文将重点探讨如何为一家具有大规模用户基础的保险代理企业设计一个高可用、高性能的O2O平台微服务架构。 #### 二、微服务架构简介 微服务架构是一种将单一应用程序分解成一组小型服务的方法,每个服务运行在其...
本篇文章将深入探讨大型网站架构的核心要点,并借鉴这些公司的成功案例。 首先,大型网站架构的核心原则之一是“分层”。这意味着将系统分解为多个独立的层次,如前端展示层、应用逻辑层、数据访问层等,每一层都...
综上所述,《大规模Web服务开发技术》这本书不仅覆盖了基础知识和技术要点,还提供了大量实战经验和技巧分享,对于想要深入了解这一领域的架构师来说具有很高的参考价值。通过学习这些知识,可以更好地应对实际工作...
- **背景**: 随着业务发展,单一数据库难以应对大规模数据量带来的性能瓶颈。 - **解决方案**: 通过水平切分技术,将数据库拆分成多个部分,以提高系统整体性能。 - **方法**: 可采用范围法或哈希法进行数据切分...
- **分布式事务**:2PC、TCC等解决大规模系统中的事务一致性问题。 4. **APP开发**: - **跨平台技术**:React Native、Flutter等实现iOS和Android的跨平台开发。 - **推送服务**:极光推送、个推等实现消息推送...
而BASE理论则提供了与传统ACID事务模型不同的、更为宽松的一致性保障方法,这对于构建大规模、高可用的分布式系统提供了理论支持。 其次,架构设计的实践通常涉及多个层次的考量。作者可能按照典型的分层架构模型,...
1. **TSDB(时间序列数据库)**:针对时序数据的特点进行优化设计,能够高效存储和查询大规模的时间序列数据。 2. **BIStudio**:商业智能工具,提供丰富的报表制作与数据分析功能。 3. **XSpark**:基于Spark框架的...
- **大规模访问负载**:在高峰时段,微博需要应对的是千万级别的并发访问,缓存系统需要有高度的可扩展性和鲁棒性。 - **数据一致性**:在分布式缓存环境中,如何保证各个节点间数据的一致性是一个技术难题。 - **...
- **美团订单处理实战**:分析美团大规模订单处理系统的技术细节和实践经验。 #### 四、资料下载与学习建议 - **网盘链接**:[点击此处](https://pan.baidu.com/share/init?surl=QPnxh02ksaI9ApNr4n2DHw)访问百度...
在当前的互联网时代,分布式系统已经成为企业级应用的主流架构。Java作为广泛使用的编程语言,其在分布式领域的应用非常广泛。本篇将深入探讨分布式Java应用的基础知识和实践要点,通过源码分析帮助读者理解如何构建...
为了应对大规模并发请求,我们可以采用负载均衡(Load Balancing)技术,通过分布式服务器集群将请求分散到多台机器上,避免单点压力过大。同时,异步处理和队列(如消息队列MQ)可以进一步提高系统的并发能力,使得...
高性能高并发服务器架构是互联网行业中一个至关重要的领域,它涉及到如何...通过不断学习和实践,我们可以逐步提升系统处理能力,满足大规模用户的需求。同时,持续的监控、优化和迭代也是保证系统持续高效运行的关键。
分布式系统能够提升系统的可扩展性和容错能力,适合处理大规模并发访问的场景。在金融领域,分布式转型可以摆脱对单一技术或供应商的依赖,实现自主可控,提升金融科技应用水平。 缓存数据库的使用是分布式系统中的...
### 电商技术架构及其环境搭建 #### 一、电商行业背景与发展趋势 随着中国经济的快速发展,电子商务已成为推动经济增长的重要力量之一。据统计,截至2012年底,中国电子商务市场的交易规模达到了7.85万亿元人民币...
同时,Go的静态类型和内存管理确保了系统的稳定性和安全性,这对于构建大规模的云服务至关重要。 2. **新浪微博Redis优化历程** 微博在Redis缓存优化过程中,Go语言的性能优势得以显现。Go语言与C/C++类似的运行...