缓存数据库目前最经常使用的两种就是 Redis 和 Memcached,与 Memcached 相比 Redis 其一大特色是支持丰富的数据类型(Memcached 只能用 string 类型)。Redis 由于其丰富的数据结构所以应用范围不局限于缓存,有不少场景用 Redis 来实现能够大大减小工做量。这篇文章我想总结一下 Redis 不一样数据结构能够应用的场景。固然 Redis 的使用场景远不止文章所列的这些,不过了解了一些业界的用法,能够开阔本身的思路。html
若是要灵活应用 Redis,首先要熟悉 Redis 各类数据结构所支持的各类命令。不过本文不打算对命令最介绍,由于已经有了许多关于这方面的资料。关于命令的如何使用,能够参考下面两篇文章:redis
中文版本能够看:redisdoc.com/index.html算法
英文版本能够看官网:redis.io/commands数据库
String 数据类型是最经常使用、最简单的 key-value 类型,普通的 key-value 存储均可以归为此类。value 不只能够是字符串,也能够是数字。string 是二进制安全的,因此你彻底能够把一个图片文件的内容做为 string 来存储。Redis 的 string 能够彻底实现目前 Memcached 的功能。除了提供与 Memcached 同样的get、set、incr、decr 等操做外,Redis还额外提供了下面一些操做:数组
缓存是使用最多的场景了,对于字符串和数字能够直接存取。不过更多时候面临的是须要将一个结构体或者对象里的数据缓存起来。存储时能够将结构化数据先序列化,再set到redis中,查询时,先get到后再反序列化到对象中。缓存
使用缓存时,尽可能要设定过时时间,否则缓存数据过多会很快将redis撑满。安全
Redis是处理命令是单线程处理的,所以Redis的INCR、INCRBY、DECR、DECRBY等指令能够实现原子计数的效果。对于业务上一些简单的统计和计数需求能够经过Redis的这些命令来实现。bash
GetSet设置新值,返回旧值。好比实现一个计数器,能够用GetSet获取计数并重置为0。session
分布式id生成器应用最普遍的是Twitter开源的SnowFlake算法。若是并发请求量不是很大的状况下,也可用Redis的INCR和INCRBY命令实现idmaker,即生成全局惟一的id,并且仍是严格自增的。数据结构
Redis能够经过EXPIRE命令给任意key设置过时时间,所以对于须要按期过时的数据能够Redis来存储,能够很方便的实现过时功能。好比实现一个分布式session系统。创建session会话时,将session_key存储到Redis中,并设定过时时间。验证session_key时先根据uid路由到对应的redis,如取不到session_key,则表示session_key已过时,须要从新登陆;如取到session_key且校验经过则从新更新此session_key的过时时间便可。
Set nx或SetNx命令仅当key不存在时才Set成功。能够用来选举Master或实现分布式锁:全部Client不断尝试使用SetNx master myName抢注Master,成功的那位不断使用Expire刷新它的过时时间。若是Master挂掉了key就会失效,剩下的节点又会发生新一轮抢夺。
加锁时经过set nx ex|px命令获取锁,若是set成功,说明加锁成功。加锁成功时,同时会设定一个超时自动释放的时间,避免发生死锁。释放锁时经过lua脚本先get到锁信息,确认为加锁者则del删除这个key,完成锁的释放。关于Redis分布式锁,后面会单独用一篇文章来讲明。
加锁命令:
SET resource-name anystring NX EX max-lock-time
复制代码
释放锁:
if redis.call("get",KEYS[1]) == ARGV[1]
then
return redis.call("del",KEYS[1])
else
return 0
end
复制代码
Redis的GetBit,SetBit,BitOp,BitCount命令用来进行位操做。BitMap的玩法,好比统计一个用户的签到次数,用户签一次到就将相应的offset的位置1,而后经过bitcount就能够统计指定范围内1的个数,也就是该用户的签到次数。能够分别统计,一周内的签到次数,一个月内的签到次数等。
Redis的Append,SetRange,GetRange,StrLen命令,分别实现对文本进行扩展、替换、截取和求长度,对特定数据格式很是有用。
对于HTTP请求,为了防止接口恶意被刷或者限制用户的操做频率,经常会使用频率限制组件,限制用户在一个时间周期(好比1秒)内,只能请求接口2次。而Redis经过lua脚本和INCR命令能够很方便的实现周期性的频率限制。示例代码以下:
# KEYS和ARGV是redis命令传入lua脚本的参数,KEYS[1]是频率限制的KEY,ARGV[1]是增长的次数,ARGV[2]是KEY过时时间
# 将计数加ARGV[1],若是KEY不存在,INCRBY命令会建立KEY,并初始化该KEY的值为ARGV[1]
"local current = redis.call('INCRBY',KEYS[1],ARGV[1]);"
# 若是增长后的值与传入的值相同,说明是新建立的KEY,表示是该周期内第一次更新,则给该key设定过时时间
"if tonumber(current) == tonumber(ARGV[1]) "
"then "
"redis.call('EXPIRE',KEYS[1], ARGV[2]) "
"end "
# 返回当前计数值
"return current";
复制代码
String 在 redis 内部存储就是一个字符串,不过该字符串是 Redis 本身实现的动态字符串对象,动态字符能够自动进行扩容。对于整数也是按字符串进行存储,不过执行 INCR 这类对数值类型才能进行的命令时,Redis 会将字符串先转换成数值类型进行运算,而后将运算结果再转成字符串写进内存中。
Hash存的是字符串和字符串值之间的映射。Hash将对象的各个属性存入Map里,能够只读取/更新对象的某些属性。
对于用户信息好比用户的昵称、年龄、性别、积分等,若是使用字符串类型进行缓存,须要将用户数据先序列化后再存储,这时,若只需修改其中某一项属性值,须要将全部值反序列化出来,而后修改该项数据的值,再序列化后存储回去。这样序列化与反序列化的开销比较大。
string和hash均可以储存结构化数据,那么这两种数据结构该如何选择呢?
[1] 若是大多数时候要访问结构化数据中的大多数字段,则使用string,反之则使用hash;
[2] 若是大多数时候只修改结构化数据中某一个字段的值,则使用hash,反之则使用string;
没有固定的选择方法和模式,须要根据须要权衡考量。
不过这里须要注意,Redis提供了接口(hgetall)能够直接取到所有的属性数据,可是若是内部hash的成员不少,那么涉及到遍历整个内部Map的操做,因为Redis单线程模型的缘故,这个遍历操做可能会比较耗时,而对其它客户端的请求彻底不响应,这点须要格外注意。
好比User对象,除了id有时还要按name来查询,能够单独额外建一个key为name_id的Hash对象保存从name到id的映射关系。在插入User对象时(set user:101 {"id":101,"name":"calvin"}), 顺便往这个hash插入一条(hset name_id calvin 101),这时calvin做为hash里的一个key,值为101。按name查询的时候,用hget user:name:id calvin 就能从名为calvin的key里取出id。假如须要使用多种索引来查找某条数据时可使用,一个hash key搞定,避免使用多个string key存放索引值。
hash结构的字段也能够支持与string相同的HINCRBY等操做,所以一样可用于实现idmaker,计数等。相比与string类型的优点是hash能够只用一个key实现多个不一样的计数。若是一个
Redis Hash对应Value内部实际就是一个HashMap,这里会有2种不一样实现,这个Hash的成员比较少时Redis为了节省内存会采用相似一维数组的方式来紧凑存储,而不会采用真正的HashMap结构,对应的value redisObject的encoding为zipmap,当成员数量增大时会自动转成真正的HashMap,此时encoding为ht。
List 是一个双向链表,支持双向的 pop/push。由于采用的是链表来实现,所以即便 list 里有百万个元素,也能够在常数时间复杂度内完成 push 操做。不过链表也使得按 index 访问元素的时间复杂度变成了 O(N)。从左 push 仍是从右 push,江湖规矩通常从左端 push,右端 pop,即lpush/rpop,并且还有 blocking 的版本 blpop/brpop,客户端能够阻塞在那直到有消息到来。还有 rpoplpush/ brpoplpush,弹出来返回给 client的同时,把本身又推入另外一个 list,llen 获取列表的长度。还有按值进行的操做:lrem(按值删除元素)、linster(插在某个值的元素的先后),复杂度是 O(N),N 是 list 长度,由于 list 的值不惟一,因此要遍历所有元素,而 set 查找只要 O(log(N)) 的时间复杂度。
好比 twitter 的关注列表、粉丝列表、评论列表等也能够用 redis 的 list 结构来实现。
能够利用 list 的 push 操做,将任务存在 list 中,而后工做线程再用 pop 操做将任务取出执行。若是消费者取到消息后就宕机了怎么办?
解决方法之一是加多一个 sorted set,分发的时候同时发到 list 与 sorted set,以分发时间为 score,用户把任务作完了以后要用 ZREM 消掉 sorted set 里的 job,而且定时从 sorted set中取出超时没有完成的任务,从新放回 list。
另外一个作法是为每一个 worker 多加一个的 list,弹出任务时改用 rpoplpush,将 消息同时放到 worker 本身的 list 中,完成时用 lrem 消掉。若是集群管理(如 zookeeper)发现 worker 已经挂掉,就将 worker 的 list 内容从新放回主 list。
可是这两种方法对于一样的数据都要存储多份,并不高效。更好的办法是使用 ack 机制来保证消息的可靠性,使用 rrange 来取消息,经过单独的位置偏移量来记录消费的位置,收到 ack 后更新偏移量,而后删除已经消费的元素。
利用 lrange 能够很方便的实现 list 内容分页的功能。
取最新 N 个数据的操做:lpush 用来插入一个内容 ID,做为关键字存储在列表头部。lrem 用来限制列表中的项目数最多为5000。若是用户须要的检索的数据量超越这个缓存容量,这时才须要把请求发送到数据库。
Redis list 的实现是压缩列表或者双向链表,数据较少时用压缩列表,数据较多时用链表。便可以支持反向查找和遍历,更方便操做,不过带来了部分额外的内存开销,redis 内部的不少实现,包括发送缓冲队列等也都是用的这个数据结构。
是一种无序的集合,集合中的元素没有前后顺序,不重复。将重复的元素放入 set 会自动去重。
一个没有排序要求的集合,可是要求元素不能重复,那么 set 是最合适的选择。而且 set 提供了判断某个成员是否在一个 set 集合内的重要接口,这个也是 list 所不能提供的。
将用户的关注列表放在一个 set 中,set 能够保证去重,这样就不会重复关注,接口能够实现幂等。另外 redis 还为集合提供了求交集、并集、差集等操做,能够很是方便的实现如共同关注、共同喜爱、二度好友等功能,对上面的全部集合操做,你还可使用不一样的命令选择将结果返回给客户端仍是存集到一个新的集合中。又好比 QQ 有一个社交功能叫作“好友标签”,你们能够给你的好友贴标签,好比“大美女”、“土豪”、“欧巴”等等,这里也能够把每个用户的标签都存储在一个集合之中。
set 的内部实现是整数集合或者是 hashmap,所以 set 能够很快的实现查询用户是否在 set 中。
有序集合,与 set 相比 sorted set 在知足去重的要求下还实现了排序功能,也使得有序集合的使用场景会更多。有序集合元素放入集合时还要提供该元素的分数,有序集合会根据分数进行排序。
不少场景须要按照时间顺序排列列表,通常须要时间最近的排在最前面,那么就能够用时间戳作score来实现按时间排序的列表。
sorted set能够根据分数进行排序,所以能够将排行榜的指标数据做为分数,用户信息做为value保存在sorted set结构中。 获得前100名高分用户很简单:ZREVRANGE leaderboard 0 99。查询某个用户的排名也很简单:ZRANK leaderboard 。
好比掘金推荐的文章会根据时间和热度等信息来计算每一个文章的权重值,能够用这个权重值作score,value为文章的id,那么就能够实现热榜的文章列表了。
延时任务使用的场景也很是多,好比电商业务用户30分钟内未付款就自动取消订单等。经过redis的zset能够很方便的实现延时任务,具体能够看我这篇文章:基于REDIS实现延时任务
sorted set的使用跳跃表(SkipList)来现实数据有序存储。另外为了实现较快查询指定元素的分数,redis 还单独在 hashmap 里放了成员到 score 的映射,所以能够在 O(1) 的时间复杂度类查询指定元素的分数。