redis 运维和开发指南-学习笔记

为什么用 redis

  • 速度快 10万每秒的读写速度,c语言实现、单线程,预防多线程可能存在的竞争问题
  • 基于键值对的数据结构服务器 字符串、哈希、列表、集合、有序集合、位图、hyperloglog GEO(地理位置定位)
  • 功能丰富
    • 提供了键过期功能,可以用来实现缓存
    • 提供了发布订阅功能,可以用来实现消息系统
    • 支持Lua脚本功能,可以利用Lua创造出新的Redis命令
    • ·提供了简单的事务功能,能在一定程度上保证事务特性
    • ·提供了流水线(Pipeline)功能,这样客户端能将一批命令一次性传到
      Redis,减少了网络的开销
  • 简单稳定,早期版本2万行代码,3.0以后 代码增加至 5万行。
  • 持久化,提供RDB和AOF 两种策略将内存的数据保存在硬盘中
  • 主从复制
  • 高可用和分布式

redis API

ttl命令会返回键的剩余过期时间,它有3种返回值:

  • 大于等于0的整数:键剩余的过期时间。
    64
  • -1:键没设置过期时间。
  • -2:键不存在

如下所示

1
2
3
4
5
6
7
8
9
10
11
12
127.0.0.1:6379> set hello word
OK
127.0.0.1:6379> EXPIRE hello 10
(integer) 1
127.0.0.1:6379> ttl hello
(integer) 7
127.0.0.1:6379> ttl hello
(integer) 1
127.0.0.1:6379> ttl hello
(integer) 0
127.0.0.1:6379> ttl hello
(integer) -2

键的数据结构类型

1
2
3
4
5
6
7
8
127.0.0.1:6379> set a b
OK
127.0.0.1:6379> type a
string
127.0.0.1:6379> RPUSH mylist a b c d e f g
(integer) 7
127.0.0.1:6379> type mylist
list

redis 内部数据结构

type命令实际返回的就是当前键的数据结构类型,它们分别是:string(字符串)、hash(哈希)、list(列表)、set(集合)、zset(有序集合),但这些只是Redis对外的数据结构,实际上每种数据结构都有自己底层的内部编码实现,而且是多种实现,这样Redis会在合适的场景选择合适的内部编码,

image-20190423153512157

1
2
3
4
127.0.0.1:6379> OBJECT encoding hello
"embstr"
127.0.0.1:6379> OBJECT encoding mylist
"quicklist"

这样设计的好处:

  • 第一,可以改进内部编码,而对外的数据结构和命令没有影响,这样一旦开发出更优秀的内部编码,无需改动外部数据结构和命令。
  • 第二,多种内部编码实现可以在不同场景下发挥各自的优势,例如ziplist比较节省内存,但是在列表元素比较多的情况下,性能会有所下降,这时候Redis会根据配置选项将列表类型的内部实现转换为linkedlist。

单线程架构

Redis使用了单线程架构和I/O多路复用模型来实现高性能的内存数据库服务

第一,纯内存访问,Redis将所有数据放在内存中,内存的响应时长大约为100纳秒,这是Redis达到每秒万级别访问的重要基础。

第二,非阻塞I/O,Redis使用epoll作为I/O多路复用技术的实现,再加上Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件,不在网络I/O上浪费过多的时间。

第三,单线程避免了线程切换和竞态产生的消耗。

第四,单线程可以简化数据结构和算法的实现。如果对高级编程语言熟悉的读者应该了解并发数据结构实现不但困难而且开发测试比较麻烦。

第五,单线程避免了线程切换和竞态产生的消耗,对于服务端开发来说,锁和线程切换通常是性能杀手。

缺陷:对于每个命令的执行时间是有要求的。如果某个命令执行过长,会造成其他命令的阻塞,对于Redis这种高性能的服务来说是致命的,所以Redis是面向快速执行场景的数据库。

set 、setnx、setxx

set命令有几个选项:

  • ex seconds:为键设置秒级过期时间。
  • px milliseconds:为键设置毫秒级过期时间。
  • nx:键必须不存在,才可以设置成功,用于添加。
  • xx:与nx相反,键必须存在,才可以设置成功,用于更新。

setnx和setxx在实际使用中有什么应用场景吗?以setnx命令为例子,由Redis的单线程命令处理机制,如果有多个客户端同时执行setnx key value,根据setnx的特性只有一个客户端能设置成功,setnx可以作为分布式锁的一种实现方案,Redis官方给出了使用setnx实现分布式锁的方法:http://redis.io/topics/distlock

字符串、哈希、列表、集合

mset 和mget

1
n次get 时间 = n次网络时间+n次命令时间

使用mget 后

1
n次get 时间 = 1次网络时间+ n次命令时间

字符串

字符串的内部编码

字符串类型的内部编码有3种:

  • int:8个字节的长整型。

  • embstr:小于等于39个字节的字符串。

  • raw:大于39个字节的字符串。

Redis会根据当前值的类型和长度决定使用哪种内部编码实现

如下:

1
2
3
4
5
6
7
8
9
10
11
12
127.0.0.1:6379> set key 123
OK
127.0.0.1:6379> object encoding key
"int"
127.0.0.1:6379> set key "hello world"
OK
127.0.0.1:6379> object encoding key
"embstr"
127.0.0.1:6379> set key "hello worldsfdsgdfghfgjghjghjtyyrtyrtyrtyrtytr........ttttytytryrtyrty."
OK
127.0.0.1:6379> object encoding key
"raw"

哈希类型

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
127.0.0.1:6379> hset user:1 name tome
(integer) 1
127.0.0.1:6379> hset user:1 age 14
(integer) 1
127.0.0.1:6379> hkeys user:1
1) "name"
2) "age"
127.0.0.1:6379>
27.0.0.1:6379> HMSET user:2 name tom age 14 city hangzhou
OK
127.0.0.1:6379> HMGET user:2 name city
1) "tom"
2) "hangzhou"
127.0.0.1:6379> HEXISTS user:2 name
(integer) 1
127.0.0.1:6379> HVALS user:2
1) "tom"
2) "14"
3) "hangzhou"
127.0.0.1:6379> hgetall user:1 # 获取所有的可以
1) "name"
2) "tome"
3) "age"
4) "14"
127.0.0.1:6379> HSTRLEN user:1 name # 计算value 的长度
(integer) 4

在使用hgetall时,如果哈希元素个数比较多,会存在阻塞Redis的可能。
如果开发人员只需要获取部分field,可以使用hmget,如果一定要获取全部
field-value,可以使用hscan命令,该命令会渐进式遍历哈希类型。

哈希类型的内部编码
  • ziplist(压缩列表):当哈希类型元素个数小于hash-max-ziplist-entries配置(默认512个)、同时所有值都小于hash-max-ziplist-value配置(默认64字节)时,Redis会使用ziplist作为哈希的内部实现,ziplist使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比hashtable更加优秀。
  • hashtable(哈希表):当哈希类型无法满足ziplist的条件时,Redis会使用hashtable作为哈希的内部实现,因为此时ziplist的读写效率会下降,而hashtable的读写时间复杂度为O(1)。

列表

从右边插入

1
2
3
4
5
6
7
127.0.0.1:6379> rpush listkey c b a
(integer) 3
127.0.0.1:6379> lrange listkey 0 -1
1) "c"
2) "b"
3) "a"
127.0.0.1:6379>

从左边插入

1
2
127.0.0.1:6379> LPUSH key a b c
(integer) 3

从指定元素前面或后面插入

1
2
3
4
5
6
7
8
127.0.0.1:6379> LINSERT key before b c # before 前面 after 后面
(integer) 4
127.0.0.1:6379> LRANGE key 0 -1
1) "c"
2) "c"
3) "b"
4) "a"
127.0.0.1:6379>

获取列表长度和指定下标的元素

1
2
3
4
127.0.0.1:6379> LINDEX key -1
"a"
127.0.0.1:6379> LLEN key
(integer) 4

删除

1
2
lpop key 
rpop key

慢查询

Redis提供了slowlog-log-slower-than和slowlog-max-len配置来解决这两个问题。slowlog-log-slower-than就是那个预设阀值,它的单位是微秒(1秒=1000毫秒=1000000微秒),默认值是10000。

如果slowlog-log-slower-than=0会记录所有的命令,slowlog-log-slowerthan<0对于任何命令都不会进行记录。

1
2
3
4
5
6
7
8
9
10
11
12
13
27.0.0.1:6379> config set slowlog-log-slower-than 20000
OK
127.0.0.1:6379> config set slowlog-max-len 1000
OK
127.0.0.1:6379> config rewrite
OK
127.0.0.1:6379>
127.0.0.1:6379> SLOWLOG get
(empty list or set)
127.0.0.1:6379> SLOWLOG len
(integer) 0
127.0.0.1:6379> SLOWLOG reset
OK

慢查询功能可以有效地帮助我们找到Redis可能存在的瓶颈,但在实际使用过程中要注意以下几点:

  • slowlog-max-len配置建议:线上建议调大慢查询列表,记录慢查询时Redis会对长命令做截断操作,并不会占用大量内存。增大慢查询列表可以减缓慢查询被剔除的可能,例如线上可设置为1000以上。
  • slowlog-log-slower-than配置建议:默认值超过10毫秒判定为慢查询,需要根据Redis并发量调整该值。由于Redis采用单线程响应命令,对于高流量的场景,如果命令执行时间在1毫秒以上,那么Redis最多可支撑OPS不到1000。因此对于高OPS场景的Redis建议设置为1毫秒。

  • 慢查询只记录命令执行时间,并不包括命令排队和网络传输时间。因此客户端执行命令的时间会大于命令实执行时间。因为命令执行排队机制,慢查询会导致其他命令级联阻塞,因此当客户端出现请求超时,需要检
    查该时间点是否有对应的慢查询,从而分析出是否为慢查询导致的命令级联阻塞。

  • 由于慢查询日志是一个先进先出的队列,也就是说如果慢查询比较多的情况下,可能会丢失部分慢查询命令,为了防止这种情况发生,可以定期执行slow get命令将慢查询日志持久化到其他存储中(例如MySQL),然后可以制作可视化界面进行查询,第13章介绍的Redis私有云CacheCloud提供了这样的功能,好的工具可以让问题排查事半功倍