Redis 入门知识
Redis 的存储类型
5大类型
- String 类型
基本存储结构,以字符串的方式存储简单信息,包括二进制。所以可以存储json字符串、图片、邮箱,一个字符串最大为512M。 - Hash 类型
Redis hash 是键值对集合。
Redis hash是一个 String 类型的 field 和value 的映射表, hash 特别适合用于存储对象。 - List 类型 (有序,可重复)
Redis 中最简单的字符串列表,按照插入顺利排序。 可以在头部插入,也可以在尾部插入。
它的另一作用是可以当做消息队列, push 插入,pop取出。 - Set 类型 (无序,不可重复)
类似 list ,最大的区别是有序和唯一 - ZSet 类型 (排序,不可重复)
ZSet 与 Set 一样也是 String 类型元素的集合,其不能元素重复。不同的是ZSet 集合中每一个原色都会关联一个double 类型的分数。redis 正是通过分数实现元素的排序。ZSet 集合中元素事唯一的,但分数有可能是相同的。
基本脚本
通用
exists key
是否存在
expire key 秒数
设置过期时间
ttl key
查看过期时间
del key
删除key
type key
查看 key的存储类型
keys *
查看所有的key
STRING
HASH
LIST
SET
ZSET
过期删除策略
通过 expire 命令设置key的过期时间
通过 ttl 查看key的过期剩余时间
- 返回 -1 表示没有设置过期
- 返回 -2 表示key 已经被删除
- 返回大于0的数字,表示过期剩余时间
所谓定期删除,指的是 redis 默认是每隔 100ms 就随机抽取⼀些设置了过期时间的 key,检查其
是否过期,如果过期就删除
过期删除策略:
- 惰性删除:当
key
被访问时,如果timeout
则删除 key、 - 定期删除:每隔
100ms
就随机抽取⼀些设置了过期时间的 key,如果timeout
则删除
由于惰性删除策略,有可能会存在大量的过期的 key没有删除,并存在磁盘内
此时又该如何处理?
答案: 走 内存回收策略 volatile-ttl / volatile-random
Redis 内存回收策略
需要内存回收时,默认使用 noeviction 策略
各种回收策略:
- noeviction(不删除策略):当内存空间不足时,所有请求内存的指令,全部报错,无法申请。
- allkeys-lru: 所有的key里面淘汰使用次数最少的key
- volatile-lru:在所有设置了过期时间的key里,淘汰掉使用次数最好的key
- allkeys-random:随机移除某个key。
- volatile-random:在所有设置了过期时间的key里,随机删除某个key
- volatile-ttl:在所有设置了过期时间的key里,过期时间快到期的 key
注:
LRU 策略,这个是一般不太适合的,毕竟LRU的实现会存在大量的队列位置调换,比较麻烦。
Redis 的发布/订阅
角色:发布者(producer)、渠道(channel)、订阅者(consumer)
模式:发布者发布消息给渠道,渠道推送给订阅者
发布方式:指定发布 与 匹配发布
简单流程如下图:
Redis 持久化策略
暂不学习,公司没用于持久化存储
RDB
优点:
- rdb 文件是一个紧凑文件,直接使用 rdb文件 就可以还原数据
- 数据保存会开启一个子进程,不会影响其他进程
- 恢复的效率高于 AOF
AOF
Lua脚本
Redis 服务存在两个问题:
- 原子性问题:
Redis虽然是单一线程的,但仍然会存在线程安全问题。Redis作为数据服务器,是提供给多个客户端使用的。多个客户端的操作就相当于同一个进程下的多个线程,如果多个客户端之间没有做好数据的同步策略,就会产生数据不一致的问题。 - 效率问题
Redis 本身的吞吐量是非常高的,因为它是基于内存的数据库。在实际使用过程中,有一个非常重要的影响因素,就是网速。每一个请求,代表一个操作,如果网速缓慢,整个过程会被拉长。
Lua脚本的使用,解决 redis 并发问题
- Redis服务内置了 lua脚本的使用环境。
- Lua 脚本能够使系统内部的所有指令进行串行化执行
- Lua 可以进行指令合并,并保证原子性
Pipeline 有什么好处,为什么要用
因为快,为了效率。使与不使差距 100倍速度
Redis客户端执行一条命令分为如下4个部分:
- 发送命令
- 命令排队
- 命令执行
- 返回结果。
发送命令
与返回结果
,属于网络对接每次都有IO消耗。使用pipeline
(流水线)就是将多个命令,打包一次性发给redis,执行完后再统一返回。
命令排队
与命令执行
,是在redis中进行,必然是一一进行(串行进行)
Redis 事务
Redis
是弱事务,如果是运行时的错误,是无法进行回滚的,之前运行成功的就是成功了。
实现原理:
当开启事务(multi
)后,每一个需要执行的命令都会放到消息队列中,提交(exec
)时批量执行脚本。discard
是进行回滚。
当提交脚本后,存在执行报错的脚本,针对之前执行成功的脚本,无法rollback
,对于之后能成功执行的脚本,不会执行。
提交事务
multi 是开启事务
.....
exec 提交事务
回滚事务
multi 是开启事务
.....
discard 回滚事务(清空消息队列)
Redis 分布式锁
原理:
锁的特性是互斥
主从模式
一个主服务,多个从服务。从服务数据,回到主服务中获取数据,保证数据的一致,但高峰期会存在数据的差异性。
redis 本身的主从过程是异步的,且属于乐观复制。认为 当主数据库写的时候,其他从数据库也能写成功。所以异步向从数据库发送数据,发送完就写入本地,不会等响应结果。
Redis 也提供了配置文件,设置异步限制
- min-slaves-to-write 3 表示只有当3个或以上的slave连接到master,master才是可写的
- min-slaves-max-lag 10 表示允许slave最长失去连接的时间,如果10秒还没收到slave的响应,则master认为该slave以断开
读写分离
分担单一服务区的压力。将主服务其设置为 写服务器,从服务器设置为 读服务器。如果主服务器宕机,系统无法写入数据,只能通过从库读取
全量复制
当从数据库启动时,全部复制主数据库的内容。
一般,新从库会执行全量复制,之后再执行增量复制。
增量复制
当从数据库启动时,到主数据库的backlog中查看 replica offset 的位置,并继续进行内容复制。如果没有 replica offset ,就是全量复制。
哨兵模式
在主从模式下,哨兵的工作内容: 1. 监控各个服务 2. 选举新的主数据库。哨兵集群模式下,哨兵之间会相互监督。
简单流程:
- Sentinel节点会定期向master节点发送心跳包来判断存活状态,一旦master节点没有正确响应,Sentinel会把 master设置为“主观不可用状态”,然后它会把“主观不可用”发送给其他所有的Sentinel节点去确认,当确认的 Sentinel节点数大于>quorum时,则会认为master是“客观不可用”,接着就开始进入选举新的master流程。
- 但是这里又会遇到一个问题,就是Sentinel中,本身是一个集群,如果多个节点同时发现master节点达到客观不可用状态,那谁来决策选择哪个节点作为maste呢?这个时候就需要从Sentinel集群中选择一个leader来做决策。而这里使用了分布式算法,只要保证过半数节点通过提议即可
- 如果之前的主服务器回复,也会降级为从服务器参与工作。
哨兵监听哨兵:是使用 发布订阅模式。每一个新加入的节点,都会向 channel发布自己的消息,这样其他节点就是到 它的存在。
哨兵监听数据库:主从数据库的方式,是通过 心跳方式发送请求,查看服务是否返回正确
击穿(高并发 + key过期)
系统在高并发的情况下,redis 的key 由于过期被清除了,大量的请求,绕过redis 直接请求到数据库。
解决方法:
- 双重检测,针对莫一时刻,串行化数据库查询,减少请求。
private static volaite Object lockHelp = new Object();
public String getValue(String key) {
String value = redis.get(key, String.class);
if (value == "null" || value == null || StringUtils.isBlank(value) {
synchronized (lockHelp) {
value = redis.get(key, String.class);
if (value == "null" || value == null || StringUtils.isBlank(value) {
value = db.query(key);
redis.set(key, value, 1000);
}
}
}
return value;
}
- 使用互斥锁
public String get(key) {
String value = redis.get(key);
if (value == null) { //代表缓存值过期
//设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db
if (redis.setnx(key_mutex, 1, 3 * 60) == 1) { //代表设置成功
value = db.get(key);
redis.set(key, value, expire_secs);
redis.del(key_mutex);
return value;
} else { //这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可
sleep(10);
get(key); //重试
}
} else {
return value;
}
}
穿透(没有的key)
系统请求时,请求一个 redis 中不会存储的key,导致直接请求数据库
解决方式:第一次访问时如果key不存在,则在缓存中设置一个空值(null),并设置较短的过期时间
雪崩(大量key过期)
如果缓存系统出现故障或者大量的key在相同或者相近时间过期,大量的并发流量就会直接到达数据库。
解决方式:
- 针对故障,最常用的一种方案就是保证Redis的高可用,将Redis缓存部署成高可用集群
- 针对过期时间,可以使用双重检测,针对莫一时刻,串行化进行数据库查询。