1.概念
1.1 单线程架构
1.1.1 Redis是用了单线程架构和I/O多路复用模型来实现高新能的内存数据库服务
1.1.2 为什么单线程还能这么快:
① 纯内存访问,Redis将所有数据放在内存中,内存的响应时长大约为100纳秒,这是Redis达到每秒万级别访问的重要基础;
② 非阻塞I/O,Redis使用epoll作为I/O多路复用技术的实现,再加上Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件,不在网络I/O上浪费过多时间;
③ 单线程避免了线程切换和竞态产生的消耗。
1.1.3 单线程问题
对于每个命令的执行时间是有要求的。如果某个命令执行过长,会造成其他命令的阻塞,对于Redis这种高性能的服务来说是致命的,所以Redis是面向快速执行场景的数据库。
1.2 字符串
字符串类型是Redis最基础的数据结构。首先键都是字符串类型,而且其他几种数据结构都是在字符串类型基础上构建的。字符串类型的值实际可以是字符串、数字、甚至是二进制,但值最大不能超过512MB。
1.2.1 内部编码
字符串类型的内部编码有3种:
① int:8个字节的长整数;
② embstr:小于等于39个字节的字符串;
③ raw:大于39个字节的字符串。
1.2.2 典型使用场景
① 缓存
② 计数
③ 共享session:
一个分布式Web服务将用户的session信息(例如用户登录信息)保存在各自服务器中,这样会造成一个问题,出于负载均衡的考虑,分布式服务会将用户的访问均衡到不同服务器上,用户刷新一次访问可能会发现需要重新登录,这个问题是用户无法容忍的。
为了解决这个问题,可以使用Redis将用户的Session进行集中管理。在这种模式下只要保证Redis是高可用和扩展性的,每次用户更新或者查询登录信息都直接从Redis中集中获取。
④ 限速:
例如一些网站限制一个ip地址不能在一秒之内访问超过n次等。
1.3 哈希
在Redis中,哈希类型是指键值本身又是一个键值对结构,形如value={{field1,value1},…{ fieldN,valueN}}:
1.3.1 内部编码
哈希类型的内部编码有两种:
① ziplist(压缩列表):当哈希类型元素个数小于hash-max-ziplist-entries配置(默认512个)、同时所有值都小于hash-max-ziplist-value配置(默认64字节)时,Redis会使用ziplist作为哈希的内部实现,ziplist使用更加紧凑的结构实现多个元素的连续存储,所以节省内存方面比hashtable更加优秀;
② hashtable(哈希表):当哈希类型无法满足ziplist的条件时,Redis会使用hashtable作为哈希的内部实现,因为此时ziplist的读写效率会下降,而hashtable的读写时间复杂度为O(1)。
1.4 列表
列表(list)类型是用来存储多个有序的字符串,列表中的每个字符串称为元素。在Redis中,可以对列表两端插入(push)和弹出(pop)等,它可以充当栈和队列的角色。
1.4.1 内部编码
列表类型的内部编码有两种:
① ziplist(压缩列表):当列表的元素个数小于list-max-ziplist-entries配置(默认512个)、同时所有值都小于list-max-ziplist-value配置(默认64字节)时,Redis会使用ziplist作为列表的内部实现;
② linkedlist(链表):当列表类型无法满足ziplist的条件时,Redis会使用linkedlist作为列表的内部实现。
1.4.2典型使用场景
① 消息队列:Redis的lpush+brpop命令组合即可实现阻塞队列,生产者客户端使用lrpush从列表左侧插入元素,多个消费者客户端使用brpop命令阻塞式的“抢”列表尾部的元素,多个客户端保证了消费的负载均衡和高可用性。
② 文章列表
1.5 集合
集合(set)类型也是用来保存多个字符串元素,但和列表类型不一样的是,集合中不允许有重复元素,并且集合中的元素是无序的,不能通过索引下标获取元素。Redis除了支持集合内的增删改查,同时还支持多个集合去交集、并集、差集等。
1.5.1 内部编码
集合类型的内部编码有两种:
① intset(整数集合):当集合中元素都是整数且元素个数小于set-max-intset-entries配置(默认512个)时,Redis会选用intset来作为集合的内部实现,从而减少内存的使用;
② hashtable(哈希表):当集合类型无法满足intset的条件时,Redis会使用hashtable作为集合的内部实现。
1.5.2典型使用场景
① 标签
1.6 有序集合
有序集合中元素不能有重复,但可以排序,与列表使用索引下标作为排序依据不同的是,它给每个元素设置一个分数(score)作为排序依据。
1.6.1 内部编码
有序集合类型的内部编码有两种;
① ziplist(压缩列表):当有序列表的元素个数小于zset-max-ziplist-entries配置(默认128个)、同时所有元素的值都小于zset-max-ziplist-value配置(默认64字节)时,Redis会使用ziplist作为列表的内部实现;
② skiplist(跳跃表):当有序集合类型无法满足ziplist的条件时,Redis会使用skiplist作为有序集合的内部实现。
1.6.2典型使用场景
① 排行榜系统
2.功能
2.1 慢查询分析
许多存储系统(例如MySQL)提供慢查询日志帮助开发和运维人员定位系统存在的慢操作。所谓慢查询日志就是系统在命令执行前后计算每条命令的执行时间当超过预设阈值,就将这条命令的相关信息记录下来,Redis也提供类似的功能。
Redis客户端执行一条命令分为如下4步:
① 发送命令
② 命令排队
③ 命令执行
④ 返回操作
慢查询只统计步骤③的时间。
慢查询的两个配置参数:
① 预设阈值:slowlog-log-slower-than(单位微妙);
② Redis使用了一个列表来存储慢查询日志,slowlog-max-len就是列表的最大长度。
在Redis中有两种修改配置的方法,一种是修改配置文件,另一种是使用config set命令动态修改:
① config set slowlog-log-slower-than 20000
② config set slowlog-max-len 1000;
③ config rewrite
如果要Redis将配置文件持久化到本地配置文件,需要执行config rewrite命令。
2.2 事务与Lua
为了保证多条命令组合的原子性,Redis提供了简单的事务功能以及集成Lua脚本来解决这个问题。
2.2.1 事务
Redis提供了简单的事务功能,将一组需要一起执行的命令放到multi和exec两个命令之间。multi命令代表事务开始,exec命令代表事务结束,它们之间的命令是原子顺序执行的。
Redis支持的是弱事务,它并不支持回滚功能,开发人员需要自己修复这类问题。
有些应用场景需要在事务之前,确保事务中的key没有被其他客户端修改过,才执行事务,否则不执行(类似乐观锁)。Redis提供了watch命令来解决这类问题。
Lua脚本功能为Redis开发和运维人员带来如下好处:
① Lua脚本在Redis中是原子执行的,执行过程中间不会插入其他命令;
② Lua脚本可以帮助开发人员和运维人员创造出自己定制的命令,并可以将这些命令常驻Redis内存中,实现复用效果;
③ Lua脚本可以将多条命令一次性打包,有效地减少网络开销。
2.3 发布订阅
Redis提供了基于“发布/订阅”模式的消息机制,此种模式下,消息发布者和订阅者不进行直接通信,发布者客户端向指定的频道(channel)发布消息,订阅该频道的每个客户端都可以收到该消息:
Redis主要提供了发布消息(publish channel message)、订阅频道(subscribe channel [channel…])、取消订阅(unsubscribe channel [channel…])以及按照模式订阅(psubscribe pattern [pattern…])和取消订阅(punsubscribe pattern [pattern…])等命令。
有关订阅命令有两点需要注意:
① 客户端在执行订阅命令之后进入订阅状态,只能接收subscribe、psubscribe、unsubscribe、punsubscribe的四个命令。
② 新开启的订阅客户端,无法收到该频道之前的消息,因为Redis不会对发布的消息进行持久化。和专业的消息队列系统(如Kafka\RocketMQ)相比,Redis的发布订阅略显粗糙,例如无法实现消息堆积和回溯。
2.3.1使用场景
聊天室、公告牌、服务间消息解耦都可以使用发布订阅模式。
2.4 GEO
Redis 3.2版本提供了GEO(地理信息定位)功能,支持存储地理位置信息用来实现诸如附近位置、摇一摇这类依赖于地理位置信息的功能。
① 增加地理位置信息
geoadd key longitude latitude member [longitude latitude member…]
例:127.0.0.1:6379> geoadd cities:locations 116.28 39.55 beijing
(integer) 1
返回结果代表添加成功的个数,如果cities:locations没有包含beijing,那么返回结果为1,如果已经存在则返回0(可用作修改);
② 获取地理位置信息
geopos key member [member…]
③ 获取两个地理位置的距离
geodist key member1 member2 [unit]
其中unit代表返回结果的单位:m\km\mi(英里)\ft(尺)
④ 获取指定位置范围内的地理信息位置集合
georadius key longitude latitude radiusm|km|ft|mi [withcoord] [withdist] [withhash] [COUNT count] [asc|desc] [store key] [storedist key]
georadiusbymember key member radiusm|km|ft|mi [withcoord] [withdist] [withhash] [COUNT count] [asc|desc] [store key] [storedist key]
georadius和georadiusbymember两个命令作用一样的,都以一个地理位置为中心算出指定半径内的其他地理位置信息,不同的是georadius命令的中心位置给出了具体的经纬度,georadiusbymember只需给出成员即可。
可选参数说明:
- withcoord:返回结果中包含经纬度;
- withdist:返回结果中包含离中心节点位置的距离;
- withhash:返回结果中包含geohash;
- COUNT count:指定返回结果的数量;
- asc|desc:返回结果按照离中心节点的距离做升序或者降序;
- store key:将返回结果的地理位置信息保存到指定键;
- storedist key:将返回结果离中心节点的距离保存到指定键。
⑤ 获取geohash
geohash key member [member…]
Redis使用geohash将二维经度转换为一维字符串。
Geohash有如下提点:
- GEO的数据类型为zset,Redis将所有地理位置信息的geohash存放在zset中;
- 字符串越长,表示的位置更精确;
- 两个字符串越相似,它们之间的距离越近,Redis利用字符串前缀匹配算法实现相关命令;
- Geohash编码和经纬度是可以相互转换的。
Redis正是使用有序集合并结合geohash的特性实现GEO的若干命令。
⑥ 删除地理位置信息
zrem key member
GEO没有提供删除成员的命令,但因为GEO的底层实现是zset,所以可以借助zrem命令实现对地理位置信息的删除。
3.客户端
3.1 客户端通信协议
① 客户端与服务端之间的通信协议是在TCP协议之上构建的;
② Redis制定了RESP(Redis Serialization Protocol,Redis序列化协议)实现客户端与服务端的正常交互。
3.2 Jedis
3.2.1 Jedis的使用
Jedis的使用方法非常简单:
//生成一个Jedis对象,这个对象负责和指定的Redis实例进行通信
Jedis jedis = new Jedis(“127.0.0.1”,6379);
Jedis.set(“hello”,”world”);
String value = jedis.get(“hello”);
Jedis.close();
3.2.2 Jedis连接池的使用
上面介绍的是Jedis直连方式,所谓直连是指Jedis每次都会新建TCP连接,使用后再断开连接,对于频繁访问Redis的场景显然不是高效的使用方式。因此生产环境中一般使用连接池的方式对Jedis连接进行管理,所有Jedis对象预先放在池子中(JedisPool),每次要连接Redis,只需要在池子中借,用完了在归还给池子。
客户端连接Redis使用的是TCP协议,直连的方式每次需要建立TCP连接,而连接池的方式是可以预先初始化好Jedis连接,所以每次只需要从Jedis连接池借用即可,而借用和归还操作是在本地进行的,只有少量的并发同步开销,远远小于新建TCP连接的开销。另外直连的方式无法限制Jedis对象的个数,在极端情况下可能会造成连接泄露,而连接池的形式可以有效保护和控制资源的使用。
Jedis连接池(通常JedisPool是单例的):
//common-pool连接池配置,这里使用默认配置
GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
//初始化Jedis连接池
JedisPool jedisPool = new JedisPool(poolConfig,“127.0.0.1”,6379);
Jedis jedis = null;
Try{
//1.从连接池获取jedis对象
Jedis = jedisPool.getResource();
//2.执行操作
Jedis.get(“hello”);
}catch(){
}finally{
if(jedis != null){
//如果使用JedisPool,close操作不是关闭连接,代表归还连接池
Jedis.close();
}
}
3.2.3 客户端API
① client list
client list命令能列出与Redis服务端相连的所有客户端连接信息:
输出结果的每一行代表一个客户端的信息,可以看到每行包含了十几个属性,它们是每个客户端的一些执行状态。
- 输入缓存区:qbuf、qbuf-free
Redis为每个客户端分配了输入缓存区,它的作用是将客户端发送的命令临时保存,同时Redis会从输入缓存区拉取命令并执行,输入缓存区为客户端发送命令到Redis执行命令提供了缓存功能。
client list中qbuf、qbuf-free分别代表这个缓存区的总容量和剩余容量,Redis没有提供相应的配置来规定每个缓存区的大小,输入缓存区会根据输入内容大小的不同动态调整,只是要求每个客户端缓存区的大小不能超过1G,超过后客户端将被关闭。
输入缓存区不受maxmemory控制,假设一个Redis实例设置maxmemory为4G,已经存储了2G数据,但是如果此时输入缓存区使用了3G,已经超过maxmemory限制,可能会产生数据丢失、键值淘汰、OOM等情况。
监控输入缓存区异常的方法:
a) 通过定期执行client list命令,收集qbuf和qbuf-free找到异常的连接记录并分析,最终找到可能出问题的客户端;
b) 通过info命令的info client模块,找到最大的输入缓存区。 - 输出缓存区:obl(固定缓存区长度)、oll(动态缓存区列表的长度)、omem(使用的字节数)
Redis为每个客户端分配了输出缓存区,它的作用是保存命令执行的结果返回给客户端,为Redis和客户端交互提供缓存。与输入缓存区不同的是,输出缓存区的容量可以通过参数client-output-buffer-limit来进行设置。和输入缓存区相同的是,它也不受maxmemory控制,超过maxmemory限制,可能会产生数据丢失、键值淘汰、OOM等情况。
实际上输出缓存区由两部分组成:固定缓存区(16KB)和动态缓存区,其中固定缓存区返回比较小的执行结果,而动态缓存区返回比较大的结果。固定缓存区使用的是字节数组,动态缓存区使用的是列表。
监控输出缓存区的方法:
a) 通过定期执行client list命令,收集obl、oll、omem找到异常的连接记录并分析,最终找到可能出问题的客户端;
b) 通过info命令的info client模块,找到最大的输出缓存区列表最大对象数。 - 客户端的存活状态
client list中的age和idle分别代表当前客户端已经连接的时间和最近一次的空闲时间。
知识点参考《Redis开发与运维》 付磊 张益军