如今Redis已几乎是大中型系统必备的组件,掌握redis是每个程序员必修的课程。
虽然Redis的使用很简单,但是如果没有参与过架构设计,在面试的时候也很容易被问得哑口无言。
此前笔者看到了一篇写redis架构演进比较好的文章,为了能够帮助大家从零开始掌握redis,笔者在这篇文章的基础上加入了各种架构下Redis的部署方法,并详细介绍了springboot应用redis的方法。
目录
单机版 Redis
Redis单机架构
假设现在有一个应用程序需要引入 Redis 来提高查询性能,此时可以选择部署一个单机版的 Redis 来缓存查询结果。
这个架构非常简单,应用程序把 Redis 当做缓存,从 MySQL 中查询数据后写入到 Redis 中,之后应用再从 Redis 中读取这些数据。流程图如下:
单机版Redis怎么安装?
单机Redis的安装比较简单,按照如下步骤操作即可。
1.获取redis资源(最新版本6.2.4)
wget http://download.redis.io/releases/redis-6.2.4.tar.gz
2.解压
tar xzvf redis-6.2.4.tar.gz
3.安装
cd redis-6.2.4
make MALLOC=libc # 需要先安装gcc 命令:yum install gcc-c++ -y
cd src
make install PREFIX=/usr/local/redis
4.移动配置文件到安装目录下
cd ../
mkdir /usr/local/redis/etc
mv redis.conf /usr/local/redis/etc
5.配置redis为后台启动
vi /usr/local/redis/etc/redis.conf
设置远程可以访问(图1):先注释掉 bind 127.0.0.1,在设置protected-mode no
将redis以守候进程的形式运行(即后台运行): daemonize no 改成daemonize yes
设置密码:requirepass ******
设置日志路径:logfile "/usr/local/redis/etc/redis.log"
6.设置redis开机启动
cp /home/redis-6.2.4/utils/redis_init_script /etc/init.d/redis
vi /etc/init.d/redis
1)设置可通过chkconfig启动:在第二行添加 # chkconfig: 2345 10 90 附图4
2) 修改文件路径
EXEC=/usr/local/redis/bin/redis-server #服务端路径
CLIEXEC=/usr/local/redis/bin/redis-cli #客户端路径
CONF="/usr/local/redis/etc/redis.conf" #配置文件
3)保存后执行chkconfig redis on
4) reboot 重启服务器
7、验证是否安装成功(附图4)
/usr/local/redis/bin/redis-cli
8、如果不设置开机启动redis,可执行如下命令启动
/usr/local/redis/bin/redis-server /usr/local/redis/etc/redis.conf
附图1:设置远程可以访问
附图2:设置密码:
附图3:设置可通过chkconfig启动
附图4:验证是否安装成功
springboot整合redis
创建一个SpringBoot项目。
pom.xml文件redis依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
在appliaction.yml配置redis数据库连接信息
spring:
#redis配置
redis:
#Redis服务器地址
host: 192.168.159.102
#Redis服务器连接端口
port: 6379
#Redis数据库索引(默认为0)
database: 0
#密码
password: ******
redis工具
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Component;
//工具类中使用Autowired注解需要加上Compoent
@Component
public class RedisUtil {
RedisTemplate redisTemplate;
/**
* 修改默认的序列化方式,防止存储键值包含\xac\xed\x00\x05t\x00\特殊字符
* */
@Autowired(required = false)
public void setRedisTemplate(RedisTemplate redisTemplate) {
RedisSerializer stringSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringSerializer);
redisTemplate.setValueSerializer(stringSerializer);
redisTemplate.setHashKeySerializer(stringSerializer);
redisTemplate.setHashValueSerializer(stringSerializer);
this.redisTemplate = redisTemplate;
}
// 判断是否存在key
public boolean hasKey(String key) {
return redisTemplate.hasKey(key);
}
// 从redis中获取值
public Object get(String key) {
return redisTemplate.opsForValue().get(key);
}
// 向redis插入值
public boolean set(final String key, Object value) {
boolean result = false;
try {
redisTemplate.opsForValue().set(key, value);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
}
数据持久化:数据恢复
数据持久化方案
如果Redis 因为某些原因宕机了,这时会引发缓存雪崩,即所有的查询,都会打到后端 MySQL 上,导致 MySQL 压力剧增,严重的话甚至会压垮 MySQL。
这时应该怎么办?宕机恢复首选解决方案当然是赶紧重启 Redis,让它可以继续提供服务。但是,因为之前 Redis 中的数据都在内存中,尽管 Redis 重启了,之前的数据也都丢失了,查询还是都会打到后端 MySQL 上,MySQL 的压力还是很大。
因此我们在部署Redis的时候,要让它把内存数据持久化到磁盘上,这样Redis重启时数据才不会丢失。
那么,数据持久化具体应该怎么做呢?
Redis的数据持久化方案有两种, 「RDB」和「AOF」:
RDB: RDB 是 Redis 默认的持久化方案。它只持久化某一时刻的数据快照到磁盘上(创建一个子进程来做)。RDB会在指定目录下生成一个dump.rdb文件。Redis 重启会通过加载dump.rdb文件恢复数据。
AOF: 每一次写操作都持久到磁盘(主线程写内存,根据策略可以配置由主线程还是子线程进行数据持久化)
这两种持久化方式区别如下:
- RDB 采用二进制 + 数据压缩的方式写磁盘,这样文件体积小,数据恢复速度也快,但由于不是实时持久化,存在数据丢失的可能性。
- AOF 记录的是每一次写命令,数据最全,但文件体积大,数据恢复速度慢。
选择持久化方案时,可以根据业务要求进行选择:
- 如果业务对于数据丢失不敏感,采用 RDB 方案持久化数据
- 如果业务对数据完整性要求比较高,采用 AOF 方案持久化数据
- 如果业务既想保障业务完整性,又想快速恢复,折中的方案是采用「混合持久化」。
「混合持久化」具体来说就是当 AOF rewrite 时,Redis 先以 RDB 格式在 AOF 文件中写入一个数据快照,再把在这期间产生的每一个写命令,追加到 AOF 文件中。因为 RDB 是二进制压缩写入的,这样 AOF 文件体积就变得更小了。
RDB详解
我们通过模拟数据丢失、恢复来说明RDB怎么使用。
1、配置
修改redis配置文件 vi /usr/local/redis/etc/redis.conf
,设置2分钟内如果有5个key更新则持久化
1、配置持久化策略
# save <seconds> <changes> save <指定时间间隔> <执行指定次数更新操作>
# save "" #若不想用RDB方案,可以把 save "" 的注释打开,下面三个save规则注释。
save 900 1 #900秒内(15分钟)有1个key更改则持久化
#save 300 10 #300秒内(5分钟)有10个key更改则持久化
save 120 5
save 60 10000 #60秒内(1分钟)有1000个key更改则持久化
2、指定保存的快照的文件名,采用默认的 dump.rdb
dbfilename dump.rdb
3、指定保存的快照存放目录
dir ./ 改成 dir /usr/local/redis/etc/
重启 :
1)关闭redis
2)启动 /usr/local/redis/bin/redis-server /usr/local/redis/etc/redis.conf
2、模拟数据丢失
设置5个key
2分钟后,/usr/local/redis/etc 目录下生成dump.rdb文件
备份dump.rdb文件
执行flushall清空redis数据模拟数据丢失
重启redis,发现数据为空
3、故障恢复
关闭redis,删除dump.rdb,将备份的dump-bak.rdb改名成dump.rdb,重启redis。
注意:shutdown会重新生成快照,所以一定要先关闭redis,再将备份文件改名成dump.rdb,否则数据仍然会为空。
AOF详解
Redis 默认不开启。它的出现是为了弥补RDB的不足(数据的不一致性),所以它采用日志的形式来记录每个写操作,并追加到文件中。Redis 重启的会时候会根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。
一、配置
修改redis.conf配置文件,然后重启Redis
1、开启AOF
appendonly yes
2、指定日志文件名,默认值为 appendonly.aof
appendfilename "appendonly.aof"
3、指定日志存放目录(与RDB快照同个目录)
dir ./ 改成 dir /usr/local/redis/etc/
4、指定更新日志条件
# appendfsync always #每次发生数据变化立刻写入到磁盘中。性能较差,但数据完整性比较好
appendfsync everysec #出厂默认推荐,每秒异步记录一次(默认值)
# appendfsync no #不同步
5、配置重写触发机制
# 当AOF文件大小是上次rewrite后大小的一倍且文件大于64M时触发
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
重写机制的意义:AOF的工作原理是将写操作追加到文件中,当文件太大的时候IO速度慢,所以Redis增加了重写机制,当AOF文件的大小超过所设定的阈值时,Redis就会对AOF文件的内容压缩。重写时Redis 会fork出一条新进程,读取内存中的数据,并重新写到一个临时文件中,最后替换旧的aof文件。
二、日志
添加数据
这时候能够看到etc目录下生成了appendonly.aof文件。打开文件,可以看到此前执行的操作。
主从复制:数据备份
为什么要做主从复制
一个实例宕机,只能用恢复数据来解决,那我们是否可以部署多个 Redis 实例,然后让这些实例数据保持实时同步,这样当一个实例宕机时,我们在剩下的实例中选择一个继续提供服务就好了。
这个方案就是接下来要讲的「主从复制」。
此时,你可以部署多个 Redis 实例,架构模型就变成了这样:
我们把实时读写的节点叫做 master,另一个实时同步数据的节点叫做 slave。
采用多副本的方案,它的优势是:
- 缩短不可用时间:master 发生宕机,我们可以把 slave 提升为 master 继续提供服务
- 提升读性能:让 slave 分担一部分读请求,提升应用的整体性能
主从复制的工作原理
Redis的主从结构可以采用一主多从或者级联结构。主从刚刚连接的时候,进行全量同步;全同步结束后,进行增量同步。
全量同步
Redis全量复制一般发生在Slave初始化阶段,这时Slave需要将Master上的所有数据都复制一份。
增量同步
Redis增量复制是指Slave初始化后开始正常工作时主服务器发生的写操作同步到从服务器的过程。
增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令。
怎么配置主从
1、环境准备:
server1 : 192.168.159.101 master
server2 : 192.168.159.102 slave
2、配置:
修改slave 的redis.conf,设置master对应的ip和端口及认证密码后重启redis
# redis-master 的ip
slaveof 192.168.159.101 6379
#redis-master的密码
masterauth ******
怎么做主从切换
当主库发生故障时,我们需要把从库设置为主库,否则无法改或添加新的KEY
关闭主从同步
127.0.0.1:6379> slaveof NO ONE
如果redis.conf中slaveof配置未注释掉,从库重启后会再次建立主从同步
springboot实现读写分离
redis实现主从复制后,我们就可以做读写分离。其中,主服务器用来写数据,从服务器用来读数据。
1. pom.xml文件引入jar包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
2. 在appliaction.yml配置redis数据库连接信息
spring:
#redis配置
redis:
master:
#Redis服务器地址
host: 192.168.159.102
#Redis服务器连接端口
port: 6379
#Redis数据库索引(默认为0)
database: 0
#密码
password: ******
timeout: 20000
pool:
maxActive: 8
minIdle: 0
maxIdle: 8
maxWait: -1
slave:
#Redis服务器地址
host: 192.168.159.103
#Redis服务器连接端口
port: 6379
#Redis数据库索引(默认为0)
database: 0
#密码
password: ******
timeout: 20000
pool:
maxActive: 8
minIdle: 0
maxIdle: 8
maxWait: -1
3. redis数据库连接配置类(对应appliaction.yml中的配置)
public class RedisProperties {
public Integer getDatabase() {
return database;
}
public void setDatabase(Integer database) {
this.database = database;
}
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public Integer getPort() {
return port;
}
public void setPort(Integer port) {
this.port = port;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Integer getTimeout() {
return timeout;
}
public void setTimeout(Integer timeout) {
this.timeout = timeout;
}
public Pool getPool() {
return pool;
}
public void setPool(Pool pool) {
this.pool = pool;
}
private Integer database;
private String host;
private Integer port;
private String password;
private Integer timeout;
private Pool pool;
public static class Pool {
private Integer maxActive;
public Integer getMaxActive() {
return maxActive;
}
public void setMaxActive(Integer maxActive) {
this.maxActive = maxActive;
}
public Integer getMinIdle() {
return minIdle;
}
public void setMinIdle(Integer minIdle) {
this.minIdle = minIdle;
}
public Integer getMaxIdle() {
return maxIdle;
}
public void setMaxIdle(Integer maxIdle) {
this.maxIdle = maxIdle;
}
public Integer getMaxWait() {
return maxWait;
}
public void setMaxWait(Integer maxWait) {
this.maxWait = maxWait;
}
private Integer minIdle;
private Integer maxIdle;
private Integer maxWait;
}
}
4. 构建redis连接池
父类:
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import redis.clients.jedis.JedisPoolConfig;
public class RedisConfig {
public JedisConnectionFactory getRedisConnFactory(
RedisProperties redisProperties) {
JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory();
jedisConnectionFactory.setDatabase(redisProperties.getDatabase());
jedisConnectionFactory.setHostName(redisProperties.getHost());
jedisConnectionFactory.setPort(redisProperties.getPort());
jedisConnectionFactory.setPassword(redisProperties.getPassword());
jedisConnectionFactory.setTimeout(redisProperties.getTimeout());
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxIdle(redisProperties.getPool().getMaxIdle());
jedisPoolConfig.setMinIdle(redisProperties.getPool().getMinIdle());
jedisPoolConfig.setMaxTotal(redisProperties.getPool().getMaxActive());
jedisPoolConfig
.setMaxWaitMillis(redisProperties.getPool().getMaxWait());
jedisPoolConfig.setTestOnBorrow(true);
jedisConnectionFactory.setPoolConfig(jedisPoolConfig);
return jedisConnectionFactory;
}
public RedisTemplate buildRedisTemplate(
RedisConnectionFactory redisConnectionFactory) {
RedisSerializer redisSerializer = new StringRedisSerializer();
RedisTemplate redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(redisSerializer);
redisTemplate.setValueSerializer(redisSerializer);
redisTemplate.setHashKeySerializer(redisSerializer);
redisTemplate.setHashValueSerializer(redisSerializer);
return redisTemplate;
}
}
主节点连接池构建:
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
/**
* master主机,用于写
*/
@Configuration
@EnableCaching
public class MasterRedisConf extends RedisConfig {
@Primary
@Bean(name = "masterJedisConnectionFactory")
@Override
public JedisConnectionFactory getRedisConnFactory(
@Qualifier("masterRedisProperties") RedisProperties redisProperties) {
return super.getRedisConnFactory(redisProperties);
}
@Bean(name = "masterRedisTemplate")
@Override
public RedisTemplate<Object, Object> buildRedisTemplate(
@Qualifier("masterJedisConnectionFactory") RedisConnectionFactory redisConnectionFactory) {
return super.buildRedisTemplate(redisConnectionFactory);
}
@Bean(name = "masterRedisProperties")
@ConfigurationProperties(prefix = "spring.redis.master")
public RedisProperties getBaseDBProperties() {
return new RedisProperties();
}
}
从节点连接池构建:
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
/**
* slave主机,用于读
*/
@Configuration
@EnableCaching
public class SlaveRedisConf extends RedisConfig {
@Bean(name = "slaveJedisConnectionFactory")
@Override
public JedisConnectionFactory getRedisConnFactory(
@Qualifier("slaveRedisProperties") RedisProperties redisProperties) {
return super.getRedisConnFactory(redisProperties);
}
@Bean(name = "slaveRedisTemplate")
@Override
public RedisTemplate<Object, Object> buildRedisTemplate(
@Qualifier("slaveJedisConnectionFactory") RedisConnectionFactory redisConnectionFactory) {
return super.buildRedisTemplate(redisConnectionFactory);
}
@Bean(name = "slaveRedisProperties")
@ConfigurationProperties(prefix = "spring.redis.slave")
public RedisProperties getBaseDBProperties() {
return new RedisProperties();
}
}
哨兵:故障自动切换
什么是哨兵
「主从复制:数据备份」不仅节省了数据恢复的时间,还能提升性能,那它有什么问题吗?
当 master 宕机时,我们需要「手动」把 slave 提升为 master,这个过程也是需要花费时间的。
那么能不能把这个切换的过程,变成自动化呢?
对于这种情况,我们需要一个「故障自动切换」机制,这就是我们经常听到的「哨兵」所具备的能力。
现在,我们可以引入一个「观察者」,让这个观察者去实时监测 master 的健康状态,这个观察者就是「哨兵」。
具体如何做?
- 哨兵每间隔一段时间,询问 master 是否正常
- master 正常回复,表示状态正常,回复超时表示异常
- 哨兵发现异常,发起主从切换
有了这个方案,就不需要人去介入处理了,一切就变得自动化了。
但这里还有一个问题,如果 master 状态正常,但这个哨兵在询问 master 时,它们之间的网络发生了问题,那这个哨兵可能会误判。
这个问题怎么解决?
答案是,我们可以部署多个哨兵,让它们分布在不同的机器上,它们一起监测 master 的状态,流程就变成了这样:
- 多个哨兵每间隔一段时间,询问 master 是否正常
- master 正常回复,表示状态正常,回复超时表示异常
- 一旦有一个哨兵判定 master 异常(不管是否是网络问题),就询问其它哨兵,如果多个哨兵(设置一个阈值)都认为 master 异常了,这才判定 master 确实发生了故障
- 当判定 master 故障后,哨兵发起主从切换,修改redis.conf配置文件
哨兵协商判定 master 异常后,这里还有一个问题:由哪个哨兵来发起主从切换呢?
答案是,选出一个哨兵「领导者」,由这个领导者进行主从切换。
在选举哨兵领导者时,我们可以制定这样一个选举规则:
- 每个哨兵都询问其它哨兵,请求对方为自己投票
- 每个哨兵只投票给第一个请求投票的哨兵,且只能投票一次
- 首先拿到超过半数投票的哨兵,当选为领导者,发起主从切换
其实,这个选举的过程就是我们经常听到的:分布式系统领域中的「共识算法」。
什么是共识算法?
我们在多个机器部署哨兵,它们需要共同协作完成一项任务,所以它们就组成了一个「分布式系统」。
在分布式系统领域,多个节点如何就一个问题达成共识的算法,就叫共识算法。
在这个场景下,多个哨兵共同协商,选举出一个都认可的领导者,就是使用共识算法完成的。
这个算法还规定节点的数量必须是奇数个,这样可以保证系统中即使有节点发生了故障,剩余超过「半数」的节点状态正常,依旧可以提供正确的结果,也就是说,这个算法还兼容了存在故障节点的情况。
共识算法在分布式系统领域有很多,例如 zookeeper,哨兵选举领导者这个场景,使用的是 Raft 共识算法,因为它足够简单,且易于实现。
现在,我们用多个哨兵共同监测 Redis 的状态,这样一来,就可以避免误判的问题了,架构模型就变成了这样:
怎么配置哨兵
准备3台redis服务器,1主2从,在此基础上我们搭建哨兵。
服务类型 | IP地址 | 端口 |
---|---|---|
Redis (主) | 192.168.159.101 | 6379 |
Redis(从) | 192.168.159.102 | 6379 |
Redis(从) | 192.168.159.103 | 6379 |
Sentinel | 192.168.159.101 | 26379 |
Sentinel | 192.168.159.102 | 26379 |
Sentinel | 192.168.159.103 | 26379 |
在 Redis 安装目录下可以找到 sentinel.conf 文件,然后对其进行修改(每一个哨兵的配置都是一样的)。
1.移动配置文件sentinel.conf到安装目录下
mv /home/redis-4.0.8/sentinel.conf /usr/local/redis/etc
2.修改配置文件
vi /usr/local/redis/etc/sentinel.conf
#禁止保护模式
protected-mode no
#设置后台运行(直接添加)
daemonize yes
#设置日志保存路径
dir tmp/ 改成 dir /usr/local/redis/etc/
#设置日志文件(直接添加)
logfile "sentinel.log"
#配置监听的主服务器,这里 sentinel monitor 代表监控
#mymaster代表服务器名称,可以自定义
#192.168.11.128代表监控的主服务器
#6379代表端口u
#2代表只有两个或者两个以上的哨兵认为主服务器不可用的时候,才会做故障切换操作
sentinel monitor mymaster 192.168.159.101 6379 2
#sentinel auth-pass 定义服务的密码
#mymaster服务名称
#abcdefg Redis服务器密码
sentinel auth-pass mymaster ******
# master或slave多长时间(默认30秒)不能使用后标记为s_down状态。
sentinel down-after-milliseconds mymaster 5000
3、设置开机启动
vi /etc/init.d/redis
#在EXEC后面添加如下配置
SlEXEC=/usr/local/redis/bin/redis-sentinel
#在CONF后面添加如下配置
SlCONF="/usr/local/redis/etc/sentinel.conf"
#在 $EXEC $CONF后面添加
$SlEXEC $SlCONF
重启服务器后,查看哨兵是否有正常运行:
哨兵功能验证
1、在3台redis服务器上分别打开redis客户端 /usr/local/redis/bin/redis-cli
2、通过 info replication命令查看当前角色,可以看到159.101服务器为master,其他2台为slave
3、关闭159.101的redis服务,再通过info replication命令查看102、103服务,可以看到102变成master,并且能够设置key
4、重启101的redis服务后, 102仍然为master
5、关闭102主机,103变成master
SpringBoot使用哨兵
在springboot中引入哨兵模式比较简单,只需要在配置文件中增加如下配置,使用与【单机版Redis】中【springboot整合redis】一样。
spring.redis.sentinel.master=mymaster
spring.redis.sentinel.nodes=192.168.159.101:26379,192.168.159.102:26379,192.168.159.103:26379
详细配置如下:
server:
port: 8080
spring:
#redis配置
redis:
#Redis服务器地址(任意填其中一个即可)
host: 192.168.159.101
#Redis服务器连接端口
port: 6379
#Redis数据库索引(默认为0)
database: 0
#密码
password: ******
## 连接超时时间(毫秒)
timeout: 60000
pool:
## 连接池最大连接数(使用负值表示没有限制)
max-active: 300
## 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1
## 连接池中的最大空闲连接
max-idle: 100
## 连接池中的最小空闲连接
min-idle: 20
#哨兵的配置列表
sentinel:
master: mymaster
##哨兵集群
nodes: 192.168.159.101:26379,192.168.159.102:26379,192.168.159.103:26379
分片集群:横向扩展
随着时间的发展,你的业务体量开始迎来了爆炸性增长,此时你的架构模型,还能够承担这么大的流量吗?
我们一起来分析一下:
- 稳定性:Redis 故障宕机,我们有哨兵 + 副本,可以自动完成主从切换
- 性能:读请求量增长,我们可以再部署多个 slave,读写分离,分担读压力
- 性能:写请求量增长,但我们只有一个 master 实例,这个实例达到瓶颈怎么办?
看到了么,当你的写请求量越来越大时,一个 master 实例可能就无法承担这么大的写流量了。
要想完美解决这个问题,此时你就需要考虑使用「分片集群」了。
什么是「分片集群」?
简单来讲,一个实例扛不住写压力,那我们是否可以部署多个实例,然后把这些实例按照一定规则组织起来,把它们当成一个整体,对外提供服务,这样不就可以解决集中写一个实例的瓶颈问题吗?
所以,现在的架构模型就变成了这样:
现在问题又来了,这么多实例如何组织呢?
我们制定规则如下:
- 每个节点各自存储一部分数据,所有节点数据之和才是全量数据
- 制定一个路由规则,对于不同的 key,把它路由到固定一个实例上进行读写
而分片集群根据路由规则所在位置的不同,还可以分为两大类:
- 客户端分片
- 服务端分片
客户端分片指的是,key 的路由规则放在客户端来做,就是下面这样:
这个方案的缺点是,客户端需要维护这个路由规则,也就是说,你需要把路由规则写到你的业务代码中。
如何做到不把路由规则耦合在业务代码中呢?
你可以这样优化,把这个路由规则封装成一个模块,当需要使用时,集成这个模块就可以了。
这就是 Redis Cluster 的采用的方案。
Redis Cluster 内置了哨兵逻辑,无需再部署哨兵。
当你使用 Redis Cluster 时,你的业务应用需要使用配套的 Redis SDK,这个 SDK 内就集成好了路由规则,不需要你自己编写了。
再来看服务端分片。
这种方案指的是,路由规则不放在客户端来做,而是在客户端和服务端之间增加一个「中间代理层」,这个代理就是我们经常听到的 Proxy。
而数据的路由规则,就放在这个 Proxy 层来维护。
这样一来,你就无需关心服务端有多少个 Redis 节点了,只需要和这个 Proxy 交互即可。
Proxy 会把你的请求根据路由规则,转发到对应的 Redis 节点上,而且,当集群实例不足以支撑更大的流量请求时,还可以横向扩容,添加新的 Redis 实例提升性能,这一切对于你的客户端来说,都是透明无感知的。
业界开源的 Redis 分片集群方案,例如 Twemproxy、Codis 就是采用的这种方案。
cluster集群特点
- 多个redis节点网络互联,数据共享
- 所有的节点都是一主一从(也可以是一主多从),其中从不提供服务,仅作为备用
- 不支持同时处理多个key(如MSET/MGET),因为redis需要把key均匀分布在各个节点上,并发量很高的情况下同时创建key-value会降低性能并导致不可预测的行为
- 支持在线增加、删除节点
- 客户端可以连接任何一个主节点进行读
集群配置
1、环境准备
准备3台服务器,分别安装2个Redis。
服务类型 | IP地址 | 端口 |
---|---|---|
Redis | 192.168.159.101 | 7001,7002 |
Redis | 192.168.159.102 | 7003,7004 |
Redis | 192.168.159.103 | 7005,7006 |
以101为例:
1)创建文件夹
mkdir /usr/local/redis/etc/cluster
cp /usr/local/redis/etc/redis.conf /usr/local/redis/etc/cluster/redis_7001.conf
cp /usr/local/redis/etc/redis.conf /usr/local/redis/etc/cluster/redis_7002.conf
mkdir /usr/local/redis/etc/cluster/redis_7001
mkdir /usr/local/redis/etc/cluster/redis_7002
2)修改配置文件
# vi /usr/local/redis/etc/cluster/redis_7001.conf
bind 192.168.159.101
port 7001
daemonize yes
pidfile "/var/run/redis_7001.pid"
logfile "/usr/local/redis/etc/cluster/redis_7001/redis.log"
dir "/usr/local/redis/etc/cluster/redis_7001"
masterauth "******"
requirepass "******"
appendonly yes
cluster-enabled yes
cluster-config-file nodes_7001.conf
cluster-node-timeout 15000
# slaveof 192.168.159.103 6379
7002也按照上述修改配置。
3)启动redis服务
将上述6个redis全部启动。
/usr/local/redis/bin/redis-server /usr/local/redis/etc/cluster/redis_7001.conf
4)创建集群
选择一台服务器,创建集群
redis版本>=5.xxx,直接使用 ./redis-cli --cluster create 指令构建redis集群。
redis版本<5.xxx,需要安装ruby、rubygems环境,使用 ./redis-trib.rb create 指令构建redis集群
版本>=5.xxx:
/usr/local/redis/bin/redis-cli -a crm2021 --cluster create 192.168.159.101:7001 192.168.159.101:7002 192.168.159.102:7003 192.168.159.102:7004 192.168.159.103:7005 192.168.159.103:7006 --cluster-replicas 1
可以看到集群架构如下:
服务说明 | IP地址 | 端口 | 哈希 |
---|---|---|---|
分片1-主 | 192.168.159.101 | 7001 | 0-5460 |
分片1-从 | 192.168.159.102 | 7004 | |
分片2-主 | 192.168.159.102 | 7003 | 5461-10922 |
分片2-从 | 192.168.159.103 | 7006 | |
分片3-主 | 192.168.159.103 | 7005 | 10923-16383 |
分片3-从 | 192.168.159.101 | 7002 |
集群操作
登陆集群
# /usr/local/redis/bin/redis-cli -c -h 192.168.159.101 -p 7001
查看集群信息
192.168.159.101:7001> CLUSTER INFO
列出节点信息
192.168.159.101:7001> CLUSTER NODES
设置、查看缓存
与单机版一样,直接用set 命令设置,当hash不是属于当前节点时,会自动切到相应的节点
springboot整合redis集群
maven配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
配置文件application.yml
server:
port: 8080
spring:
#redis配置
redis:
#Redis服务器地址(任意选其中一个即可)
# host: 192.168.159.101
#Redis服务器连接端口
# port: 6379
#Redis数据库索引(默认为0)
database: 0
#密码
password: ******
## 连接超时时间(毫秒)
connectTimeout: 60000
## 读取数据超时(毫秒)
soTimeout: 50000
## 超时重试
maxAttempts: 3
pool:
## 连接池最大连接数(使用负值表示没有限制)
maxTotal: 300
## 连接池最大阻塞等待时间(使用负值表示没有限制)
maxWaitMillis: -1
## 连接池中的最大空闲连接
maxIdle: 100
## 连接池中的最小空闲连接
minIdle: 20
#哨兵的配置列表
cluster:
nodes: 192.168.159.101:7001,192.168.159.101:7002,192.168.159.102:7003,192.168.159.102:7004,192.168.159.103:7005,192.168.159.103:7006
配置加载
属性类:
package com.ffcs.crm.redis.util;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
public class RedisProperties {
public Integer getDatabase() {
return database;
}
public void setDatabase(Integer database) {
this.database = database;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Integer getTimeout() {
return timeout;
}
public void setTimeout(Integer timeout) {
this.timeout = timeout;
}
public GenericObjectPoolConfig getPool() {
return pool;
}
public void setPool(GenericObjectPoolConfig pool) {
this.pool = pool;
}
public Cluster getCluster() {
return cluster;
}
public void setCluster(Cluster cluster) {
this.cluster = cluster;
}
private Integer database;
private String password;
private Integer timeout;
private GenericObjectPoolConfig pool;
private Cluster cluster;
private Integer connectTimeout;
private Integer soTimeout;
private Integer maxAttempts;
public Integer getConnectTimeout() {
return connectTimeout;
}
public void setConnectTimeout(Integer connectTimeout) {
this.connectTimeout = connectTimeout;
}
public Integer getSoTimeout() {
return soTimeout;
}
public void setSoTimeout(Integer soTimeout) {
this.soTimeout = soTimeout;
}
public Integer getMaxAttempts() {
return maxAttempts;
}
public void setMaxAttempts(Integer maxAttempts) {
this.maxAttempts = maxAttempts;
}
public static class Cluster {
private String nodes;
public String getNodes() {
return nodes;
}
public void setNodes(String nodes) {
this.nodes = nodes;
}
}
}
配置类:
package com.ffcs.crm.redis.util;
import java.util.HashSet;
import java.util.Set;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;
@Configuration
public class RedisConfig {
@Bean(name = "redisProperties")
@ConfigurationProperties(prefix = "spring.redis")
public RedisProperties getProperties() {
return new RedisProperties();
}
@Bean(name = "jedisCluster")
public JedisCluster getJedisCluster(@Qualifier("redisProperties") RedisProperties redisProperties) {
String[] redisnodes = redisProperties.getCluster().getNodes().split(",");
Set<HostAndPort> nodes = new HashSet<>();
for(String node:redisnodes) {
String[] arr=node.split(":");
HostAndPort hostAndPort = new HostAndPort(arr[0], Integer.parseInt(arr[1]));
nodes.add(hostAndPort);
}
JedisCluster cluster = new JedisCluster(nodes,redisProperties.getConnectTimeout(),redisProperties.getSoTimeout(),
redisProperties.getMaxAttempts(),redisProperties.getPassword(),redisProperties.getPool());
return cluster;
}
}
工具类
package com.ffcs.crm.redis.util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import redis.clients.jedis.JedisCluster;
//工具类中使用Autowired注解需要加上Compoent
@Component
public class RedisUtil {
@Autowired
private JedisCluster jedisCluster;
// 从redis中获取值
public Object get(String key) {
return jedisCluster.get(key);
}
// 判断是否存在key
public boolean hasKey(String key) {
return jedisCluster.exists(key);
}
// 向redis插入值
public boolean set(final String key, String value) {
boolean result = false;
try {
jedisCluster.set(key, value);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
}
参考文章
https://mp.weixin.qq.com/s/RDyZUdk9DMDUdLpRG_o9oQ