写在前面:
1.这篇文章的内容不是我自己写的,有的是我在网上收录的知识点,有的是我自己的观点。
2.对于一些引用的知识点,我会在章节前添加引用连接。如果不加,我真的忘了。
3.原作者发现你的内容在哪里?我没有添加连接。请联系我,我会很快添加链接。
4.如果知识缺失、错误、图片挂断,请及时给我发私信,我会立即修改。
5.知识是免费的,我只是一个处理器,从网络,还在网络上。希望大家学习顺利。
感谢:
?Redis教程 - Redis知识体系详解 | Java 全栈知识体系 (pdai.tech)
死磕 Redis - Java 技术驿站 (cmsblogs.com)
Redis中文教程
Redis 教程 | 菜鸟教程 (runoob.com)
深入学习Redis(1):Redis内存模型
深入学习Redis(2):持久化
深入学习Redis(3):主从复制
深入学习Redis(4):哨兵
深入学习Redis(5):集群
...
写在前面:
Redis知识体系
概念和基础
Redis与Memcache的比较
为什么用Redis?
Redis为什么快?
使用哪些场景?Redis
数据结构
5种基础类型
3种特殊类型
Stream
对象机制
底层数据结构
核心知识
持久化
订阅发布
事件
事务
概念
没有隔离等级
不保证原子性
三个阶段的事务
相关命令
案例
高可用
主从复制
哨兵机制
集群
应用实践
锁-toc" style="margin-left:80px;">分布式锁
多级缓存
布隆过滤器
面试题大全
缓存数据一致性
缓存穿透
缓存雪崩
缓存击穿
待整理...
Redis知识体系
概念和基础
Redis与Memcache的比较
1、Redis和Memcache数据存储在内存中,都是内存数据库。memcache还可用于缓存其他东西,如图片、视频等;
2、Redis不仅支持简单k/v同时提供类型数据list,set,hash存储等数据结构;
3、虚拟内存--Redis当物理内存用完时,一些长期未使用的内存可以使用value 交换到磁盘;
4、过期策略--memcache在set时指定,例如set key1 0 0 八、即永不过期。Redis可以通过例expire 设定,例如expire name 10;
5、分布式--设定memcache集群,利用magent做一主多从;redis可以做一主多从。都可以一主一从;
6、存储数据安全--memcache挂掉后,数据没了;redis可以定期保存到磁盘(持久化);
7、灾难恢复--memcache挂掉后,数据不可恢复; redis数据丢失后可以通过aof恢复;
8、Redis支持数据的备份,即master-slave模式的数据备份;
为什么用Redis?
1、很少改变的数据不断请求数据库效率很低,加入缓存后可以先查缓存,缓存没有时再查数据库。
2、速度快,完全基于内存,网络层使用epoll解决高并发问题,单线程(复杂存取的线程)模型避免了不必要的上下文切换。
2、丰富的数据类型,Redis有8种数据类型,当然常用的主要是 String、Hash、List、Set、 SortSet 这5种类型,他们都是基于键值的方式组织数据。每一种数据类型提供了非常丰富的操作命令,可以满足绝大部分需求,如果有特殊需求还能自己通过 lua 脚本自己创建新的命令(具备原子性);
3、除了提供的丰富的数据类型,Redis还提供了像慢查询分析、性能测试、Pipeline、事务、Lua自定义命令、Bitmaps、HyperLogLog、发布/订阅、Geo等个性化功能。
4、Redis的代码开源在GitHub,代码非常简单优雅,任何人都能够吃透它的源码;它的编译安装也是非常的简单,没有任何的系统依赖;有非常活跃的社区,各种客户端的语言支持也是非常完善。另外它还支持事务(没用过)、持久化、主从复制让高可用、分布式成为可能。
Redis为什么快?
1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);
2、数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;
3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
4、使用多路I/O复用模型,非阻塞IO;
5、使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;
以上几点都比较好理解,下边我们针对多路 I/O 复用模型进行简单的探讨: 多路 I/O 复用模型 多路I/O复用模型是利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,在 空闲的时候,会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。
这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用 技术可以让单个线程高效的处理多个连接请求(尽量减少网络 IO 的时间消耗),且 Redis在内存中操作数据的速度非常快,也就是说内存内的操作不会成为影响Redis性能的瓶颈,主要由以上几点造就了 Redis 具有很高的吞吐量。
哪些场景使用Redis
热点数据(经常会被查询,但是不经常被修改或者删除的数据),首选是使用redis缓存,毕竟强大到冒泡的QPS和极强的稳定性不是所有类似工具都有的,而且相比于memcached还提供了丰富的数据类型可以使用,另外,内存中的数据也提供了AOF和RDB等持久化机制可以选择,要冷、热的还是忽冷忽热的都可选。
结合具体应用需要注意一下:很多人用spring的AOP来构建redis缓存的自动生产和清除,过程可能如下:
-
Select 数据库前查询redis,有的话使用redis数据,放弃select 数据库,没有的话,select 数据库,然后将数据插入redis
-
update或者delete数据库钱,查询redis是否存在该数据,存在的话先删除redis中数据,然后再update或者delete数据库中的数据
上面这种操作,如果并发量很小的情况下基本没问题,但是高并发的情况请注意下面场景:
为了update先删掉了redis中的该数据,这时候另一个线程执行查询,发现redis中没有,瞬间执行了查询SQL,并且插入到redis中一条数据,回到刚才那个update语句,这个悲催的线程压根不知道刚才那个该死的select线程犯了一个弥天大错!于是这个redis中的错误数据就永远的存在了下去,直到下一个update或者delete。
诸如统计点击数等应用。由于单线程,可以避免并发问题,保证不会出错,而且100%毫秒级性能!爽。
命令:INCRBY
当然爽完了,别忘记持久化,毕竟是redis只是存了内存!
-
相当于消息系统,ActiveMQ,RocketMQ等工具类似,但是个人觉得简单用一下还行,如果对于数据一致性要求高的话还是用RocketMQ等专业系统。
-
由于redis把数据添加到队列是返回添加元素在队列的第几位,所以可以做判断用户是第几个访问这种业务
-
队列不仅可以把并发请求变成串行,并且还可以做队列或者栈使用
用于数据量上亿的场景下,例如几亿用户系统的签到,去重登录次数统计,某用户是否在线状态等等。
想想一下腾讯10亿用户,要几个毫秒内查询到某个用户是否在线,你能怎么做?千万别说给每个用户建立一个key,然后挨个记(你可以算一下需要的内存会很恐怖,而且这种类似的需求很多,腾讯光这个得多花多少钱。。)好吧。这里要用到位操作——使用setbit、getbit、bitcount命令。
原理是:
redis内构建一个足够长的数组,每个数组元素只能是0和1两个值,然后这个数组的下标index用来表示我们上面例子里面的用户id(必须是数字哈),那么很显然,这个几亿长的大数组就能通过下标和元素值(0和1)来构建一个记忆系统,上面我说的几个场景也就能够实现。用到的命令是:setbit、getbit、bitcount
-
验证前端的重复请求(可以自由扩展类似情况),可以通过redis进行过滤:每次请求将request Ip、参数、接口等hash作为key存储redis(幂等性请求),设置多长时间有效期,然后下次请求过来的时候先在redis中检索有没有这个key,进而验证是不是一定时间内过来的重复提交
-
秒杀系统,基于redis是单线程特征,防止出现数据库“爆破”
-
全局增量ID生成,类似“秒杀”
例如新闻列表页面最新的新闻列表,如果总数量很大的情况下,尽量不要使用select a from A limit 10这种low货,尝试redis的 LPUSH命令构建List,一个个顺序都塞进去就可以啦。不过万一内存清掉了咋办?也简单,查询不到存储key的话,用mysql查询并且初始化一个List到redis中就好了。
谁得分高谁排名往上。命令:ZADD(有续集,sorted set)
最近在研究股票,发现量化交易是个非常好的办法,通过臆想出来规律,用程序对历史数据进行验证,来判断这个臆想出来的规律是否有效,这玩意真牛!有没有哪位玩这个的给我留个言,交流一下呗。
数据量太大、数据访问频率非常低、修改特别频繁
数据结构
5种基础类型
String(字符串): string类型时二进制安全的,意思就是可以存储任何数据;
Hash(哈希): 类似于map : java中的Map<String,Object>;
List(列表):实际上是链表,可以添加任何一个元素在表头或者表位;
Set(集合):无序无重复;
Zset(有序集合):跟set一样也是string类型元素的集合。关联一个double分数;
String
redisTemplate.opsForValue().set(); redisTemplate.opsForValue().get();
Hash
//Redis hash 是一个 string 类型的 field(字段) 和 value(值) 的映射表,hash 特别适合用于存储对象。 Redis 中每个 hash 可以存储 232 - 1 键值对 private void hash() { Map userInfo = new HashMap<>(); userInfo.put("name", "Lucy"); userInfo.put("description", "a cool girl"); //put : (map的key, hashkey(name等),value redisTemplate.opsForHash().put("userInfo", "sex", "Female"); //putAll:放入map对象 redisTemplate.opsForHash().putAll("userInfo", userInfo); //values参数key对应的map值,返回结果为List<Object> ,keys方法也类似 System.err.println(redisTemplate.opsForHash().values("userInfo").toString()); //delete删除对应的字段 redisTemplate.opsForHash().delete("userInfo", "name"); //确定字段是否在map中存在,bool类型 Boolean is_save = redisTemplate.opsForHash().hasKey("userInfo", "name"); //multiGet,获取多个值 Collection<Object> keys = new ArrayList<>(); keys.add("name"); keys.add("sex"); System.out.println(redisTemplate.opsForHash().multiGet("userInfo", keys)); }
List
List实现数据结构是双端链表。
public void list() { List userInfo = new ArrayList(); redisTemplate.delete("userInfo"); userInfo.add("Nancy"); userInfo.add("a sunny girl"); redisTemplate.opsForList().leftPush("userInfo", "first String before nancy"); redisTemplate.opsForList().leftPush("userInfo", "second String before nancy"); //左右插入l/rpush , all 是集合 redisTemplate.opsForList().rightPushAll("userInfo", userInfo); System.err.println(redisTemplate.opsForList().leftPop("userInfo").toString()); System.err.println(redisTemplate.opsForList().rightPop("userInfo").toString()); System.err.println("在出栈后的userInfoList列表数据:" + redisTemplate.opsForList().range("userInfo", 0, -1)); }
L / Rpush——链表头尾(左右插入元素) Lrange——模板中是range,返回区间值,0 到 -1是返回全集 L / Rpop——头尾(左右)出栈,会删除元素 Lindex——根据索引,取出元素 Llen——链表长度,元素个数 Lrem——根据key,删除n个value Ltrim——根据索引,删除指定元素 Rpoplpush——出栈,入栈 Lset——根据index,设置value Linsert before——根据value,在之前插入值 Linsert after——根据value,在之后插入值
Set
add remove pop //出并删除元素 move //将集合1中的指定元素移到集合2 isMember //是否存在元素 randomMember //随机取元素 members //遍历元素
Zset
Redis 有序集合和集合一样也是string类型元素的集合,且不允许重复的成员。
不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。
有序集合的成员是唯一的,但分数(score)却可以重复。
1:zadd:添加元素,格式是zadd zset的key score值项的值,Score和项可以是多对,score可以是整数,
也可以是浮点数,还可以是+inf表示正无穷大,-inf表示负无穷大
2:zrange:获取索引区间内的元素
3:zrangebyscore:获取分数区间内的元素
4:zrem:删除元素,格式是zrem zset的key 项的值,项的值可以是多个
5:zcard:获取集合中元素个数,格式是zcard zset的key
6:zincrby:增减元素的Score,格式是zincrby zset的key 正负数字项的值
7:zcount:获取分数区间内元素个数,格式是zcount zset的key 起始score 终止score
8:zrank:获取项在zset中的索引,格式是zrank zset的key 项的值
9:zscore:获取元素的分数,格式是zscore zset的key 项的值,返回项在zset中的score
10:zrevrank:获取项在zset中倒序的索引,格式是zrevrank zset的key 项的值
11:zrevrange:获取索引区间内的元素,格式是zrevrange zset的key 起始索引终止索引(withscores)
插曲
使用完Hash后再在同一个字段插入List有报错:
org.springframework.data.redis.RedisSystemException: Error in execution; nested exception is io.lettuce.core.RedisCommandExecutionException:
WRONGTYPE Operation against a key holding the wrong kind of value
原因: 可能是有同名Key写入不同类型值导致的问题。比如说:存进去是hash,去除操作用了list的操作。
解决方案:删除key重新写入:
redisTemplate.delete("userInfo");
3种特殊类型
Geospatal:地理位置,Geospatal底层是zset通过来实现的。
HyperLogLog:基数统计,就是不重复的元素的个数,网站用户数。
BitMaps:位图,只有0 和 1 两个状态,可以应用在统计用户每日打卡情况。
Geospatal
geoadd key longitude(经度) latitude(纬度) member(名称) [longitude latitude member ...]
将指定经纬度的地理位置及其名称添加到指定的zset集合中,支持一次添加多个,该命令的返回值为添加到zset有序集合的数目。如添加一个经纬度为116.413384 39.910925(表示潮州的经纬度,经纬度之间以空格分开)到key为cityzset集合中,如下
geopos key member [member ...]
根据key、member名称获取指定的经纬度,如获取上面我们保存的潮州的地理位置,支持一次获取多个。
geodist key member1 member2 [m|km|ft|mi]]
返回两个给定位置之间的距离,可以应用在求两个地理位置之间的距离。如果两个位置之间的其中一个不存在, 那么命令返回空值。其中指定单位的参数 unit 必须是以下单位的其中一个(默认为m):
-
m
表示单位为米。 -
km
表示单位为千米。 -
mi
表示单位为英里。 -
ft
表示单位为英尺。
示例: 计算广州和北京之间的距离。
georadius key longitude(经度) latitude(纬度) radius [m|km|ft|mi]] [withcoord] [withdist]
通过该命令可以实现以给定的经纬度为中心,找出指定半径内的元素member,如可以应用在查找附近的人。
georadiusbymember key member radius [m|km|ft|mi]] [withcoord] [withdist]
找出指定元素周围的其他元素,与上一条命令不同的是,georadiusbymember是根据member和指定的查找距离radius来搜索,并将查找结果保存到另外的集合中。
zrem key member [member ...]
geo的底层是zset,因此可以用zrem key member来删除指定的member元素
zrange key min max
查看指定key中对应的所有member元素
HyperLogLog
pfadd key element1 element2 [element...]
添加1个或n个element到指定key对应的集合中
pfmerge destkey sourcekey1 sourcekey2 [sourcekey....]
合并n个指定的元素的基数到指定的destkey元素的基数中(取并集)
pfcount key
统计指定key中对应的基数值(自动去重)
如下示例:
BitMaps
(1条消息) 一看就懂系列之 详解redis的bitmap在亿级项目中的应用_咖啡色的羊驼-CSDN博客
setbit key offset value
设置offset和value必须 为Integer类型,且value只能取 0 或 1
getbit key offset bitcount key [start end]
统计指定范围内value取值为1的数量
bitop AND|OR|NOT|XOR destkey key [key...]
命令可以在不同的字符串之间执行按位运算,提供的位运算有逻辑并、逻辑或、逻辑异或,并将结果保存到destkey中。
bitpos key bit [start] [end]
查找指定范围内为0或1的第一位。
应用:统计员工一周出勤情况(0代表缺勤、1代表出勤):
Stream
Redis Stream 是 Redis 5.0 版本新增加的数据结构。
Redis Stream 主要用于消息队列(MQ,Message Queue),Redis 本身是有一个 Redis 发布订阅 (pub/sub) 来实现消息队列的功能,但它有个缺点就是消息无法持久化,如果出现网络断开、Redis 宕机等,消息就会被丢弃。
简单来说发布订阅 (pub/sub) 可以分发消息,但无法记录历史消息。
而 Redis Stream 提供了消息的持久化和主备复制功能,可以让任何客户端访问任何时刻的数据,并且能记住每一个客户端的访问位置,还能保证消息不丢失。
Redis Stream 的结构如下所示,它有一个消息链表,将所有加入的消息都串起来,每个消息都有一个唯一的 ID 和对应的内容:
每个 Stream 都有唯一的名称,它就是 Redis 的 key,在我们首次使用 xadd 指令追加消息时自动创建。
上图解析:
-
:消费组,使用 XGROUP CREATE 命令创建,一个消费组有多个消费者(Consumer)。
-
:游标,每个消费组会有个游标 last_delivered_id,任意一个消费者读取了消息都会使游标 last_delivered_id 往前移动。
-
:消费者(Consumer)的状态变量,作用是维护消费者的未确认的 id。 pending_ids 记录了当前已经被客户端读取的消息,但是还没有 ack (Acknowledge character:确认字符)。
-
- 添加消息到末尾
-
- 对流进行修剪,限制长度
-
- 删除消息
-
- 获取流包含的元素数量,即消息长度
-
- 获取消息列表,会自动过滤已经删除的消息
-
- 反向获取消息列表,ID 从大到小
-
- 以阻塞或非阻塞方式获取消息列表
-
- 创建消费者组
-
- 读取消费者组中的消息
-
- 将消息标记为"已处理"
-
- 为消费者组设置新的最后递送消息ID
-
- 删除消费者
-
- 删除消费者组
-
- 显示待处理消息的相关信息
-
- 转移消息的归属权
-
- 查看流和消费者组的相关信息;
-
- 打印消费者组的信息;
-
- 打印流信息
对象机制
LPUSH
和 LLEN
只能用于列表键, 而 SADD
和 SRANDMEMBER
只能用于集合键, 等等; 另外一些命令, 比如 DEL
、 TTL
和 TYPE
, 可以用于任何类型的键;
但是要正确实现这些命令, 必须为不同类型的键设置不同的处理方式: 比如说, 删除一个列表键和删除一个字符串键的操作过程就不太一样。
.
集合类型就可以由字典和整数集合两种不同的数据结构实现, 但是, 当用户执行 ZADD 命令时,不必关心集合使用的是什么编码, 将新元素添加到集合就可以了。
。
为了解决以上问题, , 这个系统的主要功能包括:
redisObject 对象.
基于 redisObject 对象的类型检查.
基于 redisObject 对象的显式多态函数.
对 redisObject 进行分配、共享和销毁的机制.
Redis 针对不同的数据结构定义统一的数据结构 redisObject 来进行管理。
换句话说,redisObject 是所有数据结构的最外层的一层结构定义。
redisObject 由五个属性组成
表示当前值对象的一个数据类型,在上一级视视频中,我们用来验证 bitmaps,typeloglogs ,geo底层的数据结构类型的时候使用的 type 命令。
所以 的取值有六种。
的方式查看当前 kye-value
的数据类型
Redis 针对每种数据结构都设计有多种编码格式进行数据存储。比如说:单纯一个字符串的编码Redis 就提供了 int,embstr,raw 三种编码格式。
关于编码后面会谈到。而 encoding 就是用来表示当前数据存储采用的是那种编码格式。
encoding 相关属性值在源码中有定义,相关代码如下:
查看当前 kye-value
的数据类型
,当 Redis 配置了最大内存配置 maxmemory 后,再触发执行执行 lru 策略的时候,用来辅助 lru 策略删除数据用的。
,当 refcount =0时,表示可以安全回收当前对象。
当配置有 maxmemory 时,存在共享对象池,[0-1000] 范围内的数值存在共享对象,这些共享对象被引用的次数越多相应的 refcount 越大。
查看当前 key-value
的引用次数
ptr 就是真实存储数据的指针。指向真实数据。
:set age 18
假设此时 Redis 中存在一个字符串,如上
此时该字符串对应的一个 redisObject 抽象图如下:
根据图片我们能够知道Redis中该字符串的讯息
首先 age 的,
并且 该字符串的 。
lru 是这个对象最近一次被访问的时间
refcount 表示的当前对象被引用的次数
-
根据给定的key,在数据库字典中查找和他相对应的redisObject,如果没找到,就返回NULL;
-
检查redisObject的type属性和执行命令所需的类型是否相符,如果不相符,返回类型错误;
-
根据redisObject的encoding属性所指定的编码,选择合适的操作函数来处理底层的数据结构;
-
返回数据结构的操作结果作为命令的返回值。
比如现在执行LPOP命令:
redis一般会把一些常见的值放到一个共享对象中,这样可使程序避免了重复分配的麻烦,也节约了一些CPU时间。
:
1.各种命令的返回值,比如成功时返回的OK,错误时返回的ERROR,命令入队事务时返回的QUEUE,等等
2.包括0 在内,小于REDIS_SHARED_INTEGERS的所有整数(REDIS_SHARED_INTEGERS的默认值是10000)
-
如果共享对象是保存整数值(0~9999)的字符串对象,那么验证操作的复杂度为O(1)
-
如果共享对象是保存字符串值的字符串对象,那么验证操作的复杂度为 O(N)
-
如果共享对象是包含了多个值(或者对象) 对象,比如列表对象或哈希对象,那么验证操作的复杂度为 O(N^2)
如果对复杂度较高的对象创建共享对象,需要消耗很大的CPU,用这种消耗去换取内存空间,是不合适的
-
每个redisObject结构都带有一个refcount属性,指示这个对象被引用了多少次;
-
当新创建一个对象时,它的refcount属性被设置为1;
-
当对一个对象进行共享时,redis将这个对象的refcount加1;
-
当使用完一个对象后,或者消除对一个对象的引用之后,程序将对象的refcount减1;
-
当对象的refcount降至0 时,这个RedisObject结构,以及它引用的数据结构的内存都会被释放。
底层数据结构
-
简单动态字符串 - sds
-
压缩列表 - ZipList
-
快表 - QuickList
-
字典/哈希表 - Dict
-
整数集 - IntSet
-
跳表 - ZSkipList
这是一种用于存储二进制数据的一种结构, 具有动态扩容的特点。
,因为有了表头结构的存在,所以SDS比传统C字符串在某些方面更加优秀,并且能够兼容传统C字符串。
sds是的,它可以存储任意二进制数据,不像C语言字符串那样以‘\0’来标识字符串结束。
因为传统符合ASCII编码,这种编码的操作的特点就是: 。即,当读一个字符串时,只要遇到’\0’结尾,就认为到达末尾,就忽略’\0’结尾以后的所有字符。因此,如果传统字符串保存图片,视频等二进制文件,操作文件时就被截断了。
SDS表头的buf被定义为字节数组,因为判断是否到达字符串结尾的,这意味着它可以存放任何二进制的数据和文本数据,包括’\0’。
SDS 和传统的 C 字符串获得的做法不同,传统的C字符串遍历字符串的长度,遇零则止,复杂度为O(n)。而SDS表头的len成员就保存着字符串长度,所以获得字符串长度的操作复杂度为。
总结下sds的特点是:可动态扩展内存、二进制安全、快速遍历字符串 和与传统的C语言字符串类型兼容。
sds结构一共有五种Header定义,其目的是为了满足不同长度的字符串可以使用不同大小的Header,从而节省内存。 Header部分主要包含以下几个部分: + len:表示字符串真正的长度,不包含空终止字符 + alloc:表示字符串的最大容量,不包含Header和最后的空终止字符 + flags:表示header的类型。
// 五种header类型,flags取值为0~4 #define SDS_TYPE_5 0 #define SDS_TYPE_8 1 #define SDS_TYPE_16 2 #define SDS_TYPE_32 3 #define SDS_TYPE_64 4
在RedisObject中,SDS的两种存储形式
而Redis 的字符串共有两种存储方式,在长度特别短时,使用 emb 形式存储 (embedded),当长度超过 44 时,使用 raw 形式存储。
embstr 存储形式是这样一种存储形式,它将 RedisObject 对象头和 SDS 对象连续存在一起,使用 malloc 方法一次分配。而 raw 存储形式不一样,它需要两次 malloc,两个对象头在内存地址上一般是不连续的。
在字符串比较小时,SDS 对象头的大小是capacity+3——SDS结构体的内存大小至少是 3。意味着分配一个字符串的最小空间占用为 19 字节 (16+3)。
如果总体超出了 64 字节,Redis 认为它是一个大字符串,不再使用 emdstr 形式存储,而该用 raw 形式。而64-19-结尾的\0,所以empstr只能容纳44字节。
。(字符串最大长度为 )
的容量大小定义在sds.h文件中,默认是 1024 * 1024,也就是1MB。
通过上面的源代码可以看出,扩容策略是字符串在长度小于 SDS_MAX_PREALLOC 之前,扩容空间采用加倍策略,也就是保留 100% 的冗余空间。当长度超过 SDS_MAX_PREALLOC 之后,为了避免加倍后的冗余空间过大而导致浪费,每次扩容只会多分配 SDS_MAX_PREALLOC大小的冗余空间。
释放用于优化SDS的字符串缩短操作。
-
当要缩短SDS保存的字符串时,程序并不立即使用内存充分配来回收缩短后多出来的字节,而是使用表头的成员将这些字节记录起来,并等待将来使用。
压缩列表-
ziplist是list键、hash键以及zset键的底层实现之一()
这些键的常规底层实现如下:
-
:双向链表
-
:字典dict
-
:跳跃表zskiplist
但是当list键里包含的元素较少、并且每个元素要么是小整数要么是长度较小的字符串时,redis将会用ziplist作为list键的底层实现。同理hash和zset在这种场景下也会使用ziplist。
既然已有底层结构可以实现list、hash、zset键,为什么还要用ziplist呢?当然是为了节省内存空间,我们先来看看ziplist是如何压缩的。
ziplist是由*,类似于数组,ziplist在内存中是连续存储的,但是不同于数组,为了节省内存 ziplist的每个元素所占的内存大小可以不同(数组中叫元素,ziplist叫节点,下文都用“节点”),每个节点可以用来存储一个整数或者一个字符串。
-
zlbytes: ziplist的长度(单位: 字节),是一个32位无符号整数
-
zltail: ziplist最后一个节点的偏移量,反向遍历ziplist或者pop尾部节点的时候有用。
-
zllen: ziplist的节点(entry)个数
-
entry: 节点
-
zlend: 值为0xFF,用于标记ziplist的结尾
普通数组的遍历是根据数组里存储的数据类型 找到下一个元素的,例如int类型的数组访问下一个元素时每次只需要移动一个sizeof(int)就行(实际上开发者只需让指针p+1就行,在这里引入sizeof(int)只是为了说明区别)。 上文说了,ziplist的每个节点的长度是可以不一样的,而我们面对不同长度的节点又不可能直接sizeof(entry),那么它是怎么访问下一个节点呢?
每个节点由三部分组成:prevlength、encoding、data
-
prevlengh: 记录上一个节点的长度,为了方便反向遍历ziplist
-
encoding: 当前节点的编码规则,下文会详细说
-
data: 当前节点的值,可以是数字或字符串
为了节省内存
-
entry的前8位小于254,则这8位就表示上一个节点的长度
-
entry的前8位等于254,则意味着上一个节点的长度无法用8位表示,后面32位才是真实的prevlength。
: 其中整数节点分为6类:
整数节点的encoding的长度为8位,其中用来区分整数节点和字符串节点(高2位为11时是整数节点),用来区分整数节点的类型,定义如下:
#define ZIP_INT_16B (0xc0 | 0<<4)//整数data,占16位(2字节) #define ZIP_INT_32B (0xc0 | 1<<4)//整数data,占32位(4字节) #define ZIP_INT_64B (0xc0 | 2<<4)//整数data,占64位(8字节) #define ZIP_INT_24B (0xc0 | 3<<4)//整数data,占24位(3字节) #define ZIP_INT_8B 0xfe //整数data,占8位(1字节) /* 4 bit integer immediate encoding */ //整数值1~13的节点没有data,encoding的低四位用来表示data #define ZIP_INT_IMM_MASK 0x0f #define ZIP_INT_IMM_MIN 0xf1 /* 11110001 */ #define ZIP_INT_IMM_MAX 0xfd /* 11111101 */
值得注意的是 最后一种encoding是存储整数0~12的节点的encoding,它没有额外的data部分,encoding的表示这个类型,就是它的data。这种类型的节点的encoding大小介于ZIP_INT_24B与ZIP_INT_8B之间(1~13),但是为了表示整数0,取出低四位xxxx之后会将其作为实际的data值(0~12)。在函数zipLoadInteger中,我们可以看到这种类型节点的取值方法:
... } else if (encoding >= ZIP_INT_IMM_MIN && encoding <= ZIP_INT_IMM_MAX) { ret = (encoding & ZIP_INT_IMM_MASK)-1; } ...
字符串节点分为3类:
-
当data小于63字节时(2^6),节点存为上图的第一种类型,为00,表示data的长度。
-
当data小于16383字节时(2^14),节点存为上图的第二种类型,为01,后续14位表示data的长度。
-
当data小于4294967296字节时(2^32),节点存为上图的第二种类型,为10,下一字节起连续32位表示data的长度。
快速列表-QuickList
何为quicklist,上次说到ziplist每次变更的时间复杂度都非常高,因为必须要重新生成一个新的ziplist来作为更新后的list,如果一个list非常大且更新频繁,那就会给redis带来非常大的负担。如何既保留ziplist的空间高效性,又能不让其更新复杂度过高?
其实说白了就是把ziplist和普通的双向链表结合起来。每个双链表节点中保存一个ziplist,然后每个ziplist中存一批list中的数据(具体ziplist大小可配置),这样既可以避免大量链表指针带来的内存消耗,也可以避免ziplist更新导致的大量性能损耗,将大的ziplist化整为零。
quicklist结构在quicklist.c中的解释为一个由。
-
压缩列表ziplist结构本身就是一个连续的内存块,由表头、若干个entry节点和压缩列表尾部标识符zlend组成,通过一系列编码规则,提高内存的利用率,使用于存储整数和短字符串。
-
压缩列表ziplist结构的缺点是:每次插入或删除一个元素时,都需要进行频繁的调用realloc()函数进行内存的扩展或减小,然后进行数据”搬移”,甚至可能引发连锁更新,造成严重效率的损失。
quicklist是由ziplist组成的双向链表,链表中的每一个节点都以压缩列表ziplist的结构保存着数据,而ziplist有多个entry节点,保存着数据。相当与一个quicklist节点保存的是。
-
quicklist宏观上是一个双向链表,因此,它具有一个双向链表的有点,进行插入或删除操作时非常方便,虽然复杂度为O(n),但是不需要内存的复制,提高了效率,而且访问两端元素复杂度为O(1)。
-
quicklist微观上是一片片entry节点,每一片entry节点内存连续且顺序存储,可以通过二分查找以 log2(n)log2(n) 的复杂度进行定位。
代码
/* * quicklist */ typedef struct quicklist { //头结点 quicklistNode *head; //尾节点 quicklistNode *tail; //所有ziplist中entry数量 unsigned long count; //quicklistNodes节点数量 unsigned int len; //ziplist中entry能保存的数量,由list-max-ziplist-size配置项控制 int fill : 16; //压缩深度,由list-compress-depth配置项控制 unsigned int compress : 16; } quicklist;
注释
:ziplist 中 entry 能保存的数量,由 list-max-ziplist-size 配置项控制
Copy 表示了单个节点(quicklistNode)的负载比例(fill factor),负数限制 quicklistNode 中的 ziplist 的字节长度, 正数限制 quicklistNode 中的 ziplist 的最大长度。 Copy-5: 最大存储空间: 64 Kb <-- 通常情况下不要设置这个值 -4: 最大存储空间: 32 Kb <-- 非常不推荐 -3: 最大存储空间: 16 Kb <-- 不推荐 -2: 最大存储空间: 8 Kb <-- 推荐 -1: 最大存储空间: 4 Kb <-- 推荐 对于正整数则表示最多能存储到你设置的那个值, 当前的节点就装满了 通常在 -2 (8 Kb size) 或 -1 (4 Kb size) 时, 性能表现最好
:压缩深度,由 list-compress-depth 配置项控制
Copy 表示 quicklist 中的节点 quicklistNode, 除开最两端的 compress 个节点之后, 中间的节点都会被压缩(LZF压缩算法)。
Copytypedef struct quicklistNode { //前节点指针 struct quicklistNode *prev; //后节点指针 struct quicklistNode *next; //数据指针。当前节点的数据没有压缩,那么它指向一个ziplist结构;否则,它指向一个quicklistLZF结构。 unsigned char *zl; //zl指向的ziplist实际占用内存大小。需要注意的是:如果ziplist被压缩了,那么这个sz的值仍然是压缩前的ziplist大小 unsigned int sz; //ziplist里面包含的数据项个数 unsigned int count : 16; //ziplist是否压缩。取值:1--ziplist,2--quicklistLZF unsigned int encoding : 2; //存储类型,目前使用固定值2 表示使用ziplist存储 unsigned int container : 2; //当我们使用类似lindex这样的命令查看了某一项本来压缩的数据时,需要把数据暂时解压,这时就设置recompress=1做一个标记,等有机会再把数据重新压缩 unsigned int recompress : 1; unsigned int attempted_compress : 1; /* node can't compress; too small */ unsigned int extra : 10; /* more bits to steal for future usage */ } quicklistNode;
Copytypedef struct quicklistLZF { unsigned int sz; //压缩后的ziplist大小 char compressed[];//柔性数组,存放压缩后的ziplist字节数组 } quicklistLZF;
创建
/* 创建一个新的quicklist. * 使用quicklistRelease()释放quicklist. */ quicklist *quicklistCreate(void) { struct quicklist *quicklist; quicklist = zmalloc(sizeof(*quicklist)); quicklist->head = quicklist->tail = NULL; quicklist->len = 0; quicklist->count = 0; quicklist->compress = 0; quicklist->fill = -2; quicklist->bookmark_count = 0; return quicklist; }
create就没啥好说的了,但这里需要提醒下,fill值默认是-2,也就是说每个quicklistNode中的ziplist最长是8k字节,可以更具自己业务需求调整具体配置。
-1: 每个quicklistNode节点的ziplist所占字节数不能超过4kb。(建议配置) -2: 每个quicklistNode节点的ziplist所占字节数不能超过8kb。(默认配置&建议配置) -3: 每个quicklistNode节点的ziplist所占字节数不能超过16kb。 -4: 每个quicklistNode节点的ziplist所占字节数不能超过32kb。 -5: 每个quicklistNode节点的ziplist所占字节数不能超过64kb。 任意正数: 表示:ziplist结构所最多包含的entry个数,最大为215215。
插入
头插和尾插都调用了quicklistNodeAllowInsert先判断了是否能直接在当前头|尾节点能插入,如果能就直接插入到对应的ziplist里,否则就需要新建一个新节点再操作了。 还记得上文中我们说的fill字段吗,quicklistNodeAllowInsert其实就是根据fill的具体值来判断是否已经超过最大容量。
特定插入比较麻烦总结如下:
如果当前被插入节点不满,直接插入。 如果当前被插入节点是满的,要插入的位置是当前节点的尾部,且后一个节点有空间,那就插到后一个节点的头部。 如果当前被插入节点是满的,要插入的位置是当前节点的头部,且前一个节点有空间,那就插到前一个节点的尾部。 如果当前被插入节点是满的,前后节点也都是满的,要插入的位置是当前节点的头部或者尾部,那就创建一个新的节点插进去。 否则,当前节点是满的,且要插入的位置在当前节点的中间位置,我们需要把当前节点分裂成两个新节点,然后再插入。
删除
删除相对于插入而言简单多了,我先看的插入逻辑,插入中有节点的分裂,但删除里却没有节点的合并,。
字典/哈希表 - Dict
(1条消息) 【Redis源码剖析】 - Redis内置数据结构之字典dict_上善若水,人淡如菊-CSDN博客
typedef struct dictht{ //哈希表数组 dictEntry **table; //哈希表大小 unsigned long size; //哈希表大小掩码,用于计算索引值 //总是等于 size-1 unsigned long sizemask; //该哈希表已有节点的数量 unsigned long used; }dictht
哈希表是由数组 table 组成,table 中每个元素都是指向 dict.h/dictEntry 结构,dictEntry 结构定义如下:
typedef struct dictEntry{ //键 void *key; //值 union{ void *val; uint64_tu64; int64_ts64; }v; //指向下一个哈希表节点,形成链表 struct dictEntry *next; }dictEntry
著作权归Java 全栈知识体系所有。 链接:Redis进阶 - 数据结构:底层数据结构详解 | Java 全栈知识体系
key 用来保存键,val 属性用来保存值,值可以是一个指针,也可以是uint64_t整数,也可以是int64_t整数。
注意这里还有一个指向下一个哈希表节点的指针,我们知道哈希表最大的问题是存在哈希冲突,如何解决哈希冲突,有开放地址法和链地址法。这里采用的便是链地址法,通过next这个指针可以将多个哈希值相同的键值对连接在一起,用来。
著作权归Java 全栈知识体系所有。 链接:Redis进阶 - 数据结构:底层数据结构详解 | Java 全栈知识体系
-
:Redis计算哈希值和索引值方法如下:
#1、使用字典设置的哈希函数,计算键 key 的哈希值 hash = dict->type->hashFunction(key); #2、使用哈希表的sizemask属性和第一步得到的哈希值,计算索引值 index = hash & dict->ht[x].sizemask;
-
:这个问题上面我们介绍了,方法是链地址法。通过字典里面的 *next 指针指向下一个具有相同索引值的哈希表节点。
-
:当哈希表保存的键值对太多或者太少时,就要通过 rerehash(重新散列)来对哈希表进行相应的扩展或者收缩。具体步骤:
1、如果执行扩展操作,会基于原哈希表创建一个大小等于 ht[0].used*2n 的哈希表(也就是每次扩展都是根据原哈希表已使用的空间扩大一倍创建另一个哈希表)。相反如果执行的是收缩操作,每次收缩是根据已使用空间缩小一倍创建一个新的哈希表。
2、重新利用上面的哈希算法,计算索引值,然后将键值对放到新的哈希表位置上。
3、所有键值对都迁徙完毕后,释放原哈希表的内存空间。
-
:
1、服务器目前没有执行 BGSAVE 命令或者 BGREWRITEAOF 命令,并且负载因子大于等于1。
2、服务器目前正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令,并且负载因子大于等于5。
ps:负载因子 = 哈希表已保存节点数量 / 哈希表大小。
什么叫渐进式 rehash?也就是说扩容和收缩操作不是一次性、集中式完成的,而是分多次、渐进式完成的。如果保存在Redis中的键值对只有几个几十个,那么 rehash 操作可以瞬间完成,但是如果键值对有几百万,几千万甚至几亿,那么要一次性的进行 rehash,势必会造成Redis一段时间内不能进行别的操作。所以Redis采用渐进式 rehash,这样在进行渐进式rehash期间,字典的删除查找更新等操作可能会在两个哈希表上进行,第一个哈希表没有找到,就会去第二个哈希表上进行查找。但是进行 增加操作,一定是在新的哈希表上进行的。
整数集 - IntSet
整数集合(intset)是集合类型的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis 就会使用整数集合作为集合键的底层实现。
Intset是集合键的底层实现之一,如果一个集合满足只保存整数元素和元素数量不多这两个条件,那么 Redis 就会采用 intset 来保存这个数据集。intset的数据结构如下:
typedef struct intset { uint32_t encoding; // 编码模式 uint32_t length; // 长度 int8_t contents[]; // 数据部分 } intset;
其中,encoding 字段表示该整数集合的编码模式,Redis 提供三种模式的宏定义如下:
// 可以看出,虽然contents部分指明的类型是int8_t,但是数据并不以这个类型存放 // 数据以int16_t类型存放,每个占2个字节,能存放-32768~32767范围内的整数 #define INTSET_ENC_INT16 (sizeof(int16_t)) // 数据以int32_t类型存放,每个占4个字节,能存放-2^32-1~2^32范围内的整数 #define INTSET_ENC_INT32 (sizeof(int32_t)) // 数据以int64_t类型存放,每个占8个字节,能存放-2^64-1~2^64范围内的整数 #define INTSET_ENC_INT64 (sizeof(int64_t))
length 字段用来保存集合中元素的个数。
contents 字段用于保存整数,数组中的元素要求不含有重复的整数且按照从小到大的顺序排列。在读取和写入的时候,均按照指定的 encoding 编码模式读取和写入。
inset中最值得一提的就是升级操作。当intset中添加的整数超过当前编码类型的时候,intset会自定升级到能容纳该整数类型的编码模式,如 1,2,3,4,创建该集合的时候,采用int16_t的类型存储,现在需要像集合中添加一个大整数,超出了当前集合能存放的最大范围,这个时候就需要对该整数集合进行升级操作,将encoding字段改成int32_6类型,并对contents字段内的数据进行重排列。
Redis提供intsetUpgradeAndAdd函数来对整数集合进行升级然后添加数据。其升级过程可以参考如下图示:
Intset是集合键的底层实现之一,如果一个集合满足只保存整数元素和元素数量不多这两个条件,那么 Redis 就会采用 intset 来保存这个数据集。intset的数据结构如下:
typedef struct intset { uint32_t encoding; // 编码模式 uint32_t length; // 长度 int8_t contents[]; // 数据部分 } intset;
其中,encoding 字段表示该整数集合的编码模式,Redis 提供三种模式的宏定义如下:
// 可以看出,虽然contents部分指明的类型是int8_t,但是数据并不以这个类型存放 // 数据以int16_t类型存放,每个占2个字节,能存放-32768~32767范围内的整数 #define INTSET_ENC_INT16 (sizeof(int16_t)) // 数据以int32_t类型存放,每个占4个字节,能存放-2^32-1~2^32范围内的整数 #define INTSET_ENC_INT32 (sizeof(int32_t)) // 数据以int64_t类型存放,每个占8个字节,能存放-2^64-1~2^64范围内的整数 #define INTSET_ENC_INT64 (sizeof(int64_t))
length 字段用来保存集合中元素的个数。
contents 字段用于保存整数,数组中的元素要求不含有重复的整数且按照从小到大的顺序排列。在读取和写入的时候,均按照指定的 encoding 编码模式读取和写入。
inset中最值得一提的就是升级操作。当intset中添加的整数超过当前编码类型的时候,intset会自定升级到能容纳该整数类型的编码模式,如 1,2,3,4,创建该集合的时候,采用int16_t的类型存储,现在需要像集合中添加一个大整数,超出了当前集合能存放的最大范围,这个时候就需要对该整数集合进行升级操作,将encoding字段改成int32_6类型,并对contents字段内的数据进行重排列。
Redis提供intsetUpgradeAndAdd函数来对整数集合进行升级然后添加数据。其升级过程可以参考如下图示:
跳表-ZSkipList
数据结构之Redis-跳表 - 简书 (jianshu.com)
将有序链表通过建立索引层的方式改造为可以支持折半查找算法,可以快速增删改查。
附带
String与Hash的选择
String 存JSON,取出后再转回对象。
如果存储的都是比较结构化的数据,比如用户数据缓存,或者经常需要操作数据的一个或者几个,特别是如果一个数据中如果filed比较多,但是每次只需要使用其中的一个或者少数的几个,使用hash是一个好的选择,因为它提供了hget 和 hmget,而无需取出所有数据再在代码中处理。
反之,如果数据差异较大,操作时常常需要把所有数据都读取出来再处理,使用string 是一个好的选择。
当然,也可以听Redis 的,放心的使用hash 吧。
还有一种场景:如果一个hash中有大量的field(成千上万个),需要考虑是不是使用string来分开存储是不是更好的选择。
核心知识
持久化
深入学习Redis(2):持久化 - 编程迷思 - 博客园 (cnblogs.com)
Redis 运行时数据保存在内存中, 一旦重启则数据将全部丢失.
Redis 提供了两种持久化方式:
-
RDB 持久化: 生成某个时间点的快照文件
-
AOF 持久化(append only file): 日志追加模式(Redis协议格式保存)
Redis可以同时使用以上两种持久化
案例
传感器数据的持久化,我们的传感器数据有如下几个特点
-
数据量大,查询DB缓慢
-
数据固定,不会涉及更改
-
日期查询,每天数据量一致
为了保证查询速度,我们在前端固定了以天为单位的查询。
在Redis中保存了以"年-月-日-设备编号"为key,数据集List为value的List存储。
每天00:10分使用工作调动线程池开启对前一天数据的加载,放入Redis。
因为不需要考虑实时性深夜访问量小,同时也需要保证数据恢复速度,所以我们使用了RDB。
至于当天的数据,我们直接通过偏移量去数据库查询,然后丢到缓存,每天00:11分自动过期。
非当天的数据,如果缓存不存在,那么查询DB在丢到缓存手动BGSAVE。
RDB
所谓内存快照,顾名思义就是给