Redis入门

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的集合类型的通用规则:

  1. create if not exists:如果集合不存在,先创建一个,再进行操作。
  2. drop if no element:当集合中最后一个元素被移除之后,数据结构会被自动删除,内存也会被回收。Redis所有的数据结构都可以设置过期时间。

需要注意两点:

  1. 过期时间以对象为单位。
  2. 如果一个String设置了过期时间,再次进行set操作,它的过期时间会消失。

关于Redis的分布式锁的奥义:

  1. 多个进程同时对同个对象进行get和set操作。
  2. 使用setnt、expire和del组合设置锁。
  3. 由于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进行查询有两个缺点:

  1. 没有offset和limit参数,一次性返回所有符合正则表达式的key。
  2. keys算法的时间复杂度是O(n),数据量大时,keys查询会影响Redis服务。因为Redis是单线程程序,其他指令必须等keys命令执行完才进行。

使用scan替代keys有以下四个优点:

  1. 时间复杂度是O(n),但是使用了游标,不会阻塞进程。
  2. 提供了limit参数,可以控制每次返回的最大条数。limit不是限定返回结果的数量,而是限定服务器每次遍历的字典槽位数量。
  3. 和keys一样,提供了模式匹配功能。
  4. 服务端不需要保存游标的状态,游标的唯一状态就是scan返回给客户端的游标整数。

使用scan存在以下三个不确定因素:

  1. scan返回的结果可能会重复,需要客户端去重。
  2. 遍历过程中,如果有数据改动,修改后的数据不确定能不能遍历到。
  3. 单次返回结果是空的不代表遍历结束,要根据返回的游标值是否为零决定。

scan遍历的顺序并不是从第一维数组的第0位遍历到末尾,而是采用高位进位加法来遍历。这样遍历是考虑到字典扩容和缩容时避免槽位的遍历重复和遗漏。(详情参考《Redis深度历险——核心原理与应用实践》的P84)

高位进位加法的进位顺序和二进制加法进位顺序相反。即从左边加,往右边进,最终会无重复地遍历到所有槽位。

有时候,由于业务人员使用不当,在Redis中会形成很大的对象,比如hash或者zset。这在迁移Redis集群数据时会造成卡顿。另外在需要扩容时,内存需要开辟一块更大的空间,同样也会造成卡顿。回收这样一块大的内存时,会再次造成卡顿。因此,在平时业务开发中,应该要尽量避免产生大的key。

如何定位大key?

  1. 使用scan指令扫描key。
  2. 使用type指令获取每个key的类型。
  3. 根据每个key的类型,使用对应数据结构的size或者len方法得到该key的大小。
  4. 对所有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不会剧烈抬升,但是扫描时间会变长。

总结

纸上得来终觉浅,绝知此事要躬行!

参考资料

  1. Redis官方文档
  2. 《Redis深度历险——核心原理与应用》,钱文品
  3. 【狂神说Java】Redis最新超详细版教程通俗易懂
 
本文内容转自冰部落,仅供学习交流,版权归原作者所有,如涉及侵权,请联系删除。

声明:
本平台/个人所提供的关于股票的信息、分析和讨论,仅供投资者进行研究和参考之用。
我们不对任何股票进行明确的买入或卖出推荐。
投资者在做出投资决策时,应自行进行充分的研究和分析,并谨慎评估自己的风险承受能力和投资目标。
投资有风险,入市需谨慎。请投资者根据自身的判断和风险承受能力,自主决策,理性投资。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注