Redis开发与运维学习笔记
《Redis开发与运维》读书笔记
一、初始Redis
1、Redis特性与优点
- 速度快。redis所有数据都存放于内存;是用C语言实现,更加贴近硬件;使用了单线程架构,避免了多线程竞争问题
- 基于键值对的数据结构,支持的数据结构丰富。它主要提供了5种数据结构: 字符串、 哈希、 列表、 集合、 有序集合, 同时在字符串的基础之上演变出了位图(Bitmaps) 和HyperLogLog两种神奇的“数据结构”, 并且随着LBS(Location Based Service, 基于位置服务) 的不断发展, Redis3.2版本中加入有关GEO(地理信息定位) 的功能
- 功能丰富。键过期功能,用来实现缓存;提供了发布订阅功能,实现消息系统;支持Lua脚本;提供简单的事务功能;提供了Pipeline,一次性将一批命令传给redis服务器,减小了网络开销
- 简单,稳定。源码少,3.0版本源代码只有5w行左右
- 客户端语言多。
- 持久化。两种持久化方式: RDB和AOF
- 主从复制。复制功能是分布式Redis的基础
-
高可用和分布式。2.8版本提供了高可用实现Redis Sentinel, 它能够保证Redis节点的故障发现和故障自动转移。 Redis从3.0版本正式提供了分布式实现Redis Cluster, 它是Redis真正的分布式实现, 提供了高用、 读写和容量的扩展性。
2、应用场景
- 缓存。
- 排行榜系统。redis提供了列表和有序集合支持。
- 计数器应用。
- 社交网络。
- 消息队列系统。redis提供了订阅发布和阻塞队列功能,虽然和专业的消息队列软件比不够强大,但是可以满足一般的消息队列需求
3、redis常用操作
- 服务端启动。配置文件启动,redis-server /opt/redis/redis.conf
- 客户端启动。redis-cli -h 127.0.0.1 -p 6379
- 停止redis服务。redis-cli shutdown nosave|save 关闭过程:断开与客户端的连接;持久化文件生成。不建议kill -9 暴力杀进程,极端下会造成丢失数据的情况
二、API的理解和使用
1、全局命令
- 查看所有键。【keys *】会遍历所有键,禁用
- 键总数。【dbsize】不会遍历所有键,直接获取redis内置的键总数
- 检查键是否存在。【exists key】存在返回1,不存在返回0
- 删除键。【del key】返回结果为成功删除的键的个数
- 设置键过期。【expire key seconds】秒级别;【expireat key timestamp 】秒级时间戳;【pexpire key milliseconds】毫秒级过期 ;【pexpireat key milliseconds-timestamp 】毫秒级时间戳
- 检查键过期。【ttl key】返回键的剩余过期时间,秒级;【pttl key】毫秒级;-1,键没设置过期时间;-2 键不存在;
- 清除过期时间。【persist】;对于字符串类型键,执行set命令也会去掉过期时间;setex命令作为set+expire的组合, 不但是原子执行, 同时减少了一次网络通讯的时间
- 查看键的类型。【type key】键不存在,返回none
- 键重命名。rename key newkey。如果在rename newkey之前,newkey已经存在,newkey的值会被覆盖。防止被覆盖,提供了renamenx命令。重命名期间会删除原来的key,如果key对应的值过大,存在阻塞redis的可能。
- 随机返回一个键。randomkey。
2、数据结构和内部编码
每种数据结构都有底层的内部编码实现,而且是多种实现,redis会在合适的场景下选择合适的内部编码。
3、单线程架构
- Redis使用了单线程架构和IO多路复用模型(epoll作为多路复用技术的实现,非阻塞IO)来实现。
- 每次客户端的请求都会经过发送命令、执行命令、返回结果三个阶段。
- 所有客户端命令都会放入到同一个队列中,然后逐个被执行。
- 单线程避免了线程切换和竞态产生的消耗
4、字符串,最大不能超过512M
(1)命令
- 设置值。set key value [ex seconds] [px milliseconds] [nx|xx]。其中setnx可以作为分布式锁的实现
- 获取值。 get key。不存在返回 nil(空)
- 批量设置值,mset key value [key value ...] ;批量获取值,mget key [key ...] 。一次请求的网络时间大于命令处理时间。学会使用批量操作,能减少大量的网络消耗。提高业务处理效率。但是一次批量操作过多,有可能导致Redis阻塞或者网络拥堵
- 计数。incr、decr、incrby、decrby、incrbyfloat。很多其他的语言和存储系统通过cas实现计数,会有一定的cpu开销。redis单线程模型,完全不存在这个问题。
- 追加值。append key value。
- 字符串长度。strlen key。每个中文占用三个字节,也就是三个长度。
- 设置并返回原值。getset key value
- 设置指定位置的字符。setrange key offset value
- 获取部分字符串。getrange key start end。时间复杂度O(n)
(2)内部编码,有三种
- int。8个字节的长整型
- embstr。小于等于39个字节的字符串
- raw。大于39个字节的字符串
(3)应用场景
- 缓存
- 计数
- 共享session
- 限速。每个用户的请求频率,每个ip的请求频率
5、哈希
(1)命令
- 设置field值。hset key field value。hsetnx,和setnx作用相同,只不过作用域由键变为field
- 获取field值。hget key field。如果field不存在,返回nil
- 删除field。hdel key field [field ...]
- 计算field个数。hlen key
- 批量设置和获取field-value。hmget key field [field ...] ;hmset key field value [field value ...] 。
- 判断field是否存在。hexists key field。存在返回1,不存在返回0
- 获取所有field。hkeys key
- 获取所有value。hvals key
- 获取所有field-value。hgetall key。如果获取的元素比较多,可能会阻塞Redis。
- hincrby key field;hincrbyfloat key field
- 计算value的字符串长度。hstrlen key field
(2)内部编码
- ziplist(压缩列表)。当哈希类型元素个数小于hash-max-ziplist-entries
配置(默认512个) 、 同时所有值都小于hash-max-ziplist-value配置(默认64
字节) 时, Redis会使用ziplist作为哈希的内部实现, ziplist使用更加紧凑的
结构实现多个元素的连续存储, 所以在节省内存方面比hashtable更加优秀 - hashtable(哈希表)。当哈希类型无法满足ziplist的条件时,Redis会使用hashtable作为哈希的内部实现
6、列表。列表中的每个字符串称为元素(element),一个列表最多存储2^32-1个元素。可以充当栈和队列,比较灵活。列表中的元素是有序的且可重复的,可以通过下标获取某个元素
(1)命令
- 添加操作。从右边插入数据 rpush key value [value ...];从左边插入数据 lpush key value [value ...];向某个元素前或后插入数据 linsert key before|after pivot value
- 查找。获取指定范围的元素列表 lrange key start end(包含end);获取指定下标的元素 lindex key index(-1为最后一个元素);获取列表长度 llen key
- 删除。从左侧弹出元素 lpop key;从右侧弹出元素 rpop key;删除指定元素 lrem key count value(count>0 从左到右,删除最多count个元素;count<0 从右到左;count=0,删除所有);按照索引范围修剪列表 ltrim key start end
- 修改。修改执行索引下标的元素 lset key index newValue;
- 阻塞操作。blpop key [key ...] timeout ;brpop key [key ...] timeout 。需要注意两点:第一点,如果blpop多个key,一旦有一个键能弹出元素,立刻给客户端返回;第二点,如果多个客户端对同一个key执行blpop,那么最先执行blpop的客户端可以获取到弹出的值。
(2)内部编码
- ziplist(压缩列表)。当列表的元素个数小于list-max-ziplist-entries配置(默认512个),同时列表中每个元素的值都小于list-max-ziplist-value配置时(默认64字节),Redis会选用ziplist来作为列表的内部实现来减少内存的使用
- linkedlist(链表):当列表类型无法满足ziplist的条件时, Redis会使用linkedlist作为列表的内部实现。
- Redis3.2版本提供了quicklist内部编码,简单地说它是以一个ziplist为节点的linkedlist,它结合了ziplist和linkedlist两者的优势,为列表类型提供了一种更为优秀的内部编码实现
(3)应用场景
- 消息队列。Redis的lpush+brpop命令组合即可实现阻塞队列, 生产者客户端使用lrpush从列表左侧插入元素, 多个消费者客户端使用brpop命令阻塞式的“抢”列表尾部的元素, 多个客户端保证了消费的负载均衡和高可用性。
- 文章列表。哈希存储每篇文章的详细信息。列表存储每个人的文章列表。支持分页获取,如果列表较大,获取列表中间元素的性能会变差,可以使用redis3.2版本的quicklist内部编码实现。
- lpush+lpop=Stack(栈) ;lpush+rpop=Queue(队列) ;lpush+brpop=Message Queue(消息队列,阻塞队列); lpush+ltrim=Capped Collection(有限集合)
7、集合(set)。不允许有重复元素,无序,不能通过索引下标获取元素。一个集合最多可以存储2^32-1个元素。支持集合内的增删改查,集合的交集、并集、差集。
(1)命令。集合内操作。
- 添加元素。sadd key element [element ...]
- 删除元素。srem key element [element ...]
- 计算元素个数。scard key 。时间复杂度O(1),不会遍历集合。
- 判断元素是否在集合中。sismember key element 。在集合内返回1,不在返回0
- 随机从集合中返回指定个数元素。srandmember key [count]
- 从集合随机弹出指定个数的元素。spop key [count]
- 获取所有元素。smembers key 。如果元素过多,存在阻塞redis的可能。可以用sscan来完成。
(2)命令,集合间操作。
- 求多个集合的交集。sinter key [key ...]
- 求多个集合的并集。suinon key [key ...]
- 求多个集合的差集。sdiff key [key ...]
- 将集合操作的结果保存。sinterstore/suionstore/sdiffstore destination key [key ...]
(3)内部编码
- intset(整数集合) : 当集合中的元素都是整数且元素个数小于set-maxintset-entries配置(默认512个)时, Redis会选用intset来作为集合的内部实现,从而减少内存的使用。
- hashtable(哈希表) : 当集合类型无法满足intset的条件时, Redis会使用hashtable作为集合的内部实现。
(4)使用场景
- 标签(tag)。使用sinter命令计算用户的共同兴趣。
- 生成随机数,抽奖
8、有序集合。元素可以排序,每个元素设置一个分数(score)作为排序的依据。集合成员不能重复,但是每个元素的score可以重复
(1)命令。集合内的命令。
- 添加成员。zadd key score member [score member ...]
·Redis3.2为zadd命令添加了nx、 xx、 ch、 incr四个选项:
·nx: member必须不存在, 才可以设置成功, 用于添加。
·xx: member必须存在, 才可以设置成功, 用于更新。
·ch: 返回此次操作后, 有序集合元素和分数发生变化的个数
·incr: 对score做增加, 相当于后面介绍的zincrby。 - 计算成员个数。zcard key
- 计算某个成员的分数。zscore key member
- 计算成员排名。zrank key member (分数由低到高);zrevrank key member (分数由高到低)
- 删除成员。zrem key member [member ...]
- 增加成员分数。zincrby key increment member
- 返回指定排名范围的成员。zrange key start end [withscores] (排名由低到高);zrevrange key start end [withscores] (排名由高到低)
- 返回指定分数范围的成员。zrangebyscore key min max [withscores] [limit offset count] 。其中zrangebyscore按照分数从低到高返回, zrevrangebyscore反之。 例如
下面操作从低到高返回200到221分的成员, withscores选项会同时返回每个
成员的分数。 [limit offset count]选项可以限制输出的起始位置和个数 。同时min和max还支持开区间(小括号) 和闭区间(中括号) , -inf和
+inf分别代表无限小和无限大。 - 返回指定分数范围成员个数。zcount key min max
- 删除指定排名内的升序元素。zremrangebyrank key start end
- 删除指定分数范围的成员。zremrangebyscore key min max
(2)命令,集合间的操作
- 交集。zinterstore destination numkeys key [key ...] [weights weight [weight ...]][aggregate sum|min|max]
- destination: 交集计算结果保存到这个键。
·numkeys: 需要做交集计算键的个数。
·key[key...]: 需要做交集计算的键。
weights weight[weight...]: 每个键的权重, 在做交集计算时, 每个键中
的每个member会将自己分数乘以这个权重, 每个键的权重默认是1。
·aggregate sum|min|max: 计算成员交集后, 分值可以按照sum(和) 、
min(最小值) 、 max(最大值) 做汇总, 默认值是sum - 并集。zunionstore destination numkeys key [key ...] [weights weight [weight ...]][aggregate sum|min|max] 。
(3)内部编码
- ziplist(压缩列表) : 当有序集合的元素个数小于zset-max-ziplistentries配置(默认128个) , 同时每个元素的值都小于zset-max-ziplist-value配置(默认64字节) 时, Redis会用ziplist来作为有序集合的内部实现, ziplist
可以有效减少内存的使用 - skiplist(跳跃表) : 当ziplist条件不满足时, 有序集合会使用skiplist作为内部实现, 因为此时ziplist的读写效率会下降。
(4)使用场景
- 排行榜
9、键管理
(1)键迁移
-
dump+restore可以实现在不同的Redis实例之间进行数据迁移的功能, 整个迁移的过程分为两步:
1) 在源Redis上, dump命令会将键值序列化, 格式采用的是RDB格式。
2) 在目标Redis上, restore命令将上面序列化的值进行复原, 其中ttl参数代表过期时间, 如果ttl=0代表没有过期时间。
第一, 整个迁移过程并非原子性的,而是通过客户端分步完成的。 第二, 迁移过程是开启了两个客户端连接, 所以dump的结果不是在源Redis和目标Redis之间进行传输 -
migrate命令也是用于在Redis实例间进行数据迁移的, 实际上migrate命令就是将dump、 restore、 del三个命令进行组合, 从而简化了操作流程。
migrate命令具有原子性, 而且从Redis3.0.6版本以后已经支持迁移多个键的功能, 有效地提高了迁移效率, migrate在10.4节水平扩容中起到重要作用。
第一, 整个过程是原子执行的, 不需要在多个Redis实例上开启客户端的, 只需要在源Redis上执行migrate命令即可。
第二, migrate命令的数据传输直接在源Redis和目标Redis上完成的。
第三, 目标Redis完成restore后会发送OK给源Redis, 源Redis接收后会根据migrate对应的选项来决定是否在源Redis上删除对应的键。
migrate host port key|"" destination-db timeout [copy] [replace] keys key1 key2 key3
key|""。要迁移的键,多个键此处为""。
keys key1 key2 key3。迁移多个键
(2)键遍历
- 全量遍历键。keys pattern。支持pattern匹配。如果非要使用,在一个不对外提供服务的Redis从节点上执行, 这样不会阻塞到客户端的请求, 但是会影响到主从复制
-
渐进式遍历。每次执行scan, 可以想象成只扫描一个字典中的一部分键, 直到将字典中的所有键遍历完毕。
scan cursor [match pattern] [count number]
·cursor是必需参数, 实际上cursor是一个游标,第一次遍历从0开始,每次scan遍历完都会返回当前游标的值,直到游标值为0,表示遍历结束。
·match pattern是可选参数,它的作用的是做模式的匹配,这点和keys的模式匹配很像。
·count number是可选参数,它的作用是表明每次要遍历的键个数,默认值是10,此参数可以适当增大。
除了scan以外,Redis提供了面向哈希类型、集合类型、有序集合的扫描遍历命令,解决诸如hgetall、 smembers、 zrange可能产生的阻塞问题,对应的命令分别是hscan、 sscan、 zscan,它们的用法和scan基本类似
渐进式遍历可以有效的解决keys命令可能产生的阻塞问题,但是scan并非完美无瑕,如果在scan的过程中如果有键的变化(增加、 删除、 修改),scan并不能保证完整的遍历出来所有的键
(3)数据库管理
- 切换数据库。select db。各个数据库之间没有联系,可以存在相同的键。redis是单线程的,多个数据库使用同一个cpu,彼此至今还是会受影响。
- 清除数据库。flushdb/flushall。如果当前数据库键值数量比较多,flushdb/flushall存在阻塞Redis的可能性
三、小功能,大用处
1、慢查询分析
(1)慢查询只是统计的命令执行的时间,不包括客户端发送命令、命令排队、返回结果的时间。
- slowlog-log-slower-than预设阀值,它的单位是微秒(1秒=1000毫秒=1000000微秒),默认值是10000。slowlog-log-slower-than=0会记录所有的命令,slowlog-log-slowerthan<0对于任何命令都不会进行记录。
- 实际上Redis使用了一个列表来存储慢查询日志,slowlog-max-len就是列表的最大长度
- 在Redis中有两种修改配置的方法, 一种是修改配置文件, 另一种是使用config set命令动态修改。如果要Redis将配置持久化到本地配置文件,需要执行config rewrite命令
- 获取慢查询日志。slowlog get [n] ,n可以指定条数。每个慢查询日志有4个属性组成, 分别是慢查询日志的标识id、 发生时间戳、 命令耗时、 执行命令和参数
- 获取慢查询日志当前的列表长度,slowlog len。
- 慢查询日志列表清空。slowlog reset。
(2)最佳实践
- slowlog-max-len配置建议: 线上建议调大慢查询列表, 记录慢查询时Redis会对长命令做截断操作, 并不会占用大量内存。 增大慢查询列表可以
减缓慢查询被剔除的可能, 例如线上可设置为1000以上。 - slowlog-log-slower-than配置建议: 默认值超过10毫秒判定为慢查询,需要根据Redis并发量调整该值。 由于Redis采用单线程响应命令, 对于高流量的场景, 如果命令执行时间在1毫秒以上, 那么Redis最多可支撑OPS不到1000。 因此对于高OPS场景的Redis建议设置为1毫秒。
- 慢查询只记录命令执行时间, 并不包括命令排队和网络传输时间。 因此客户端执行命令的时间会大于命令实际执行时间。
- 定期执行slow get命令将慢查询日志持久化到其他存储中(例如MySQL) , 然后可以制作可视化界面进行查询
2、Redis Shell
(1)Redis-cli
- -h 服务端ip
- -p 端口
- -r (repeat)将命令执行多次。redis-cli -r 3 ping
- -i (interval)每个几秒执行几次。redis-cli -r 5 -i 1 ping
- -a (auth)密码
- --slave。将当前客户端模拟成服务端的从节点。
- --rdb。生成RDB持久化文件,保存到本地。可用来做持久化文件的定期备份
- --eval。执行lua脚本
- --latency。测试客户端到目标redis服务的网络延迟;--latency-history,每隔多久输出一次网络延迟;--latency-dist,使用统计图表的形式从控制台输出延迟统计信息
- --stat。实时获取redis的重要统计信息。key的数量、内存占用量、客户端数量、请求数量、连接数量
(2)redis-server
- redis-server --test-memory 1024。检测当前操作系统能否稳定地分配指定容量的内存给Redis。整个内存检测的时间比较长。该功能更偏向于调试和测试
(3)redis-benchmark。可以为Redis进行基准性能测试
- -c。代表客户端的并发数量(默认是50)
- -n(num) 。代表客户端请求总量(默认是100000) 。redis-benchmark-c100-n20000代表100各个客户端同时请求Redis, 一共执行20000次
- -r(random) 选项, 可以向Redis插入更多随机的键
- --csv选项会将结果按照csv格式输出, 便于后续处理, 如导出到Excel等
3、Pipeline。
(1)概述。Redis客户端执行一次命令,需要经历发送命令、命令排队、命令执行、返回结果四个过程。4个过程统称为一次Round Trip Time ,往返时间。
Pipeline(流水线)机制能将一组Redis命令进行组装,通过一次RTT传输给Redis,再将这组Redis命令的执行结果按顺序返回给客户端
(2)性能测试。执行速度一般比逐条执行要快,客户端和服务端的网络延迟越大,Pipeline的效果越明显
(3)原生批量命令和Pipeline对比。
- 原生批量命令是原子的,Pipeline是非原子的。
- 原生批量命令是一个命令对应多个key,Pipeline支持多个命令。
- 原生批量命令是Redis服务端支持实现的,而Pipeline需要服务端和客户端的共同实现。
(4)最佳实践
- 一次组装Pipeline数据量过大, 一方面会增加客户端的等待时间, 另一方面会造成一定的网络阻塞, 可以将一次包含大量命令的Pipeline拆分成多次较小的Pipeline来完成
- Pipeline只能操作一个Redis实例
4、事务与lua
(1)事务
- 简单事务功能,multi和exec命令之间的命令,能够原子顺序执行。如果事务之中存在命令拼写错误,整个事务不会执行;如果存在运行时错误,事务并不会回滚
- watch命令,是事务执行之前,确保事务中的key没有被其他客户端修改过,修改过的话就不执行事务。类似于乐观锁
- Redis的事务比较简单,无法保证事务回滚,无法实现事务内命令之间的逻辑运算
(2)lua用法简述。由c语言实现,许多应用选用它作为脚本语言。尤其在游戏领域
(3)Redis中使用lua
- eval 执行lua脚本。eval 脚本内容 key个数 key列表 参数列表。对应 jedis.eval
- evalsha 执行lua脚本。
script load命令可以将脚本内容加载到Redis内存中,返回SHA1。对应jedis.scriptLoad
evalsha 脚本SHA1值 key个数 key列表 参数列表 - lua 的Redis API。
使用redis.call函数实现对Redis的访问。例如:redis.call("set", "hello", "world");redis.call("get", "hello")
还可以使用redis.pcall函数实现对Redis的调用,如果redis.call执行失败,那么脚本执行结束会直接返回错误,而redis.pcall会忽略错误继续执行脚本 - 开发提示
Lua可以使用redis.log函数将Lua脚本的日志输出到Redis的日志文件中,但是一定要控制日志级别。
Redis3.2提供了Lua Script Debugger功能用来调试复杂的Lua脚本 - Redis管理Lua脚本
script load 将Lua脚本加载到Redis内存中
scripts exists sha1 [sha1 …] 判断sha1是否已经加载到Redis内存中
script flush 清除Redis内存已经加载的所有Lua脚本
script kill 用于杀掉正在执行的Lua脚本。如果此时lua脚本阻塞redis了,可以使用此命令快速将脚本杀掉。如果正在执行的是写操作,script kill 将不会生效,只能通过shutdown save停掉redis服务。
5、Bitmaps
(1)概述。Bitmaps本身的数据结构就是字符串,支持对字符串的位进行操作。
(2)每个独立用户是否访问过网站存放在Bitmaps中,将访问的用户记做1,没有访问的用户记做0,用偏移量作为用户的id
- 设置值。setbit key offset value 。初始化的时候,如果偏移量非常大,整个初始化过程会非常慢,可能会造成redis阻塞。
- 获取值。gitbit key offset 。返回0说明没有访问过
- 获取Bitmaps指定范围值为1的个数 。bitcount [start][end] 。
- Bitmaps间的运算 。bitop op destkey key[key....] 。做多个Bitmaps的and(交集) 、 or(并集) 、 not(非) 、 xor(异或) 操作并将结果保存在destkey中
- 计算Bitmaps中第一个值为targetBit的偏移量。bitpos key targetBit [start] [end]
(3)Bitmaps分析
假设网站有1亿用户, 每天独立访问的用户有5千万, 如果每天用集合类型和Bitmaps分别存储活跃用户。
集合类型内存量:64位*50000000=400M。
Bitmaps内存量:1位*50000000=12.5M。
6、HyperLogLog
- Redis中hyperloglog是用来做基数统计的,其优点是:在输入元素的数量或者体积非常非常大的时候,计算基数所需的空间总是固定的,并且是很小的。在Redis里面,每个Hyperloglog键只需要12Kb的大小就能计算接近2^64个不同元素的基数,但是hyperloglog只会根据输入元素来计算基数,而不会存储元素本身,所以不能像集合那样返回各个元素本身
- pfadd key element [element …]。添加操作。如果添加成功返回1
- pfcount key [key …]。求一个或多个key的基数,计算独立用户数
- pfmerge destkey sourcekey [sourcekey ...]。合并。pfmerge可以求出多个HyperLogLog的并集并赋值给destkey
- HyperLogLog内存占用量小得惊人, 但是用如此小空间来估算如此巨大的数据, 必然不是100%的正确, 其中一定存在误差率。 Redis官方给出的数字是0.81%的失误率
7、发布订阅
- 发布消息。publish channel message 。
- 订阅消息。subscribe channel [channel ...]。多个客户端订阅同一个频道,都会受到消息推送,类似于广播。
- 取消订阅。unsubscribe [channel [channel ...]]
- 按照模式订阅和取消订阅。psubscribe pattern [pattern...];punsubscribe [pattern [pattern ...]]
- 订阅查询。pubsub channels [pattern],查看活跃的频道;pubsub numsub [channel ...],查看频道订阅数;pubsub numpat,查看模式订阅数
8、GEO
- 增加地理位置信息。geoadd key longitude latitude member [longitude latitude member ...]。longitude、 latitude、 member分别是该地理位置的经度、 纬度、 成员。添加和更新都是用此命令,可以同时添加多个地理位置信息。
- 获取地理位置信息。geopos key member [member ...]
- 获取两个地址位置的距离。geodist key member1 member2 [unit]。unit代表返回结果的单位,m(meters) 代表米、km(kilometers) 代表公里、mi(miles) 代表英里、ft(feet) 代表尺
-
获取指定位置范围内的地理信息位置集合。georadius key longitude latitude/georadiusbymember key member radiusm|km|ft|mi [withcoord] [withdist][withhash] [COUNT count] [asc|desc] [store key] [storedist key]
都是以一个地理位置为中心算出指定半径内的其他地理信息位置 -
获取geohash。geohash key member [member ...]。Redis使用geohash将二维经纬度转换为一维字符串。
GEO的数据类型为zset, Redis将所有地理位置信息的geohash存放在zset中
字符串越长, 表示的位置更精确, 例如geohash长度为9时, 精度在2米左右
两个字符串越相似, 它们之间的距离越近, Redis利用字符串前缀匹配算法实现相关的命令
geohash编码和经纬度是可以相互转换的 - 删除地理位置信息。zrem key member。GEO没有提供删除成员的命令, 但是因为GEO的底层实现是zset, 所以可以借用zrem命令实现对地理位置信息的删除。
四、客户端
1、客户端通讯协议
通讯协议是建立在TCP协议之上的。Redis制定了RESP(REdis Serialization Protocol, Redis序列化协议) 实现客户端与服务端的正常交互
2、Java客户端Jedis
基本使用、Jedis连接池、Pipeline用法、执行Lua脚本等功能
3、客户端管理
(1)客户端API。client .. 命令
-
client list。列出与Redis服务端相连的所有客户端连接信息。输出结果的每一行代表一个客户端的信息, 可以看到每行包含了十几个属性
id: 客户端连接的唯一标识, 这个id是随着Redis的连接自增的, 重启Redis后会重置为0
addr: 客户端连接的ip和端口。
fd: socket的文件描述符, 与lsof命令结果中的fd是同一个, 如果fd=-1代表当前客户端不是外部客户端, 而是Redis内部的伪装客户端。
name: 客户端的名字
age和idle分别代表当前客户端已经连接的时间和最近一次的空闲时间。当age等于idle时,说明连接一直处于空闲状态
flag是用于标识当前客户端的类型, 例如flag=S代表当前客户端是slave客户端、 flag=N代表当前是普通客户端, flag=O代表当前客户端正在执行monitor命令 -
输入缓冲区: qbuf(总容量)、 qbuf-free(剩余容量)。
redis为每个客户端分配了输入缓冲区, 它的作用是将客户端发送的命令临时保存, 同时Redis从会输入缓冲区拉取命令并执行, 输入缓冲区为客户端发送命令到Redis执行命令提供了缓冲功能
输入缓冲区会根据输入内容大小的不同动态调整, 只是要求每个客户端缓冲区的大小不能超过1G, 超过后客户端将被关闭。输入缓冲区过大主要是因为Redis的处理速度跟不上输入缓冲区的输入速度 -
输入缓冲使用不当会产生两个问题:
·一旦某个客户端的输入缓冲区超过1G, 客户端将会被关闭。
·输入缓冲区不受maxmemory控制,一旦超过maxmemory限制, 可能会产生数据丢失、 键值淘汰、 OOM等情况 -
监控输入缓冲区异常的方法有两种:
通过定期执行client list命令,收集qbuf和qbuf-free找到异常的连接记录并分析,最终找到可能出问题的客户端。能精准定位客户端,但是执行速度较慢,频繁指定可能阻塞redis
通过info命令的info clients模块,找到最大的输入缓冲区,client_biggest_input_buf代表最大的输入缓冲区,可以设置超过10M(阈值)就进行报警。定位不精准,执行速度快。 -
输出缓冲区: obl、 oll、 omem。
Redis为每个客户端分配了输出缓冲区, 它的作用是保存命令执行的结果返回给客户端, 为Redis和客户端交互返回结果提供缓冲。
输出缓冲区由两部分组成: 固定缓冲区(16KB) 和动态缓冲区, 其中固定缓冲区返回比较小的执行结果, 而动态缓冲区返回比较大的结果
client list中的obl代表固定缓冲区的长度, oll代表动态缓冲区列表的长度, omem代表使用的字节数
监控输出缓冲区的方法同输入缓冲区监控方法 -
输出缓冲区的容量可以通过参数client-outputbuffer-limit来进行设置,输出缓冲区也不会受到maxmemory的限制
配置规则:client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>
<class>: 客户端类型, 分为三种。 a) normal: 普通客户端; b)slave: slave客户端, 用于复制; c) pubsub: 发布订阅客户端。
<hard limit>: 如果客户端使用的输出缓冲区大于<hard limit>, 客户端会被立即关闭。
<soft limit>和<soft seconds>: 如果客户端使用的输出缓冲区超过了<softlimit>并且持续了<soft limit>秒, 客户端会被立即关闭 -
输出缓冲区出现异常的概率相对会比较大,如何预防;
进行上述监控, 设置阀值, 超过阀值及时处理
限制普通客户端输出缓冲区的, 把错误扼杀在摇篮中
适当增大slave的输出缓冲区的,master节点写入较大, slave客户端的输出缓冲区可能会比较大, 一旦slave客户端连接因为输出缓冲区溢出被kill, 会造成复制重连
限制容易让输出缓冲区增大的命令, 例如, 高并发下的monitor命令就是一个危险的命令。
及时监控内存, 一旦发现内存抖动频繁, 可能就是输出缓冲区过大。 - client setName和client getName。client setName用于给客户端设置名字, 这样比较容易标识出客户端的来源。
- client kill ip:port。此命令用于杀掉指定IP地址和端口的客户端。
-
client pause timeout(毫秒)。client pause命令用于阻塞客户端timeout毫秒数,在此期间客户端连接将被阻塞。生产环境中,暂停客户端成本非常高
该命令可以在如下场景起到作用:
client pause只对普通和发布订阅客户端有效,对于主从复制(从节点内部伪装了一个客户端)是无效的,也就是此期间主从复制是正常进行的,此命令可以用来让主从复制保持一致
client pause可以用一种可控的方式将客户端连接从一个Redis节点切换到另一个Redis节点。 -
monitor命令用于监控Redis正在执行的命令
每个客户端都有自己的输出缓冲区, 既然monitor能监听到所有的命令, 一旦Redis的并发量过大,monitor客户端的输出缓冲会暴涨, 可能瞬间会占用大量内存
(2)客户端相关配置
-
Redis提供了maxclients参数来限制最大客户端连接数, 一旦连接数超过maxclients, 新的连接将被拒绝。
可以通过info clients来查询当前Redis的连接数
axclients默认值是10000,可以通过config set maxclients对最大客户端连接数进行动态设置 - Redis提供了timeout(单位为秒)参数来限制连接的最大空闲时间,一旦客户端连接的idle时间超过了timeout,连接将会被关闭。默认的timeout是0,动态设置config set timeout 30
- tcp-keepalive:检测TCP连接活性的周期,默认值为0,也就是不进行检测,如果需要设置,建议为60,那么Redis会每隔60秒对它创建的TCP连接进行活性检测,防止大量死连接占用系统资源
- tcp-backlog:TCP三次握手后,会将接受的连接放入队列中,tcpbacklog就是队列的大小,它在Redis中的默认值是511。通常来讲这个参数不需要调整,但是这个参数会受到操作系统的影响
(3)客户端统计片段
info clients命令
- connected_clients:代表当前Redis节点的客户端连接数,需要重点监控,一旦超过maxclients,新的客户端连接将被拒绝。
- client_longest_output_list: 当前所有输出缓冲区中队列对象个数的最大值。
- client_biggest_input_buf: 当前所有输入缓冲区中占用的最大容量。
- blocked_clients:正在执行阻塞命令(例如blpop、 brpop、brpoplpush)的客户端个数。
info stats 命令
- total_connections_received: Redis自启动以来处理的客户端连接数总数
- rejected_connections: Redis自启动以来拒绝的客户端连接数, 需要重点监控
4、客户端常见异常
(1)无法从连接池获取到连接
-
JedisPool中的Jedis对象个数是有限的, 默认是8个。如果连接池中没有空闲Jedis对象,新的请求就需要进行等待(例如设置了maxWaitMillis>0)
在maxWaitMillis时间内仍然无法获取到Jedis对象就会抛出异常:JedisConnectionException: Could not get a resource from the pool
如果设置了blockWhenExhausted=false, 那么调用者发现池子中没有资源时, 会立即抛出异常不进行等待 -
造成没有资源的原因非常多:
客户端: 高并发下连接池设置过小, 出现供不应求
客户端: 没有正确使用连接池, 比如没有进行释放
客户端: 存在慢查询操作, 这些慢查询持有的Jedis对象归还速度会比较慢,造成池子满了
服务端: 客户端是正常的, 但是Redis服务端由于一些原因造成了客户端命令执行过程的阻塞
(2)客户端读写超时,SocketTimeoutException: Read timed out
造成该异常的原因也有以下几种:
·读写超时间设置得过短。
·命令本身就比较慢。
·客户端与服务端网络不正常。
·Redis自身发生阻塞。
(3)客户端连接超时,SocketTimeoutException: connect timed out
造成该异常的原因也有以下几种:
连接超时设置得过短, 可以通过下面代码进行设置:jedis.getClient().setConnectionTimeout(time);
Redis发生阻塞, 造成tcp-backlog已满, 造成新的连接失败。
客户端与服务端网络不正常。
(4)客户端缓冲区异常
造成这个异常的原因可能有如下几种:
输出缓冲区满。
长时间闲置连接被服务端主动断开
不正常并发读写: Jedis对象同时被多个线程并发操作, 可能会出现上述异常
(5)Lua脚本正在执行
如果Redis当前正在执行Lua脚本, 并且超过了lua-time-limit, 此时Jedis调用Redis时, 会收到下面的异常。
JedisDataException: BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.
(6)Redis正在加载持久化文件
Jedis调用Redis时, 如果Redis正在加载持久化文件, 那么会收到下面的异常:
JedisDataException: LOADING Redis is loading the dataset in memory
(7)Redis使用的内存超过maxmemory配置
Jedis执行写操作时, 如果Redis的使用内存大于maxmemory的设置, 会收到下面的异常, 此时应该调整maxmemory并找到造成内存增长的原因
JedisDataException: OOM command not allowed when used memory > 'maxmemory'.
(8)客户端连接数过大
- 如果客户端连接数超过了maxclients, 新申请的连接就会出现如下异常:
JedisDataException: ERR max number of clients reached - 一般来说可以从两个方面进行着手解决:
如果应用方是分布式结构的话,可以通过下线部分应用节点(例如占用连接较多的节点),使得Redis的连接数先降下来。从而让绝大部分节点可以正常运行,此时再通过查找程序bug或者调整maxclients进行问题的修复。
果此时客户端无法处理, 而当前Redis为高可用模式(例如Redis Sentinel和Redis Cluster) , 可以考虑将当前Redis做故障转移。
但是无论从哪个方面进行处理, 故障的快速恢复极为重要, 当然更为重要的是找到问题的所在, 否则一段时间后客户端连接数依然会超过maxclients。
5、客户端案例分析
(1)Redis主节点内存陡增
- 服务端现象: Redis主节点内存陡增, 几乎用满maxmemory, 而从节点内存并没有变化
- 客户端现象: 客户端产生了OOM异常, 也就是Redis主节点使用的内存已经超过了maxmemory的设置, 无法写入新的数据
-
分析原因:
确实有大量写入, 但是主从复制出现问题: 查询了Redis复制的相关信息, 复制是正常的, 主从数据基本一致。主从的键个数基本一致,使用dbsize命令
其他原因造成主节点内存使用过大: 排查是否由客户端缓冲区造成主节点内存陡增, 使用info clients命令发现客户端输出缓冲区不正常,client_longest_output_list:225698
通过client list命令找到omem不正常的连接, 一般来说大部分客户端的omem为0,redis-cli client list | grep -v "omem=0"
最后发现是因为有客户端在执行monitor命令造成的
(2)客户端周期性超时
- 客户端现象: 客户端出现大量超时, 经过分析发现超时是周期性出现的
- 服务端现象: 服务端并没有明显的异常, 只是有一些慢查询操作
-
原因分析:
网络原因: 服务端和客户端之间的网络出现周期性问题, 经过观察网络是正常的
客户端: 由于是周期性出现问题, 就和慢查询日志的历史记录对应了一下时间, 发现只要慢查询出现, 客户端就会产生大量连接超时, 两个时间点基本一致
最终找到问题是慢查询操作造成的, 通过执行hlen发现有200万个元素, 这种操作必然会造成Redis阻塞, 有定时任务代码每5分钟执行一次hgetall操作
五、持久化
1、RDB。RDB持久化是把当前进程数据生成快照保存到硬盘的过程, 分为手动触发和自动触发
(1)触发机制
- 手动触发:save命令。阻塞当前Redis服务器,直到RDB过程完成。线上环境不建议使用
- 手动触发:bgsave命令。Redis进程执行fork操作创建子进程,RDB持久化由子进程负责。阻塞只发生在fork阶段,时间很短。
- 自动触发:使用save相关配置:“save m n”,表示m秒内发生了n次数据修改,自动触发bgsave。
- 自动触发:从节点执行全量复制操作,主节点自动执行bgsave生成RDB文件,发给从节点
- 自动触发:执行debuge reload命令重新加载Redis时,会自动触发save操作
- 自动触发:执行shutdown命令时,如果没有开启AOF功能,会自动触发bgsave
(2)流程说明
- 监测是否存在正在执行的子进程,如RDB/AOF进程,如果存在直接返回
- 父进程创建子进程,fork过程中父进程会阻塞,通过info stats 命令查看latest_fork_usec选项查看最近一次fork的耗时,单位为微秒
- 父进程fork完成后,返回“Backgroung saving started”,不在阻塞父进程,开始响应其他命令
- 子进程创建RDB文件,根据父进程内存生成快照,完成后对原有文件进行原子替换
- 子进程发送信号给父进程表示完成,父进程更新统计信息
(3)RDB文件的处理
- RDB文件保存在dir配置的路径下,名字为dbfilename配置。可以通过命令动态修改 config set dir;config set dbfilename
- Redis采用默认的LZF算法对生成的RDB文件进行压缩,默认开启,虽然压缩过程会消耗cpu,但是可以大幅降低压缩后的文件体积,方便保存到磁盘和通过网络传输到从节点,线上建议开启
- 如果Redis加载损坏的RDB文件时拒绝启动,可以使用Redis提供的redis-check-dump工具检测RDB文件生成对应的错误报告
(4)RDB的优缺点
- 优点:RDB是一个紧凑压缩的二进制文件,代表Redis在某个时间点上的快照。适用于备份、全量复制等场景。
- 优点:Redis加载RDB恢复数据的速度远远快于AOF的方式
- 缺点:RDB没有办法做到实时持久化
- 缺点:REB文件使用特定的二进制格式保存,存在老版本的服务器无法兼容新版RDB格式的问题。
2、AOF。以独立日志的形式记录每次写命令。重启时再重新执行AOF文件中的命令,达到恢复数据的目的
(1)配置
- 开启AOF功能需要设置配置: appendonly yes, 默认不开启。 AOF文件名通过appendfilename配置设置, 默认文件名是appendonly.aof。 保存路径同RDB持久化方式一致, 通过dir配置指定。
- 工作流程操作: 命令写入(append) 、 文件同步(sync) 、 文件重写(rewrite) 、 重启加载(load)
(2)命令写入缓存
- 写入的命令内容直接是文本协议格式,会把内容追加到缓冲区。文本协议具有很好的兼容性
- 先写入缓冲区,避免了每次的命令直接追加到硬盘,提高性能。
(3)文件同步到磁盘,由参数appendfsync控制。设置不同值含义不同
- always。命令写入缓冲区之后,调用系统fsync命令同步到磁盘,fsync完成后线程返回
- everysec。命令写入缓冲区之后,调用系统的write操作,write操作完成后线程返回。fsync同步操作由专门的线程每秒调用一次
- no。命令写入缓冲区之后,调用系统的write操作,不进行fsync操作。同步到硬盘由操作系统负责,通常同步周期最长30秒
- write操作会触发延迟写(delayed write)机制,Linux内核提供页缓冲区提高硬盘IO性能。write写入系统缓冲区之后直接返回。同步硬盘操作依赖于操作系统调度机制
- fsync操作,针对单个文件操作,强制磁盘同步。将阻塞直到写入硬盘完成后返回。
- always配置太耗费性能,只能支持大约几百tps并发写入;no配置,每次系统不同周期不可控;everysec,建议采用的同步策略,也是默认配置
(4)AOF文件重写。把Redis进程内的数据转化成写命令同步更新到新的AOF文件,AOF重新降低了占用空间,更小的AOF文件可以更快的被Redis加载
-
重写之后的AOF文件变小的原因 :
进程内已经超时的数据不在写入文件
旧的AOF文件中含有无效命令,新的AOF文件只保留最终数据的写入命令
多条写命令可以合并成一个