工作中有些场景用到了Redis,需要了解一下,本文作为学习笔记,以备后查。
1. 诞生历程
2008年的时候Antirez创建了一个访客信息网站lloogg.com——同类产品有CNZZ、百度统计、Google Analytics等——可以查看最多10000条最新浏览记录。最初实现的时候是为每一个网站创建一个列表(list),不同网站的访问记录进入到不同的列表。如果列表长度超过了用户指定的长度,它需要把最早的记录删除(先进先出)。
当lloogg.com的用户越来越多的时候,它需要维护的列表数量也越来越多,这种记录最新请求和删除最早请求的操作也越来越多;它最初使用的数据库是MySQL,每一次记录和删除都要读写磁盘,因为数据量和并发量太大,在这种情况下无论怎么去优化数据库都不管用了。
考虑到最终限制数据库性能的瓶颈在于磁盘,所以Antirez打算放弃磁盘,自己去实现一个具有列表结构的数据库原型,把数据放在内存而不是磁盘,这样可以大幅提升列表的push和pop的效率。Antirez发现这种思路确实可以解决这个问题后,用C语言重写了这个内存数据库,并加上了持久化功能。于是,2009年Redis横空出世,从最开始只支持列表的数据库,到现在支持多种数据类型,并且提供了一系列高级特性。
Redis的全称是REmote DIctionary Service。从其诞生历史可以看到,在某些场景中,关系型数据库并不适合用来存储Web应用的数据。那么,关系型数据库和非关系型数据库(或者说SQL和NoSQL)有哪些区别?
2. 定位与基本特性
2.1 SQL与NoSQL
一般Web项目中会首先考虑使用关系型数据库来存储业务数据,例如Oracle、MySQL、SQLServer等。关系型数据库的特点:
- 以表格的形式,基于行存储数据,二维模式
- 存储的是结构化的数据,数据存储有固定的模式(schema),数据需要适应表结构
- 表与表之间存在关联
- 大部分关系型数据库都支持SQL操作,支持复杂的关联查询
- 通过支持事务(ACID)来提供严格或实时的数据一致性
- Atomicity,原子性
- Consistency,一致性
- Isolation,隔离性
- Durability,持久性
但使用关系型数据库也存在一些限制,比如:
- 要想实现扩容的话,只能垂直扩展,例如,如果磁盘限制了数据的存储,就要通过堆硬件的方式扩大磁盘容量,不支持动态的扩缩容;水平扩容需要复杂的技术来实现(分库分表)
- 表结构修改困难,因此存储的数据格式也受到限制
- 在高并发和大数据量的情况下,关系型数据库通常会把数据持久化到磁盘,基于磁盘的读写压力较大
为了规避关系型数据库的一系列问题,就产生了非关系型数据库,一般称为”non-relational”或者”Not Only SQL”。NoSQL最开始的含义是不提供SQL的数据库。非关系型数据库的特点:
- 存储非结构化的数据,比如文本、图片、音频、视频等
- 标语表之间没有关联,可扩展性强
- 保证数据的最终一致性,遵循BASE
- Basically Available,基本可用
- Soft-state,软状态
- Eventually Consistent,最终一致性
- 支持海量数据的存储和高并发的高效读写
- 支持分布式,能够对数据进行分片存储,扩缩容简单
对于不同的存储类型,有各种各样的非关系型数据库,常见的几种类型有:
- KV存储,常见的有Redis、MemcacheDB
- 文档存储,MongoDB
- 列存储,HBase
- 图(Graph)存储,Neo4j
- 对象存储
- XML存储
- ……
详见https://hostingdata.co.uk/nosql-database/,这个网站上列举了各种NoSQL数据库。
2.2Redis特性
官网介绍:https://redis.io/topics/introduction、http://www.redis.cn/
硬件层面有CPU缓存,浏览器也有缓存,手机应用也有缓存等等,将数据缓存起来的原因就是从原始位置获取数据的代价太大了。将常用的数据放到一个临时位置存储起来,取回就更快一些。
Redis的特性:
- 更丰富的数据类型
- 进程内与跨进程;单机与分布式
- 功能丰富:持久化机制、过期策略
- 支持多种编程语言
- 高可用集群
3. 安装与基本操作
3.1 安装
CentOS7安装Redis单实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
371.下载、安装到目录/usr/local/soft/
cd /usr/local/soft/
wget http://download.redis.io/releases/redis-5.0.5.tar.gz
2.解压
tar -zxvf redis-5.0.5.tar.gz
3.安装gcc依赖(Redis是C语言编写的,编译时需要此依赖)
yum install gcc
4.编译安装
cd redis-5.0.5
make MALLOC=libc
将/usr/local/soft/redis-5.0.5/src目录下二进制文件安装到/usr/local/bin
cd src
make install
5.修改配置文件(默认配置文件/usr/local/soft/redis-5.0.5/redis.conf)
daemonize yes #后台启动
bind 0.0.0.0 #非本机也能访问
requirepass yourpassword #访问密码
6.使用指定配置文件启动Redis
/usr/local/soft/redis-5.0.5/src/redis-server /usr/local/soft/redis-5.0.5/redis.conf
7.进入客户端
/usr/local/soft/redis-5.0.5/src/redis-cli
8.1 停止Redis(客户端中执行)
shutdown
8.2 停止Redis
ps -aux | grep redis
kill -9 xxxx
9.远程连接
redis-cli -h [host] -p [port] -a [password]CentOS7中使用Docker安装Redis
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
191.获取最新镜像
docker pull redis
2.查看已下载的镜像
docker images
3.因为Docker安装的Redis默认没有配置文件,所以需要挂载主机的配置文件
mkdir -p /usr/local/soft/redis/conf/
mkdir -p /usr/local/soft/redis/data/
4.复制redis.conf文件到主机/usr/local/soft/redis/conf/目录下,并修改配置文件
daemonize yes #必须注释此行!否则容器无法启动!!!
bind 0.0.0.0 #非本机也能访问
5.运行Redis服务端
docker run -d --name redis -p 6379:6379 -v /usr/local/soft/redis/conf/redis.conf:/etc/redis/redis.conf -v /usr/local/soft/redis/data/:/data redis:latest redis-server /etc/redis/redis.conf --appendonly yes --requirepass "yourpassword"
6.本地连接(远程连接同上)
docker exec -it redis redis-cli -a [yourpassword]参数配置方式
- redis.conf
- 设置启动参数
- config set
3.2 基本操作
Redis默认有16个库(0-15),可以在配置文件中修改,默认使用第一个db0,这些库没有完全隔离,不适合把不同的库分配给不同的业务使用。它使用字典结构的存储方式,采用key-value存储,key和value的最大长度限制是512M。命令参考:http://redisdoc.com/index.html
命令 | 含义 |
---|---|
config get databases | 查看数据库个数 |
select 0 | 切换数据库 |
flushdb | 清空当前数据库 |
flushall | 清空所有数据库 |
set [key] [value] | 存值 |
get [key] | 取值 |
keys * | 查看所有键 |
dbsize | 获取键总数 |
exists [key] | 查看键是否存在 |
del [key] [value] | 删除键 |
rename [key] [value] | 重命名键 |
type [key] | 查看类型 |
4. 基本数据类型
Redis的基本数据类型包含8种:String、Hash、List、Set、ZSet、BitMaps、Hyperloglogs、Streams。
4.1 String 字符串
存储类型
最基本也是最常用的数据类型就是String,可以用来存储字符串、整数、浮点数
操作命令
命令 | 含义 |
---|---|
mset name zhangsan sex male | 设置多个值(批量操作,原子性) |
setnx name zhangsan | 设置值,如果key存在则不成功 基于此可实现分布式锁,用del key释放锁 |
set key value [expiration EX seconds|PX milliseconds][NX|XX] | 多参数设置过期时间 |
incr count incrby count100 |
整数值递增 |
decrcount decrbycount100 |
整数值递减 |
set f 3.3 incrbyfloat 6.6 |
浮点数增量 |
mget name sex | 获取多个值 |
strlen name | 获取长度 |
append name lisi | 字符串追加内容 |
getrange f 0 10 | 获取指定范围的字符 |
存储(实现)原理
因为Redis是KV数据库,它是通过hashtable实现的,所以每个键值对都会有一个dictEntry(源码dict.h),里面定义了分别定义了指向key、value、下一个dictEntry的指针。key是字符串,但是Redis没有直接使用C语言的字符数组,而是存储在自定义的SDS中;value既不是直接作为字符串存储,也不是直接存储在SDS中,而是存储在redisObject中。实际上5种常用的数据类型的任何一种,都是通过redisObject来存储的。
字符串类型的内部编码有3种:
- int,存储8个字节的长整型(long, 2^63-1)
- embstr,代表embstr格式的SDS(Simple Dynamic String),存储小于44个字节的字符串
- raw,存储大于44个字节的字符串
应用场景
- 缓存。String类型,热点数据缓存、对象缓存、全页缓存等,可以提升热点数据的访问速度
- 分布式数据共享。String类型,Redis是分布式的独立服务,可以在多个应用之间共享,例如分布式session(spring-session-data-redis)
- 分布式锁。String类型的setnx方法,只有不存在时才能添加成功,返回true
- 全局ID。int类型,incrby方法,利用原子性
- 计数器。int类型,incr方法;文章阅读量,点赞数,允许一定的延迟,先写入redis再定时同步到数据库
- 限流。int类型,incr方法,以访问者ip等信息作为key,访问一次增加一次计数,超过次数则返回false
- 位统计。String类型的BITCOUNT,bit非常节省空间(1MB=8388608bit),可以用来做大数据量的统计,例如在线用户统计、留存用户统计。
如果一个对象的value有多个值的时候,存储时如果用序列化(JSON/Prorobuf/XML)会增加序列化和反序列化的开销,并且不能单独获取、修改一个值;此时可以通过key分层的方式来实现存储
1 | 利用key分层来存储一个key对应的多个value |
4.2 Hash 哈希
存储类型
包含键值对的无序散列表,value只能是字符串,不能嵌套其它类型。
同样是存储字符串,Hash与String的主要区别:
- 把所有相关的值聚集到一个key中,节省内存空间
- 只使用一个key,减少key冲突
- 当需要批量获取值的时候,只需要使用一个命令,减少内存/IO/CPU的消耗
Hash不适用的场景:
- Field不能单独设置过期时间
- 没有bit操作
- 需要考虑数据量分布的问题(value值非常大的时候,无法分布到多个节点)
操作命令
命令 | 含义 |
---|---|
hset h1 e 5 | 设置值 |
hmset h1 a 1 b 2 c 3 d 4 | 批量设置值 |
hget h1 a | 获取单个值 |
hmget h1 a b c d | 批量获取值 |
hkeys h1 | 获取所有键 |
hvals h1 | 获取键对应的所有值 |
hgetall h1 | 获取所有键、值 |
hexists h1 a | 存在则返回1,否则返回0 |
hlen h1 | 获取长度 |
hdel h1 | 删除键、值 |
存储(实现)原理
Redis的Hash本身也是一个KV结构,类似于Java中的HashMap。外层的哈希(Redis KV的实现)只用到了hashtable。当存储hash数据类型时,将其称为内层的哈希。内层的哈希底层可以使用两种数据结构实现:
- ziplist:OBJ_ENCODING_ZIPLIST(压缩列表)
- hashtable:OBJ_ENCODING_HT(哈希表)
ziplist 是一个经过特殊编码的双向链表,它不存储指向上一个链表节点和指向下一 个链表节点的指针,而是存储上一个节点长度和当前节点长度,通过牺牲部分读写性能,来换取高效的内存空间利用率,是一种时间换空间的思想。只用在字段个数少,字段值小的场景里面。
当hash对象同时满足以下两个条件的时候,使用ziplist编码:
- 所有的键值对的健和值的字符串长度都小于等于 64byte(一个英文字母 一个字节)
- 哈希对象保存的键值对数量小于 512 个
一个哈希对象超过配置的阈值(键和值的长度有>64byte,键值对个数>512 个)时, 会转换成哈希表(hashtable)。
在 Redis 中,hashtable被称为字典(dictionary),它是一个数组+链表的结构。Redis 的 KV 结构是通过一个 dictEntry 来实现的,它又对 dictEntry 进行了多层的封装,从最底层到最高层 dictEntry——dictht——dict——OBJ_ENCODING_HT。
为什么要定义两个哈希表?redis 的 hash 默认使用的是 ht[0],ht[1]不会初始化和分配空间。哈希表 dictht 是用链地址法来解决碰撞问题的。在这种情况下,哈希表的性能取决 于它的大小(size 属性)和它所保存的节点的数量(used 属性)之间的比率:
- 比率在 1:1 时(一个哈希表 ht 只存储一个节点 entry),哈希表的性能最好
- 如果节点数量比哈希表的大小要大很多的话(这个比例用 ratio 表示,5表示平均一个 ht 存储 5 个 entry),那么哈希表就会退化成多个链表,哈希表本身的性能优势就不再存在
第二种情况下需要扩容,Redis里面的这种操作叫做rehash,步骤如下:
- 为字符 ht[1]哈希表分配空间,这个哈希表的空间大小取决于要执行的操作,以 及 ht[0]当前包含的键值对的数量(ht[1]的大小为第一个大于等于 ht[0].used*2)
- 将所有的 ht[0]上的节点 rehash 到 ht[1]上,重新计算 hash 值和索引,然后放 入指定的位置
- 当 ht[0]全部迁移到了 ht[1]之后,释放 ht[0]的空间,将 ht[1]设置为 ht[0]表, 并创建新的 ht[1],为下次 rehash 做准备
当dict_can_resize 为1并且已使用节点数和字典大小之间的比率(dict_force_resize_ratio)超过 1:5时,触发扩容。
应用场景
- String可以做的事情,Hash都可以做
- 存储对象类型的数据。比如对象或一张表的数据,比String节省了更多key的空间,也便于集中管理
- 购物车。key:用户id; field:商品id; value:商品数量。数量加减:hincrby, 删除:hdel, 全选:hgetall, 商品数:hlen
4.3 List 列表
存储类型
存储有序的字符串(从左至右),元素可以重复,可以充当队列和栈的角色
操作命令
命令 | 含义 |
---|---|
lpush [key] [value] | 向列表左侧增加元素 |
lpush [key] [value] [value] | 向列表左侧增加多个元素 |
rpush [key] [value] | 向列表右侧增加元素 |
lpop [key] | 从列表左侧取出元素 |
rpop [key] | 从列表右侧取出元素 |
blpop [key] | 当给定列表内没有任何元素可供弹出时,连接将被命令阻塞,直到等待超时或发现可弹出元素为止 |
lindex [key] 0 | 取出列表左侧第一个元素 |
lindex [key] 0 -1 | 从左至右取出列表所有元素 |
存储(实现)原理
在早期的版本中,数据量较小时用 ziplist 存储,达到临界值时转换为 linkedlist 进 行存储,分别对应 OBJ_ENCODING_ZIPLIST 和 OBJ_ENCODING_LINKEDLIST;3.2 版本之后,统一用 quicklist 来存储。quicklist 存储了一个双向链表,每个节点 都是一个 ziplist。quicklist(快速列表)是 ziplist 和 linkedlist 的结合体。
应用场景
- 用户消息时间线。利用list的有序性
- 消息队列。List 提供了两个阻塞的弹出操作:BLPOP/BRPOP,可以设置超时时间:
- BLPOP:BLPOP [key] timeout 移出并获取列表的第一个元素, 如果列表没有元素 会阻塞列表直到等待超时或发现可弹出元素为止
- BRPOP:BRPOP [key] timeout 移出并获取列表的最后一个元素, 如果列表没有元 素会阻塞列表直到等待超时或发现可弹出元素为止
4.4 Set 集合
存储类型
String类型的无序集合,最大存储数量2^32-1(40亿左右)
操作命令
命令 | 含义 |
---|---|
sadd myset a b c d e f g | 添加一个或多个元素 |
smembers myset | 获取所有元素 |
scard myset | 统计元素个数 |
srandmember key | 随机获取一个元素 |
spop myset | 随机弹出一个元素 |
srem myset d e f | 移除一个或多个元素 |
sismember myset a | 查看元素是否存在 |
存储(实现)原理
Redis 用 intset 或 hashtable 存储 set。如果元素都是整数类型,就用 inset 存储。 如果不是整数类型,就用 hashtable(数组+链表的存来储结构)。
应用场景
抽奖。随机获取元素spop myset
点赞、签到、打卡
1
2
3
4
5
6
7假设微博的 ID 是 t1001,用户 ID 是 u3001。
用 like:t1001 来维护 t1001 这条微博的所有点赞用户。
点赞了这条微博:sadd like:t1001 u3001
取消点赞:srem like:t1001 u3001
是否点赞:sismember like:t1001 u3001
点赞的所有用户:smembers like:t1001
点赞数:scard like:t1001商品标签
1
2
3sadd tags:i5001 画面清晰细腻
sadd tags:i5001 真彩清晰显示屏
sadd tags:i5001 流畅至极商品筛选
1
2
3sdiff set1 set2 获取差集
sinter set1 set2 获取交集(intersection)
sunion set1 set2 获取并集用户关注、推荐模型
4.5 ZSet 有序集合
存储类型
sorted set,有序的 set,每个元素有个 score。score 相同时,按照 key 的 ASCII 码排序
数据结构对比:
数据结构 | 是否允许重复元素 | 是否有序 | 有序实现方式 |
---|---|---|---|
列表 list | 是 | 是 | 索引下标 |
集合 set | 否 | 否 | 无 |
有序集合 zset | 否 | 是 | 分值score |
操作命令
命令 | 含义 |
---|---|
zadd myzset 10 java 20 php 30 ruby 40 cpp 50 python | 添加元素 |
zrange myzset 0 -1 withscores zrevrange myzset 0 -1 withscores |
获取全部元素 |
zrangebyscore myzset 20 30 | 根据分值区间获取元素 |
zrem myzset php cpp | 移除元素,也可以根据 score rank 删除 |
zcard myzset | 统计元素个数 |
zincrby myzset 5 python | 分值递增 |
zcount myzset 20 60 | 根据分值统计个数 |
zrank myzset java | 获取元素 rank |
zsocre myzset java | 获取元素 score |
存储(实现)原理
同时满足以下条件时使用 ziplist 编码:元素数量小于 128 个、所有 member 的长度都小于 64 字节。在 ziplist 的内部,按照 score 排序递增来存储。插入的时候要移动之后的数据。超过阈值之后,使用 skiplist+dict 存储。
应用场景
排行榜
1
2id 为 6001 的新闻点击数加 1:zincrby hotNews:20190926 1 n6001
获取今天点击最多的 15 条:zrevrange hotNews:20190926 0 15 withscores
4.6 其它数据类型
4.6.1 BitMaps
Bitmaps 是在字符串类型上面定义的位操作。一个字节由 8 个二进制位组成。
BITOP 命令支持 AND 、 OR 、 NOT 、 XOR 这四种操作中的任意一种参数:
BITOP AND destkey srckey1 … srckeyN ,对一个或多个key求逻辑与,并将结果保存到 destkey
BITOP OR destkey srckey1 … srckeyN,对一个或多key求逻辑或,并将结果保存到 destkey BITOP XOR destkey srckey1 … srckeyN,对一个或多个key求逻辑异或,并将结果保存到 destkey
BITOP NOT destkey srckey,对给定key求逻辑非,并将结果保存到 destkey
应用场景:用户访问统计、在线用户统计
4.6.2 Hyperloglogs
Hyperloglogs:提供了一种不太准确的基数统计方法,比如统计网站的 UV,存在 一定的误差。
4.6.3 Streams
5.0 推出的数据类型。支持多播的可持久化的消息队列,用于实现发布订阅功能,借 鉴了 kafka 的设计
参考
- https://redis.io/
- http://redisdoc.com/
- https://www.redis.net.cn/
- 《Redis入门指南》李子骅
- 《Redis 设计与实现》黄健宏