目录
2.类似于java运行时异常(如1/0,除数不能为0错误) (逻辑错误)
监控 Watch(相当于java加锁) (面试: redis的watch命令监控实现秒杀系统)
1.无论第一步是先删除缓存还是先修改数据库都会导致脏数据的出现
1.redisson实现的分布式锁的执行流程/合理地控制锁的有效时长(失效时间)
一主二从集群搭建(命令或文件配置)(这种方式的redis集群实际工作用不到,仅供基础学习)
文件方式配置(一主二从,持久化的,对于哨兵模式,不建议使用这种)
Redis 是单线程的!为什么
Redis是基于内存实现的,使用Redis时,CPU几乎不会成为Redis性能瓶颈,Redis的瓶颈是机器的内存和网络带宽(网络),既然可以使用单线程来实现,就使用单线程了!所有就使用了单线程了!
内存访问速度:由于Redis将数据存储在内存中,数据访问速度非常快,通常接近于CPU的缓存访问速度。这意味着CPU在读取或写入数据时很少需要等待,从而减少了CPU的空闲时间。
计算密集度:Redis的操作通常是简单的数据查找、插入、删除和计算集合操作(如交集、并集等)。这些操作在CPU层面上的计算复杂度相对较低,因此不太可能使CPU成为瓶颈。
并发处理:Redis使用单线程模型来处理客户端请求(尽管有IO多路复用技术来同时处理多个连接),但这并不意味着Redis不能利用多核CPU。通过部署多个Redis实例或使用Redis集群,可以水平扩展以利用多核CPU的并行处理能力。
Redis 是C 语言写的,官方提供的数据为 100000+ 的QPS,完全不比同样是使用 key-vale的Memecache差!
Redis 为什么单线程还这么快?
1、高性能的服务器不一定是多线程的
2、多线程不一定比单线程效率高!(多线程CPU上下文会切换消耗资源!)
速度:CPU>内存>硬盘
核心:redis 是将所有的数据全部放在内存中的,所以说使用单线程去操作效率就是最高的,多线程(CPU上下文会切换:耗时的操作!)
对于内存系统来说,如果没有上下文切换效率就是最高的!
多次读写都是在一个CPU上的,在内存情况下,这个就是最佳的方案!
Redis-Key(操作redis的key命令)
keys * # 查看所有的key
set name123 kuangshen # set key
EXISTS name123 # 判断当前的key是否存在
move name123 # 移除当前的key
EXPIRE name123 10 # 设置key的过期时间,单位是秒
ttl name123 # 查看当前key的剩余时间
type name123 # 查看当前key的一个类型
String
扩展字符串操作命令
- APPEND key123 "hello" # 追加字符串,如果当前key不存在,就相当于setkey
- STRLEN key123 # 获取字符串的长度!
数字增长命令
- set views123 0 # 初始浏览量为0
- incr views123 # 自增1 浏览量变为1
- decr views123 # 自减1 浏览量-1
- INCRBY views123 10 # 可以设置步长,指定增量10!
字符串范围range命令
- GETRANGE key123 0 3 # 截取字符串 [0,3]
- GETRANGE key123 0 -1 # 获取全部的字符串 和 get key是一样的
- SETRANGE key123 1 xx # 替换指定位置1开始的字符串!
设置过期时间命令
setex (set with expire) //设置过期时间
例:setex key123 30 "hello" //设置key123 的值为hello,30秒后过期
setnx (set if not exist) //key不存在在设置,key存在则回滚设置失败(在分布式锁中会常常使用)
例:setnx mykey123 "MongoDB" //如果mykey不存在创建成功,存在,创建失败不会替换值
批量设置值
- mset k1 v1 k2 v2 k3 v3 # 同时设置多个key和value值(k1:v1, k2:v2, k3:v3)
- mget k1 k2 k3 # 根据多个key同时获取多个值
- msetnx k1 v1 k4 v4 # msetnx 是一个原子性的操作,要么一起成功,要么一起失败!
-
- 由于k1已经存在,所以setnx一定会失败,由于是原子性操作k4也会跟着失败
string设置对象,但最好使用hash来存储对象
- set user:1 {name:zhangsan,age:3} # 设置一个key为user:1的对象,值为 json字符来保存一个对象!
-
- key值为user:{id}, value值为json
- mset user:1:name zhangsan user:1:age 2 #批量创建对象
-
- 这里的key是一个巧妙的设计: user:{id}:{filed} , 如此设计在Redis中是完全OK了!
- mget user:1:name user:1:age #批量获取对象中的值
组合命令getset,先get然后在set
- getset db redis # 如果不存在值,则返回 nil ,但同时值被设置成了redis
- getset db mongodb # 如果存在值,获取原来的值,并设置新的值
Hash
相当于Map集合,key-value!,只是value存的是map,也就是key-map,值是map集合
- hash本质和String类型没有太大区别,还是一个简单的key-value
- hset myhash field codeyuaiiao
-
- 命令含义:hset key mapkey mapvalue
hash命令:
hash基础的增删查
- hset myhash field1 yuaiiao # 添加或修改一个具体的值
- hget myhash field1 # 获取一个字段值
- hmset myhash field4 hello field5 byebye # 添加多个字段值进map集合
- hmget myhash field3 field4 # 获取多个指定字段值
- hgetall myhash # 获取hash全部字段值(包含了key和value)
- hdel myhash field1 field2 # 删除一个或多个指定字段值
hash扩展命令
- hlen myhash # 获取hash表的字段数量
- hexists myhash field1 # 判断hash表中指定字段是否存在
- hkeys myhash # 获取所有field(相当于key)
- hvals myhash # 获取所有value
- hincrby myhash field3 2 # 指定增量
- hincrby myhash field3 -2 # 指定减量
- hsetnx myhash field4 yuaiiao # 如果不存在可以设置,如果存在则不可设置
总结
hash变更的用户数据user表,name,age字段 ,尤其是用户信息之类的,经常变动的信息!
hash更适合于对象的存储,String更加适合字符串存储
List
Set
Redis的Set结构与java中的HashSet类似,可以看做是一个value为null的HashMap.因为也是一个hash表,因此具备与hashset类似的特征:
无序
元素不可重复
查找快
支持交集,并集,差集等功能
ZSet
Redis事务
mysql事务本质
- ACID特性,要么同时成功,要么同时失败,保证原子性!
Redis事务本质
- 一组命令的集合! 一个事务中的所有命令都会被序列化,在事务执行过程中,会按照顺序执行!
- 一次性,顺序性,排他性! 执行一系列的命令!
------队列 set get set 执行-----
- Redis 事务没有隔离级别的概念!
-
- 不会出现幻度,脏读,不可重复读等问题
- 所有的命令在事务中,并没有直接被执行,只有发起执行命令的时候才会执行! Exec
- Redis单条命令是保证原子性的,但是redis事务是一组命令的集合,所以不保证原子性!
redis的事务命令:
- 开启事务(multi)
- 命令入队(要执行的命令) (事务队列)
- 执行事务(exec)
正常执行事务 exec
127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379> set key hello # 执行命令
QUEUED
127.0.0.1:6379> set key1 yuaiiao
QUEUED
127.0.0.1:6379> get key
QUEUED
127.0.0.1:6379> get key1
QUEUED
127.0.0.1:6379> exec # 执行事务
1) OK
2) OK
3) "hello"
4) "yuaiiao"
127.0.0.1:6379>
放弃事务 discard
127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379> set key 1
QUEUED
127.0.0.1:6379> set key2 3
QUEUED
127.0.0.1:6379> discard # 放弃事务
OK
127.0.0.1:6379> get key2 #事务队列中的命令都不会被执行
(nil)
127.0.0.1:6379>
命令异常,事务回滚
1.类似于java编译型异常(语法错误)
命令语法导致执行错误,事务中所有的命令都不会被执行。
127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379> set k1 v1 # 执行命令
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> getset k3 v3
QUEUED
127.0.0.1:6379> getset k3 # 错误的命令
(error) ERR wrong number of arguments for 'getset' command
127.0.0.1:6379> set k4 v4
QUEUED
127.0.0.1:6379> exec # 执行事务报错
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get k2 # 事务执行失败,得不到值,所有的命令都不会被执行
(nil)
127.0.0.1:6379>
2.类似于java运行时异常(如1/0,除数不能为0错误) (逻辑错误)
命令逻辑执行错误 , 那么执行命令的时候,其他的命令是可以正常执行的,只是错误命令抛出异常!
证明事务不保证原子性
127.0.0.1:6379> set k1 "v1" # 字符串
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incr k1 # 对字符串进行 自增1 运行时异常错误
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> get k2
QUEUED
127.0.0.1:6379> exec # 错误的命令报错但是其余命令都能执行
1) (error) ERR value is not an integer or out of range
2) OK
3) OK
4) "v2"
127.0.0.1:6379> get k2 # 其余命令正常执行
"v2"
127.0.0.1:6379> get k3
"v3"
127.0.0.1:6379>
监控 Watch(相当于java加锁) (面试: redis的watch命令监控实现秒杀系统)
悲观锁
很悲观,认为什么时候都会出问题,无论做什么都会加锁,效率低下
乐观锁
很乐观,认为什么时候都不会出现问题,所以不会上锁,更新数据的时候去判断一下,在此期间是否有人修改过这个数据,使用version字段比较。
Redis用watch做乐观锁实现步骤
1.获取最新version
2.更新的的时候比较version,version没变更新成功,version改变进入自旋。
redis的乐观锁watch
- watch加锁,记得用完需要unwatch解锁
redis监视watch测试案例
取钱正常执行成功的流程
127.0.0.1:6379> set money 100 #存钱100
OK
127.0.0.1:6379> set out 0 #取钱0
OK
127.0.0.1:6379> watch money # 监视money对象
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decrby money 20
QUEUED
127.0.0.1:6379> incrby out 20
QUEUED
127.0.0.1:6379> exec # 事务正常结束 , 期间数据没有发生变动 ,这个时候就正常执行成功了!
1) (integer) 80
2) (integer) 20
127.0.0.1:6379>
取钱出现并发问题的流程
- 测试多线程修改值,使用watch当做redis 的乐观锁操作
-
- 在开一个redis-client客户端,一共有两个客户端
- 客户端1:开启事务,money取钱20
- 客户端2:这时候直接把money修改成1000
-
- 客户端1:继续执行,out存钱20,这时候执行事务会
-
-
- 执行返回nil,修改失败
-
# 客户端1:开启事务,监视money对象,money取钱20
127.0.0.1:6379> watch money # 监视money对象
OK
127.0.1:6379> multi
OK
127.0.0.1:6379> decrby money 20
QUEUED
# 客户端1还未提交,客户端2:这时候直接把money修改成1000
# set money 10000
# 客户端1:继续执行,out存钱20,然后执行事务,乐观锁对比money版本号改动了,执行失败
# 执行之前,另一个线程,修改了我们的值,就会导致事务执行失败!
# 127.0.0.1:6379> get money
"80"
# 127.0.0.1:6379> set money 1000
# OK
127.0.0.1:6379> incrby out 20 #out存钱20
QUEUED
127.0.0.1:6379> exec # 提交事务,监视money的version是否变化,有变化事务回滚,结果返回nil
(nil)
127.0.0.1:6379>
- redis事务执行失败后的自旋步骤
-
- 先释放监控锁watch,在重新获取锁重复以上步骤,进行自旋
127.0.0.1:6379> unwatch # 释放锁(监控),如果发现事务执行失败,就先解锁
OK
127.0.0.1:6379> watch money # 重新获取锁,获取最新的值,再次监视,select version
OK
127.0.0.1:6379> multi # 开启事务执行正常操作
OK
127.0.0.1:6379> decrby money 20
QUEUED
127.0.0.1:6379> incrby out 20
QUEUED
127.0.0.1:6379> exec # 在比对监视的值是否发生了变化,如果没有变化,可以执行成功呢,如果变化了就执行失败,在重新以上步骤
1) (integer) 980
2) (integer) 40
127.0.0.1:6379>
- 如果修改失败,获取最新的值就好
Redis的Java客户端
我们要使用java来操作redis
有springboot整合了,我们也要学习jedis
什么是jedis?
- jedis 是redis官方推荐的java连接开发工具! 使用java操作Redis的中间件 ! 如果你要使用java 操作redis, 那么一定要对jedis十分熟悉
测试jedis
- 导入对应的依赖
<!--导入jedis-->
<dependencies>
<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.3.0</version>
</dependency>
<!--导入 fastjson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.70</version>
</dependency>
</dependencies>
2.编码测试
- 连接redis数据库
- 操作命令
- 断开连接
package com.kuang;
import redis.clients.jedis.Jedis;
public class TestPing {
public static void main(String[] args) {
// 1、 new Jedis 对象即可
Jedis jedis = new Jedis("127.0.0.1", 6379);
// jedis 所有的命令就是我们之前学习的所有指令!所以之前的指令学习很重要!
System.out.println(jedis.ping());
}
}
结果输出:
jedis 所有的命令就是我们之前学习的所有指令!所以之前的指令学习很重要!
Springboot整合Redis
说明 :在 SpringBoot2.x之后, 原来使用的jedis 被替换为了 lettuce ?
-
- jedis :底层采用的直连, 多个线程操作的话 ,是不安全的, 如果想要避免不安全, 使用jedis pool 连接池 ! 更像 BIO模式,阻塞的.
- lettuce : 采用netty , 实例可以再多个线程中进行共享,不存在线程不安全的情况 ! 可以减少线程数量,不需要开连接池, 更像NIO模式非阻塞的
自定义RedisTemplate
redis关于对象的保存,对象需要序列化。
- 对象如果不序列化保存,则会报错
分析redisTemplate源码为什么对象需要序列化
- 分析源码序列化配置
新建config/RedisConfig
- 不使用JDK序列化,key使用哪个string序列化,value使用json序列化
package com.kuang.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
// 这是我给大家写好的一个固定模板,大家在企业中,拿去就可以直接使用!
// 自己定义了一个 RedisTemplate
@Bean
@SuppressWarnings("all")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
// 我们为了自己开发方便,一般直接使用 <String, Object>
//源码是<Object,Object>类型,可以自定义把Object转换成String类型
RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
template.setConnectionFactory(factory);
// Json序列化配置
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer =
new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// String 的序列化,解决redis存储字符串是转义字符,看着像乱码
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value 序 列 化 方 式 采 用 jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash 的 value 序 列 化 方 式 采 用 jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
StringRedisTemplate:
在使用String类型存储自定义对象时:
存入到Redis的数据会存储一个该类对象的位置:
比如:
"@class": "com.sky.test.User",
这种方法更麻烦一点,需要每次手动地序列化,反序列化。
缓存
缓存就是数据交换的缓冲区(称作Cache),是存贮数据的临时地方,一般读写性能较高。
缓存:缓存穿透,缓存击穿,缓存雪崩
双写一致性,缓存的持久化
数据过期策略,数据淘汰策略
缓存穿透
缓存穿透:查询一个不存在的数据(在缓存中和数据库中都不存在),mysql查询不到数据也不会直接写入缓存,就会导致每次请求都查数据库。
危害:如果有人恶意地让很多线程并发地请求访问这些不存在的数据,那么这些所有的请求都会到达数据库。而数据库的并发不会很高,请求到达一定的量则会把数据库搞垮,导致数据库宕机。
解决方案一:缓存空数据
缓存空数据,查询返回的数据为空,仍把这个空结果进行缓存 。即{key:1,value:null}
优点:简单
缺点:消耗内存,可能会发生不一致的问题
解决方案二:布隆过滤器
优点:内存占用较少,没有多余key
缺点:实现复杂,存在误判
首先是缓存预热时往布隆过滤器中添加数据(存储数据过程)。
布隆过滤器主要是用于检索一个元素是否在一个集合中。
我们当时使用的是redisson实现的布隆过滤器。
它的底层主要是先去初始化一个比较大数组(bitmap),里面存放的二进制0或1。在一开始都是0,当一个key来了之后经过3次hash计算,模于数组长度找到数据的下标然后把数组中原来的0改为1,这样的话,三个数组的位置就能标明一个key的存在。
查找的过程也是一样的。
缺点:
布隆过滤器有可能会产生一定的误判,我们一般可以设置这个误判率,大概不会超过5%,其实这个误判是必然存在的,要不就得增加数组的长度,其实已经算是很划算了,5%以内的误判率一般的项目也能接受,不至于高并发下压倒数据库。
误判示例如下图:
误判率:数组越小误判率就越大,数组越大误判率就越小,但是同时带来了更多的内存消耗。
缓存击穿
缓存击穿的意思是对于设置了过期时间的key,缓存在某个时间点过期的时候,恰好这时间点对这个key有大量的并发请求过来,这些请求发现缓存过期,一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把DB压垮。
解释:对于Redis中正好过期的数据(Redis不存在数据了),此时如果有请求来访问这些数据,正常来说是会去查DB,同时DB把数据更新到Redis,再把数据返回。那么Redis也就得到了刷新,后续redis也可以继续为DB分担压力。
但是把DB数据更新到Redis的过程中,可能会花费过多的时间(可能是因为DB刷新到redis的数据是多表的,多表统计费时间),在这个时间段内redis的数据未重建完成,大量的并发请求过来的话则会全部走DB,会瞬间把DB压垮。
如图所示:
解决方案一:互斥锁(分布式锁)
使用互斥锁:当缓存过期失效时,不立即去load db,先使用如redis的setnx去设置一个互斥锁,当操作成功返回时再进行load db的操作并回设缓存,否则重试get缓存的方法。
如图所示:
互斥锁保证了同时只能有一个线程获得锁去查询数据库并重建redis缓存数据。保证了数据的强一致性。
缺点:性能较低。
解决方案二:逻辑过期
Redis中的热点数据的key不设置过期时间,设置逻辑过期字段。
1、在设置key的时候,设置一个过期时间字段一块存入缓存中,不给当前key设置过期时间
2、当查询的时候,从redis取出数据后判断时间是否过期
3、如果过期则开通另外一个线程进行数据同步,当前线程正常返回数据,这个数据不是最新
如:
key:1 value:{"id":"123", "title":"张三", "expire":153213455}
这种方案也是在查询DB和重置逻辑过期时间时加上互斥锁,其它线程来查询缓存时要不就是得到还未更新的过期数据,要不就得到更新后的数据。保证了在多个线程并发访问时不把其它线程全部拦截住(就是不会让它们一遍又一遍地重试获取数据)。
相比方案一更为高可用、性能优。但由于可能会得到逻辑过期数据,导致数据并不是绝对一致的。
特点:逻辑过期,更注重用户体验,高可用,性能优。但不能保证数据绝对一致。
缓存雪崩
缓存雪崩是指设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。或者是Redis服务宏机,导致大量请求到达数据库,带来巨大压力。
缓存雪崩与缓存击穿的区别:
雪崩是很多key,击穿是某一个key缓存。
第一种情况的解决方案是将缓存失效时间分散开,比如可以在原有的失效时间(TTL)基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
其余解决方案:
1.利用Redis集群提高服务的可用性(哨兵模式,集群模式)
2.给缓存业务添加降级限流策略(ngxin或spring cloud gateway) (降级限流策略可做为系统的保底策略,适用于穿透,击穿,雪崩)
3.给业务添加多级缓存 (Guava或Caffeine)
持久化
Redis是内存数据库,如果不将内存中的数据库状态保存到磁盘,那么一旦服务器进程退出,服务器中的数据库状态也会消失(断电即失),所以redis提供了持久化功能。
1.RDB
RDB执行原理
这里存在一个问题:就是如果子进程在读共享的内存数据时,主进程正在对共享的内存数据进行更改。那么子进程就可能会得到一些脏数据。
解决方法:
主进程执行写操作时,共享数据会改成只读数据。且会拷贝一份数据去执行主进程的写操作。
2.AOF
AOF全称为Append Only File(追加文件)Redis处理的每一个写命令都会记录在AOF文件,可以看做是命令日志文件。
bgrewriteaof命令
因为是记录命令,AOF文件会比RDB文件大的多。而且aof会记录对同一个key的多次写操作,但只有最后一次写操作才有意义。通过执行bgrewriteaof命令,可以让AOF文件执行重写功能,用最少的命令达到相同效果。
RDB与AOF对比
RDB和AOF各有自己的优缺点,如果对数据安全性要求较高,在实际开发中往往会结合两者来使用。
双写一致性
redis做为缓存,mysql的数据如何与redis进行同步呢? (双写一致性)
这个双写一致性最好要根据项目实际业务背景来说,一般分为两种情况:
1.一致性要求高
2.允许延迟一致
1.一致性要求高
1.延迟双删
读操作:缓存命中,直接返回;缓存未命中查询数据库,写入缓存,设定超时时间
写操作:延迟双删
1.无论第一步是先删除缓存还是先修改数据库都会导致脏数据的出现
所以重点是进行二次删除缓存以及第二次删除时做到延时删除。
2.删除两次缓存的原因
如果在第一次删除缓存和修改数据库之间的时间里,另一个线程此时来查询缓存了(未命中,查询数据库),那么此时写入缓存的则是未更新的数据库的数据,为脏数据。如下图所示:
所以在更新完数据库后再次删除缓存可以将这种情况下的脏数据尽量消除。
所以对缓存进行两次删除可以降低脏数据的出现,但是不能杜绝。
3.延时删除的原因
因为数据库一般是主从模式的,读写分离了,主库的数据同步到从库需要一定的时间,故先要延迟一会。
问题:因为延迟的时间不好控制,所以还是可能会出现脏数据。
总结
延迟双删极大地控制了脏数据的风险,但不可杜绝脏数据的风险。
2.加读写锁
能保证强一致性,但性能低。
强一致性的,采用redisson提供的读写锁
共享锁:读锁readLock,加锁之后,其他线程可以共享读操作
排他锁:独占写锁writeLock,加锁之后,阻塞其他线程读写操作
2.允许延迟一致(较为主流)
能保证最终一致性,会有短暂延迟。
1.异步通知保证数据的最终一致性
2.基于Canal的异步通知
数据过期策略
Redis对有些数据设置有效时间,数据过期以后,就需要将数据从内存中删除掉。可以按照不同的规则进行删除,这种删除规则就被称之为数据的删除策略(数据过期策略)。
方案一惰性删除:
惰性删除,在设置该key过期时间后,我们不去管它,当需要该key时,我们在检查其是否过期,如果过期,我们就删掉它,反之返回该key。
优点:对CPU友好,只会在使用该key时才会进行过期检查,对于很多用不到的key不用浪费时间进行过期检查
缺点:对内存不友好,如果一个key已经过期。但是一直没有使用,那么该key就会一直存在内存中,内存永远不会释放
方案二定期删除:
定期删除,就是说每隔一段时间,我们就对一些key进行检查,删除里面过期的key。
定期清理的两种模式:
1.SLOW模式是定时任务,执行频率默认为10hz,每次不超过25ms,以通过修改配置文件redis.conf的hz选项来调整这个次数
2.FAST模式执行频率不固定,每次事件循环会尝试执行,但两次间隔不低于2ms,每次耗时不超过1ms
优点:可以通过限制删除操作执行的时长和频率来减少删除操作对CPU的影响。另外定期剧除,也能有效释放过期键占用的内存。
缺点:难以确定删除操作执行的时长和频率。
定期清理控制时长和频率--->尽量少占用主进程的操作--->减少对CPU的影响
总结
Redis的过期删除策略:情性删除+定期删除两种策略进行配合使用。
数据淘汰策略
数据的淘汰策略:当Redis中的内存不够用时,此时在向Redis中添加新的key,那么Redis就会按照某一种规则将内存中的数据删除掉,这种数据的删除规则被称之为内存的淘汰策略。
Redis支持8种不同策略来选择要删除的key:
八种不同策略
noeviction:不淘汰任何key,但是内存满时不允许写入新数据,默认就是这种策略。
volatile-ttl: 对设置了TTL的key,比较key的剩余TTL值,TTL越小越先被淘汰。 allkeys-random:对全体key,随机进行淘汰
volatile-random:对设置了TTL的key,随机进行淘汰
allkeys-lru:对全体key,基于LRU算法进行淘汰
volatile-lru:对设置了TTL的key,基于LRU算法进行淘汰
allkeys-lfu:对全体key,基于LFU算法进行淘汰
volatile-lfu:对设置了TTL的key,基于LFU算法进行淘汰
LRU(Least Recently Used)算法:最近最少使用。用当前时间减去最
后一次访问时间,这个值越大则淘汰优先级越高。
例:key1是在6s之前访问的,key2是在9s之前访问的,删除的就是key2
LFU(Least Frequently Used)算法:最少频率使用。会统计每个key的
访问频率,值越小淘汰优先级越高。
例:key1最近5s访问了6次,key2最近5s访问了9次,删除的就是key1
数据淘汰策略--使用建议
1.优先使用alkeys-lru策略。充分利用LRU算法的优势,把最近最常访问的数据留在缓存中。如果业务有明显的冷热数据区分,建议使用这种策略。
2.如果业务中数据访问频率差别不大,没有明显冷热数据区分,建议使用allkeys-random,随机选择淘汰。
3.如果业务中有置顶的需求,可以使用volatile-lru策略,同时置顶数据不设置过期时间,这些数据就一直不被删除
会淘汰其他设置过期时间的数据。
4.如果业务中有短时高频访问的数据,可以使用allkeys-lfu 或volatile-lfu 策略。
常见问题:
1.数据库有1000万数据,Redis只能缓存2ow数据,如何保证Redis中的数据都是热点数据?
使用allkeys-lru(挑选最近最少使用的数据淘汰)淘汰策略,留下来的都是经常访问的热点数据
2.Redis的内存用完了会发生什么?
默认的配置(noeviction):会直接报错
分布式锁
场景
通常情况下,分布式锁使用的场景:
集群情况下的定时任务,抢单抢券,秒杀,幂等性场景
引入与基本介绍
如果项目是单体项目,只启动了一台服务,那遇到这类抢单问题时(防止超卖),可以加synchronized锁解决。(解决多线程并发环境下的问题)
但是项目服务是集群部署的话,那么synchronized锁这种本地锁(只能保证单个JVM内部的多个线程之间互斥,不能让集群下的多个JVM下的多个线程互斥)(只对本服务器有效)会失效,需要使用外部锁,也就是分布式锁。
例1(抢券场景):
例2:
分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁。
实现分布式锁的方式有很多,常见的有三种:
redis分布式锁实现原理
基本介绍
设置超时失效时间的原因(避免死锁):
如果某个线程拿到锁在执行业务时,服务器突然宕机,此时这个线程还没来得及释放锁,而如果没有设置过期时间的话,这个锁就没办法得到释放了,别的线程怎么也获取不到这个锁了,就造成了死锁。而设置了过期时间的话,锁到时间了就会自动释放。
总结
缺陷
以上的问题是比较小的可能出现的,但是我们用Redis实现的分布式锁去解决又显得尤为困难,所以我们可以去使用Redisson框架,它底层提供了以上问题的解决方案,方便了我们去解决问题。
Redisson实现的分布式锁
redisson是Redis的一个框架。
Redisson是一个在Redis的基础上实现的Java驻内存数据网格。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。
快速入门:
在Redisson中需要手动加锁,并且可以控制锁的失效时间和等待时间,当锁住的一个业务还没有执行完成的时候,在redisson中引入了一个看门狗机制,就是说每隔一段时间就检查当前业务是否还持有锁,如果持有就增加锁的持有时间,当业务执行完成之后需要释放锁。
在高并发下,一个业务有可能会执行很快,先客户1持有锁的时候,客户2来了以后并不会马上拒绝,它会自旋不断尝试获取锁(while循环获取),如果客户1释放之后,客户2就可以马上持有锁,性能也得到了提升。
1.redisson实现的分布式锁的执行流程/合理地控制锁的有效时长(失效时间)
原因:如果锁的有效时间设置的不合理,可能业务还没执行完锁就释放了,那此时其它线程来也可以获取到锁,就破坏了业务执行的原子性,业务数据会受到影响。
方法:根据业务所需时间实时给锁续期。
//可重试:利用信号量和PubSub功能实现等待,唤醒,获取锁失败的重试机制。
releaseTime默认是30s。
另外开一个线程“看门狗”来监视持有锁的线程并做续期任务(每隔releaseTime/3的时间做一次续期)。
public void redislock() thr throws interruptedexception {
//获取锁(重入锁),执行锁的名称
RLock lock = redissonClient.getLock("lock");
//尝试获取锁,
//参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间(锁失效时间),时间单位
//boolean islock = lock.tryLock(10,30,TimeUnit.SECONDS);
boolean isLock = lock.tryLock(10,TimeUnit.SECONDS);
//参数:1.锁的最大等待时间:锁通过while循环来不断尝试获取锁的最大等待时间,如果这个时间内没有获取到锁则放弃获取锁。
// 2.锁自动释放时间:最好不要设置或者设置为-1,否则不会启动看门狗线程进行续期任务。
// 3.时间单位
//加锁,释放锁,设置过期时间,给锁续期等操作都是基于lua脚本完成。
//Lua脚本可以调用Redis命令来保证多条命令执行的原子性。
//判断是否获取成功
if(isLock){
try{
System.out.println("执行业务");
} finally {
//释放锁
lock.unlock();
}
}
}
原子性问题:
Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。
2.可重入
Redis实现的锁是不可重入的,但redisson实现的锁是可重入的。
作用:避免死锁的产生。
这个重入其实在内部就是判断是否是当前线程持有的锁。如果是当前线程持有的锁就会计数,如果释放锁就会在计算上减一。
存储数据的时候采用的hash结构,大key可以按照自己的业务进行定制,其中小key是当前线程的唯一标识(线程id),value是当前线程重入的次数。
public void add1(){
RLock lock = redissonClient.getLock("heimalock");
boolean islock = lock.tryLock();
//执行业务
add2();
//释放锁
lock.unlock();
}
public void add2(){
RLock lock = redissonClient.getLock("heimalock");
boolean islock = lock.trylock();
//执行业务
...
//释放锁
lock.unlock();
}
底层获取锁和释放锁等操作都很复杂,都是有多个步骤,所以是用Lua脚本写确保各个操作的原子性。
3.主从一致性
redisson实现的分布式锁不能解决主从一致性问题。
比如,当线程1加锁成功后,Master节点数据会异步复制到Slave节点,当数据还没来得及同步到Slave节点时,当前持有Redis锁的Master节点宕机,Slave节点被提升为新的Master节点。(按道理主节点和从节点的数据应该要是一模一样的,加锁的信息也要一模一样(其实就是一个setnx数据而已))
假如现在来了一个线程2,再次加锁,因为Master节点数据还没来得及同步过来(从节点已经被这把锁锁住且线程一已经拿到了这把锁的信息还未更新过来),所以会在新的Master节点上加锁成功,这个时候就会出现两个线程同时持有一把锁的问题。
两个线程同时获取一把锁--->违背了锁的互斥性(锁失效了)。
红锁:
红锁算法的基本思想是,当需要锁定多个资源时,可以在多个Redis节点上分别获取锁,只有当大多数节点上
的锁都被成功获取时,整个锁才算获取成功。这样可以提高系统的容错性和可用性。
我们可以利用Redisson提供的红锁来解决这个问题,它的主要作用是,不能只在一个redis实例上创建锁,应该是在多个redis实例上创建锁,并且要求在大多数Redis节点上都成功创建锁,红锁中要求是Redis的节点数量要过半。这样就能避免线程1加锁成功后Master节点宕机导致线程2成功加锁到新的Master节点上的问题了。
意思就是线程来的时候要获取多个Redis节点的锁才算成功,才可以执行代码。
如果一个主节点宕机(主节点的数据还没来得及同步到从节点,与以上同理),它的从节点变成主节点,那么此时另一个线程来是不可以获取到锁的,因为这个线程必须要获取到所有的节点的锁才能成功获取到锁,它只能拿到宕机的那个主节点的从节点的锁(因为主节点的数据还没来得及同步到从节点),所以会获取锁失败。
只要有一个节点是存活的,其它线程就不可以拿到锁,锁就不会失效。
缺点
如果使用了红锁,因为需要同时在多个节点上都添加锁,性能就变的很低了,并且运维维护成本也非常高,所以,我们一般在项目中也不会直接使用红锁,并且官方也暂时废弃了这个红锁。
所以强一致性要求高的业务,建议使用zookeeper实现的分布式锁,它是可以保证强一致性的。
Redis发布订阅
- Redis 发布订阅(pub/sub)是一种消息通信模式: 发送者(pub)发送消息,订阅者(sub)接受消息.微博,微信,关注系统
- Redis 客户端可以订阅任意数量的频道。
订阅/发布消息图
- 第一个: 消息发送者, 第二个 :频道 第三个 :消息订阅者!
- 下图展示了频道 channel1 , 以及订阅这个频道的三个客户端 —— client2 、 client5 和 client1 之间的关系:
- 当有新消息通过 PUBLISH 命令发送给频道 channel1 时, 这个消息就会被发送给订阅它的三个客户端:
命令
- 这些命令被广泛用于构建即时通信应用,比如网络聊天室和实时广播,实时提醒
测试
- 订阅端(消费者)
-
- 开启客户端1
127.0.0.1:6379> subscribe codeyuaiiao //订阅一个频道,频道名称:codeyuaiiao 订阅的时候频道就建立了
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "codeyuaiiao"
3) (integer) 1
# 等待读取推送(客户端2发送消息,客户端1这边接收消息)
1) "message" # 消息
2) "codeyuaiiao" # 消息来自哪个频道
3) "hello world" # 消息的具体内容
1) "message"
2) "codeyuaiiao"
3) "hello yuhaijiao"
- 发送端:(生产者)
-
- 再开启一个客户端2
127.0.0.1:6379> publish codeyuaiiao "hello world" # 发布者发布消息到频道
(integer) 1
127.0.0.1:6379> publish codeyuaiiao "hello yuhaijiao" # 发布者发布消息到频道
(integer) 1
127.0.0.1:6379>
Redis发布订阅原理
- Redis是使用C实现的,通过分析 Redis 源码里的 pubsub.c 文件,了解发布和订阅机制的底层实现,籍此加深对 Redis 的理解。
- Redis 通过 PUBLISH(发送消息) 、SUBSCRIBE(订阅频道) 和 PSUBSCRIBE(订阅多个频道) 等命令实现发布和订阅功能。
例如微信订阅公众号:
- 通过 SUBSCRIBE 命令订阅某频道后
-
- Redis-server 里维护了一个字典
- 字典的键就是一个个频道
- 字典的值则是一个链表,链表中保存了所有订阅这个 channel 的客户端
- SUBSCRIBE 命令的关键, 就是将客户端添加到给定 channel 的订阅链表中
- 通过publish命令向订阅者发送消息,redis-server会使用给定的频道作为键,在它所维护的channel字典中查找记录了订阅这个频道的所有客户端的链表,遍历这个链表,将消息发布给所有订阅者.
总结
Pub/Sub 从字面上理解就是发布(Publish)与订阅(Subscribe),在Redis中,你可以设定对某一个key值进行消息发布及消息订阅,当一个key值上进行了消息发布后,所有订阅它的客户端都会收到相应的消息。这一功能最明显的用法就是用作实时消息系统,比如普通的即时聊天,群聊等功能。
使用场景
- 实时消息系统
- 实时聊天! (频道当做聊天室,将信息回显给所有人即可! )
- 订阅,关注系统都是可以的
稍微复杂的场景我们就会使用 消息中间件MQ
Redis消息队列
概念
消息队列(Message Queue),字面意思就是存放消息的队列。最简单的消息队列模型包括3个角色:
消息队列:存储和管理消息,也被称为消息代理(message broker)
生产者:发送消息到消息队列
消费者:从消息队列获取消息并处理消息
例(秒杀抢券业务):
生产者:判断是否有资格抢券(券的剩余数量大于0且当前用户之前未抢到券),如果有资格则将订单相关信息写入消息队列。
消费者:开启一个独立的线程去接收消息,完成下单(把订单信息写入Mysql数据库)
这样秒杀抢单的业务和真正写数据库的业务就实现了分离,变成了异步操作,解耦合了。
秒杀抢单的业务:秒杀这里因为不用写数据库(比较耗时),并发能力大大提高。
写数据库的业务:可以根据自己的节奏慢慢地去取订单写数据库,不会让数据库有太大的压力,保证数据库抗得住。
Redis提供了三种不同的方式来实现消息队列:
list结构:基于List结构模拟消息队列
PubSub:基本的点对点消息模型
Stream:比较完善的消息队列模型
基于List结构模拟消息队列(可实现阻塞队列的效果)
支持持久化:因为list类型redis本身是用链表做存储数据的,只是我们把它当成消息队列来用,故对数据可以持久化
基于PubSub(发布订阅)的消息队列
主要内容就是上面学习的Redis发布订阅
优点:
采用发布订阅模型,支持多生产,多消费
缺点:
不支持数据持久化
无法避免消息丢失
消息堆积有上限,超出时数据丢失
不支持持久化:因为PubSub本身就只是用来做发布订阅功能的,如果没有人订阅某个频道,那么往这个频道发布数据后,数据会丢失,Redis不会保存这个数据。
基于Stream的消息队列
基本知识
Stream是Redis 5.0引入的一种新数据类型,可以实现一个功能非常完善的消息队列。
例:
注意
当我们指定起始id为$时,代表读取最新的消息,如果我们处理一条消息的过程中,又有超过一条以上的消息到达队列,则下次获取时也只能获取到最新的一条,会出现漏读消息的问题。
Stream类型消息队列的XREAD命令特点:
1.消息可回溯
2.一个消息可以被多个消费者读取
3.可以阻塞读取
4.有消息漏读的风险
消费者组
消费者组:将多个消费者划分到一个组中,监听同一个队列。
具备下列特点:
Stream类型消息队列的XREADGROUP命令特点:
消息可回溯
可以多消费者争抢消息,加快消费速度
可以阻塞读取
没有消息漏读的风险
有消息确认机制,保证消息至少被消费一次
Redis集群(分布式缓存)
单点Redis的问题
1.并发能力问题
解决方法:搭建主从集群,实现读写分离。实现高并发。
2.故障恢复问题
解决方法:利用Redis哨兵,实现健康检测和自动恢复。保障高可用。
3.存储能力问题
解决方法:搭建分片集群,利用插槽机制实现动态扩容。
主从复制
单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,就需要搭建主从集群,实现读写分离。
主从数据同步原理
1.主从全量同步
注:
1.判断是否第一次同步:
从节点的replid与主节点的不一样则说明这个从节点是第一次同步。
2.只有第一次同步的时候主节点才会生成RDB文件,第一次之后的同步会根据偏移量利用repl_baklog日志文件进行同步数据。
2.主从增量同步(slave重启或后期数据变化)
3.总结
全量同步:
1.从节点请求主节点同步数据(replication id, offset)
2.主节点判断是否是第一次请求,是第一次就与从节点同步版本信息(replicationid和offset)
3.主节点执行bgsave,生成rdb文件后,发送给从节点去执行
4.在rdb生成执行期间,主节点会以命令的方式记录到缓冲区(一个日志文件)
5.把生成之后的命令日志文件发送给从节点进行同步
增量同步:
1.从节点请求主节点同步数据,主节点判断不是第一次请求,不是第一次就获取从节点的offset值
2.主节点从命令日志中获取offset值之后的数据,发送给从节点进行数据同步
哨兵模式
redis提供了哨兵模式来实现主从集群的自动故障恢复,从而极大地保障了Redis主从的高可用。
哨兵模式的结构与作用
redis提供了哨兵 (Sentinel)机制来实现主从集群的自动故障恢复。
结构:
作用:
监控:Sentinel会不断检查您的master和slave是否按预期工作
自动故障恢复:如果master故障, Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主
通知:Sentinel充当redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给redis的客户端
服务状态监控
Sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个实例发送ping命令:
主观下线:如果某sentinel节点发现某实例未在规定时间响应,则认为该实例主观下线。
客观下线:若超过指定数量(quorum)的sentinel都认为该实例主观下线,则该实例客观下线。quorum值最好
超过sentinel实例数量的一半。
哨兵选主规则(主节点宕机后,选从节点为主节点的规则)
1.首先判断主与从节点断开时间长短,如超过指定值就排除该从节点
2.然后判断从节点的slave-priority值,越小优先级越高
3.如果slave-prority一样,则判断slave节点的offset值,越大优先级越高.
4.最后是判断slave节点的运行id大小,越小优先级越高。
第三条最重要!!!
Redis集群(哨兵模式)的脑裂问题
有的时候由于网络等原因可能会出现脑裂的情况,就是说,由于redis的master节点和redis的salve节点和sentinel处于不同的网络分区,使得sentinel没有能够心跳感知到主节点,所以通过选举的方式提升了一个salve为master,这样就存在了两个master,就像大脑分裂了一样,这样会导致客户端还在old master那里写入数据,新节点无法同步数据,当网络恢复后,sentinel会将old master降为salve,这时再从新master同步数据,就会导致old master中的大量数据丢失。
----------->
------------>
解决方法
在redis的配置中设置两个配置参数
1.(min-replicas-to-write 1)设置最少的salve节点个数为1,设置至少要有一个从节点才能同步数据
2.(min-replicas-max-lag 5)设置主从数据复制和同步的延迟时间不能超过5秒
达不到要求就拒绝请求,就可以避免大量的数据丢失。
总结:
我们可以修改redis的配置,可以设置最少的从节点数量至少为一个以及缩短主从数据同步的延迟时间(不能超过5秒),达不到要求就拒绝Redis客户端的请求(不让客户端写入数据到老的主节点),这样就可以避免大量的数据丢失。
分片集群
主从和哨兵可以解决高可用,高并发读的问题。但是依然有两个问题没有解决;
1.海量数据存储问题
2.高并发写的问题
使用分片集群可以解决上述问题,分片集群特征:
1.集群中有多个master,每个master保存不同数据
2.每个master都可以有多个slave节点
3.master之间通过ping监测彼此健康状态 (这点类似于之前的哨兵模式)
4.客户端请求可以访问集群任意节点,最终都会被转发到正确节点 (路由:客户端请求可以访问集群任意节点,最终都会被转发到正确节点。)
具体的路由规则:
Redis分片集群引入了哈希槽的概念,Redis集群有16384个哈希槽,每个key通过 CRC16 校验后对 16384 取模来
决定放置哪个槽,集群的每个节点负责一部分hash槽。
Redis分片集群中数据的存储和读取:
redis分片集群引入了哈希槽的概念,redis集群有16384个哈希槽,将16384个插槽分配到不同的实例
读写数据:根据key的有效部分计算哈希值。对16384取余(有效部分,如果key前面有大括号的
内容就是有效部分,如果没有,则以key本身做为有效部分)余数做为插槽,寻找插槽所在的实例
redis集群环境部署(环境配置)
只配置从库,不用配置主库!
- 原因:redis默认都是主库
查看当前redis库的信息,分析是否是主库
- 命令:info replication
127.0.0.1:6379> info replication # 查看当前库的信息
# Replication
role:master # 角色 master
master connected_slaves:0 # 没有从机
master_replid:b63c90e6c501143759cb0e7f450bd1eb0c70882a
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
搭建redis集群准备工作
- 复制3个redis.conf配置文件,然后修改对应的集群信息
- 分别修改3个redis.conf对应的以下4个属性配置
- port端口修改
- pid名字
- log文件名字
- dump.rdb名字
- 修改完毕之后,启动我们的3个redis服务器
-
- 分别启动3个redis服务命令
- 启动完毕,通过进程查看信息
一主二从集群搭建(命令或文件配置)(这种方式的redis集群实际工作用不到,仅供基础学习)
命令方式配置
默认情况下, 每台Redis 服务器都是主节点 ; 我们一般情况下只用配置从机就好了!
- 认老大! 一主 (6379)二从(6380,6381)
- 配置从机,去6380和6381配置,命令:
127.0.0.1:6380> SLAVEOF 127.0.0.1 6379 # SLAVEOF host 6379 登录6380找6379主机当自己的老大!
OK
127.0.0.1:6380> info replication # 查询信息redis6380的服务信息
role:slave # 查看当前角色是从机
master_host:127.0.0.1 # 可以的看到主机的信息和端口号
master_port:6379
- 配置结果查询:6380,6381的角色role变成了slave从机
- 如果两个都配置完了,就有两个从机了
-
- 登录6379主机,查看主机下的两个从节点
- 真实的主从配置应该在配置文件中配置,这样的话是永久的, 我们这里使用的是命令,暂时的!
文件方式配置(一主二从,持久化的,对于哨兵模式,不建议使用这种)
- 登录redis从机,进入redis.conf配置文件配置replicaof
- 如果主机有密码,则配置主机密码
一主二从细节
- 主机可以写, 从机不能写只能读! 主机中的所有信息和数据,都会自动被从机保存.
测试主机写,从机读
- 主机写:
- 从机读:
- 从机写,会报错
测试: 主机宕机断开连接,从机会有什么变化
- 从机没有变化,依然指向主机,并且只能读不能写
- 如果想把从机改为主机,只能手动去设置,或者配置哨兵通过选举,将从机变为主机
测试2:这个时候, 主机如果回来了,从机有什么变化
- 从机依旧可以直接获取到主机写的信息!保证高可用性
测试3:如果从机断了,会有什么后果
- 由于是使用命令行来配置的从机,这个时候如果从机重启了,就会变成主机 (所以建议在redis.conf配置文件中配置从机)!
- 但只要重新将主机变为从机, 立马就会从主机中获取值!
主从复制原理
- Slave启动成功连接到master后会发送一个sync同步命令
- Master接到命令,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令, 在后台进程完毕之后,master将传送整个数据文件到slave,并完成一次完全同步.
- 全量复制: 而slave服务在接收到数据库文件数据后, 将其存盘并加载到内存中.
- 增量复制: Master继续将新的所有收集到的修改命令依次传给slave,完成同步.
- 但是只要是重新连接master, 一次完全同步(全量复制)将被自动执行! 我们的数据一定可以在从机中看到!
一主两从的第二种搭建方式(层层链路)哨兵模式的手动版
层层链路
- 79是主节点
- 80是79的从节点
- 81是79的从节点
- 上一个M连接下一个S!
- 这时候也可以完成我们的主从复制!
如果没有老大了,这个时候能不能选择一个老大出来呢? 手动!
- 谋朝篡位
-
- 如果主机断开了连接, 我们可以使用
slaveof no one
让自己变成主机! 其他的节点就可以手动连接到最新的这个主节点(手动)! 如果这个时候老大修复了, 那就只能重新配置连接! - 所以建议使用命令配置集群,方便将从节点改为主节点后,不用在去改配置文件
- 如果主机断开了连接, 我们可以使用
I/O多路复用模型
redis为什么这么快
用户空间和内核空间
Linux系统中一个进程使用的内存情况划分两部分:内核空间,用户空间。
用户空间只能执行受限的命令(Ring3),而且不能直接调用系统资源(比如网卡数据),必须通过内核提供的接口来访问。
内核空间可以执行特权命令(Ring0),调用一切系统资源。
Linux系统为了提高IO效率,会在用户空间和内核空间都加入缓冲区:
写数据时,要把用户缓冲数据拷贝到内核缓冲区,然后写入设备
读数据时,要从设备读取数据到内核缓冲区,然后拷贝到用户缓冲区
如图所示
阻塞IO
顾名思义,阻塞IO就是两个阶段都必须阻塞等待
阶段一:
1.用户进程尝试读取数据(比如网卡数据)
2.此时数据尚未到达,内核需要等待数据
3.此时用户进程也处于阻塞状态
阶段二:
1.数据到达并拷贝到内核缓冲区,代表已就绪
2.将内核数据拷贝到用户缓冲区
3.拷贝过程中,用户进程依然阻塞等待
4.拷贝完成,用户进程解除阻塞,处理数据
可以看到,阻塞IO模型中,用户进程在两个阶段都是阻塞状态。
非阻塞IO
顾名思义,非阻塞IO的recvfrom操作会立即返回结果而不是阻塞用户进程。
阶段一:
1.用户进程尝试读取数据(比如网卡数据)
2.此时数据尚未到达,内核需要等待数据
3.返回异常给用户进程
4.用户进程拿到error后,再次尝试读取
5.循环往复,直到数据就绪
阶段二:
将内核数据拷贝到用户缓冲区
拷贝过程中,用户进程依然阻塞等待
拷贝完成,用户进程解除阻塞,处理数据
可以看到,非阻塞IO模型中,用户进程在第一个阶段是非阻露,第二个阶段是阻塞状态。虽然是非阻塞,但性能并没有得到提高。而且忙等机制会导致CPU空转,CPU使用率暴增。
IO多路复用
Redis网络模型
Redis通过IO多路复用来提高网络性能,并且支持各种不同的多路复用实现,并且将这些实现进行封装,提供了统一的高性能事件库。
主要是IO多路复用+事件派发机制:
Redis 6.0之后,为了提升性能,引入了多线程处理: