Redis高可用集群构架原理及高并发
Redis高可用集群构架原理及高并发(https://www.jianshu.com/p/52428c5f330e)
0.6322020.11.30 16:18:41字数 3,358阅读 261一、集群方案比较
1.1 哨兵模式
哨兵模式
在Redis3.0以前的版本要实现集群一般是借助哨兵sentinel工具来监控master节点的状态,如果master节点异常,则会做主从切换,将某一台slave作为master,哨兵的配置略微复杂,并且性能和高可用性等各方面表现一般,特别是在主从切换的瞬间存在访问瞬断的情况。
1.2 高可用集群模式
高可用集群模式
Redis集群是一个由多个主从节点群组成的分布式服务器群,它具有复制、高可用和分片特性。Redis集群不需要sentinel哨兵也能完成节点移除和故障转移的功能。需要将每个节点设置成集群模式,这种集群模式没有中心节点,可水平扩展,据官方文档称可以线性扩展到1000节点。Redis集群的性能和高可用性均优于之前版本的哨兵模式,且集群配置非常简单。
二、集群搭建
Redis 3.0(2012年发布3.0,2017年发布4.0)版本后支持使用Redis-Cluster来搭建集群,本文将介绍在Ubuntu 18.04 64下搭建Redis集群。因为Redis集群中至少应该有奇数个主节点,所以本文将创建6个Redis节点,其中3个为主节点,3个为从属节点,用于从主节点拉取数据进行备份。这里搭建的是伪集群模式,当然真正的分布式集群的配置方法几乎一样,搭建伪集群的步骤如下
2.1 Redis安装
参考Ubuntu安装Redis
安装成功后开始进行集群搭建。
2.2 配置并启动6个节点
创建集群需要的目录
cp /etc/redis/redis.conf /etc/redis/cluster/redis.conf
修改配置文件中的下面选项
cd /etc/redis
redis-server cluster/7770/redis.conf
redis-server cluster/7771/redis.conf
redis-server cluster/7772/redis.conf
redis-server cluster/7773/redis.conf
redis-server cluster/7774/redis.conf
redis-server cluster/7775/redis.conf
查看是否启动成功
apt-get install ruby
gem install redis
如果Redis是通过下载源码编译安装的方式,在Reids的src目录下有个redis-trib.rb文件,将该文件复制到/etc/redis目录下。本文Redis是通过apt-get install的方式直接安装的Redis,因此我先找到redis-trib.rb文件在哪个位置,然后在复制到/etc/redis目录下
# 复制到/etc/redis目录下
cp /usr/share/doc/redis-tools/examples/redis-trib.rb /etc/redis/redis-trib.rb
安装好ruby后,输入以下命令开启集群
redis-cli -c -p 7770
cluster info
127.0.0.1:7770> set name alanchen
-> Redirected to slot [5798] located at 127.0.0.1:7771
OK
127.0.0.1:7771> set age 18
-> Redirected to slot [741] located at 127.0.0.1:7770
OK
127.0.0.1:7770> get name
-> Redirected to slot [5798] located at 127.0.0.1:7771
"alanchen"
127.0.0.1:7771> get age
-> Redirected to slot [741] located at 127.0.0.1:7770
"18"
127.0.0.1:7770>
2.5 主从测试
开头说过,在集群过程中可以通过主从的分配来提高Redis的可用性。比如这个例子,集群有7770、7771和7772这3个主节点,如果这3个节点都没有从节点,假设7771宕机了,那么整个集群就会因为缺少7771节点范围的哈希槽而变得不可用。
所以我们在集群建立的时候,一定要为每个主节点都添加了从节点, 比如像上面的例子那样,集群包含主节点7770、7771和7772以及从节点7773、7774和7775, 那么即使7770宕系统也可以继续正常工作。
当7770这个主节点宕机后,Redis集群将会选择7770的从节点7773作为新的主节点以确保集群正常的工作。当重新启动7770后,其自动变为了7773的从节点,角色完成了转换。
为了验证这个理论,下面将7770节点杀死,然后观察
>
>redis.clients >
>jedis >
>2.9.0 >
>
Java代码
requirepass password_123
三、高并发场景库存重复扣减问题分析
先看以下这段扣减库存的代码
@RestController
public class StockController {
@Autowired
StringRedisTemplate stringRedisTemplate;
@RequestMapping("/deduct_stock")
public String deductStock(){
synchronized (this){
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if(stock >0){
int realStock = stock -1;
stringRedisTemplate.opsForValue().set("stock",realStock+"");
System.out.println("扣减成功,剩余库存:"+realStock+"");
}else{
System.out.println("扣减失败,库存不足");
}
return "end";
}
}
}
加同步锁synchronized在单体应用下是有效的,可以解决并发问题,但如果应用哪天升级成了集群部署或分布式部署后就无效了,比如,用Nginx搭配两台服务做负载均衡形成集群架构,两台tomcat同时提供服务时,synchronized就无效了,需要通过分布式锁来解决。
备注:synchronized只能在当前的JVM里生效
image.png
四、分布式架构下如何实现Redis分布式锁
我们可以利用Redis中的setnx key value 命令来简单实现Redis分布式锁
4.1 setnx key value
可用版本:>=1.0.0
时间复杂度:O(1)
只在键key不存在的情况下,将键key的值设置为value;若key已经存在,则setnx命令不做任何动作。
setnx是set if Not eXists 的简写。
命令在设置成功时返回1,设置失败时返回0。
4.2 Redis实现分布式简单版
/**
* @author Alan Chen
* @description 高并发场景库存重复扣减问题--Redis实现分布式优化版本1
* @date 2020/12/1
*/
@RestController
public class StockController {
@Autowired
StringRedisTemplate stringRedisTemplate;
@RequestMapping("/deduct_stock")
public String deductStock(){
String lockKey = "product_001";
try{
//加锁
// 与jedis.setnx(key,value)同效果;
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "iPhone");
if(!result){
return "抢购人数太多,请稍后重试";
}
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if(stock >0){
int realStock = stock -1;
stringRedisTemplate.opsForValue().set("stock",realStock+"");
System.out.println("扣减成功,剩余库存:"+realStock+"");
}else{
System.out.println("扣减失败,库存不足");
}
}finally {
//释放锁
stringRedisTemplate.delete(lockKey);
}
return "end";
}
}
4.4 优化版本2
通过分析,以上代码其实还有问题,比如在极端情况下,在执行try代码块里的代码时,服务器出现宕机或者程序发布被运维人员手动kill -9结束程序,那finally代码块里的释放锁的代码依然得不到执行,锁没有被释放出现死锁问题。解决这个问题,我们可以为锁加一个超时时间,到了超时时间自动释放锁。
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "iPhone",10,TimeUnit.SECONDS);
加锁的同时设置超时时间,Redis底层会保证这两步的原子性。完成代码如下
/**
* @author Alan Chen
* @description 高并发场景库存重复扣减问题--Redis实现分布式优化版本3
* @date 2020/12/1
*/
@RestController
public class StockController {
@Autowired
StringRedisTemplate stringRedisTemplate;
@RequestMapping("/deduct_stock")
public String deductStock(){
String lockKey = "product_001";
String clientId = UUID.randomUUID().toString();
try{
//加锁的同时设置超时时间
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId,10,TimeUnit.SECONDS);
if(!result){
return "抢购人数太多,请稍后重试";
}
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if(stock >0){
int realStock = stock -1;
stringRedisTemplate.opsForValue().set("stock",realStock+"");
System.out.println("扣减成功,剩余库存:"+realStock+"");
}else{
System.out.println("扣减失败,库存不足");
}
}finally {
//释放锁(本线程加的锁,只能被本线程释放)
if(clientId.equals(stringRedisTemplate.opsForValue().get(lockKey)))
stringRedisTemplate.delete(lockKey);
}
return "end";
}
}
4.5 优化版本4
关于这个超时时间有这样一个问题,某一个请求(线程)遇到慢查询或者GC等特殊情况,执行时间就是超过了设置的超时时间,就会提前释放锁,如果每次执行都超过了设置的超时时间,那每次都会提前释放锁,这个锁就永久失效失去意义了。那这个超时时间,到底设置多长合适呢?其实设置多少都不合适,设置太短就有上面提到的问题,如果设置太长(设置几个小时、几天)就失去了设置超时时间的意义,出现死锁的时候需要等待长时间才能自动解锁,出现长时间系统功能不可用。
要解决这个锁被提前释放的问题,可以加一个看门狗,或开启一个定时器去帮锁续命,重新设置一次超时时间。
自己实现分布式锁,细节和潜在的坑比较多,我们完全可以用现成的且成熟的分布式锁框架来帮我们实现分布式锁,这个框架就是下面要介绍的Redisson框架
五、基于Redisson框架实现分布式锁
Redisson官网
5.1 Redisson框架实现分布式锁
1、导入Redisson依赖
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Bean
public Redisson redisson(){
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(0);
return (Redisson) Redisson.create(config);
}
}
3、Redisson实现分布式锁代码
/**
* @author Alan Chen
* @description 高并发场景库存重复扣减问题-Redisson实现分布式锁
* @date 2020/12/1
*/
@RestController
public class StockController {
@Autowired
StringRedisTemplate stringRedisTemplate;
@Autowired
Redisson redisson;
@RequestMapping("/deduct_stock")
public String deductStock(){
String lockKey = "product_001";
//获得锁
RLock redissonLock = redisson.getLock(lockKey);
try{
//加锁并设置超时时间
redissonLock.lock(30,TimeUnit.SECONDS);
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if(stock >0){
int realStock = stock -1;
stringRedisTemplate.opsForValue().set("stock",realStock+"");
System.out.println("扣减成功,剩余库存:"+realStock+"");
}else{
System.out.println("扣减失败,库存不足");
}
}finally {
// 释放锁
redissonLock.unlock();
}
return "end";
}
}
4、Redisson分布式锁实现原理
Redisson分布式锁实现原理
5.2 性能优化
redissonLock.lock锁住了库存,解决了分布式并发问题(通过排队实现了串行),但会损失一部分性能,在需要提高性能的情况下,可以通过以下方式提供高性能
5.2.1 分段加锁
例如将商品001的100个库存,分成10段,分别对这10段进行加锁,理论上性能就可以提升10倍。但要注意在每段库存数量不够时,需要合并分段的库存。
5.2.2 用Redis集群
Redis集群
六、Redis缓存与数据库双写不一致问题及解决方案
6.1 双写不一致场景描述
场景一
场景二
6.2 解决方案
方案一:采用延时双删策略,但这种方案延时时间并不好把握,因此不能百分之百解决问题。
方案二:采用内存队列进行排队,但这种方案容易产生积压阻塞,性能较差,容易出问题。
方案三:采用canal框架
canal
方案四:采用分布式锁来解决(更严谨且推荐)。采用分布式锁有性能问题,可用通过分段加锁、采用Redis集群、读写锁(Redisson提供了ReadWriteLock,Redis的使用场景一般都是读多写少)
参考资料