一、概要
背景
- 文件,是全量IO扫描,放硬盘上,吞吐只有百MB/s,寻址时间ms级别。
- mysql等数据库,通过datapage分治,把索引(B+Tree)放内存,内存中寻址ns级别。当并发大的时候,吞吐还是受硬盘影响。
- 同一数据,内存占用的大小会比磁盘小。
那么,能不能把全部数据都放内存里?关系数据库可以做到,例如SAP HANA,太贵。都放磁盘上,太慢。
上述两个极端,折中,可选用非关系型数据库是redis、memcache。将最频繁使用的部分热点数据放redis,不常用的放mysql。
介绍
- 『Redis』,叫做远程字典服务(REmote DIctionay Server取首字母),是一种开源的NoSQL(非关系型数据库)数据库。由antirez开发,是c语言编写的一款开源的基于内存的数据结构存储器,常用于数据库、缓存和消息中间件。
- Redis结构丰富,提供字符串、散列、列表、集合、有序集合、HyperLogLog、位图、流、坐标等一系列丰富的数据结构。用户可通过流水线、事务、Lua脚本、模块等内置的特性拓展自定义数据结构。
- Redis功能完备,提供自动过期、流水线、事务、数据持久化、发布和订阅(消息中间件)、主从复制、集群高可用(Sentinel和Cluster)等附加功能,可用作分布式锁。
- Redis性能优秀,它将所有数据存储在内存中,读写速度非常快,支持并发10W QPS。并且实现数据结构和特性时选用高效的算法来处理操作并节省内存。
- Redis单进程单线程,是线程安全的,采用IO多路复用机制(EPOLL)。避免了不必要的上下文切换,不用去考虑各种锁的性能消耗。可用来实现并行的串行化。但从6.x开始,开始支持io多线程。
- Redis支持广泛,无论大小公司,或者不同编程语言,都可方便的使用。
安装
免安装运行,访问『Try Redis』](https://try.redis.io/),可以在线运行大部分操作命令。
Linux安装:
1 | $ sudo yum install gcc make #安装依赖工具 |
常用环境配置
1 | # 配置redis环境变量 |
mac和win10安装,推荐docker。
1 | $ docker run --name redis -d -p 6379:6379 redis:latest |
mac本地安装
1 | $ brew install redis #brew安装 |
连通测试
redis-cli客户端连通测试:
1 | $ docker exec -it 960f /bin/bash |
python代码测试使用
1 | # redis-py客户端,安装命令:pip install redis |
- 如果使用可视化客户端,推荐官方RedisInsight
- 如果需要监控redis,可使用Grafana
配置文件
1 | include /path/to/local.conf #可通过include引用配置 |
基础命令
1 |
|
系统信息
1 | > help #帮助 |
二、常用数据类型
类型 | 特性 | 场景 |
---|---|---|
string | 可包含任何二进制数据,最大512MB | 缓存、计数、限流、共享session、分布式锁 |
hash | 键值对,适合存储对象 | 用户信息、页面访问量、聚合数据 |
list | 双向链表,元素有序可重复(最多2^32-1个),两端都可插入弹出元素 | 消息队列(lpush+rpop,队列可阻塞,不支持ack)、栈(lpush+lpop)、文章列表(分页) |
set | 哈希表实现,元素无序不重复(最多2^32-1个),提供交集、并集、差集操作 | 共同标签(好友),点赞等唯一性操作、已中奖的无法再抽奖spop |
zset | 增加score权重,元素按score有序排列 | 排行榜、带权重的消息队列 |
HyperLogLog | 计算集合的基数而创建的概率算法 | ip统计等,每周/每月/每年计数器pfmerge |
GEO | 地理位置坐标相关操作 | 坐标存取、距离计算、范围查找 |
bitmap | 本质还是个string,通过一个bit位来表示某元素状态 | 大量用户的签到、在线等统计 |
string(字符串)
内部编码有3种
- int,8个字节的长整型,redis启动时会建立10000个0-9999的redisObject变量作为共享变量
- embstr,小于等于44个字节的字符串,包括浮点数,embedded string,sds结构体和redisObject对象分配在连续的内存空间
- raw,大于44个字节的字符串,内存空间不连续
1 | > set name "herr" # 设置值 |
hash(哈希,字典)
哈希类型的内部编码有两种:
- ziplist(压缩列表):当哈希类型元素个数小于hash-max-ziplist-entries配置(默认512个)同时所有值都小于hash-max-ziplist-value配置(默认64字节)时使用。ziplist使用更加紧凑的结构实现多个元素的连续存储,所以比hashtable更加节省内存。
- hashtable(哈希表):当ziplist不能满足要求时,会使用hashtable。
hash较string优缺点:
1、优点:当数据量大,如使用hash会导致key值变少,在执行持久化,统计、查找等操作时,减少内存和cpu消耗。
2、缺点,hash命令较string少,且无法给某个属性单独设置过期时间。
1 | > hset myhash name herr #设置 |
list(列表)
列表的内部编码有两种:
- ziplist(压缩列表):当哈希类型元素个数小于list-max-ziplist-entries配置(默认512个)同时所有值都小于list-max-ziplist-value配置(默认64字节)时使用。ziplist使用更加紧凑的结构实现多个元素的连续存储,所以比hashtable更加节省内存。
- linkedlist(链表):当ziplist不能满足要求时,会使用linkedlist。
1 | > lpush mylist a b c #左侧推入,有lpushx命令,只对已存在的列表执行左侧推入 |
set(集合)
集合类型的内部编码有两种:
- intset(整数集合):当集合中的元素都是整数且元素个数小于set-max-intset-entries配置(默认512个)时,redis会选用intset来作为集合的内部实现,从而减少内存的使用。
- hashtable(哈希表):当intset不能满足要求时,会使用hashtable。
如数据集可以无序且不重复,优先使用set,添加删除的复杂度为O(1),效率比list高。
如数据集有序且不重复,优先使用zset。
如数据集有序且可重复,只能使用list。
1 | > sadd myset t1 t2 t3 #添加数据 |
zset(sorted set,有序集合)
有序集合类型的内部编码有两种:
- ziplist(压缩列表):当有序集合的元素个数小于list-max-ziplist-entries配置(默认128个)同时所有值都小于list-max-ziplist-value配置(默认64字节)时使用。ziplist使用更加紧凑的结构实现多个元素的连续存储,更加节省内存。
- skiplist(跳跃表):当不满足ziplist的要求时,会使用skiplist。
1 | > zadd myzset 1 z1 |
Geo
- Redis 3.2版本添加的新特性
- 可以将经纬度格式的地理坐标存储到redis中,并进行距离计算、范围查找等计算
1 | > geoadd mygeo 116.401394 39.916263 tiananmen 116.433015 39.909179 beijingxizhan 116.417851 39.914271 wangfujing 116.392267 39.913994 zhongnanhai 116.404341 39.906633 qianmen #添加地理位置坐标 |
HyperLogLog
- Redis在2.8.9版本添加了HyperLogLog结构
- HyperLogLog是用来做基数(不重复元素)统计的概率算法,统计出的是近似值而不是实际值
- 在输入元素的数量或者体积非常非常大(2^64个)时,计算基数所需的空间总是固定的、并且是很小的(12KB内存),比单独存储所有元素并进行统计操作,占用的内存小的多
1 | > pfadd mypf "redis" #添加指定元素 |
Stream
- Stream是Redis 5.0版本新增加的数据结构,是基于RadixTree(基数树)数据结构实现的。
- Stream主要用于消息队列,借鉴了kafka。Redis本身是有一个发布订阅 (pub/sub) 来实现消息队列的功能,但它有个缺点就是消息无法持久化,如果出现网络断开、宕机等,消息就会被丢弃。列表可实现消息队列,但查找需要遍历整个列表,效率不高。有序集合可高效的进行范围查找,但没有列表和发布订阅提供的阻塞弹出。而且以上3种结构,他们的元素都只能是单个值。Stream是上面3种数据结构的综合体,是实现消息队列应用的最佳选择。
- Stream提供了消息的持久化和主备复制功能,可以让任何客户端访问任何时刻的数据,并且能记住每一个客户端的访问位置,还能保证消息不丢失。
- 每个Stream都有唯一的名称,它就是Redis的key,在我们首次使用xadd指令追加消息时自动创建。
- 每个Stream都可以挂多个消费组,每个消费组会有个游标last_delivered_id在Stream数组之上往前移动,表示当前消费组已经消费到哪条消息了。
- 每个消费组(Consumer Group)都有一个Stream内唯一的名称,消费组不会自动创建,它需要单独的指令xgroup create进行创建,需要指定从Stream的某个消息ID开始消费,这个ID用来初始化last_delivered_id变量。
- 每个消费组的状态都是独立的,相互不受影响。也就是说同一份Stream内部的消息会被每个消费组都消费到。同一个消费组可以挂接多个消费者,这些消费者之间是竞争关系,任意一个消费者读取了消息都会使游标last_delivered_id往前移动。每个消费者者有一个组内唯一名称。
- 消费者(Consumer)内部会有个状态变量pending_ids,它记录了当前已经被客户端读取的消息,但是还没有ack。如果客户端没有ack,这个变量里面的消息ID会越来越多,一旦某个消息被ack,它就开始减少。这个pending_ids变量在Redis官方被称之为PEL,也就是Pending Entries List,这是一个很核心的数据结构,它用来确保客户端至少消费了消息一次,而不会在网络传输的中途丢失了没处理。
- 消费者组消息,通过xadd创建,状态为“未递送”。通过xreadgroup操作,状态变为“待处理”(pending),通过xack操作,状态变为“已确认”。
1 | #生产消费消息 |
bitmap
- 位图是由多个二进制位组成的数组,但本质上还是个string,位图通过一个bit位来表示某个元素对应的值或者状态,会极大的节省储存空间。
- offset为2^32-1(分配512MB)需要300ms,offset为2^30-1(分配128MB)需要80ms,offset为2^28-1(分配32MB)需要30ms,offset为2^26-1(分配8MB)需要8ms
- 对位图的拓展,是以字节为单位进行的,会将未被设置的二进制位初始化为0。
1 | # h的ascii为104,二进制01101000 |
三、流水线与事务
- 流水线,redis允许客户端把任意多条命令请求打包在一起,一次性的发送给服务器质性,并一次性的将执行结果全部返回给客户端。需要客户端支持。
- 事务,事务中所有命令都被序列化并会按序执行,并不可打断。Readis事物是原子性的,若执行则全部执行,若失败则全部失败。功能较弱,不建议过多使用。
事务总有ACID特性,redis具备ACI特性,且在特定持久化模式下具备D特性。
- 原子性(Atomic),都么都成功,要么都失败
- 一致性(Consistent),执行成功与否,不会对数据库造成破坏
- 隔离性(Isolate),不同事务之间是独立的
- 耐久性(Durable),事务执行完毕后会存储在硬盘。停机后修改也不会丢失
1 | > multi #开启事务 |
四、订阅与发布
Redis的发布与订阅功能,可以让客户端通过广播方式,将消息同时发送给可能存在的多个客户端,且发送客户端不需要知道接收消息的客户端的具体信息。
1 | # 起一个客户端(订阅者) |
五、持久化机制
redis为了保证效率,数据缓存在了内存中,但是会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件中,以保证数据的持久化。Redis的持久化策略有两种:
- RDB(Redis DataBase):快照形式是直接把内存中的数据保存到一个dump的文件中,定时保存,保存策略。Redis默认是快照RDB的持久化方式。
- AOF(Append Only File):把所有的对Redis的服务器进行修改的命令都存到一个日志文件里,命令的集合。AOF需要手动开启,可以配置按实时,按秒,按内核。
既想用RDB的快速恢复,又需要AOF的恢复完整性,可使用RDB-AOF混合持久化。
RDB持久化
RDB持久化产生的文件都以.rdb后缀结尾。
1 | > save #阻塞服务器并创建RDB文件,会创建新的RDB文件,然后替换掉已有的RDB文件 |
除了手动使用命令创建RDB外,还可以通过配置文件的save选项,让redis在满足执行条件时自动执行bgsave命令。
1 | save 900 1 #表示900秒内如果至少有1个key 的值变化,则保存 |
RGB文件结构包含以下7部分,
RDB文件标识符,为REDIS这5个字符
版本号,4个字符的数字
设备附加信息,redis服务器和系统信息
数据库数据,0-15个库的数据依次排列。单个数据库的结构为:数据库号码、键值对总数、带过期时间的键值对数量、键值对数据。单个键值对数据的结构如下:过期时间(可选)、LUR信息(可选)、LFU信息(可选)、类型、键、值。
Lua脚本缓存,保存Lua脚本
EOF,结束符,0xFF
CRC64校验和,校验文件是否损坏
当Redis服务器启动时,会根据RDB文件进行恢复。
当使用save命令后重启,将丢失save命令之后产生的所有数据。当使用bgsave命令后重启,丢失的数据量取决于成功执行的bgsave命令的开始时间。
RDB持久化的缺陷在于,丢失的数据取决于创建RDB文件的间隔。然后RDB是全量持久化操作,会消耗大量资源,不太可能通过增加生成频率来保证数据安全。
AOF持久化
与全量式的RDB持久化功能不同,AOF提供的是增量式的持久化功能。服务器每次执行完命令后,都会以协议文本的方式将被执行的命令追加到AOF文件的末尾。
AOF持久化需要配置文件appendonly yes来开启。
冲洗:针对硬盘的多次写操作,系统不会直接把数据写入硬盘,而是会将数据写入位于内存的缓冲区中,等到满足写入条件,会执行flush系统调用,将缓冲区的数据冲洗至硬盘。使用appendfsync来控制冲洗频率。默认为everysec(每隔1s)。其他选项有always(每执行一个写命令)、no(不主动冲洗,由操作系统决定)。
重写:当命令越来越多,AOF文件会越来越大,redis可以将冗余命令进行合并重写,会生成一个全新的AOF文件。可以执行bgrewriteaof命令显式的触发重写操作,为异步命令。也可以通过配置文件的两个参数auto-aof-rewrite-min-size和auto-aof-rewrite-percentage进行设置。
AOF持久化优点:安全性高。但缺点也很明显:存储的是协议文本,文件体积会大。需要根据命令恢复,效率不高。重写也需要消耗大量资源。
RDB-AOF混合持久化
下面为RDB和AOF的比较,Redis在4.0引入了混合持久化模式。之前版本可以同时使用RDB和AOF。总的来说,在数据持久化的问题上,4.0以后的版本优先使用混合持久化。4.0前版本优先选用AOF作为数据持久化手段,把RDB作为辅助的备份手段。
名称 | RDB持久化 | AOF持久化 |
---|---|---|
备份策略 | 全量备份 | 增量备份 |
保存间隔 | 较长 | 默认1秒 |
数据还原速度 | 快 | 一般 |
阻塞 | save会,但bgsave或者自动不会 | 不会 |
适用性 | 更适合数据备份,默认开启 | 适合数据持久化,默认关闭 |
启动优先级 | 低 | 高 |
文件体积 | 小 | 大 |
恢复速度 | 快 | 慢 |
数据安全性 | 丢数据 | 根据策略决定 |
轻重 | 重 | 轻 |
混合持久化的配置如下,重写时会像执行BGSAVE命令一样生成RDB数据,并将这些数据写入AOF文件中,之后执行的命令会继续以协议文本的方式追加。
1 | appendonly yes #AOF持久化模式打开 |
当一个支持RDB-AOF混合持久化模式的Redis服务器启动并载入AOF文件时,会先检查是否包含RDB格式的内容,并优先加载后,再加载之后的AOF数据。
六、Lua脚本
redis执行lua脚本是以原子方式执行的,执行过程中其他请求会被阻塞,直到eval命令执行完毕为止。
1 | > eval "return 'hello herr'" 0 #使用eval执行Lua脚本,0代表脚本需要处理的键数量,后面可跟key |
redis在Lua5.1环境中内置了一些函数库,如4个Lua标准库base、table、string、math,以及redis(redis定制包)、bit(二进制按位操作)、struct(Lua值和C结构转换)、cjson(json编解码)、cmspack(MessagePack打包和解包)等。
脚本调试:
1 | # cat debug.lua,lua文件 |
输出日志可用redis.debug()函数处理。调试命令如下:
1 | Redis Lua debugger help: |
七、模块
- Redis提供了流水线、事务、Lua脚本用于拓展Redis服务器的功能,但性能有损耗,编程略复杂,且可能无法满足想要的数据结构。
- Redis为提升Redis的拓展性,在4.0添加了模块,允许开发者通过Redis开放的一簇API,将Redis用作网络服务和数据存储平台,通过C语言在Redis基础上构建任意的数据结构。
- 常用的模块有ReJSON(在Redis中存储、更新和获取JSON数据)、RediSQL(在Redis中嵌入SQLite)、RediSearch(基于Redis的全文检索)等。
1 | $ cd /usr/local/redis/src/modules #系统自带模块 |
ReJSON
1 | $ git clone https://github.com/RedisLabsModules/rejson.git |
RediSQL
1 | $ git clone https://github.com/RedBeardLab/rediSQL/ |
RediSearch
1 | $ git clone https://github.com/RedisLabsModules/RediSearch.git |
八、多机
主从复制(master-slave replication)
- Redis开启复制功能后,存储目标数据库的服务器被称为主服务器(master server),存储数据副本的服务器被称为从服务器(replica)。
- 一个主服务器可以拥有任意多个从服务器,且可以通过多级从服务器形成树状服务器结构。
- 主服务器既可以执行写操作,也可以执行读操作。而从服务器只能执行读操作。
- 主服务器每次执行写操作之后,都会与所有从服务器进行数据同步,以保持主从服务器之间的数据一致性。
- 复制功能,可以给读性能带来线性级别的提升,且可以降低系统故障时丢失数据的可能性,配合Sentinel功能,可以为整个Redis系统提供高可用性。
当本地启动了6378和6379两个redis后,需要将6379设为主,6378设为从,便可实现主从服务器拥有相同的数据。同步流程如下:
- 主服务器执行BGSAVE命令,生成RDB文件,并使用缓冲区存储在BGSAVE命令后执行的所有写命令。
- 主服务器通过套接字将RDB文件传给从服务器。Redis2.8.18版本引入无须硬盘的复制,主库不会创建RDB文件,而是新建子进程,子进程通过套接字直接将RDB文件写入从库。配置参数为repl-diskless-sync yes
- 从服务器接收到RDB文件并加载所有数据
- 从服务器加载RDB完成后,开始上线接受命令请求,主服务器会把缓冲区中的所有写命令发送给从服务器执行
- 主从数据达到一致状态后,会进行在线更新,每当主服务器执行完一个写命令,会将相同命令发送给从服务器执行,该过程为异步,可能导致短暂的不一致
- 如因故障下线的从服务器重新上线时,需要重新进行同步,Redis2.8支持增量的部分同步方式,可避免直接进行完整同步。
1 | $ redis-cli -p 6378 |
配置文件如下:
1 | replicaof 127.0.0.1 6379 |
针对lua脚本的复制,有两种方式,脚本传播模式和命令传播模式。redis允许用户在脚本中选择性的设置,可通过redis.set_repl()函数进行设置。
- 脚本传播模式,是主库将被执行的脚本以及参数复制到AOF文件以及从服务器中。但要求函数必须是纯函数,不能是随机结果的函数。
- 命令传播模式,是主库将执行脚本所产生的写命令用事务包裹起来,复制到AOF文件以及从服务器中。
哨兵(sentinel)
- Redis提供了RedisSentinel工具,通过心跳检测的方式监视所有主从服务器,并在某个主服务器下线时自动对其实施故障转移,构建成新的主从关系。
- sentinel会继续对下线的服务器进行心跳检测,当重新上线时,会把他变成从服务器。
- 可通过replica-priority选项设置从服务器优先级,较高的从服务器会优先被选择。
1 | $ /opt/redis5/bin/redis-sentinel /etc/redis/sentinel.conf #运行redis-sentinel工具,指定一个可写的配置文件 |
配置文件内容如下:
1 | port 26379 #sentinel端口号 |
Sentinel管理命令
1 | 127.0.0.1:26379> sentinel masters #查看正在监视的全部主服务器信息 |
集群(cluster)
- 根据数据库概述中的AKF方法论,通过复制和sentinel,解决的是X轴的拓展,实现高可用。Y轴的拓展也很容易实现,只需要不同的模块使用自己的redis即可。那么只剩下了z轴的拓展,需要分片。
- Redis集群通过将数据库分散存储到多个节点来平衡各个节点的负载压力,将数据库空间划分为16384个槽(slot)来实现数据分片(sharding),集群中的各个主节点会分别处理其中的一部分槽。
- Redis集群采用无代理方式,基本无性能损耗。向集群添加新节点或移除已有节点都可以在线进行,无须停机,简单易用。
集群搭建:
1 | #快速搭建集群 |
在生产环境中,一般都需要手动搭建集群,定制节点数量和槽分配。
1 | $ mkdir redis-cluster |
散列标签
1 | $ redis-cli -c -p 30001 |
从节点读命令
1 | $ redis-cli -c -p 30002 #连接到主节点 |
集群管理工具redis-cli –cluster
1 | $ redis-cli --cluster help |
集群管理命令
1 | $ redis-cli -c -p 30001 |
槽管理命令
1 | 127.0.0.1:30001> cluster slots #查看槽与节点的关联信息 |
九、其他
Redis开发规范
1、键值设计
- key名可读性和可管理性
- value选择合适数据类型,拒绝bigkey
- 控制key的生命周期
2、命令使用
- 命令返回数量N不能过多,可拿scan代替
- 禁用keys、flushall、flushdb等命令
- monitor命令时,不要长时间使用
- 合理使用select库
- 使用pineline等批量操作提高效率
- 事务功能较弱,不建议过多使用
缓存故障及解决方案
1、缓存击穿
是指单个Key非常热点,在不停地扛着大量的请求,大并发集中对这一个点进行访问,当这个Key在失效的瞬间,持续的大并发直接落到了数据库上,就在这个Key的点上击穿了缓存。
- 可通过synchronized+双重检查机制,某个key只让一个线程查询,阻塞其它线程
- 设置热点数据永不过期,或者自动续期
- 实现互斥锁,如使用redis的SETNX实现锁效果。但存在死锁风险
2、缓存雪崩
缓存雪崩式缓存击穿的升级版。因为多个key大面积的缓存失效,打崩了DB。
- 每个Key的过期时间加个随机值,保证数据不会再同一时间大面积失效。
- 哨兵、集群等高可用
- 服务降级,如从应用的本地内存获取
3、缓存穿透
是指缓存和数据库中都没有的数据,所以每次查询都会查询数据库从而导致数据库崩溃。和没有缓存一样,透透的。
- 在接口层做参数校验
- 针对上亿的大数据量,利用redis的布隆过滤器
- 缓存空值