Redis基础篇

工作中有些场景用到了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的特性:

  1. 更丰富的数据类型
  2. 进程内与跨进程;单机与分布式
  3. 功能丰富:持久化机制、过期策略
  4. 支持多种编程语言
  5. 高可用集群

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
    37
    #1.下载、安装到目录/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(客户端中执行)
    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
    19
    #1.获取最新镜像
    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种:

  1. int,存储8个字节的长整型(long, 2^63-1)
  2. embstr,代表embstr格式的SDS(Simple Dynamic String),存储小于44个字节的字符串
  3. 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
2
3
4
#利用key分层来存储一个key对应的多个value
mset student:1:sno 001 student:1:sname zhangsan student:1:sex male
#一次获取多个值
mget student:1:sno student:1:sname student:1:sex

4.2 Hash 哈希

存储类型

包含键值对的无序散列表,value只能是字符串,不能嵌套其它类型。

同样是存储字符串,Hash与String的主要区别:

  1. 把所有相关的值聚集到一个key中,节省内存空间
  2. 只使用一个key,减少key冲突
  3. 当需要批量获取值的时候,只需要使用一个命令,减少内存/IO/CPU的消耗

Hash不适用的场景:

  1. Field不能单独设置过期时间
  2. 没有bit操作
  3. 需要考虑数据量分布的问题(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编码:

  1. 所有的键值对的健和值的字符串长度都小于等于 64byte(一个英文字母 一个字节)
  2. 哈希对象保存的键值对数量小于 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,步骤如下:

  1. 为字符 ht[1]哈希表分配空间,这个哈希表的空间大小取决于要执行的操作,以 及 ht[0]当前包含的键值对的数量(ht[1]的大小为第一个大于等于 ht[0].used*2)
  2. 将所有的 ht[0]上的节点 rehash 到 ht[1]上,重新计算 hash 值和索引,然后放 入指定的位置
  3. 当 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
    3
    sadd tags:i5001 画面清晰细腻
    sadd tags:i5001 真彩清晰显示屏
    sadd tags:i5001 流畅至极
  • 商品筛选

    1
    2
    3
    sdiff 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
    2
    id 为 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 的设计

参考