Redis详细入门指南 | 青训营

117 阅读12分钟

简介

Redis是一种基于内存的数据库,对数据的读写操作均在内存中完成,因此具有非常快的读写速度,常用于缓存、分布式锁、消息队列等场景。Redis单线程处理所有命令,因此不存在并发竞争的问题。此外,Redis支持数据持久化防止重启数据丢失。

1、为什么需要Redis?

(1) 原因

随着流量逐日增大,数据一主多从的存放结构难以承受巨大的流量压力和读写压力,因此诞生了分库分表,而Mysql也从单机演进出了集群(如图所示)。然而在快速的数据量增长下,我们需要一种新的解决方案。

图片1.jpg

考虑实际的业务场景,数据可按照一段时间内的访问频率分为冷数据热数据,热数据即经常会被访问到的数据。可以粗略地估计,热数据在接下来受到访问的概率也比较高。如果我们能把热数据存到缓存中,使得请求不再需要访问数据库,那么读写压力会减小许多。

图片2.jpg

(2) Redis为什么快?为什么用Redis做Mysql的缓存?

Redis具备高并发,单台设备上Redis的QPS(每秒处理请求数)可以每秒十万,而Mysql单机的QPS难以到达每秒一万。

Redis基于内存实现,而内存由CPU控制,其读写不会受磁盘缓慢的IO速度限制。

Redis采用I/O多路复用模型,提供了基于事件的回调机制,且Redis线程不会阻塞在某个特定客户端的请求上。

Redis采用单线程,避免了上下文切换引起的CPU开销和防止并发竞争导致的加锁开销。

Redis拥有高效的数据结构,在Redis中,string的底层是SDS,可保存二进制文件且能 O(1)O(1) 地获取长度;List的底层是quicklistHashzset的底层是紧凑列表listpack。这些数据结构保证了能高效地使用内存。

2、Redis数据类型

(1) 字符串

字符串的创建和查询语句分别为set key valueget key

set name hello
get name

代码中的name相当于字符串的变量名,hello相当于值,因此上述代码的查询结果是hello。如果key不存在时,查询结果为nil

我们可以用del删除字符串。

127.0.0.1:6379> del name
(integer) 1
127.0.0.1:6379> get name
(nil)

这个时候查询结果就是nil了。

appendstrlen则用于拼接字符串和查询字符串长度。

127.0.0.1:6379> set key hello
OK
127.0.0.1:6379> strlen key
(integer) 5
127.0.0.1:6379> append key ,world
(integer) 11
127.0.0.1:6379> get key
"hello,world"

输出结果为511hello,world

注意在Redis中空格是分隔符,这使得下面这样写会报错:

set key hello world

错误:(error) ERR syntax error

自增自减

Redis中可以使用incrdecrincrbydecrby实现对一个数字+1-1+特定值-特定值

set num 0
incr num
incrby num 2
decr num
decrby num 3

num0变成1,再变成3,再变成2,最后变成-1

批量添加/查询

使用msetmget可以批量添加字符串和批量查询。

mset key1 v1 key2 v2
mget key1 key2

输出1) "v1"2) "v2"

设置过期时间

给数据设置过期时间后,当经过了这么多时间后,数据就会从数据库中删除掉。使用setex设置过期时间,并可以通过ttl查看key的过期时间。

127.0.0.1:6379> setex test 15 a
OK
127.0.0.1:6379> ttl test
(integer) 13
127.0.0.1:6379> ttl test
(integer) 10
127.0.0.1:6379> ttl test
(integer) 4
127.0.0.1:6379> ttl test
(integer) -2

最后输出-2表示该key已过期。

判断是否存在

有的时候我们想判断key是否存在但又不想修改值该怎么办呢?可以使用exists语句判断是否存在,存在返回1反之则0

127.0.0.1:6379> set key val
OK
127.0.0.1:6379> exists key
(integer) 1
127.0.0.1:6379> del key
(integer) 1
127.0.0.1:6379> exists key
(integer) 0

也可以使用setnx,使用setnx设置时若key存在则不会做任何操作,返回0;否则新增数据。

127.0.0.1:6379> set key val
OK
127.0.0.1:6379> setnx key val2
(integer) 0
127.0.0.1:6379> del key
(integer) 1
127.0.0.1:6379> setnx key val2
(integer) 1

(2) 哈希

Redis中Hash是一个键值对集合,一定程度上可以理解为map[string]string,适合存储对象。

添加和查询只需要在对字符串语句前加h,即hsethget

127.0.0.1:6379> hset person1 id 1
(integer) 1
127.0.0.1:6379> hset person1 name nihao
(integer) 1
127.0.0.1:6379> hset person1 job worker
(integer) 1
127.0.0.1:6379> hgetall person1
1) "id"
2) "1"
3) "name"
4) "nihao"
5) "job"
6) "worker"

如上述代码所示,成功设置了三个string的键值对。

同理在讲解字符串时使用的msetmget批量增删改查,strlen获取长度,exists判断存在均可修改后使用。

127.0.0.1:6379> hmset person2 id 2 name hi job cook
OK
127.0.0.1:6379> hmget person2 id job
1) "2"
2) "cook"

使用hmsethmget可以一次向一个哈希内添加/获取多个键值对。

127.0.0.1:6379> hmset person2 id 2 name hi job cook
OK
127.0.0.1:6379> hlen person2
(integer) 3
127.0.0.1:6379> hexists person2 id
(integer) 1
127.0.0.1:6379> hexists person2 lalala
(integer) 0

hlen获得的是哈希内的键值对数量,而稍有不同的是,hexists是判断该哈希是否存在某个键,如上述代码所示,哈希存在id这个键但不存在lalala这个键。

(3) 列表

插入、弹出元素与查询

列表的结构非常类似于数据结构中的双端队列(deque),当然其底层实现是quicklist。因此可以头插和尾插,也可以头弹出和尾弹出。

具体地,lpushrpushlpoprpop分别表示左/右端插入、左/右端弹出。列表还支持通过下标范围索引元素,即lrange start end,会查询[start,end]中的所有值,注意end-1时表示最后一个元素,-2是表示倒数第二个,以此类推。

127.0.0.1:6379> lpush list 1
(integer) 1
127.0.0.1:6379> lpush list 2
(integer) 2
127.0.0.1:6379> rpush list 3
(integer) 3
127.0.0.1:6379> rpush list 4
(integer) 4
127.0.0.1:6379> lrange list 0 -1
1) "2"
2) "1"
3) "3"
4) "4"
127.0.0.1:6379> lpop list
"2"
127.0.0.1:6379> lrange list 0 -1
1) "1"
2) "3"
3) "4"

可以看到,由于是双端队列的结构,所以后插入的2在最左边,因此lpop弹出的是2。除了给定下标范围查询外,也可以用lindex指定具体下标查询。

127.0.0.1:6379> lindex list 1
"3"

删除元素

lrem根据给定值删除对应值的相应数量元素,并返回删除的元素数量。即lrem key count valuevalue表示要删除的值,count表示删的数量,如果集合中没有这么多数量也不会报错,会全部删除。

127.0.0.1:6379> lrange list 0 -1
1) "1"
2) "3"
3) "4"
4) "1"
127.0.0.1:6379> lrem list 2 1
(integer) 2
127.0.0.1:6379> lrem list 2 3
(integer) 1
127.0.0.1:6379> lrange list 0 -1
1) "4"

在移除2113后,列表只剩下4

截取元素(子数组)

ltrim start end会取原列表的[start,end]子数组,并原地更改本列表。

127.0.0.1:6379> lrange list 0 -1
1) "3"
2) "2"
3) "1"
4) "4"
127.0.0.1:6379> ltrim list 1 2
OK
127.0.0.1:6379> lrange list 0 -1
1) "2"
2) "1"

修改列表中元素的值

lset key index value可以指定下标修改成对应的值。

127.0.0.1:6379> lset list 0 114514
OK
127.0.0.1:6379> lindex list 0
"114514"

(4) 集合

集合即C++中的unordered_set,数据是无序且唯一的,不存在重复值。

增删查

sadd用于向集合中加入元素(可一次加入多个),srem用于从集合中移除元素(若存在返回1,反之则0)。sismember可判断元素是否存在集合内,scard用于查询集合的大小。

127.0.0.1:6379> sadd st1 v1 v2 v3 v4
(integer) 4
127.0.0.1:6379> sismember st1 v2
(integer) 1
127.0.0.1:6379> smembers st1
1) "v2"
2) "v1"
3) "v3"
4) "v4"
127.0.0.1:6379> scard st1
(integer) 4
127.0.0.1:6379> srem st1 v5
(integer) 0
127.0.0.1:6379> srem st1 v3
(integer) 1
127.0.0.1:6379> smembers st1
1) "v2"
2) "v1"
3) "v4"

随机查询/删除

srandmember key [count]用于从集合中随机抽取count个元素,spop key [count]则从集合中随机删除count个元素。

127.0.0.1:6379> srandmember st1 2
1) "v1"
2) "v2"
127.0.0.1:6379> srandmember st1 2
1) "v2"
2) "v4"
127.0.0.1:6379> spop st1 1
1) "v2"
127.0.0.1:6379> smembers st1
1) "v1"
2) "v4"

集合的并交差

集合间的运算可以很好地解决类似于共同好友的需求。smove key1 key2 val表示将key1val的元素移除到key2中,若key1不存在val则返回0,只要存在那么一定会从key1中删除后再插入到key2中(无论是否已存在),返回1。

sdiffsintersunion分别表示差集交集补集sdiff A BAB=A(AB)A-B = A-(A\cap B)sinter A BABA\cap Bsunion A BABA\cup B

127.0.0.1:6379> sadd s1 1 2 3 4 5
(integer) 5
127.0.0.1:6379> sadd s2 3 4 5 6 7
(integer) 5
127.0.0.1:6379> sinter s1 s2
1) "3"
2) "4"
3) "5"
127.0.0.1:6379> sunion s1 s2
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
6) "6"
7) "7"
127.0.0.1:6379> sdiff s1 s2
1) "1"
2) "2"
127.0.0.1:6379> sdiff s2 s1
1) "6"
2) "7"

(5) 有序集合

有序集合zset其实就是在set的基础上给每个元素增加了一个得分,在有序集合中,元素将按照得分从小到大排序,若得分相同,则比较元素的字典序。

插入元素使用zadd key1 score1 value1 score2 value2……,查询时可以使用下标范围查询zrange start end,查询排序后位于[start,pos]下标的元素,也可以使用值范围查询zrangebyscore min max,查询值位于[min,max]的元素。两种查询均可在后面加上withscores表示是否输出各元素的得分。

127.0.0.1:6379> zadd zset 2 a 3 b 2 c 4 d
(integer) 4
127.0.0.1:6379> zrangebyscore zset -inf inf
1) "a"
2) "c"
3) "b"
4) "d"
127.0.0.1:6379> zrangebyscore zset -inf inf withscores
1) "a"
2) "2"
3) "c"
4) "2"
5) "b"
6) "3"
7) "d"
8) "4"
127.0.0.1:6379> zrange zset 1 2 withscores
1) "c"
2) "2"
3) "b"
4) "3"

如果想倒序查询,可以使用zrevrangezrevrange by score。食用方式相反即可。

127.0.0.1:6379> zrevrange zset 1 2 withscores
1) "b"
2) "3"
3) "c"
4) "2"

set中的scardsrem只需要换成zcardzrem即可查询zset大小和删除。

单点增加和排名

zincrby单点增加某元素的score,而使用zrank函数可查询元素的排名(默认从小到大排名,第一名是0,反之用revrank),元素不存在时返回nil

127.0.0.1:6379> zincrby zset 5 b
"8"
127.0.0.1:6379> zrank zset b
(integer) 3
127.0.0.1:6379> zrevrank zset b
(integer) 0

3、简单应用场景

(1) 连续签到

图片1.png

签到可以用incr num使得天数加1,每次签到后过期时间设置为当前时间到后天零点的时间差,即每次签到后,后天零点过期。

(2) 消息通知

图片2.jpg 使用列表进行通知。

(3) 实时变更的排行榜

使用zset即可,可以实现按顺序输出和单点修改分数。

图片3.jpg

(4) 限流

要求1s内放行的请求为N,超过N则禁止访问。

图片4.jpg

(5) 分布式锁

并发场景中为了避免并发竞争,可以利用Redis的特性,其为单线程运行因此不会产生并发竞争问题,而setnx可以保证写过的元素不会再被写。

图片5.jpg

4、Redis持久化

Redis作为内存数据库,如果数据不存入磁盘中,一旦服务器进程退出那么服务器状态也会消失!

(1) RDB持久化

打开redis.conf,我们会看到这样的代码:

save 900 1
save 300 10
save 60 10000

这表示周期性备份,如save 60 10000表示60秒内有10000个写入。如果有多个条件,只要有一个条件满足时,即写入rdb文件进行持久化保存。

(2) AOF持久化

同样在redis.conf中,我们可以找到这样的代码:

appendonly no

AOF持久化会记录所有执行的写入命令。每执行一条命令时就会将写入命令写入临时缓冲区aof_buf,而缓冲区和文件的同步是appendfsync

redis.conf关于AOF的一些配置如下:

# appendfsync always #每次修改都同步缓冲区和文件
appendfsync everysec #每秒同步一次
auto-aof-rewrite-percentage 100 #写入百分比
auto-aof-rewrite-min-size 64mb #写入的文件最大大小

当AOF备份文件越来越大时,效率会越来越低,这个时候可以使用AOF重写

5、缓存雪崩、击穿、穿透

(1) 缓存雪崩

大量缓存同时过期,若此时有大量的用户请求,都无法在redis中处理,只能直接访问数据库,使得数据库面临的压力骤增。

解决方法:

  • 给缓存数据均匀设置过期时间,实现时可以给过期时间加上一个随机数

  • 缓存"永久有效",只当系统内存紧张时采用过期删除/内存淘汰策略。

  • 服务熔断/请求限流

  • 使用缓存集群,避免单机宕机导致的数据雪崩。

(2) 缓存击穿

某个热点数据过期,而此时大量用户请求访问此热点数据都会直接访问数据库,使得数据库压力骤增。

可以通过不设置热点数据的过期时间,或者在热点数据即将过期时,提前通知后台进程更新缓存及重设过期时间。

(3) 缓存穿透

大量既不在缓存也不在数据库的用户请求将会导致缓存穿透。这些请求在缓存中没有找到数据会去数据库查询,在数据库中也没有找到,使得尽管有大量请求,但是没法构建缓存数据来服务后续的大量请求,产生缓存穿透。

缓存穿透可能来源于恶意攻击、数据库误删热点数据、热key过期。

减少方法:

  • 缓存空值或者默认值。下次查询时返回空值即可。

  • 使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在。

布隆过滤器拥有超高的压缩率,本质上就是多个哈希函数结合,压缩在较小的位图数组中,然而高效的查询和极小的空间很有可能导致哈希冲突,但我们注意到,哈希冲突发生时,只会导致误以为不存在的数据存在,这也就说,如果判定为不存在的数据(各哈希位只要有一位没有标记),那么就一定不存在。