文章目录[隐藏]
Redis的介绍
谈到Redis(Remote Dictionary Service),相信大家都看过一句介绍:
Redis 是完全开源免费的,遵守BSD协议,是一个高性能的key-value数据库。
这里,我也是重新整理的时候才领悟到:
完全开源免费:可以研究或者改进Redis的源码。
key-value:重点操作就是get和set。
Redis的基本用法
通用命令
# 设置key的值为value。 set key value # 查看所有的key。 keys * # 查看key的存活时间。 ttl key # 设置key的过期时间。 expire key seconds # 将key移动到db中。 move key db # 删除key。 del key # 查询key的类型。 type key # 查询key是否存在。 exists key # 清空所有db flushall # 清空当前db flushdb
String类型的基础命令
# 在key的末尾追加value。如果当前key不存在,相当于set key。 append key value # 获取key的长度。 strlen key # 自增。 incr key # 自减。 decr key # key加value。 incrby key increment # key加float类型的value incrybyfloat key increment # key减value。 decrby key decrement # 截取子串。和Java的区别是:包含end。 getrange key start end # 从offset开始修改值为value。 setrange key offset value # 如果当前的值不存在设置值,如果存在,不替换旧值。 setnx key value # 设置key的值为value,并设置过期时间,单位:秒。 setex key seconds value # 设置key的值为value,并设置过期时间,单位:毫秒。 psetex key milliseconds value # 批量设置值。 mset key value [key value ...] # 批量获取值。 mget key [key ...] # 如果key不存在,批量保存。原子性操作,要么全成功,要么全失败。 msetnx key value [key value ...] # 设置对象 set user:1 {name:caibinbing,age:18} # 批量设置对象 mset user:1:name caibinbing user:1:age 18 # 先获取key再设置value,会返回修改之前的value。 getset key value
工作中,较多使用Json设置对象。
字符串结构使用非常广泛,一个常见的用途就是缓存用户信息。
List类型的基础命令
# 从左边把value推入list中。 lpush list value [value ...] # 如果list存在,从左边把value推入list中。 lpushx list value [value ...] # 从左边开始,读取list中start到stop之间的值。 lrange list start stop # 从右边把value放入list中。 rpush list value [value ...] # 如果list存在,从右边把value推入list。 rpushx list value [value ...] # 从key的左边弹出一个元素 lpop key # 从key的右边弹出一个元素 rpop key # 从左边开始,获取key中指定index的值 lindex key index # 从左边开始,获取key的长度 llen key # 从左边开始,替换key中index位置的值为element。 # 如果key不存在,会抛异常。 lset key index element # 从key中删除count个value。 # count > 0 : 从左到右删除count个。 # count < 0 : 从右到左删除count个。 # count = 0 :删除全部。 lrem key count value # 从左边开始,在key中的pivot之前/之后插入element。 linsert key before|after pivot element # 从左边开始,返回key中第rank个element的位置。 # count : count个 # maxlen : 最多比较maxlen次。 lpos key element rank count maxlen # 从左边开始,截取list中start到stop之间的元素。list会被修改。 # 如果是ltrim key 1 0,会清空整个列表,因为区间范围长度为负数。 ltrim list start stop # 从source的右边弹出一个元素,从左边添加到destination中。 rpoplpush source destination
可以把list理解成一条链表,头尾都能增删元素。
Redis的列表结构常用来做异步队列使用。将需要延后处理的任务结构体序列化成字符串,塞进Redis的列表,另个线程从这个列表中轮训数据进行处理。
Redis的list既可以用来做队列,也可以用来做栈,关键在于lpop和rpop。
当Redis的list被用作队列时,常用于消息队列和异步逻辑处理,它会确保元素的访问顺序。
Hash类型的基础命令
# 获取key中field的值。 hget key field # 获取key中多个field的值。 hmget key field [field...] # 同时设置key的多组值。如果field已经存在,会覆盖。 hset key field value [field value...] # 当且仅当field不存在时,设置key中field的值为value。 hsetnx key field value # 获取key的所有键-值。 hgetall key # 获取key中field的值的长度。 hstrlen key field # 获取key中所有的field。 hkeys key # 获取key种所有的value。 hvals key # 获取key中field的数量(长度)。 hlen key # 判断key中field是否存在。 hexists key field # 删除key中的field。 hdel key field [field...] # 给key中的field值增加increment,increment可以是正负数。 hincrby key field increment
set类型的基础命令
# 往key中添加元素。 sadd key member [member...] # 返回set中所有元素。 smembers key # 返回key中的元素个数。 scard key # 判断member是不是key中的元素。 sismember key member # 差集,返回在第一个key中存在,在其他key中不存在的元素。 sdiff key [key...] # 交集。 sinter key [key...] # 并集。 sunion key [key...] # 把member从source移动到destination中。 smove source destination member # 删除并返回key中count个元素。 spop key [count] # 随机返回key中count个元素。 srandmember key [count] # 删除key中的member。 srem key member [member...]
Set结构可以用来存储某个活动中中奖的用户id,因为有去重功能,可以保证每个用户的唯一性。
Zset类型的基础命令
# 添加member到key中。 # nx:不存在才添加。总是添加新元素。 # xx:存在才更新。从不添加新元素。 # gt:大于当前分数值。此标志不能防止添加新元素。 # lt:小于当前分数值。此标志不能防止添加新元素。 # ch:changed的简写。返回值会变成有变化的元素数量。 # incr:加分。zadd中只能设置一组incr。 zadd key [NX|XX] [GT|LT] [CH] [INCR] score member [score member ...] # 获取key中的元素数量。 zcard key # 获取key中分值在min到max之间的元素。 zcount key min max # 给key中的member增加increment值。 zincrby key increment member # 删除并返回最多count个最高值。 zpopmax key [count] # 删除并返回最多count个最小值。 zpopmin key [count] # 从key中删除member。 zrem key member [member...] # 返回key中,索引在start到stop之间的值。 # 如果带上withscores,会返回对应的分值。 zrange key start stop [WITHSCORES] # zrange的逆序版本。从高到低排序返回。 zrevrange key start stop [WITHSCORES] # 返回member在key中的index。其中,index从0开始。 zrank key member # zrank的逆序版本。从高到低排序。 zrevrank key member # 差集。 zdiff numkeys key [key ...] [WITHSCORES] # 交集 zinter numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX] [WITHSCORES] # 并集 zunion numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX] [WITHSCORES] # 获取member的分值。如果key或者member不存在,返回nil。 zscore key member # zscore的多member版本。返回多个指定member的分值。 zmscore key member [member...]
Zset可以用来存储粉丝列表,value是粉丝的用户id,score是关注时间。可以对粉丝列表按关注时间进行排序。
还可以用来存储学生的成绩,value是学生的id,score是学生的考试成绩。可以对学生按成绩进行排序。
Geo的基础命令
# 添加member的经纬度到key中。 geoadd key longitude latitude member [longitude latitude member ...] # 返回key中member的hash值。 geohash key member [member ...] # 返回key中member的经纬度。 geopos key member [member ...] # 返回key中member1和member2之间的距离。 # m:米 # km:千米 # mi:英里 # ft:英尺 geodist key member1 member2 [m|km|ft|mi] # 返回key中指定经纬度方圆radius的member及其经纬度信息。 georadius key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key] # 返回key中指定member方圆radius的member及其经纬度信息。 GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
HyperLogLog的基础命令
# 向key中添加element。 pfadd key element [element...] # 返回key中内容的近似基数。 pfcount key [key...] # 合并多个hiperloglog到destkey中。 pfmerge destkey sourcekey [sourcekey...]
Transaction的基础命令
# 监视key。即乐观锁。 watch key [key...] # 启动事务块。 multi # 执行事务块。 # 它并不保证事务块的原子性。如果事务块中某条指令执行失败,不影响后面的指令。 exec # 放弃事务块。 discard # 取消所有key的监视。 unwatch
Redis的进阶知识
Redis所有的数据结构都以唯一的key字符串作为名称,然后通过这个唯一的key值来获取相应的value数据。不同类型的数据结构差异就在于value的结构不一样。
Redis的String是动态字符串,是可以修改的字符串,内部结构的实现采用预分配冗余空间的方式来减少内存的频繁分配。当字符串长度小于1MB时,扩容都是加倍现有的空间。如果字符串长度超过1MB,扩容时一次只会增加1MB的空间。字符串最大长度是512MB。
String类型中,如果value是一个整数,它的自增范围在signed long的最大值和最小值之间。
字符串由多个字节组成,每个字节又由8个bit组成,如此便可以将一个字符串看成很多个bit的组合,这便是bitmap(位图)的数据结构。
Redis的List是链表,而不是数组。它的插入和删除操作时间复杂度是O(1),但是索引定位的时间复杂度是O(n)。Redis的List底层是快速链表(quicklist)。当列表元素较少时,它会使用一块连续的内存,结构是压缩列表(ziplist)。当数据量较大是,它会变成quicklist。因为普通的链表需要的附加指针空间太大,除了会浪费空间,还会加重内存的碎片化。
Redis的Hash实现与Java的HashMap类似,都是“数组+链表”的二维结构。Redis的Hash的值只能是字符串,并且采用了渐进式rehash策略。
渐进式rehash指的是,Redis在rehash的时候会同时保留新旧两种hash结构。查询时会同时查询两个hash结构,再后续的定时任务以及hash操作指令中,循序渐进地将旧的hash内容迁移到新的hash结构中。迁移完成后,新的hash结构才会取代旧的hash结构。
Hash的缺点是存储消耗高于单个字符串。选择使用String还是Hash需要根据实际情况进行权衡。
Redis的Set内部的键值对是无序的,惟一的。Redis的Zset内部用的一种叫“跳跃列表”的数据结构。
在Redis的集合类型的通用规则:
- create if not exists:如果集合不存在,先创建一个,再进行操作。
- drop if no element:当集合中最后一个元素被移除之后,数据结构会被自动删除,内存也会被回收。Redis所有的数据结构都可以设置过期时间。
需要注意两点:
- 过期时间以对象为单位。
- 如果一个String设置了过期时间,再次进行set操作,它的过期时间会消失。
关于Redis的分布式锁的奥义:
- 多个进程同时对同个对象进行get和set操作。
- 使用setnt、expire和del组合设置锁。
- 由于setnx和expire需要保持原子性才能解决两个命令引起的死锁问题,所以应该改用set操作。
# EX:秒。 # PX:毫秒。 # KEEPTTL:Retain the time to live associated with the key. # NX:如果不存在才设置值。 # XX:如果存在才设置值。 # GET:返回旧的值。 SET key value [EX seconds|PX milliseconds|KEEPTTL] [NX|XX] [GET]
使用Redis做消息队列时,使用阻塞读,即blpop/brpop可以解决队列空的时候,客户端pop死循环的问题。阻塞读在队列没有数据的时候会立即进入休眠状态,一旦数据来了,立刻苏醒,消息延迟几乎为零。
Redis的位数组是自动扩展的,如果设置了某个偏移位置超出了现有的内容范围,位数组就会自动进行零扩充。
Redis的HyperLogLog需要占据12KB的存储空间。HyperLogLog可以解决很多精确度要求不高的统计问题,但是要知道某个值是否存在就无能为力了。
Redis的布隆过滤器除了可以添加元素,还能判断一个或多个元素是否存在。
# 添加一个元素。 bf.add # 判断一个元素是否存在。 bf.exists # 添加多个元素。 bf.madd # 判断多个元素是否存在。 bf.mexists
布隆过滤器对于已经见过的元素肯定不会误判,它只会误判没有见过的元素。降低误判率可以在bf.add使用之前使用bf.reserve指令显示创建。如果对应的key已经存在,bf.reserve会报错。bf.reserve有三个参数,分别是key,error_rate和initial_size。其中error_rate越低,需要的空间越大。initial_size表示预计放入元素的数量,当实际数量超过这个数值时,误判率会上升。如果不使用bf.reserve,默认的error_rate是0.01,默认的initial_size是100。
注意:布隆过滤器的initial_size设置得过大会浪费存储空间,设置得过小会影响准确率。
Redis的Geo操作没有删除指令,但可以通过zrem进行删除操作。因为Geo存储的数据结构使用的是zset。
使用geopos指令获取的经纬度坐标和geoadd添加时的坐标有少许误差,原因是GeoHash算法对二维坐标进行一维映射是有损的。
数据量大的时候使用Redis的Geo数据结构,这些数据会被放在一个zset集合中。在Redis集群中迁移数据,如果单个key的数据量过大,可能会影响线上服务。建议集群中单个key对应的数据量不超过1MB,或者Geo的数据使用单独的Redis实例进行部署。
降低单个zset集合大小可取的方法是对Geo数据进行拆分,按国家,省份,地市或者区县进行拆分。
使用keys进行查询有两个缺点:
- 没有offset和limit参数,一次性返回所有符合正则表达式的key。
- keys算法的时间复杂度是O(n),数据量大时,keys查询会影响Redis服务。因为Redis是单线程程序,其他指令必须等keys命令执行完才进行。
使用scan替代keys有以下四个优点:
- 时间复杂度是O(n),但是使用了游标,不会阻塞进程。
- 提供了limit参数,可以控制每次返回的最大条数。limit不是限定返回结果的数量,而是限定服务器每次遍历的字典槽位数量。
- 和keys一样,提供了模式匹配功能。
- 服务端不需要保存游标的状态,游标的唯一状态就是scan返回给客户端的游标整数。
使用scan存在以下三个不确定因素:
- scan返回的结果可能会重复,需要客户端去重。
- 遍历过程中,如果有数据改动,修改后的数据不确定能不能遍历到。
- 单次返回结果是空的不代表遍历结束,要根据返回的游标值是否为零决定。
scan遍历的顺序并不是从第一维数组的第0位遍历到末尾,而是采用高位进位加法来遍历。这样遍历是考虑到字典扩容和缩容时避免槽位的遍历重复和遗漏。(详情参考《Redis深度历险——核心原理与应用实践》的P84)
高位进位加法的进位顺序和二进制加法进位顺序相反。即从左边加,往右边进,最终会无重复地遍历到所有槽位。
有时候,由于业务人员使用不当,在Redis中会形成很大的对象,比如hash或者zset。这在迁移Redis集群数据时会造成卡顿。另外在需要扩容时,内存需要开辟一块更大的空间,同样也会造成卡顿。回收这样一块大的内存时,会再次造成卡顿。因此,在平时业务开发中,应该要尽量避免产生大的key。
如何定位大key?
- 使用scan指令扫描key。
- 使用type指令获取每个key的类型。
- 根据每个key的类型,使用对应数据结构的size或者len方法得到该key的大小。
- 对所有key的大小进行排序,由此定位大key。
官方已经提供了定位大key功能的指令
redis-cli -h localhost -p 7001 --bigkeys # 如果担心指令会抬升Redis的ops导致线上报警,可以增加一个休眠参数 redis-cli -h localhost -p 7001 --bigkeys -i 0.1
以上指令每个100条scan指令就会休眠0.1s。这样虽然ops不会剧烈抬升,但是扫描时间会变长。
总结
纸上得来终觉浅,绝知此事要躬行!
参考资料
- Redis官方文档
- 《Redis深度历险——核心原理与应用》,钱文品
- 【狂神说Java】Redis最新超详细版教程通俗易懂
本文内容转自冰部落,仅供学习交流,版权归原作者所有,如涉及侵权,请联系删除。 声明: 本平台/个人所提供的关于股票的信息、分析和讨论,仅供投资者进行研究和参考之用。 我们不对任何股票进行明确的买入或卖出推荐。 投资者在做出投资决策时,应自行进行充分的研究和分析,并谨慎评估自己的风险承受能力和投资目标。 投资有风险,入市需谨慎。请投资者根据自身的判断和风险承受能力,自主决策,理性投资。