1.Redis介绍
1.1什么是Nosql数据库
为了解决高并发、高可用、高可扩展,大数据存储等一系列问题而产生的数据库解决方案,就是NoSql。
NoSql,叫非关系型数据库,它的全名Not only sql。它不能替代关系型数据库,只能作为关系型数据库的一个良好补充。
1.2Nosql数据库分类
1.2.1 键值(Key-Value)存储数据库
相关产品: Tokyo Cabinet/Tyrant、Redis、Voldemort、Berkeley DB
典型应用: 内容缓存,主要用于处理大量数据的高访问负载。
数据模型: 一系列键值对
优势: 快速查询
劣势: 存储的数据缺少结构化
1.2.2文档型数据库
相关产品:CouchDB、MongoDB
典型应用:Web应用(与Key-Value类似,Value是结构化的)
数据模型: 一系列键值对
优势:数据结构要求不严格
劣势: 查询性能不高,而且缺乏统一的查询语法
1.2.3图形(Graph)数据库
相关数据库:Neo4J、InfoGrid、Infinite Graph
典型应用:社交网络
数据模型:图结构
优势:利用图结构相关算法。
劣势:需要对整个图做计算才能得出结果,不容易做分布式的集群方案。
1.3 SQL 与 NoSQL
在绝大部分时候,我们都会首先考虑用关系型数据库来存储我们的数据,比如SQLServer,Oracle,MySQL 等。
关系型数据库的特点:
1、它以表格的形式,基于行存储数据,是一个二维的模式。
2、它存储的是结构化的数据,数据存储有固定的模式(schema),数据需要适应
表结构。
3、表与表之间存在关联(Relationship)。
4、大部分关系型数据库都支持 SQL(结构化查询语言)的操作,支持复杂的关联查
询。
5、通过支持事务(ACID 酸)来提供严格或者实时的数据一致性。
但是使用关系型数据库也存在一些限制,比如:
1、要实现扩容的话,只能向上(垂直)扩展,比如磁盘限制了数据的存储,就要扩大磁盘容量,通过堆硬件的 方式,不支持动态的扩缩容。水平扩容需要复杂的技术来实现,比如分库分表。
2、表结构修改困难,因此存储的数据格式也受到限制。
3、在高并发和高数据量的情况下,我们的关系型数据库通常会把数据持久化到磁盘,基于磁盘的读写压力比 较大。
为了规避关系型数据库的一系列问题,我们就有了非关系型的数据库,我们一般把它叫做“non-relational”或者“Not Only SQL”。NoSQL 最开始是不提供 SQL 的数据库的意思,但是后来意思慢慢地发生了变化。
非关系型数据库的特点:
1、存储非结构化的数据,比如文本、图片、音频、视频。
2、表与表之间没有关联,可扩展性强。
3、保证数据的最终一致性。遵循 BASE(碱)理论。 Basically Available(基本
可用); Soft-state(软状态); Eventually Consistent(最终一致性)。
4、支持海量数据的存储和高并发的高效读写。
5、支持分布式,能够对数据进行分片存储,扩缩容简单。
1.4redis特性
官网介绍:https://redis.io/topics/introduction
中文网站: http://www.redis.cn
硬件层面有 CPU 的缓存;浏览器也有缓存;手机的应用也有缓存。我们把数据缓存起来的原因就是从原始位置取数据的代价太大了,放在一个临时位置存储起来,取回就可以快一些。
Redis 的特性:
1)更丰富的数据类型
2)进程内与跨进程;单机与分布式
3)功能丰富:持久化机制、过期策略
4)支持多种编程语言
5)高可用,集群
6)Redis可以通过一些键值类型来存储数据,Redis以单线程方式存储,既然是单线程(保证线程安全问题)
键值类型:
String字符类型
map散列类型
list列表类型
set集合类型
sortedset有序集合类型
1.5redis的应用场景
缓存(数据查询、短连接、新闻内容、商品内容等等)。(最多使用)
分布式集群架构中的session分离。
聊天室的在线好友列表。
任务队列。(秒杀、抢购、12306等等)
应用排行榜。
网站访问统计。
数据过期处理(可以精确到毫秒)
令牌生产
短信验证码
分布式锁
2.redis安装
3.redis的基本操作
默认有 16 个库(0-15),可以在配置文件中修改,默认使用第一个 db0。
databases 16
因为没有完全隔离,不像数据库的 database,不适合把不同的库分配给不同的业务使用。
切换数据库
select 1
清空当前数据库
flushdb
清空所有数据库
flushall
Redis 是字典结构的存储方式,采用 key-value 存储。key 和 value 的最大长度限制是 512M(来自官网 https://redis.io/topics/data-types-intro/)。键的基本操作。命令参考:http://redisdoc.com/index.html
存值
127.0.0.1:6379[1]> set name zhangsan
OK
取值
127.0.0.1:6379[1]> get name
"zhangsan"
查看所有键
127.0.0.1:6379[1]> keys
1) "name"
查看键是否存在
127.0.0.1:6379[1]> exists name
(integer) 1
删除键
127.0.0.1:6379[1]> del name
(integer) 1
重命名键
127.0.0.1:6379[1]> rename name newname
OK
127.0.0.1:6379[1]> keys
1) "newname"
查看键的类型
127.0.0.1:6379[1]> type newname
string
Redis 一共有几种数据类型?(注意是数据类型不是数据结构)
官网: https://redis.io/topics/data-types-intro
String、Hash、Set、List、Zset、
4.redis基本数据类型
最基本也是最常用的数据类型就是 String。set 和 get 命令就是 String 的操作命令。
为什么叫 Binary-safe strings 呢?
4.1.String字符串
存储类型 : 可以用来存储字符串、整数、浮点数。
操作命令
设置多个值(批量操作, 原子性)
127.0.0.1:6379[1]> mset name 12324 mset id 2 mset age 23
OK
127.0.0.1:6379[1]> keys
1) "2"
2) "name"
3) "mset"
4) "age"
5) "newname"
获取多个值
127.0.0.1:6379[1]> mget name age mset
1) "12324"
2) "23"
3) "id"
设置值, 如果 key 存在, 则不成功
127.0.0.1:6379> setnx name zhangsan
(integer) 1
127.0.0.1:6379> setnx name zhangsan
(integer) 0
基于此可实现分布式锁。 用 del key 释放锁。但如果释放锁的操作失败了, 导致其他节点永远获取不到锁, 怎么办?加过期时间。 单独用 expire 加过期, 也失败了, 无法保证原子性, 怎么办? 多参数
set key value [expiration EX seconds|PX milliseconds][NX|XX]
使用参数的方式
127.0.0.1:6379> set lokc 1 EX 10 NX
OK
127.0.0.1:6379> get lock #间隔10s再去获取
(nil)
(整数) 值递增
当存储的字符串是整数时,Redis提供了一个实用的命令INCR,其作用是让当前键值递增,并返回递增后的值。
127.0.0.1:6379> incr id
(integer) 1
127.0.0.1:6379> incr id
(integer) 2
127.0.0.1:6379> incr id
(integer) 3
增加指定的整数
127.0.0.1:6379> incrby id 100
(integer) 103
数值递减
127.0.0.1:6379> decr id
(integer) 102
127.0.0.1:6379> drcr id
127.0.0.1:6379> decrby id 5 #减少指定的函数
(integer) 95
获取多个值
127.0.0.1:6379> mget id name
1) "95"
2) "yehui"
获取值长度
127.0.0.1:6379> get name
"yehui"
127.0.0.1:6379> strlen name
(integer) 5
字符串追加内容
127.0.0.1:6379> append name good
(integer) 9
127.0.0.1:6379> get name
"yehuigood"
4.2存储实现原理
4.2.1数据模型
set hello word 为例,因为 Redis 是 KV 的数据库,它是通过 hashtable 实现的(我们把这个叫做外层的哈希)。所以每个键值对都会有一个 dictEntry(源码位置:dict.h),里面指向了 key 和 value 的指针。next 指向下一个 dictEntry。
typedef struct dictEntry {
void key;/key关键字定义/
void val;/value定义/
struct dictEntry next; / 指向下一个键值对节点 /
} dictEntry;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RJj2LgN0-1602211258160)(redisimage\redis1.png)]
key 是字符串,但是 Redis 没有直接使用 C 的字符数组,而是存储在自定义的 SDS中。
value 既不是直接作为字符串存储,也不是直接存储在 SDS 中,而是存储在redisObject 中。实际上五种常用的数据类型的任何一种,都是通过 redisObject 来存储的。
redisObject
redisObject 定义在 src/server.h 文件中。
typedef struct redisObject {
unsigned type:4;/ 对象的类型, 包括: OBJ_STRING、 OBJ_LIST、 OBJ_HASH、 OBJ_SET、 OBJ_ZSET /
unsigned encoding:4; / 具体的数据结构 /
unsigned lru:LRU_BITS; / 24 位, 对象最后一次被命令程序访问的时间, 与内存回收有关 /
int refcount;/ 引用计数。 当 refcount 为 0 的时候, 表示该对象已经不被任何对象引用, 则可以进行垃圾回收了/
void ptr;/ 指向对象实际的数据结构 /
} robj;
可以使用 type 命令来查看对外的类型。
127.0.0.1:6379> type name
string
内部编码
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zOiahk9B-1602211258166)(redisimage/redis2.png)]
127.0.0.1:6379> keys
1) "age"
2) "id"
3) "name"
127.0.0.1:6379> object encoding id #查看编码的类型
"int"
字符串类型的内部编码有三种:
1、int,存储 8 个字节的长整型(long,2^63-1)。
2、embstr, 代表 embstr 格式的 SDS(Simple Dynamic String 简单动态字符串),
存储小于 44 个字节的字符串。
3、raw,存储大于 44 个字节的字符串
/ object.c /
#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44
问题 1、什么是 SDS?
Redis 中字符串的实现。
SDS 又有多种结构(sds.h):sdshdr5、sdshdr8、sdshdr16、 sdshdr32、sdshdr64,用于存储不同的长度的字符串,分别代表 25=32byte,28=256byte,216=65536byte=64KB,232byte=4GB。
/ sds.h /
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; / 当前字符数组的长度 /
uint8_t alloc; /当前字符数组总共分配的内存大小 /
unsigned char flags; / 当前字符数组的属性、 用来标识到底是 sdshdr8 还是 sdshdr16 等 /
char buf[]; / 字符串真正的值 /
};
问题 2、为什么 Redis 要用 SDS 实现字符串?
SDS 的特点:
1、不用担心内存溢出问题,如果需要会对 SDS 进行扩容。
2、获取字符串长度时间复杂度为 O(1),因为定义了 len 属性。
3、通过“空间预分配”( sdsMakeRoomFor)和“惰性空间释放”,防止多次重分配内存。
4、判断是否结束的标志是 len 属性
问题 3、embstr 和 raw 的区别?
embstr 的使用只分配一次内存空间(因为 RedisObject 和 SDS 是连续的),而 raw需要分配两次内存空间(分别为 RedisObject 和 SDS 分配空间)。因此与 raw 相比,embstr 的好处在于创建时少分配一次空间,删除时少释放一次空间,以及对象的所有数据连在一起,寻找方便。而 embstr 的坏处也很明显,如果字符串的长度增加需要重新分配内存时,整个RedisObject 和 SDS 都需要重新分配空间,因此 Redis 中的 embstr 实现为只读。
问题 4:int 和 embstr 什么时候转化为 raw?
当 int 数 据 不 再 是 整 数 , 或 大 小 超 过 了 long 的 范 围(2^63-1=9223372036854775807)时,自动转化为 embstr。
127.0.0.1:6379> set k1 1
OK
127.0.0.1:6379> object encoding k1
"int"
127.0.0.1:6379> append k1 2
(integer) 2
127.0.0.1:6379> object encoding k1
"raw"
问题 5:明明没有超过阈值,为什么变成 raw 了?
127.0.0.1:6379> set k2 a
OK
127.0.0.1:6379> object encoding k2
"embstr"
127.0.0.1:6379> append k2 b
(integer) 2
127.0.0.1:6379> object encoding k2
"raw"
对于 embstr,由于其实现是只读的,因此在对 embstr 对象进行修改时,都会先转化为 raw 再进行修改。
因此,只要是修改 embstr 对象,修改后的对象一定是 raw 的,无论是否达到了 44个字节。
问题 6:当长度小于阈值时,会还原吗?
关于 Redis 内部编码的转换,都符合以下规律:编码转换在 Redis 写入数据时完成,且转换过程不可逆,只能从小内存编码向大内存编码转换(但是不包括重新 set)
问题 7:为什么要对底层的数据结构进行一层包装呢?
通过封装,可以根据对象的类型动态地选择存储结构和可以使用的命令,实现节省空间和优化查询速度。
4.3 Hash 哈希
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6w9Eq6Nk-1602211258169)(redisimage/redis3.png)]
4.3.1 存储类型
包含键值对的无序散列表。value 只能是字符串,不能嵌套其他类型。
同样是存储字符串,Hash 与 String 的主要区别?
1、把所有相关的值聚集到一个 key 中,节省内存空间
2、只使用一个 key,减少 key 冲突
3、当需要批量获取值的时候,只需要使用一个命令,减少内存/IO/CPU 的消耗
Hash 不适合的场景:
1.Field不能单独设置过期时间
2.没有bit操作
3.需要考虑数据量分布问题(value值非常大的时候,无法分不到多个点)
4.3.2 操作命令
127.0.0.1:6379> hset person id 1
(integer) 1
127.0.0.1:6379> hset person name zhangsan
(integer) 1
127.0.0.1:6379> hget person id
"1"
127.0.0.1:6379> hget person name
"zhangsan"
127.0.0.1:6379> hlen person
(integer) 2
4.3.3存储原理
Redis 的 Hash 本身也是一个 KV 的结构,类似于 Java 中的 HashMap。外层的哈希(Redis KV 的实现)只用到了 hashtable。当存储 hash 数据类型时,我们把它叫做内层的哈希。内层的哈希底层可以使用两种数据结构实现:
ziplist:OBJ_ENCODING_ZIPLIST(压缩列表)
hashtable:OBJ_ENCODING_HT(哈希表)
127.0.0.1:6379> hset h2 f aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
(integer) 1
127.0.0.1:6379> hset h3 f aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
(integer) 1
127.0.0.1:6379> object encoding h2
"ziplist"
127.0.0.1:6379> object encoding h3
"hashtable"
ziplist 压缩列表
ziplist 是一个经过特殊编码的双向链表,它不存储指向上一个链表节点和指向下一个链表节点的指针,而是存储上一个节点长度和当前节点长度,通过牺牲部分读写性能,来换取高效的内存空间利用率,是一种时间换空间的思想。只用在字段个数少,字段值小的场景里面。
ziplist 的内部结构
ziplist.c 源码第 16 行的注释:
\ …
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ei6TOCtl-1602211258171)(redisimage/redis4.png)]
typedef struct zlentry {
unsigned int prevrawlensize; / 上一个链表节点占用的长度 /
unsigned int prevrawlen; / 存储上一个链表节点的长度数值所需要的字节数 /
unsigned int lensize; /存储当前链表节点长度数值所需要的字节数 /
unsigned int len; / 当前链表节点占用的长度 /
unsigned int headersize; / 当前链表节点的头部大小(prevrawlensize + lensize) , 即非数据域的大小 /
unsigned char encoding; / 编码方式 /
unsigned char p; / 压缩链表以字符串的形式保存, 该指针指向当前节点起始位置 /
} zlentry;
编码 encoding(ziplist.c 源码第 204 行)
#define ZIP_STR_06B (0 << 6) // 小于63字节字节数组
#define ZIP_STR_14B (1 << 6) // 小于2^14-1字节的字节数组
#define ZIP_STR_32B (2 << 6) // 小于2^32-1字节的字节数组
#define ZIP_INT_16B (0xc0 | 0<<4) // int16_t整数
#define ZIP_INT_32B (0xc0 | 1<<4) // int32_t整数
#define ZIP_INT_64B (0xc0 | 2<<4) // int64_t整数
#define ZIP_INT_24B (0xc0 | 3<<4) // 3个字节长度的整数
#define ZIP_INT_8B 0xfe // 1个字节长度的整数
#define ZIP_INT_IMM_MIN 0xf1 / 11110001 / // 直接编码存储的最小值
#define ZIP_INT_IMM_MAX 0xfd / 11111101 / // 直接编码存储方式的最大值
#define INT24_MAX 0x7fffff // 3字节整数最大值 2^24-1
#define INT24_MIN (-INT24_MAX - 1) // 3字节整数最小值 -2^24
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cK2BPeom-1602211258174)(redisimage/redis5.png)]
使用ziplist存储的条件:
1)所有的键值对的键和值的字符串长度都小于等于64byte(一个英文字母一个字节);
2) 哈希对象保存的键值对数量小于512个。
/ src/redis.conf 配置 /
hash-max-ziplist-value 64 // ziplist 中最大能存放的值长度
hash-max-ziplist-entries 512 // ziplist 中最多能存放的 entry 节点数量
一个哈希对象超过配置的阈值(键和值的长度有>64byte,键值对个数>512 个)时,会转换成哈希表(hashtable)。
hashtable(dict)
在 Redis 中,hashtable 被称为字典(dictionary),它是一个数组+链表的结构。
Redis 的 KV 结构是通过一个 dictEntry 来实现的。Redis 又对 dictEntry 进行了多层的封装。
源码位置: dict.h
typedef struct dictEntry {
void key; / key 关键字定义 /
union {
void val; uint64_t u64; / value 定义 /
int64_t s64; double d;
} v;
struct dictEntry next; / 指向下一个键值对节点 /
} dictEntry;
dictEntry 放到了 dictht(hashtable 里面):
typedef struct dictht {
dictEntry table;/ 哈希表数组 /
unsigned long size;/ 哈希表大小 /
unsigned long sizemask;/ 掩码大小, 用于计算索引值。 总是等于 size-1 /
unsigned long used; / 已有节点数 /
} dictht;
ht 放到了 dict 里面:
typedef struct dict {
dictType type; / 字典类型 /
void privdata; / 私有数据 /
dictht ht[2]; / 一个字典有两个哈希表 /
long rehashidx; / rehash 索引 /
unsigned long iterators; / 当前正在使用的迭代器数量 /
} dict;
从最底层到最高层 dictEntry——dictht——dict——OBJ_ENCODING_HT
总结:哈希的存储结构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1cMdjcyU-1602211258176)(redisimage/redis6.png)]
注意: dictht 后面是 NULL 说明第二个 ht 还没用到。 dictEntry后面是 NULL 说明没有 hash 到这个地址。 dictEntry 后面是NULL 说明没有发生哈希冲突。
为什么要定义两个哈希表呢?ht[2]
redis 的 hash 默认使用的是 ht[0],ht[1]不会初始化和分配空间。
哈希表 dictht 是用链地址法来解决碰撞问题的。在这种情况下,哈希表的性能取决于它的大小(size 属性)和它 所保存的节点的数量(used 属性)之间的比率:
1.比率在 1:1 时(一个哈希表 ht 只存储一个节点 entry),哈希表的性能最好;
2.如果节点数量比哈希表的大小要大很多的话(这个比例用 ratio 表示,5 表示平均 一个 ht 存储 5 个 entry),那么哈希表就会退化成多个链表,哈希表本身的性能优势就不再存在。
在这种情况下需要扩容。Redis 里面的这种操作叫做 rehash。
rehash 的步骤:
1、为字符 ht[1]哈希表分配空间,这个哈希表的空间大小取决于要执行的操作,以及 ht[0]当前包含的键值对的数 量。扩展:ht[1]的大小为第一个大于等于 ht[0].used2。
2、将所有的 ht[0]上的节点 rehash 到 ht[1]上,重新计算 hash 值和索引,然后放入指定的位置。
3、当 ht[0]全部迁移到了 ht[1]之后,释放 ht[0]的空间,将 ht[1]设置为 ht[0]表,
并创建新的 ht[1],为下次 rehash 做准备。
问题:什么时候触发扩容
负载因子(源码位置: dict.c):
static int dict_can_resize = 1;
static unsigned int dict_force_resize_ratio = 5;
ratio = used / size,已使用节点与字典大小的比例
dict_can_resize 为 1 并且 dict_force_resize_ratio 已使用节点数和字典大小之间的比率超过 1:5,触发扩容
4.4List列表
Redis list的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销,Redis内部的很多实现,包括发送缓冲队列等也都是用的这个数据结构。
应用场景
Redis list的应用场景非常多,也是Redis最重要的数据结构之一,比如twitter的关注列表、粉丝列表等都可以用Redis的list结构来实现,再比如有的应用使用Redis的list类型实现一个简单的轻量级消息队列,生产者push,消费者pop/bpop。
4.4.1存储类型
存储有序的字符串(从左到右),元素可以重复。可以充当队列和栈的角色。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1AaVO4SP-1602211258177)(redisimage/redis7.png)]
4.4.2 操作命令
元素增减:
lpush queue a
lpush queue b c
rpush queue d e
lpop queue
rpop queue
blpop queue
brpop queue
取值
lindex queue 0
lrange queue 0 -1
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tFQrzF9F-1602211258178)(redisimage/redis8.png)]
4.4.3存储原理
统一用 quicklist 来存储。quicklist 存储了一个双向链表,每个节点都是一个 ziplist
127.0.0.1:6379> object encoding queue
"quicklist"
quicklist
quicklist(快速列表)是ziplist和linkedList的结合体
quicklist.h,head 和 tail 指向双向列表的表头和表尾
typedef struct quicklist {
quicklistNode head;/指向双向列表的头/
quicklistNode tail;/指向双向列表的尾/
unsigned long count; / 所有的 ziplist 中一共存了多少个元素 /
unsigned long len; / 双向链表的长度, node 的数量 /
int fill : 16; / fill factor for individual nodes /
unsigned int compress : 16; / 压缩深度, 0: 不压缩; /
redis.conf 相关参数:
参数 | 含义 |
---|---|
list-max-ziplist-size(fill) | 正数表示单个 ziplist 最多所包含的 entry 个数。 负数代表单个 ziplist 的大小, 默认 8k。 -1: 4KB; -2: 8KB; -3: 16KB; -4: 32KB; -5: 64KB |
list-compress-depth(compress) | 压缩深度, 默认是 0。 1: 首尾的 ziplist 不压缩; 2: 首尾第一第二个 ziplist 不压缩, 以此类推 |
quicklistNode 中的zl 指向一个 ziplist,一个 ziplist 可以存放多个元素。
typedef struct quicklistNode {
struct quicklistNode prev; / 前一个节点 /
struct quicklistNode next; / 后一个节点 /
unsigned char zl; / 指向实际的 ziplist /
unsigned int sz; / 当前 ziplist 占用多少字节 /
unsigned int count : 16; / 当前 ziplist 中存储了多少个元素, 占 16bit(下同) , 最大 65536 个 /
unsigned int encoding : 2; / 是否采用了 LZF 压缩算法压缩节点, 1: RAW 2: LZF /
unsigned int container : 2; / 2: ziplist, 未来可能支持其他结构存储 /
unsigned int recompress : 1; / 当前 ziplist 是不是已经被解压出来作临时使用 /
unsigned int attempted_compress : 1; / 测试用 /
unsigned int extra : 10; / 预留给未来使用 /
} quicklistNode;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P02KvyP0-1602211258179)(redisimage/redis9.png)]
4.4.4应用场景
消息队列
- List 提供了两个阻塞的弹出操作:BLPOP/BRPOP,可以设置超时时间。
BLPOP:BLPOP key1 timeout 移出并获取列表的第一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
BRPOP:BRPOP key1 timeout 移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或 发现可弹出元素为止。
队列:先进先出:rpush blpop,左头右尾,右边进入队列,左边出队列。
栈:先进后出:rpush brpop
4.5set 集合
4.5.1存储类型
Redis的Set是string类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。
Redis 中 集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。
集合中最大的成员数为 232 - 1 (4294967295, 每个集合可存储40多亿个成员)。
集合类型:无序、不可重复
列表类型:有序、可重复
4.5.2操作命令
添加一个或者多个元素
127.0.0.1:6379> sadd myset a
(integer) 1
127.0.0.1:6379> sadd myset b c d e f j
(integer) 6
获取所有元素
127.0.0.1:6379> smembers myset
1) "b"
2) "c"
3) "f"
4) "d"
5) "a"
6) "j"
7) "e"
统计元素的个数
127.0.0.1:6379> scard myset
(integer) 7
随机获取元素
127.0.0.1:6379> srandmember myset
"b"
弹出一个元素
127.0.0.1:6379> spop myset
"c"
127.0.0.1:6379> spop myset
"b"
移除一个或者多个元素
127.0.0.1:6379> srem myset d e f
(integer) 3
127.0.0.1:6379> smembers myset
1) "a"
2) "j"
查看元素是否存在
127.0.0.1:6379> sismember myset a
(integer) 1
127.0.0.1:6379> sismember myset b
(integer) 0
4.5.3存储的实现原理
Redis 用 intset 或 hashtable 存储 set。如果元素都是整数类型,就用 inset 存储。如果不是整数类型,就用 hashtable(数组+链表的存来储结构)。
问题:KV 怎么存储 set 的元素?key 就是元素的值,value 为 null。
如果元素个数超过 512 个,也会用 hashtable 存储
配置文件 redis.conf
set-max-intset-entries 512
127.0.0.1:6379> sadd iset 1 2 3 4 4 5
(integer) 5
127.0.0.1:6379> object encoding iset
"intset"
127.0.0.1:6379> sadd myset a b c d e f g
(integer) 6
127.0.0.1:6379> object encoding myset
"hashtable"
4.5.4应用场景
抽奖
随机获取元素spop myset
点赞、 签到、 打卡
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ALlnNO4M-1602211258180)(redisimage/redis10.png)]
这条微博的 ID 是 t1001,用户 ID 是 u3001。
用 like:t1001 来维护 t1001 这条微博的所有点赞用户。
点赞了这条微博:sadd like:t1001 u3001
127.0.0.1:6379> sadd like:t1001 u3001
(integer) 1
127.0.0.1:6379> sadd like:t1001 u3002
(integer) 1
127.0.0.1:6379> scard like:t1002
(integer) 0
127.0.0.1:6379>
127.0.0.1:6379> scard like:t1001
(integer) 2
取消点赞:srem like:t1001 u3001
是否点赞:sismember like:t1001 u3001
点赞的所有用户:smembers like:t1001
点赞数:scard like:t1001
4.6总结
4.6.1数据结构总结
对象 | 对象 type 属性值 | type 命令输出 | 底层可能的存储结构 | object encoding |
---|---|---|---|---|
字符串对象 | OBJ_STRING | “string” | OBJ_ENCODING_INT OBJ_ENCODING_EMBSTR OBJ_ENCODING_RAW | int embstr raw |
列表对象 | OBJ_LIST | “list” | OBJ_ENCODING_QUICKLIST | quicklist |
哈希对象 | OBJ_HASH | “hash” | OBJ_ENCODING_ZIPLIST OBJ_ENCODING_HT | ziplist hashtable |
集合对象 | OBJ_SET | “set” | OBJ_ENCODING_INTSET OBJ_ENCODING_HT | intset hashtable |
有序集合对象 | OBJ_ZSET | “zset” | OBJ_ENCODING_ZIPLIST OBJ_ENCODING_SKIPLIST | ziplist skiplist(包含 ht) |
4.6.2编码转换总结
对象 | 原始编码 | 升级编码 | |
---|---|---|---|
字符串对象 | INT | embstr | raw |
整数并且小于 long 2^63-1 | 超过 44 字节, 被修改 | ||
哈希对象 | ziplist | hashtable | |
键和值的长度小于 64byte, 键值对个数不 超过 512 个, 同时满足 | |||
列表对象 | quicklist | ||
集合对象 | intset | hashtable | |
元素都是整数类型, 元素个数小于 512 个, 同时满足 | |||
有序集合对象 | ziplist | skiplist | |
元素数量不超过 128 个, 任何一个 member 的长度小于 64 字节, 同时满足。 |
4.6.3 应用场景总结
缓存——提升热点数据的访问速度
共享数据——数据的存储和共享的问题
全局 ID —— 分布式全局 ID 的生成方案(分库分表)
分布式锁——进程间共享数据的原子操作保证
在线用户统计和计数
队列、 栈——跨进程的队列/栈
消息队列——异步解耦的消息机制
服务注册与发现 —— RPC 通信机制的服务协调中心(Dubbo 支持 Redis)
购物车
新浪/Twitter 用户消息时间线
抽奖逻辑(礼物、 转发)
点赞、 签到、 打卡
商品标签
用户(商品) 关注(推荐) 模型
电商产品筛选
排行榜
5.redis事物
https://redis.io/topics/transactions/
http://redisdoc.com/topic/transaction.html
5.1reids的是事物特点
1、按进入队列的顺序执行。
2、不会受到其他客户端的请求的影响
Redis 的事务涉及到四个命令:multi(开启事务),exec(执行事务),discard(取消事务),watch(监视)
5.2 事务的用法
127.0.0.1:6379> set yehui 1000
OK
127.0.0.1:6379> set rongou 1000
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decrby yehui 100
QUEUED
127.0.0.1:6379> incrby yehui 100
QUEUED
127.0.0.1:6379> exec
1) (integer) 900
2) (integer) 1000
127.0.0.1:6379> get yehui
"1000"
127.0.0.1:6379> get rongou
"1000"
127.0.0.1:6379>
通过 multi 的命令开启事务。事务不能嵌套,多个 multi 命令效果一样。multi 执行后,客户端可以继续向服务器发送任意多条命令, 这些命令不会立即被执行, 而是被放到一个队列中, 当 exec 命令被调用时, 所有队列中的命令才会被执行 , 通过 exec 的命令执行事务。如果没有执行 exec,所有的命令都不会被执行。如果中途不想执行事务了,怎么办?可以调用 discard 可以清空事务队列,放弃执行
multi
set k1 1
set k2 2
set k3 3
discard
5.3watch命令
在 Redis 中还提供了一个 watch 命令。它可以为 Redis 事务提供 CAS 乐观锁行为(Check and Set / Compare and Swap),也就是多个线程更新变量的时候,会跟原值做比较,只有它没有被其他线程修
改的情况下,才更新成新的值。我们可以用 watch 监视一个或者多个 key,如果开启事务之后,至少有一个被监视key 键在 exec 执行之前被修改了, 那么整个事务都会被取消(key 提前过期除外)。可以用 unwatch 取消。
client 1 | client 2 |
---|---|
127.0.0.1:6379> set balance 1000 OK 127.0.0.1:6379> watch balance OK 127.0.0.1:6379> multi OK 127.0.0.1:6379> incrby balance 100 QUEUED | |
127.0.0.1:6379> decrby balance 100 (integer) 900 | |
127.0.0.1:6379> exec (nil) 127.0.0.1:6379> get balance "900 |
5.4 事务可能遇到的问题
我们把事务执行遇到的问题分成两种,一种是在执行 exec 之前发生错误,一种是在执行 exec 之后发生错误。
2.4.1 在执行 exec 之前发生错误比如:入队的命令存在语法错误,包括参数数量,参数名等等(编译器错误)
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set gupao 666
QUEUED
127.0.0.1:6379> hset qingshan 2673
(error) ERR wrong number of arguments for 'hset' command
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.
在这种情况下事务会被拒绝执行,也就是队列中所有的命令都不会得到执行。
在执行 exec 之后发生错误比如,类型错误,比如对 String 使用了 Hash 的命令,这是一种运行时错误。
127.0.0.1:6379> flushall
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 1
QUEUED
127.0.0.1:6379> set k1 a b
QUEUED
127.0.0.1:6379> exec
1) OK
2) (error) ERR syntax error
127.0.0.1:6379> get k1
"1"
最后我们发现 set k1 1 的命令是成功的,也就是在这种发生了运行时异常的情况下,只有错误的命令没有被执行,但是其他命令没有受到影响。这个显然不符合我们对原子性的定义,也就是我们没办法用 Redis 的这种事务机制来实现原子性,保证数据的一致。
为什么在一个事务中存在错误,Redis 不回滚?
Redis这样做,主要是因为:
只有当发生语法错误(这个问题在命令队列时无法检测到)了,Redis命令才会执行失败, 或对keys赋予了一个类型错误的数据:这意味着这些都是程序性错误,这类错误在开发的过程中就能够发现并解决掉,几乎不会出现在生产环境。
由于不需要回滚,这使得Redis内部更加简单,而且运行速度更快。
5.3 Lua 脚本
Lua/ˈluə/是一种轻量级脚本语言,它是用 C 语言编写的,跟数据的存储过程有点类似。 使用 Lua 脚本来执行 Redis 命令的好处:
1、一次发送多个命令,减少网络开销。
2、Redis 会将整个脚本作为一个整体执行,不会被其他请求打断,保持原子性。
3、对于复杂的组合命令,我们可以放在文件中,可以实现程序之间的命令集复用。
5.3.1 在 Redis 中调用 Lua 脚本
使用 eval /ɪ’væl/ 方法,语法格式:
redis> eval lua-script key-num [key1 key2 key3 ....] [value1 value2 value3 ....]
eval 代表执行 Lua 语言的命令。
lua-script 代表 Lua 语言脚本内容。
key-num 表示参数中有多少个 key, 需要注意的是 Redis 中 key 是从 1 开始的, 如果没有 key 的参数, 那么写0。
[key1 key2 key3…]是 key 作为参数传递给 Lua 语言, 也可以不填, 但是需要和 key-num 的个数对应起来。
[value1 value2 value3 ….]这些参数传递给 Lua 语言, 它们是可填可不填的。
示例,返回一个字符串,0 个参数:
127.0.0.1:6379> eval "return 'hello world'" 0
"hello world"
5.3.2 在 Lua 脚本中调用 Redis 命令
使用 redis.call(command, key [param1, param2…])进行操作。语法格式:
redis> eval “redis.call(‘set’,KEYS[1],ARGV[1])” 1 lua-key lua-value
-
command 是命令, 包括 set、 get、 del 等。
-
key 是被操作的键。
-
param1,param2…代表给 key 的参数。
注意跟 Java 不一样,定义只有形参,调用只有实参。
Lua 是在调用时用 key 表示形参,argv 表示参数值(实参)。
5.3.3设置键值对
在 Redis 中调用 Lua 脚本执行 Redis 命令
127.0.0.1:6379> eval "return redis.call('set',KEYS[1],ARGV[1])" 1 yehui1 123
OK
以上的命令等价于
set yehui1 123
设置多个值
127.0.0.1:6379> eval "return redis.call('mset',KEYS[1],ARGV[1],KEYS[2],ARGV[2])" 2 id 1 name 234
OK
5.3.4 调用Lua脚本
创建 Lua 脚本文件:
[root@redis src]# vim test.lua
Lua 脚本内容,先设置,再取值:
redis.call('set','age','lua6666')
return redis.call('get','age')
在 Redis 客户端中调用 Lua 脚本
[root@redis src]# ./redis-cli --eval test.lua 0
得到返回值
[root@redis src]# ./redis-cli --eval test.lua 0
"lua6666"
传入参数案例:
[root@redis src]# vim test1.lua
redis.call('set',KEYS[1],ARGV[1])
return redis.call('get',KEYS[1])
调用参数
[root@redis src]# ./redis-cli --eval "test1.lua" id222 , 2344 #注意一定要有空格
"2344"
5.3.5 对IP限流
需求:在 X 秒内只能访问 Y 次。
设计思路:用 key 记录 IP,用 value 记录访问次数。
拿到 IP 以后,对 IP+1。如果是第一次访问,对 key 设置过期时间(参数 1)。否则判断次数,超过限定的次数(参数 2),返回 0。如果没有超过次数则返回 1。超过时间,key 过期之后,可以再次访问。
KEY[1]是 IP, ARGV[1]是过期时间 X,ARGV[2]是限制访问的次数 Y。
-- IP 限流, 对某个 IP 频率进行限制 , 6 秒钟访问 10 次
local num=redis.call('incr',KEYS[1])
if(tonumber(num)==1)
then
redis.call('expire',KEYS[1],ARGV[1])
return 1
elseif tonumber(num)>tonumber(ARGV[2])
then
return 0
else
return 1
end
调用:
6 秒钟内限制访问 10 次,调用测试(连续调用 10 次):
[root@redis src]# ./redis-cli --eval "ip_limit.lua" 192.168.78.132 , 6 10
(integer) 1
1.192.168.8.111 是 key 值 ,后面是参数值,中间要加上一个空格 和
一个逗号,再加上一个 空格 。即:./redis-cli –eval [lua 脚本] [key…]空格,空格[args…]
2.多个参数之间用一个 空格 分割
5.3.6缓存Lua脚本
为什么使用缓存
在脚本比较长的情况下,如果每次调用脚本都需要把整个脚本传给 Redis 服务端,会产生比较大的网络开销。为了解决这个问题,Redis 提供了 EVALSHA 命令,允许开发者通过脚本内容的 SHA1 摘要来执行脚本。
如何使用缓存
Redis 在执行 script load 命令时会计算脚本的 SHA1 摘要并记录在脚本缓存中,执行 EVALSHA 命令时 Redis 会根据提供的摘要从脚本缓存中查找对应的脚本内容,如果找到了则执行脚本,否则会返回错误:“NOSCRIPT No matching script. Please useEVAL.”
127.0.0.1:6379> script load "return 'Hello World'"
"470877a599ac74fbfda41caa908de682c5fc7d4b"
127.0.0.1:6379> evalsha "470877a599ac74fbfda41caa908de682c5fc7d4b"
(error) ERR wrong number of arguments for 'evalsha' command
127.0.0.1:6379> evalsha "470877a599ac74fbfda41caa908de682c5fc7d4b" 0
"Hello World"
自乘案例
Redis 有 incrby 这样的自增命令,但是没有自乘,比如乘以 3,乘以 5。我们可以写一个自乘的运算,让它乘以 后面的参数:
local curVal = redis.call("get", KEYS[1])
if curVal == false then
curVal = 0
else
curVal = tonumber(curVal)
end
curVal = curVal tonumber(ARGV[1])
redis.call("set", KEYS[1], curVal)
return curVal
127.0.0.1:6379> set num 1
OK
[root@redis src]# ./redis-cli --eval "incr.lua" num , 8
(integer) 8
把这个脚本变成单行,语句之间使用分号隔开
local curVal = redis.call(“get”, KEYS[1]); if curVal == false then curVal = 0 else curVal = tonumber(curVal) end; curVal= curVal tonumber(ARGV[1]); redis.call(“set”, KEYS[1], curVal); return curVal
script load ‘命令’
127.0.0.1:6379> script load 'local curVal = redis.call("get", KEYS[1]); if curVal == false then curVal = 0 else curVal =
tonumber(curVal) end; curVal = curVal tonumber(ARGV[1]); redis.call("set", KEYS[1], curVal); return curVal'
"be4f93d8a5379e5e5b768a74e77c8a4eb0434441"
调用:
127.0.0.1:6379> set num 2
OK
127.0.0.1:6379> evalsha be4f93d8a5379e5e5b768a74e77c8a4eb0434441 1 num 6
(integer) 12
6.redis效率高
6.1总结
1)纯内存结构
2)单线程
3)多路复用
6.2内存
KV 结构的内存数据库,时间复杂度 O(1)。 redis是单线程的
6.3单线程
6.3.1单线程的好处
1、没有创建线程、销毁线程带来的消耗
2、避免了上线文切换导致的 CPU 消耗
3、避免了线程之间带来的竞争问题,例如加锁释放锁死锁等等
6.4异步非阻塞
异步非阻塞 I/O,多路复用处理并发连接。
6.5 Redis单线程的原因
https://redis.io/topics/faq#redis-is-single-threaded-how-can-i-exploit-multiple-cpu–cores
因为单线程已经够用了,CPU 不是 redis 的瓶颈。Redis 的瓶颈最有可能是机器内存或者网络带宽。既然单线程容易实现,而且 CPU 不会成为瓶颈,那就顺理成章地采用单线程的方案了。
7.redis淘汰策略
7.1内存回收
Reids 所有的数据都是存储在内存中的,在某些情况下需要对占用的内存空间进行回收。内存回收主要分为两类,一类是 key 过期,一类是内存使用达到上限(max_memory)触发内存淘汰。
7.1.1过期策略(主动淘汰)
每个设置过期时间的 key 都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的 CPU 资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
源码: server.h
定期过期
typedef struct redisDb {
dict dict; / 所有的键值对 /
dict expires; / 设置了过期时间的键值对 /
dict blocking_keys; / Keys with clients waiting for data (BLPOP)/
dict ready_keys; / Blocked keys that received a PUSH /
dict watched_keys; / WATCHED keys for MULTI/EXEC CAS /
int id; / Database ID /
long long avg_ttl; / Average TTL, just for stats /
list defrag_later; / List of key names to attempt to defrag one by one, gradually. /
} redisDb;
每隔一定的时间,会扫描一定数量的数据库的 expires 字典中一定数量的 key,并清除其中已过期的 key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得 CPU 和内存资源达到最优的平衡效果。
7.1.2惰性过期(被动淘汰)
只有当访问一个 key 时,才会判断该 key 是否已过期,过期则清除。该策略可以最大化地节省 CPU 资源,却对内存非常不友好。极端情况可能出现大量的过期 key 没有再次被访问,从而不会被清除,占用大量内存。
例如 String,在 getCommand 里面会调用 expireIfNeeded server.c expireIfNeeded(redisDb db, robj key)
第二种情况,每次写入 key 时,发现内存不够,调用 activeExpireCycle 释放一部分内存。
expire.c activeExpireCycle(int type)
7.2淘汰策略
Redis 的内存淘汰策略,是指当内存使用达到最大内存极限时,需要使用淘汰算法来决定清理掉哪些数据,以保证新数据的存入。
7.2.1最大内存设置
redis.conf 参数配置:
# maxmemory <bytes>
如果不设置 maxmemory 或者设置为 0,64 位系统不限制内存,32 位系统最多使用 3GB 内存。
动态修改:
redis> config set maxmemory 2GB
redis.conf
# maxmemory-policy noeviction
设置redis淘汰策略:比如:maxmemory-policy volatile-lru
noeviction:当内存使用达到阈值的时候,执行命令直接报错
allkeys-lru:在所有的key中,优先移除最近未使用的key。
volatile-lru:在设置了过期时间的键空间中,优先移除最近未使用的key。
allkeys-random:在所有的key中,随机移除某个key。
volatile-random:在设置了过期时间的键空间中,随机移除某个key。
volatile-ttl:在设置了过期时间的键空间中,具有更早过期时间的key优先移除。
先从算法来看:
LRU,Least Recently Used:最近最少使用。判断最近被使用的时间,目前最远的数据优先被淘汰
LFU,Least Frequently Used,最不常用
random,随机删除
如果没有符合前提条件的 key 被淘汰,那么 volatile-lru、volatile-random 、
volatile-ttl 相当于 noeviction(不做内存回收)。
动态修改淘汰策略:
redis> config set maxmemory-policy volatile-lru
建议使用 volatile-lru,在保证正常服务的情况下,优先删除最近最少使用的 key。
7.2.2LRU淘汰原理
基于一个数据结构做缓存,怎么实现 LRU——最长时间不被访问的
元素在超过容量时删除?
问题:如果基于传统 LRU 算法实现 Redis LRU 会有什么问题?
需要额外的数据结构存储,消耗内存。Redis LRU 对传统的 LRU 算法进行了改良,通过随机采样来调整算法的精度。如果淘汰策略是 LRU,则根据配置的采样值 maxmemory_samples(默认是 5 个),随机从数据库中选择 m 个 key, 淘汰其中热度最低的 key 对应的缓存数据。 所以采样参 数m配置的数值越大, 就越能精确的查找到待淘汰的缓存数据,但是也消耗更多的CPU计算,执行效率降低。
问题:如何找出热度最低的数据?
Redis 中所有对象结构都有一个 lru 字段, 且使用了 unsigned 的低 24 位,这个字段用来记录对象的热度。对象被创建时会记录 lru 值。在被访问的时候也会更新 lru 的值。但是不是获取系统当前的时间戳,而是设置为全局变量 server.lruclock 的值。
源码:server.h
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:LRU_BITS; / LRU time (relative to global lru_clock) or
LFU data (least significant 8 bits frequency
and most significant 16 bits access time). /
int refcount;
void ptr;
} robj;
server.lruclock 的值怎么来的?
Redis 中 有 个 定 时 处 理 的 函 数 serverCron , 默 认 每 100 毫 秒 调 用 函 数updateCachedTime 更新一次全局变量的 server.lruclock 的值,它记录的是当前 unix时间戳。
源码:server.c
void updateCachedTime(void) {
time_t unixtime = time(NULL);
atomicSet(server.unixtime,unixtime);
server.mstime = mstime();
struct tm tm;
localtime_r(&server.unixtime,&tm);
server.daylight_active = tm.tm_isdst;
}
问题:为什么不获取精确的时间而是放在全局变量中?不会有延迟的问题吗?
这样函数 lookupKey 中更新数据的 lru 热度值时,就不用每次调用系统函数 time,可
以提高执行效率。当对象里面已经有了 LRU 字段的值,就可以评估对象的热度了。函数 estimateObjectIdleTime 评估指定对象的 lru 热度,思想就是对象的 lru 值和全局的 server.lruclock 的差值越大(越久没有得到更新), 该对象热度越低。
源码 evict.c
/ Given an object returns the min number of milliseconds the object was never
requested, using an approximated LRU algorithm. /
unsigned long long estimateObjectIdleTime(robj o) {
unsigned long long lruclock = LRU_CLOCK();
if (lruclock >= o->lru) {
return (lruclock - o->lru) LRU_CLOCK_RESOLUTION;
} else {
return (lruclock + (LRU_CLOCK_MAX - o->lru))
LRU_CLOCK_RESOLUTION;
}
}
server.lruclock 只有 24 位,按秒为单位来表示才能存储 194 天。当超过 24bit 能表
示的最大时间的时候,它会从头开始计算。
server.h
#define LRU_CLOCK_MAX ((1<<LRU_BITS)-1) / Max value of obj->lru /
在这种情况下,可能会出现对象的 lru 大于 server.lruclock 的情况,如果这种情况
出现那么就两个相加而不是相减来求最久的 key。
为什么不用常规的哈希表+双向链表的方式实现?需要额外的数据结构,消耗资源。而 Redis LRU 算法在 sample 为 10 的情况下,已经能接近传统 LRU 算法了。
https://redis.io/topics/lru-cache
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wQlc3hBJ-1602211258182)(redisimage/redis11.png)]
问题:除了消耗资源之外,传统 LRU 还有什么问题?
如图,假设 A 在 10 秒内被访问了 5 次,而 B 在 10 秒内被访问了 3 次。因为 B 最
后一次被访问的时间比 A 要晚,在同等的情况下,A 反而先被回收。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wR9GvT5U-1602211258183)(redisimage/redis12.png)]
LFU
server.h
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:LRU_BITS; / LRU time (relative to global lru_clock) or
LFU data (least significant 8 bits frequency
and most significant 16 bits access time). /
int refcount;
void ptr;
} robj;
当这 24 bits 用作 LFU 时,其被分为两部分:高 16 位用来记录访问时间(单位为分钟,ldt,last decrement time)低 8 位用来记录访问频率,简称 counter(logc,logistic counter)counter 是用基于概率的对数计数器实现的,8 位可以表示百万次的访问频率。对象被读写的时候,lfu 的值会被更新。db.c——lookupKey
void updateLFU(robj val) {
unsigned long counter = LFUDecrAndReturn(val);
counter = LFULogIncr(counter);
val->lru = (LFUGetTimeInMinutes()<<8) | counter;
}
增长的速率由,lfu-log-factor 越大,counter 增长的越慢
redis.conf 配置文件
# lfu-log-factor 10
如果计数器只会递增不会递减,也不能体现对象的热度。没有被访问的时候,计数器怎么递减呢?
减少的值由衰减因子 lfu-decay-time(分钟)来控制,如果值是 1 的话,N 分钟没有访问就要减少 N。redis.conf 配置文件
# lfu-decay-time 1
8.redis持久化
https://redis.io/topics/persistence
持久化,就是将数据保存到磁盘,机器宕机或者重启数据不丢失,如果存储到内存中的数据,会丢失。
Redis 速度快,很大一部分原因是因为它所有的数据都存储在内存中。如果断电或者宕机,都会导致内存中的数据丢失。为了实现重启后数据不丢失,Redis 提供了两种持久化的方案,一种是 RDB 快照(Redis DataBase),一种是 AOF(Append Only File)。
可以单独使用其中一种或者将2者结合
8.1RDB方式
RDB 是 Redis 默认的持久化方案,redis通过快照来将数据持久化到磁盘中。当满足一定条件的时候,会把当前内存中的数据写入磁盘,生成一个快照文件 dump.rdb。Redis 重启会通过加载 dump.rdb 文件恢复数据。
有2个参数构成:时间和改动的键的个数,当在指定时间内被更改的键的个数大于指定数值就会进行快照
8.2RDB触发
8.2.1自动触发
a)配置规则触发
redis.conf, SNAPSHOTTING,其中定义了触发把数据保存到磁盘的触发频率。如果不需要 RDB 方案,注释 save 或者配置成空字符串""。
save 900 1 # 900 秒内至少有一个 key 被修改(包括添加)
save 300 10 # 400 秒内至少有 10 个 key 被修改
save 60 10000 # 60 秒内至少有 10000 个 key 被修改
注意上面的配置是不冲突的,只要满足任意一个都会触发。RDB 文件位置和目录:
# 文件路径,
dir ./
# 文件名称
dbfilename dump.rdb
# 是否是 LZF 压缩 rdb 文件
rdbcompression yes
# 开启数据校验
rdbchecksum yes
参数 | 说明 |
---|---|
dir | rdb 文件默认在启动目录下(相对路径) config get dir 获取 |
dbfilename | 文件名称 |
rdbcompression | 开启压缩可以节省存储空间, 但是会消耗一些 CPU 的计算时间, 默认开启 |
rdbchecksum | 使用 CRC64 算法来进行数据校验, 但是这样做会增加大约 10%的性能消耗, 如果希望获取到最 大的性能提升, 可以关闭此功能。 |
问题:为什么停止 Redis 服务的时候没有 save,重启数据还在?
RDB 还有两种触发方式:
b)shutdown 触发,保证服务器正常关闭。
c)flushall,RDB 文件是空的,没什么意义(删掉 dump.rdb 演示一下)。
RDB恢复的过程:
Redis启动后会读取RDB快照文件,将数据从硬盘载入到内存,一般情况下1GB的快照文件载入到内存
的时间约为20~30秒钟,(不同服务器会有差异)
RDB快照过程如下:
1.Redis使用fork函数复制一份当前进程(父进程)的副本(子进程);
2.父进程继续接收并处理客户端发来的命令,而子进程将内存中的数据写入到硬盘中的临时文件;
3.当子进程写入完所有的数据后会用该临时文件替换旧的RDB文件
8.2.2手动触发
如果我们需要重启服务或者迁移数据,这个时候就需要手动触 RDB 快照保存。Redis提供了两条命令:
a)save
save 在生成快照的时候会阻塞当前 Redis 服务器, Redis 不能处理其他命令。如果内存中的数据比较多,会 造成 Redis 长时间的阻塞。生产环境不建议使用这个命令。
b)bgsave
执行 bgsave 时,Redis 会在后台异步进行快照操作,快照同时还可以响应客户端请求。
具体操作是 Redis 进程执行 fork 操作创建子进程(copy-on-write),RDB 持久化过程由子进程负责,完成后自动结束。它不会记录 fork 之后后续的命令。阻塞只发生在fork 阶段,一般时间很短。 用 lastsave 命令可以查看最近一次成功生成快照的时间。
SAVE和BGSAVE都是执行手动快照,但是两者有区别:
可以通过SAVE和BGSAVE命令来手动快照,2个命令的区别是前者是由主进程进行快照,会阻塞其他请求,或者是通过fork进程快照操作
8.2.3 RDB 数据的恢复
1、shutdown 持久化
添加键值
127.0.0.1:6379> set k1 1
OK
127.0.0.1:6379> set k2 2
OK
127.0.0.1:6379> set k3 3
OK
停服务器,触发 save
127.0.0.1:6379> shutdown
not connected>
备份 dump.rdb 文件
[root@redis src]# cp dump.rdb dump.rdb.bak
启动服务器
[root@redis src]# ./redis-server ../redis.conf
[root@redis src]# ./redis-cli
127.0.0.1:6379> keys
1) "k1"
2) "num"
3) "k2"
4) "k3"
数据都还在
-
模拟数据丢失
模拟数据丢失,触发 save
127.0.0.1:6379> flushdb OK
停服务器
127.0.0.1:6379> shutdown not connected>
启动服务器
[root@redis src]# ./redis-server ../redis.conf [root@redis src]# ./redis-cli 127.0.0.1:6379> keys (empty list or set) 啥都没有
3、通过备份文件恢复数据
停服务器
127.0.0.1:6379> shutdown not connected> exit
重命名备份文件 [root@redis src]# mv dump.rdb.bak dump.rdb
启动服务器
[root@redis src]# ./redis-server ../redis.conf [root@redis src]# ./redis-cli 127.0.0.1:6379> keys 1) "k1" 2) "num" 3) "k2" 4) "k3" 数据找回
8.2.4 RDB 文件的优势和劣势
一、优势
1.RDB 是一个非常紧凑(compact)的文件,它保存了 redis 在某个时间点上的数据
集。这种文件非常适合用于进行备份和灾难恢复。
2.生成 RDB 文件的时候,redis 主进程会 fork()一个子进程来处理所有保存工作,主
进程不需要进行任何磁盘 IO 操作。
3.RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
二、劣势
1、RDB 方式数据没办法做到实时持久化/秒级持久化。因为 bgsave 每次运行都要执行 fork 操作创建子进 程,频繁执行成本过高。
2、在一定间隔时间做一次备份,所以如果 redis 意外 down 掉的话,就会丢失最后一次快照之后的所有修改(数据有丢失)。如果数据相对来说比较重要,希望将损失降到最小,则可以使用 AOF 方式进行持久化。
8.3 AOF
Reis的AOF持久化策略是将发送到Redis服务端的每一条命令都记录下来,并且保存到硬盘中AOF文件,
AOF文件,AOF文件的位置和RDB文件的位置相同,都是通过dir参数设置,默认的文件名是appendonly.aof,可以通过,可以通过appendfilename参数修改
Redis 默认不开启。AOF 采用日志的形式来记录每个写操作,并追加到文件中。开启后,执行更改 Redis 数据的命令时,就会把命令写入到 AOF 文件中。Redis 重启时会根据日志文件的内容把写指令从前到后执行一次以完成数据的恢复工作
8.3.1 AOF配置
配置文件redis.conf
# 开关
appendonly no
# 文件名
appendfilename "appendonly.aof"
参数 | 说明 |
---|---|
appendonly | Redis 默认只开启 RDB 持久化, 开启 AOF 需要修改为 yes |
appendfilename “appendonly.aof” | 路径也是通过 dir 参数配置 config get dir |
AOF 文件的内容(vim 查看):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KDnevzyU-1602211258184)(redisimage/redis13.png)]
当redis服务器重启后,会将执行该aof文件,达到数据恢复的目的。
8.3.2 优化aof
可以使用bgrewriteaof命令来重写AOP文件,目的:去除数据的中间执行过程,保留最终数据命令即可
8.3.3重写策略
问题:文件越来越大,怎么办?
由于 AOF 持久化是 Redis 不断将写命令记录到 AOF 文件中,随着 Redis 不断的进行,AOF 的文件会越来越大,文件越大,占用服务器内存越大以及 AOF 恢复要求时间越长。
例如 set gupao 666,执行 1000 次,结果都是 gupao=666。
为了解决这个问题,Redis 新增了重写机制,当 AOF 文件的大小超过所设定的阈值时,Redis 就会启动 AOF 文件的内容压缩,只保留可以恢复数据的最小指令集。可以使用命令 bgrewriteaof 来重写。AOF 文件重写并不是对原文件进行重新整理,而是直接读取服务器现有的键值对, 然后用一条命令去代替之前记录这个键值对的多条命令,生成一个新的文件后去替换原来的 AOF 文件。
# 重写触发机制
auto-aof-rewrite-percentage 100
默认值为 100。 aof 自动重写配置, 当目前 aof 文件大小超过上一次重写的 aof 文件大小的百分之多少进行重写, 即当 aof 文件增长到一定大小的时候, Redis 能够调用 bgrewriteaof对日志文件进行重写。 当前 AOF 文件大小是上次日志重写得到 AOF 文件大小的二倍(设置为 100) 时, 自动启动新的日志重写过程。
auto-aof-rewrite-min-size 64mb
默认 64M。 设置允许重写的最小 aof 文件大小, 避免了达到约定百分比但尺寸仍然很小的情况还要重写。
8.3.4文件同步策略
问题:数据都是实时持久化到磁盘吗?
由于操作系统的缓存机制,AOF 数据并没有真正地写入硬盘,而是进入了系统的硬 盘缓存。什么时候把缓冲区的内容写入到 AOF 文件? 文件写入默认情况下会先写入到系统的缓存中,系统每30s同步一次,才是真正的写入到硬盘,如果在这30s服务器宕机那数据也会丢失的,Redis是可以通过配置来修改同步策略:
appendfsync everysec 每秒同步(默认的同步策略)
appendfsync no 不主动同步,由操作系统来决定(最快但是不安全)
appendfsync always 每次都同步(最安全但是最慢)
注意:即使每秒做文件同步也可能导致数据丢失。
问题:重写过程中,AOF 文件被更改了怎么办?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XRHSfofb-1602211258185)(redisimage/redis14.png)]
另外有两个与 AOF 相关的参数:
参数 | 说明 |
---|---|
no-appendfsync-on-rewrite | 在 aof 重写或者写入 rdb 文件的时候, 会执行大量 IO, 此时对于 everysec 和 always 的 aof 模式来说, 执行 fsync 会造成阻塞过长时间, no-appendfsync-on-rewrite 字段设置为默认设 置为 no。 如果对延迟要求很高的应用, 这个字段可以设置为 yes, 否则还是设置为 no, 这 样对持久化特性来说这是更安全的选择。 设置为 yes 表示 rewrite 期间对新写操作不 fsync, 暂时存在内存中,等 rewrite 完成后再写入, 默认为 no, 建议修改为 yes。 Linux 的默认 fsync 策略是 30 秒。 可能丢失 30 秒数据。 |
aof-load-truncated | aof 文件可能在尾部是不完整的, 当 redis 启动的时候, aof 文件的数据被载入内存。 重启 可能发生在 redis 所在的主机操作系统宕机后,尤其在 ext4 文件系统没有加上 data=ordered 选项, 出现这种现象。 redis 宕机或者异常终止不会造成尾部不完整现象, 可以选择让 redis 退出, 或者导入尽可能多的数据。 如果选择的是 yes, 当截断的 aof 文件被导入的时候, 会自动发布一个 log 给客户端然后 load。如果是 no,用户必须手动 redis-check-aof 修复 AOF 文件才可以。 默认值为 yes。 |
8.3.5 AOF 数据恢复
重启 Redis 之后就会进行 AOF 文件的恢复。
8.3.6 AOF 优势与劣势
优点:
1、AOF 持久化的方法提供了多种的同步频率,即使使用默认的同步频率每秒同步一次,Redis 最多也就丢失 1 秒的数据而已。
2、 缺点:
1、对于具有相同数据的的 Redis,AOF 文件通常会比 RDF 文件体积更大(RDB存的是数据快照)。
2、虽然 AOF 提供了多种同步的频率,默认情况下,每秒同步一次的频率也具有较高的性能。在高并发的情况 下,RDB 比 AOF 具好更好的性能保证。
8.3.7两种方案比较
那么对于 AOF 和 RDB 两种持久化方式,我们应该如何选择呢?如果可以忍受一小段时间内数据的丢失,毫无疑问使用 RDB 是最好的,定时生成RDB 快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度要快。否则就使用 AOF 重写。但是一般情况下建议不要单独使用某一种持久化机制,而
是应该两种一起用,在这种情况下,当 redis 重启的时候会优先载入 AOF 文件来恢复原始的数据,因为在通常情况下 AOF 文件保存的数据集要比 RDB 文件保存的数据集要完整。
来源: https://redis.io/