Redis
- 1.Redis是什么?
- 2.Redis有什么用?
- 3.Redis的基本数据类型?
- 4.Redis的特点
- 5.rdb快照模式实现数据持久化
- 6.aof模式实现数据持久化
- 7.混合持久化
- 8.缓存的淘汰策略?
- 9.什么是Redis的缓存穿透,怎么解决?
- 10.什么是Redis的缓存雪崩,怎么解决?
- 11.什么是Redis集群缓存脑裂问题,怎么解决?
- 12.Redis架构—主从复制(一主二从)
- 13.Redis架构—哨兵模式(一主二从三哨兵)
- 14.Redis架构—集群模式
- 15. 怎么保证redis和mysql数据一致性?
- 16.Redis常用指令
- 17.Redis数据类型结构图解
- 18. Jedis 和 Lettuce的区别
- 19. Redis数据类型对应的底层数据结构?
- 20. Redis Zset(Sorted Set)的底层实现?(压缩表 + 跳表)
- 21. Redis数据结构-字符串SDS
- 23. Redis实现接口限流
- 24. Redis实现 保证接口幂等性 ~ 防重Token令牌(防止表单重复提交)
Redis中文官方文档
1.Redis是什么?
- Redis是一个no sql的非关系型数据库。
- Redis是一个基于键值对的存储系统。
- 关系:表与表的关系,对象和表之间的映射关系。
2.Redis有什么用?
- ① 数据缓存
- ② 购物车数据
- ③ 评论数据存储
- ④ 秒杀
- ⑤ 实现session共享
- ⑥ 分布式锁
- ⑦ 最热商品
- ⑧ 商城中的评分
3.Redis的基本数据类型?
- ① String
- ② Hash
- ③ List
- ④ Set
- ⑤ Sorted Set(又称作 Zset)
4.Redis的特点
- ① Redis是基于内存的
- ② Reids的速度快
- ③ Redis的使用场景一定是对数据的要求不严格的时候
- ④ Redis的存储结构是KV
- ⑤ Redis提供了三种持久化模式来保证数据的持久化(rdb,aof,混合)
- ⑥ Redis提供了三种架构(主从,哨兵,集群)
5.rdb快照模式实现数据持久化
- Redis是基于内存的,所以速度快,但是Redis数据放在内存中的话,Redis一旦重启,数据就会丢失,所以持久化策略是必须的。
- rdb模式是Redis默认的持久化模式,也被成为快照模式。这个模式是将内存的数据内容直接保存到dump.rdb二进制文件中。
- 触发时机 :可以对 Redis 进行设置, 让它在“N 秒内数据集至少有 M 个改动”这一条件被满足时,自动保存一次数据集。
- 特点:因为保存的是二进制文件,所以做数据的回复相当快,适合做数据的备份。
- 保存过程: rdb模式每次在保存数据的时候,都会清空原有的dump.rdb文件,然后再将整个内存的数据全部写入到这个文件中。也就是说rdb保存的是Redis内存中某一个时刻的数据(适合备份,不适合开发用)
- 缺点: 假设刚好清空dump.rdb文件的时候断电了就会导致数据全部丢失。
6.aof模式实现数据持久化
- aof模式: 记录的是操作的命令而不是实际的数据,只要调用Redis使用了命令,那么这些命令都会被记录到aof文件中。
- 特点: aof恢复数据的效率并不高,rdb和aof同时存在的话,会以aof优先。
appendonly yes :开启aof的模式
-
aof模式的触发策略
- appendfsync always :只要有键发生改变 立马同步(每一次都触发IO操作、速度就慢下来、这种情况是不会丢数据)-----一般不用(效率太低了)。
- appendfsync everysec :每一秒钟进行数据同步一次(开发的时候一般选用他----速度上也比较快 即使出现数据的丢失也只会丢失1秒钟的数据)。
- appendfsync no :永远不同步、数据只是放到缓存里面 速度快 但是数据容易丢失。
-
aof模式如何同步数据
- 每次进行数据同步的时候使用的是追加的模式,以前的数据不删除,直接追加
-
aof模式消息的重写:为了解决AOF文件体积膨胀的问题,Redis提供了AOF重写功能:Redis服务器可以创建一个新的AOF文件来替代现有的AOF文件,新旧两个文件所保存的数据库状态是相同的,但是新的AOF文件不会包含任何浪费空间的冗余命令,通常体积会较旧AOF文件小很多。
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100 (这个表示的是必须达到100%的增加才重写 64M+64M=128M )
auto-aof-rewrite-min-size 64mb aof文件(简单的说就是至少aof文件达到64M才重写)
手动重写
bgrewriteaof :这个命令就是手动重写
重写的好处是对aof文件进行优化(最终的结果都是一样的)
如果你要去手动重写(需要关闭混合持久化的开关才能看到功能)
aof-use-rdb-preamble no
7.混合持久化
- rdb的问题: 重启Redis的时候,我们很少使用rdb来恢复内存状态,因为rdb快照模式它是基于“N 秒内数据集至少有 M 个改动”而触发持久化,因此会丢失至少1秒的数据集。
- aof的问题: 重启Redis的时候,如果使用AOF命令日志重新写入,恢复内存状态性能的话相对rdb来说要慢很多。
- Redis 4.0 为了解决以上问题,引入了混合持久化策略,使用以下配置开启混合持久化(必须先开启AOF)。
aof‐use‐rdb‐preamble yes
- 如果开启了混合持久化,AOF在重写时,不再是单一的将Redis命令写入AOF文件,而是将这一刻之前的内存数据做RDB快照处理,并且将RDB快照内容写入 新的AOF文件(混合) 的头部,将后续修改内存数据的命令追加到新的AOF文件中RDB快照内容后面,等到重写完新的AOF文件,才会覆盖原有的AOF文件,完成新旧AOF文件替换。
- 于是在Redis重启的时候,可以先加载RDB的内容,然后再重新执行文件中后面的AOF命令,这样既保证了数据不丢失,也保证了速度。
8.缓存的淘汰策略?
- Redis的内存已经到一定的阀值,需要进行淘汰缓存,来保证内存空间和数据新鲜度。
- noeviction:只要缓存满了、那么就不继续服务器里面的写的请求、读的请求是可以完成的、这种模式缓存里面的所有数据 都不会丢失、这种情况会导致参与Redis的业务会失败。
- volatile-lru:他会优先淘汰掉 设置了过期时间的这个key、然后第二步才淘汰掉使用的比较少的key 假设我们的key没有设置过期时间的话 那么不会优先淘汰。
这种模式也是咋们在开发中使用的比较多的一种缓存策略模式。 - allkeys-lfu:和lru是有区别的、这个在淘汰的时候、淘汰的是全体key的集合、不是过期的key的集合(过期这一说法没有)、这就意味着你没有设置过期时间的key 只要使用的比较少那么依然会被淘汰。
- volatile-ttl:这个淘汰策略不是LRU 、而是key剩余的寿命的ttl值 ttl值越小 越先被淘汰。
- allkeys-random:使用这个淘汰策略的时候 淘汰的是随机的key。
maxmemory-policy volatile-lru 这个就是配置缓存的淘汰策略的
maxmemory <bytes> :这个是配置Redis的缓存的大小
9.什么是Redis的缓存穿透,怎么解决?
- 缓存穿透: 简单的说就是获取数据的时候先去Redis缓存中找数据,结果没找到,又去数据库中找数据,这样的话,每一个线程进来都要去访问数据库,数据库压力逐渐变大导致崩溃,这种现象被成为缓存穿透。
- 解决方法: 假设第一次在Redis缓存中和数据库中都没找到话,那么直接在Redis缓存中将这个数据设置为"",这样的话下个线程进来就不会再去访问数据库了,就避免缓存穿透。
10.什么是Redis的缓存雪崩,怎么解决?
- 缓存雪崩 就是在某一个时间点,大量的key同时过期或者Redis集群大面积故障。
- 解决方法
- 针对大量key同时过期问题: 在计算出key的过期时间后,动态加上一个范围类的随机数作为最终key的过期时间,达到错峰的效果。
- 针对集群大面积故障问题: 做缓存的高可用,缓存做一个降级处理,做Redis的缓存预热
- 缓存预热:在使用缓存数据之前,先将数据库中要缓存的数据放到Redis中
11.什么是Redis集群缓存脑裂问题,怎么解决?
- 缓存脑裂: redis master节点出现网络问题,这时候master节点就会被降级为从节点,而新的master也会产生。假设在master节点降级前存的一些数据没有同步到从节点,这样降级之后,会导致这些数据无法同步到其它节点,从而造成数据的丢失。
- 解决方法: master节点出现网络故障的时候,让它拒绝客户端写入操作。换句话说就是给Redis master节点写入客户端数据加前提条件,比如说设置最少从节点连接数,从节点连接master的最大延迟时间。
12.Redis架构—主从复制(一主二从)
- 主从复制解决的问题:读写分离,Master负责写,Slave负责读,降低了主库压力,将读操作转交给从库。
- 主从复制存在的问题:Master崩溃,整个服务器将无法访问,被称为单点问题。
13.Redis架构—哨兵模式(一主二从三哨兵)
- 哨兵模式
- 哨兵模式解决的问题: 解决了主从复制的单点故障问题,可以实现自动选举Master。
- 哨兵模式存在的问题: 读的服务器有多个,写的服务器只有一个,并发量低。
- 哨兵的作用 :
- ① 监控主服务器和从服务器的运行状态。
- ② 哨兵监测到master宕机,会从新选举一个新的master节点,然后通过发布订阅模式通知其他的 从服务器,修改配置文件,让它们切换主机。
- ③ 哨兵节点是特殊的 Redis 节点,不存储数据。
14.Redis架构—集群模式
- 每一个纵向都是一个主从(主data=从data)
- 横向是为了扩容,所有master数据之和 才是内存中的所有数据。
15. 怎么保证redis和mysql数据一致性?
- 一致性包含三个级别:强一致性,弱一致性,最终一致性。
- 强一致性:系统写入什么,读出来的也会是什么,没有延迟
- 弱一致性:系统在写入成功后,不承诺立即可以渎到写入的值,但会尽可能地保证到某个时间级别(秒级)后,数据能够达到一致性状态
- 最终一致性:最终一致性是弱一致性地一个特例,系统会保证在一定时间内,能够达到数据地一致性状态。在大型分布式系统地的数据一致性上倾向于采用这种级别。
- 缓存延时双删策略: 删除缓存 > 更新数据库 > 休眠(比如500ms) > 删除缓存
- 目的 : 尽可能地减少Redis中出现旧数据的情况。
- 为什么要休眠? 线程A刚更新完DB就删除缓存,有可能存在一个线程B在线程A写操作之前抓取到了旧数据并没有存放到缓存当中,等到线程A更新完数据,删除缓存后,线程B再把旧数据放到Redis中,Redis中还是旧数据
16.Redis常用指令
1.key * :查看数据库中所有存在的键。
2.select [数据库的下标] :选中一个数据库。
3.del [key] :删除数据库中某一个键值对。
4.exist [key] :判断数据库中是否存在某个键。
5.expire [key] [seconds] :给某个key设置过期时间,单位秒。
6.ttl [key] :查看某个key的剩余时间。返回-1代表永远有效,返回-2表示不存在。
7.move [key] [数据库的下标] :移动一个键值对到另外一个数据库中。
8.randomkey :随机返回一个key。
===========================String===========================
1.set [key] [value] :
2.get [key] :
3.mset [k] [v] [k] [v] :一次性设置多个。
4.mget [key...] :一次性获取多个key
5.incr [key] :某个键的值 +1
6.decr [key] :某个键的值 -1
7.incrby [key] [step] :某个键的值 +step
8.decrby [key] [step] :某个键的值 -step
9.setnx [key] [value] :如果这个key不存的话,就设置这个键值对,应用在分布式锁。
===========================String使用场景===========================
1.存储对象类型数据: set user:id:1:username xiaobobo
set user:id:1:password 12345678
mset user:id:1:username xiaobobo user:id:1:password 12345678
2.如微信朋友圈点赞次数 set wechat:friend:readCount:msgId 0
incr wechat:friend:readCount:msgId
===========================Hash====================================
1.hset [集合名] [key] [value]
2.hget [集合名] [key]
3.hlen [集合名]
4.hdel [集合名] [key]
5.hincrby [集合名] [key] [step]
6.hgetall [集合名] :直接实现全选功能
7.hexists [集合名] [key]
8.hkeys [集合名] :如获取某个人购物车的所有商品
9.hvals [集合名]
10.hmset [集合名] [k] [v] [k] [v]
11.hmget [集合名] [key...]
===========================Hash使用场景===========================
1.存储对象类型数据: hmset 表名:主键名:主键值 [字段1] [字段1的值] [字段2] [字段2的值] hmset user:id:1 username zs password 123 age 25
===========================List=================================
1.lpush [k] [v...] 将1个或对多个值从横向列表的表头插入
2.lpop [k]
3.rpush [k] [v...]
4.rpop [k]
5.lrange [k] [start] [stop]
6.lrange [k] 0 -1
7.blpop [k]
8.brpop [k]
9. 栈的玩法 LPUSH + LPOP
10.构造队列 LPUSH + RPOP
11.阻塞队列 LPUSH + BRPOP
===========================LIST使用场景===========================
假设xx关注了某个公众号,如何实现在登录微信的时候,自动发送消息给xx
lpush wechat:[xx公众号]:msg:[xx的id] [发送这条消息的id]
===========================Set===================================
1.sadd [k] [v...]
2.srem [k] [v...]
3.sismember [k] [v] 检查某一个值是否在这个集合中存在
4.smembers [k] 获取这个集合中的所有数据
5.srandmember [key] [count] 从集合中选出count个元素 ,元素不从key中删除(值是随机的)
6.spop [key] [count] 从集合中选中count个元素 元素从集合中删除
7.sinterstore [destination] [key...] 将交集的结果存入新集合
8.sinter [key...] 交集运算
9.sunion [key...] 并集运算
10.sunionstore [destination] [key...] 将并集的结果存入新集合
11.sdiff [key] 差集运算
12.sdiffstore [destination] [key...] 将差集的结果存入这个集合
13.scard [key] 计算set集合的长度
===========================Sorted Set(Zset)===============================
----Sorted Set是自动根据打分实现排序的
1.zadd [key] [打分1 v1 打分2 v2...] :向Sorted Set中添加一个数据
2.zrange [key] [开始的下标] [结束的下标] [WITHSCORES] 获取集合中某一个区间的值
3.zincrby [key] [加的分值] [v] //给某一个值添加分值
4.zrevrank [key] [v] :降序查看当前数据的排名
5.zrank [key] [v] :升序查看当前数据的排名
6.zrem [key] [v] :删除某一条数据
7.zscore [key] [v] :获取某个值的分数
8.zcount [key] [min] [max] :获取得分在某一个区间的数据的个数
9.zcard [key]
===========================Sorted Set使用场景===========================
最热商品:购买数量最多
人气商品:点击数量最多
zadd product:hot [销量] [商品ID]
每次产生购买 zincrby product:hot 1 [商品ID]
16.1 LTRIM 命令分析
- LTRIM key start stop
- 如果 start 超过列表尾部下标,或者 start > end,那么该key会被清空
127.0.0.1:6379> llen mylistdemo
(integer) 0
127.0.0.1:6379> lpush mylistdemo a b c d
(integer) 4
127.0.0.1:6379> lrange mylistdemo 0 -1
1) "d"
2) "c"
3) "b"
4) "a"
127.0.0.1:6379> ltrim mylistdemo 4 3
OK
127.0.0.1:6379> lrange mylistdemo 0 -1
(empty list or set)
127.0.0.1:6379> lpush mylistdemo a b c d
(integer) 4
127.0.0.1:6379> ltrim mylistdemo 6 7
OK
127.0.0.1:6379> lrange mylistdemo 0 -1
(empty list or set)
127.0.0.1:6379> lpush mylistdemo a b c d
(integer) 4
127.0.0.1:6379> ltrim mylistdemo 2 1
OK
127.0.0.1:6379> lrange mylistdemo 0 -1
(empty list or set)
17.Redis数据类型结构图解
18. Jedis 和 Lettuce的区别
18.1 Jedis
- Jedis是直连Redis服务器,多线程环境下共享同一Jedis实例是非线程安全的
- 解决多线程线问题:
1.每个线程都会去拿自己的Jedis实例。缺点:当连接数量增多时,连接程本就越来越高。
2.使用JedisPool连接池来管理Jedis实例,(提高实例的复用性,减少系统资源浪费)。
JedisPool是线程安全的。
18.2 Lettuce
- Lettuce连接基于Netty,多线程环境下共享同一实例是线程安全的
连接实例(StatefulRedisConnection)是线程安全的,可以满足多线程环境下的并发访问,
连接实例数量可以按需调整。
19. Redis数据类型对应的底层数据结构?
20. Redis Zset(Sorted Set)的底层实现?(压缩表 + 跳表)
- WHAT + HOW + WHY
- 压缩列表 ZipList
-
什么是压缩列表?
利于快速的寻找列表的首 尾节点
-
为什么要使用ZipList?
- Redis为了节省内存空间而开发的ZipList
-
什么情况下使用ZipList?
- 1.有序集合保存的元素数量小于128
- 2.有序集合保存的所有元素的长度均小于64字节
-
- 跳表 SkipList
-
什么是跳表?
- 跳表是在单向链表的基础上增加了多级索引,通过多级索引位置的转跳,实现了快速查找元素,其思想类似于二分法。
-
为什么要使用SkipList?
- 为了快速查找元素,加快区间查找效率 (跳表的区间查找,可以快速定位到区间的起点,然后依次往后面遍历就可以了)
-
什么情况下使用SkipList?
- 不符合使用ZipList的条件就使用跳表。
-
跳表怎么查找某个元素?
-
普通单向链表
-
跳表的一级索引结构和二级索引结构,隔一个元素
-
-
Zset为什么使用跳表而不用二叉树或者红黑数呢?
- 1.跳表实现比红黑树简单易懂,可以有效的控制跳表的索引层级,来控制内存的消耗。
- 2.因为Zset有一个核心的操作zrange范围查找,范围查找的效率比红黑树高。(跳表的区间查找,可以快速定位到区间的起点,然后依次往后面遍历就可以了)
-
21. Redis数据结构-字符串SDS
21.1 SDS简介
- SDS :全称 Simple Dynamic String 简单动态字符串
21.2 SDS的数据结构
- len: buf数组中字符串的实际使用量。
- free: buf数组中空闲量。
- buf: 存储字符的数组。
struct sdshdr{
int len;
int free;
char buf[];
};
21.3 SDS相对C语言字符串的优点
- Redis使用C语言编写,而Redis不使用C语言字符串是有原因的。
- 1.获取字符串长度效率高
- C语言字符串是不记录字符串长度的,所以每次获取字符串长度时,都要对字符数组进行遍历,那么时间复杂度就为O(n)。而SDS采用len记录字符串的长度,所以统计字符串长度的时间复杂度为O(1)。
- 2.避免了缓冲区溢出
-
当使用 strcat(char *dest, char *src) 拼接两个字符串时,strcat是默认第一个字符数组的后面是有足够空间的,它会直接把第二个字符数组中的字符挨个复制到第一个字符数组的后面。如果这两个字符数组的内存空间是紧挨着的,那么当执行strcat时,第二个字符数组的就会被覆盖掉。这就是缓冲区溢出。
-
而SDS提供的所有修改字符串的API中,都会判断修改之后是否会内存溢出,如果会内存溢出,它会帮你进行内存扩容
-
- 3.减少修改字符串时内存重分配的次数
- 什么是 内存重分配?
- SDS如何减少内存重分配次数?
23. Redis实现接口限流
23.1 基于令牌桶策略
- 令牌桶策略优点: 当有突发大流量时,只要令牌桶里有足够多的令牌,请求就会被迅速执行。通常情况下,令牌桶容量的设置,可以接近服务器处理的极限,可以有效利用服务器的资源。这种策略适用于有突发特性的流量,且流量需要即时处理的场景。
- 代码模拟实现一: 固定速率放入令牌到桶中
private static final String KEY_INTERFACE_ACCESS_LIMIT = "ACCESS_LIMIT";
private static final int MAX_SIZE = 300;// 桶容量
private static final int IP2S = 5;// 每2秒装入桶中令牌数
// SpringBoot开启定时队列,每2秒填充5个令牌到桶中
@Scheduled(
initialDelay = 0L,
fixedDelay = 2000L)
public synchronized void pushToken(){
ListOperations<String, String> opsForList = redisTemplate.opsForList();
Long size = opsForList.size(KEY_INTERFACE_ACCESS_LIMIT);
log.warn("pushToken.size===>{}",size);
// 每次都判断当前SIZE + 即将插入的量 是否会大于 桶容量
if (size != null && size+IP2S > MAX_SIZE){
log.warn("pushTokenWarn==>达到MAX size");
return;
}
List<String> stringList = new ArrayList<>();
for (int i = 0; i < IP2S; i++) {
// 使用UUID保证TOKEN的唯一性,方便业务上做唯一标识
stringList.add(UUID.randomUUID().toString().replace("-",""));
}
// 固定速率放入令牌到桶中
opsForList.leftPushAll(KEY_INTERFACE_ACCESS_LIMIT, stringList);
}
- 模拟客户端消费令牌: 另外开启一个消费队列
@Scheduled(initialDelay = 0L,fixedDelay = 100L)
public synchronized boolean popToken(){
String str = redisTemplate.opsForList().rightPop(KEY_INTERFACE_ACCESS_LIMIT);
log.warn("====getToken===>{}",str);
return !StringUtils.isEmpty(str);
}
- 代码实现二:一次性把桶放满,等待被消费,下次进行队列未被消费的令牌将会被清空
// 一分钟的间隔,一次性把桶全部装满,没有消费完 下次将会被清除
@Scheduled(initialDelay = 0L,
fixedDelay = 60*1000L)
public synchronized void pushToken2(){
ListOperations<String, String> listOps = redisTemplate.opsForList();
Long size = listOps.size(KEY_INTERFACE_ACCESS_LIMIT);
if (size!=null && size>0){ // 还存在令牌就直接清空
// LTRIM key start stop 命令:start大于列表最大下标的时候 将清空列表
listOps.trim(KEY_INTERFACE_ACCESS_LIMIT,size,size);
}
// 一次把所有令牌全部放入桶中
List<String> stringList = new ArrayList<>(MAX_SIZE);
for (int i = 0; i < MAX_SIZE; i++) {
stringList.add(UUID.randomUUID().toString().replace("-",""));
}
listOps.leftPushAll(KEY_INTERFACE_ACCESS_LIMIT,stringList);
}
24. Redis实现 保证接口幂等性 ~ 防重Token令牌(防止表单重复提交)
- 进入表单提交页面的时候先调用后台接口生成一个token令牌,后台把令牌存储到Redis,并且Redis key的value保证是当前登录用户的唯一标识
- 提交表单的时候携带上这个token发送到后台进行验证
- Redis使用LUA脚本保证判断key和删除key的原子性
/**
* 存入 Redis 的 Token 键的前缀
*/
private static final String FROM_TOKEN_PREFIX = "FROM_TOKEN:";
/**
* 生成 Token 存入 Redis,并返回该 Token
* @param value 当前登录用户唯一标识
*/
public String generateToken(String value) {
// 生成令牌
String token = UUID.randomUUID().toString().replace("-", "");
// 设置Redis的key,加上带冒号的前缀使得 Redis Desktop Manager可视化软件以文件夹形式出现
String redisKey = FROM_TOKEN_PREFIX + token;
// 保存token到Redis并设置30min过期时间
redisTemplate.opsForValue().set(redisKey, value, 30, TimeUnit.MINUTES);
return token;//
}
/**
* 验证 Token 正确性
* @param token 令牌
* @param value 当前登录用户的唯一标识
*/
public boolean validToken(String token, String value) {
// 使用LUA脚本 保证对Key 判断和删除 的原子性
String script =
"if redis.call('get', KEYS[1]) == KEYS[2] then return redis.call('del', KEYS[1]) else return 0 end";
RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
// 根据 token 前缀拼接 Redis Key
String key = FROM_TOKEN_PREFIX + token;
Long result = redisTemplate.execute(redisScript, Arrays.asList(key, value));
// 根据返回结果判断是否成功成功匹配并删除 Redis 键值对,若果结果不为空和 0,则验证通过
return result != null && result != 0L;
}