Redis---主从与哨兵架构详解(学习日记第十二期)

Redis主从架构
redis主从架构搭建,配置从节点步骤:
1 1、复制一份redis.conf文件
2
3 2、将相关配置修改为如下值:
4 port 6380
5 pidfile /var/run/redis_6380.pid # 把pid进程号写入pidfile配置的文件
6 logfile "6380.log"
7 dir /usr/local/redis‐5.0.3/data/6380 # 指定数据存放目录
8 # 需要注释掉bind
9 # bind 127.0.0.1(bind绑定的是自己机器网卡的ip,如果有多块网卡可以配多个ip,代表允许客户端通
过机器的哪些网卡ip去访问,内网一般可以不配置bind,注释掉即可)
10
11 3、配置主从复制
12 replicaof 192.168.0.60 6379 # 从本机6379的redis实例复制数据,Redis 5.0之前使用slaveof
13 replica‐read‐only yes # 配置从节点只读
14
15 4、启动从节点
16 redis‐server redis.conf
17
18 5、连接从节点
19 redis‐cli ‐p 6380
20
21 6、测试在6379实例上写数据,6380实例是否能及时同步新修改数据
22
23 7、可以自己再配置一个6381的从节点
Redis主从工作原理
如果你为master配置了一个slave,不管这个slave是否是第一次连接上Master,它都会 发送一个PSYNC 命令给master请求复制数据
master收到PSYNC命令后,会在 后台进行数据持久化通过bgsave生成最新的rdb快照文件 ,持久化期 间,master会 继续接收 客户端的请求,它会把这些 可能修改数据集的请求缓存在内存中 。当 持久化进行完毕以后 master会把这份rdb文件数据集发送给slave,slave会把接收到的数据进行持久化生成rdb,然后 再加载到内存中 。然后,master再将之前 缓存在内存中的命令 发送给slave。
当master与slave之间的连接由于某些原因而 断开 时, slave能够自动重连Master ,如果master收到了 多 个slave并发连接请求,它只会进行一次持久化 ,而不是一个连接一次,然后 再把这一份持久化的数据发送 给多个并发连接的slave
主从复制(全量复制)流程图:

有全量复制那就有部分复制。

数据部分复制
当master和slave 断开重连 后,一般都会对整份数据进行复制。但从 redis2.8版本 开始,redis改用可以支 持 部分数据复制的命令PSYNC去master同步数据 ,slave与master能够在 网络连接断开重连后只进行部分 数据复制(断点续传)
master会在其内存中创建一个 复制数据用的缓存队列 ,缓存最近一段时间的数据,master和它所有的 slave都维护了 复制的数据下标offset和master的进程id ,因此,当网络连接断开后, slave会请求master 继续进行未完成的复制,从所记录的数据下标开始 。如果master进 程id变化了 ,或者 从节点数据下标 offset太旧 ,已经不在master的缓存队列里了,那么将会进行一次 全量数据的复制
主从复制(部分复制,断点续传)流程图:
如果有很多从节点,为了缓解 主从复制风暴 (多个从节点同时复制主节点导致主节点压力过大),可以做如 下架构,让部分从节点与从节点(与主节点同步)同步数据
这里讲一下应用中Java怎么使用的Redis
Jedis连接代码示例:
1、引入相关依赖:
1 <dependency>
2 <groupId>redis.clients</groupId>
3 <artifactId>jedis</artifactId>
4 <version>2.9.0</version>
5 </dependency>
访问代码:
1 public class JedisSingleTest {
2 public static void main(String[] args) throws IOException {
3
4 JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
5 jedisPoolConfig.setMaxTotal(20);
6 jedisPoolConfig.setMaxIdle(10);
7 jedisPoolConfig.setMinIdle(5);
8
9 // timeout,这里既是连接超时又是读写超时,从Jedis 2.8开始有区分connectionTimeout和soTimeou
t的构造函数
10 JedisPool jedisPool = new JedisPool(jedisPoolConfig, "192.168.0.60", 6379, 3000,
null);
11
12 Jedis jedis = null;
13 try {
14 //从redis连接池里拿出一个连接执行命令
15 jedis = jedisPool.getResource();
16
17 System.out.println(jedis.set("single", "zhuge"));
18 System.out.println(jedis.get("single"));
19
20 //管道示例
21 //管道的命令执行方式:cat redis.txt | redis‐cli ‐h 127.0.0.1 ‐a password ‐ p 6379 ‐‐pi
pe
22 /*Pipeline pl = jedis.pipelined();
23 for (int i = 0; i < 10; i++) {
24 pl.incr("pipelineKey");
25 pl.set("zhuge" + i, "zhuge");
26 }
27 List<Object> results = pl.syncAndReturnAll();
28 System.out.println(results);*/
29
30 //lua脚本模拟一个商品减库存的原子操作
31 //lua脚本命令执行方式:redis‐cli ‐‐eval /tmp/test.lua , 10
32 /*jedis.set("product_count_10016", "15"); //初始化商品10016的库存
33 String script = " local count = redis.call('get', KEYS[1]) " +
34 " local a = tonumber(count) " +
35 " local b = tonumber(ARGV[1]) " +
36 " if a >= b then " +
37 " redis.call('set', KEYS[1], a‐b) " +
38 " return 1 " +
39 " end " +
40 " return 0 ";
41 Object obj = jedis.eval(script, Arrays.asList("product_count_10016"),
Arrays.asList("10"));
42 System.out.println(obj);*/
43
44 } catch (Exception e) {
45 e.printStackTrace();
46 } finally {
47 //注意这里不是关闭连接,在JedisPool模式下,Jedis会被归还给资源池。
48 if (jedis != null)
49 jedis.close();
50 }
51 }
52 }
顺带讲下redis管道与调用lua脚本,代码示例上面已经给出:
管道(Pipeline)
客户端可以一次性发送多个请求而不用等待服务器的响应,待所有命令都发送完后再一次性读取服务的响 应,这样可以极大的降低多条命令执行的网络传输开销,管道执行多条命令的网络开销实际上只相当于一 次命令执行的网络开销。需要注意到是用pipeline方式打包命令发送,redis必须在处理完所有命令前先缓 存起所有命令的处理结果 。打包的命令越多,缓存消耗内存也越多。所以并不是打包的命令越多越好。 pipeline中发送的每个command都会被server立即执行,如果执行失败,将会在此后的响应中得到信 息;也就是pipeline并不是表达“所有command都一起成功”的语义,管道中前面命令失败,后面命令 不会有影响,继续执行。
详细代码示例见上面jedis连接示例:
1 Pipeline pl = jedis.pipelined();
2 for (int i = 0; i < 10; i++) {
3 pl.incr("pipelineKey");
4 pl.set("zhuge" + i, "zhuge");
5 //模拟管道报错
6 // pl.setbit("zhuge", ‐1, true);
7 }
8 List<Object> results = pl.syncAndReturnAll();
9 System.out.println(results);
Redis Lua脚本(放在后面Redis高并发分布式锁实战课里详细讲)
Redis在2.6推出了脚本功能,允许开发者使用Lua语言编写脚本传到Redis中执行。使用脚本的好处如下:
1、 减少网络开销 :本来5次网络请求的操作,可以用一个请求完成,原先5次请求的逻辑放在redis服务器 上完成。使用脚本,减少了网络往返时延。这点跟管道类似
2、 原子操作 :Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。 管道不是原子的,不过 redis的批量操作命令(类似mset)是原子的。
3、 替代redis的事务功能 :redis自带的事务功能很鸡肋,而redis的lua脚本几乎实现了常规的事务功能, 官方推荐如果要使用redis的事务功能可以用redis lua替代。
官网文档上有这样一段话:
1 A Redis script is transactional by definition, so everything you can do with a Redis t
ransaction, you can also do with a script,
2 and usually the script will be both simpler and faster.
从Redis2.6.0版本开始,通过内置的Lua解释器,可以使用EVAL命令对Lua脚本进行求值。EVAL命令的格 式如下:
EVAL script numkeys key [key ...] arg [arg ...]
script参数是一段Lua脚本程序,它会被运行在Redis服务器上下文中,这段脚本 不必(也不应该)定义为一 个Lua函数 。numkeys参数用于指定键名参数的个数。键名参数 key [key ...] 从EVAL的第三个参数开始算
起,表示在脚本中所用到的那些Redis键(key),这些键名参数可以在 Lua中通过全局变量KEYS数组,用1 为基址的形式访问( KEYS[1] , KEYS[2] ,以此类推)。
在命令的最后,那些不是键名参数的附加参数 arg [arg ...] ,可以在Lua中通过全局变量 ARGV 数组访问, 访问的形式和KEYS变量类似( ARGV[1] 、 ARGV[2] ,诸如此类)。例如
1 127.0.0.1:6379> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first seco
nd
2 1) "key1"
3 2) "key2"
4 3) "first"
5 4) "second"
其中 "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 是被求值的Lua脚本,数字2指定了键名参数的数 量, key1和key2是键名参数,分别使用 KEYS[1] 和 KEYS[2] 访问,而最后的 first 和 second 则是附加 参数,可以通过 ARGV[1] 和 ARGV[2] 访问它们。
在 Lua 脚本中,可以使用 redis.call() 函数来执行Redis命令
Jedis调用示例详见上面jedis连接示例:
2 jedis.set("product_stock_10016", "15"); //初始化商品10016的库存
3 String script = " local count = redis.call('get', KEYS[1]) " +
4 " local a = tonumber(count) " +
5 " local b = tonumber(ARGV[1]) " +
6 " if a >= b then " +
7 " redis.call('set', KEYS[1], a‐b) " +
8 " return 1 " +
9 " end " +
10 " return 0 ";
11 Object obj = jedis.eval(script, Arrays.asList("product_stock_10016"), Arrays.asList("1
0"));
12 System.out.println(obj);
注意,不要在Lua脚本中出现死循环和耗时的运算,否则redis会阻塞,将不接受其他的命令, 所以使用 时要注意不能出现死循环、耗时的运算。redis是单进程、单线程执行脚本。管道不会阻塞redis。
Redis哨兵高可用架构
sentinel哨兵是特殊的redis服务,不提供读写服务,主要用来监控redis实例节点。
哨兵架构下client端第一次从哨兵找出redis的主节点,后续就直接访问redis的主节点,不会每次都通过 sentinel代理访问redis的主节点,当redis的主节点发生变化,哨兵会第一时间感知到,并且将新的redis 主节点通知给client端(这里面redis的client端一般都实现了订阅功能,订阅sentinel发布的节点变动消息)
redis哨兵架构搭建步骤:
1 1、复制一份sentinel.conf文件
2 cp sentinel.conf sentinel‐26379.conf
3
4 2、将相关配置修改为如下值:
5 port 26379
6 daemonize yes
7 pidfile "/var/run/redis‐sentinel‐26379.pid"
8 logfile "26379.log"
9 dir "/usr/local/redis‐5.0.3/data"
10 # sentinel monitor <master‐redis‐name> <master‐redis‐ip> <master‐redis‐port> <quorum>
11 # quorum是一个数字,指明当有多少个sentinel认为一个master失效时(值一般为:sentinel总数/2 +
1),master才算真正失效
12 sentinel monitor mymaster 192.168.0.60 6379 2 # mymaster这个名字随便取,客户端访问时会用
到
13
14 3、启动sentinel哨兵实例
15 src/redis‐sentinel sentinel‐26379.conf
16
17 4、查看sentinel的info信息
18 src/redis‐cli ‐p 26379
19 127.0.0.1:26379>info
20 可以看到Sentinel的info里已经识别出了redis的主从
21
22 5、可以自己再配置两个sentinel,端口26380和26381,注意上述配置文件里的对应数字都要修改
sentinel集群都启动完毕后,会将哨兵集群的元数据信息写入所有sentinel的配置文件里去(追加在文件的 最下面),我们查看下如下配置文件sentinel-26379.conf,如下所示:
1 sentinel known‐replica mymaster 192.168.0.60 6380 #代表redis主节点的从节点信息
2 sentinel known‐replica mymaster 192.168.0.60 6381 #代表redis主节点的从节点信息
3 sentinel known‐sentinel mymaster 192.168.0.60 26380 52d0a5d70c1f90475b4fc03b6ce7c3c569
35760f #代表感知到的其它哨兵节点
4 sentinel known‐sentinel mymaster 192.168.0.60 26381 e9f530d3882f8043f76ebb8e1686438ba8
bd5ca6 #代表感知到的其它哨兵节点
当redis主节点如果挂了,哨兵集群会重新选举出新的redis主节点,同时会修改所有sentinel节点配置文件 的集群元数据信息,比如6379的redis如果挂了,假设选举出的新主节点是6380,则sentinel文件里的集 群元数据信息会变成如下所示:
1 sentinel known‐replica mymaster 192.168.0.60 6379 #代表主节点的从节点信息
2 sentinel known‐replica mymaster 192.168.0.60 6381 #代表主节点的从节点信息
3 sentinel known‐sentinel mymaster 192.168.0.60 26380 52d0a5d70c1f90475b4fc03b6ce7c3c569
35760f #代表感知到的其它哨兵节点
4 sentinel known‐sentinel mymaster 192.168.0.60 26381 e9f530d3882f8043f76ebb8e1686438ba8
bd5ca6 #代表感知到的其它哨兵节点
同时还会修改sentinel文件里之前配置的mymaster对应的6379端口,改为6380
1 sentinel monitor mymaster 192.168.0.60 6380 2
当6379的redis实例再次启动时,哨兵集群根据集群元数据信息就可以将6379端口的redis节点作为从节点 加入集群
哨兵的Jedis连接代码:
1 public class JedisSentinelTest {
2 public static void main(String[] args) throws IOException {
3
4 JedisPoolConfig config = new JedisPoolConfig();
5 config.setMaxTotal(20);
6 config.setMaxIdle(10);
7 config.setMinIdle(5);
8
9 String masterName = "mymaster";
10 Set<String> sentinels = new HashSet<String>();
11 sentinels.add(new HostAndPort("192.168.0.60",26379).toString());
12 sentinels.add(new HostAndPort("192.168.0.60",26380).toString());
13 sentinels.add(new HostAndPort("192.168.0.60",26381).toString());
14 //JedisSentinelPool其实本质跟JedisPool类似,都是与redis主节点建立的连接池
15 //JedisSentinelPool并不是说与sentinel建立的连接池,而是通过sentinel发现redis主节点并与其
建立连接
16 JedisSentinelPool jedisSentinelPool = new JedisSentinelPool(masterName, sentinels, co
nfig, 3000, null);
17 Jedis jedis = null;
18 try {
19 jedis = jedisSentinelPool.getResource();
20 System.out.println(jedis.set("sentinel", "zhuge"));
21 System.out.println(jedis.get("sentinel"));
22 } catch (Exception e) {
23 e.printStackTrace();
24 } finally {
25 //注意这里不是关闭连接,在JedisPool模式下,Jedis会被归还给资源池。
26 if (jedis != null)
27 jedis.close();
28 }
29 }
30 }
哨兵的Spring Boot整合Redis连接
1、引入相关依赖:
<dependency>
2 <groupId>org.springframework.boot</groupId>
3 <artifactId>spring‐boot‐starter‐data‐redis</artifactId>
4 </dependency>
5
6 <dependency>
7 <groupId>org.apache.commons</groupId>
8 <artifactId>commons‐pool2</artifactId>
9 </dependency>
springboot项目核心配置:
1 server:
2 port: 8080
3
4 spring:
5 redis:
6 database: 0
7 timeout: 3000
8 sentinel: #哨兵模式
9 master: mymaster #主服务器所在集群名称
10 nodes: 192.168.0.60:26379,192.168.0.60:26380,192.168.0.60:26381
11 lettuce:
12 pool:
13 max‐idle: 50
14 min‐idle: 10
15 max‐active: 100
16 max‐wait: 1000
访问代码:
1 @RestController
2 public class IndexController {
3
4 private static final Logger logger = LoggerFactory.getLogger(IndexController.class);
5
6 @Autowired
7 private StringRedisTemplate stringRedisTemplate;
8
9 /**
10 * 测试节点挂了哨兵重新选举新的master节点,客户端是否能动态感知到
11 * 新的master选举出来后,哨兵会把消息发布出去,客户端实际上是实现了一个消息监听机制,
12 * 当哨兵把新master的消息发布出去,客户端会立马感知到新master的信息,从而动态切换访问的maste
rip
13 *
14 * @throws InterruptedException
15 */
16 @RequestMapping("/test_sentinel")
17 public void testSentinel() throws InterruptedException {
18 int i = 1;
19 while (true){
20 try {
21 stringRedisTemplate.opsForValue().set("zhuge"+i, i+"");
22 System.out.println("设置key:"+ "zhuge" + i);
23 i++;
24 Thread.sleep(1000);
25 }catch (Exception e){
26 logger.error("错误:", e);
27 }
28 }
29 }
30 }
StringRedisTemplate与RedisTemplate详解
spring 封装了 RedisTemplate 对象来进行对redis的各种操作,它支持所有的 redis 原生的 api。在
RedisTemplate中提供了几个常用的接口方法的使用,分别是:
1 private ValueOperations<K, V> valueOps;
2 private HashOperations<K, V> hashOps;
3 private ListOperations<K, V> listOps;
4 private SetOperations<K, V> setOps;
5 private ZSetOperations<K, V> zSetOps;
RedisTemplate中定义了对5种数据结构操作
1 redisTemplate.opsForValue();//操作字符串
2 redisTemplate.opsForHash();//操作hash
3 redisTemplate.opsForList();//操作list
4 redisTemplate.opsForSet();//操作set
5 redisTemplate.opsForZSet();//操作有序set
StringRedisTemplate继承自RedisTemplate,也一样拥有上面这些操作。
StringRedisTemplate默认采用的是String的序列化策略,保存的key和value都是采用此策略序列化保存 的。
RedisTemplate默认采用的是JDK的序列化策略,保存的key和value都是采用此策略序列化保存的。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值