Redis能用来做什么

4,696 阅读13分钟

缓存数据库目前最常用的两种就是 Redis 和 Memcached,与 Memcached 相比 Redis 其一大特点是支持丰富的数据类型(Memcached 只能用 string 类型)。Redis 因为其丰富的数据结构因此应用范围不局限于缓存,有很多场景用 Redis 来实现可以大大减少工作量。这篇文章我想总结一下 Redis 不同数据结构可以应用的场景。当然 Redis 的使用场景远不止文章所列的这些,不过了解了一些业界的用法,可以开阔自己的思路。

如果要灵活应用 Redis,首先要熟悉 Redis 各种数据结构所支持的各种命令。不过本文不打算对命令最介绍,因为已经有了许多关于这方面的资料。关于命令的如何使用,可以参考下面两篇文章:

中文版本可以看:redisdoc.com/index.html

英文版本可以看官网:redis.io/commands

String

简介

String 数据类型是最常用、最简单的 key-value 类型,普通的 key-value 存储都可以归为此类。value 不仅可以是字符串,也可以是数字。string 是二进制安全的,所以你完全可以把一个图片文件的内容作为 string 来存储。Redis 的 string 可以完全实现目前 Memcached 的功能。除了提供与 Memcached 一样的get、set、incr、decr 等操作外,Redis还额外提供了下面一些操作:

  • 获取字符串长度
  • 往字符串末尾append内容
  • 设置和截取字符串的某一段内容
  • 位操作,redis最大可以支持2^23 - 1位的位操作
  • 批量设置一系列字符串的内容

应用场景

缓存

缓存是使用最多的场景了,对于字符串和数字可以直接存取。不过更多时候面临的是需要将一个结构体或者对象里的数据缓存起来。存储时可以将结构化数据先序列化,再set到redis中,查询时,先get到后再反序列化到对象中。

使用缓存时,尽量要设定过期时间,不然缓存数据过多会很快将redis撑满。

计数统计

Redis是处理命令是单线程处理的,因此Redis的INCR、INCRBY、DECR、DECRBY等指令可以实现原子计数的效果。对于业务上一些简单的统计和计数需求可以通过Redis的这些命令来实现。

GetSet设置新值,返回旧值。比如实现一个计数器,可以用GetSet获取计数并重置为0。

分布式id生成器

分布式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存的是字符串和字符串值之间的映射。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

简介

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 条数据

取最新 N 个数据的操作:lpush 用来插入一个内容 ID,作为关键字存储在列表头部。lrem 用来限制列表中的项目数最多为5000。如果用户需要的检索的数据量超越这个缓存容量,这时才需要把请求发送到数据库。

实现方式

Redis list 的实现是压缩列表或者双向链表,数据较少时用压缩列表,数据较多时用链表。即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销,redis 内部的很多实现,包括发送缓冲队列等也都是用的这个数据结构。

Set

简介

是一种无序的集合,集合中的元素没有先后顺序,不重复。将重复的元素放入 set 会自动去重。

应用场景

数据去重

一个没有排序要求的集合,但是要求元素不能重复,那么 set 是最合适的选择。并且 set 提供了判断某个成员是否在一个 set 集合内的重要接口,这个也是 list 所不能提供的。

交并集实现共同关注

将用户的关注列表放在一个 set 中,set 可以保证去重,这样就不会重复关注,接口可以实现幂等。另外 redis 还为集合提供了求交集、并集、差集等操作,可以非常方便的实现如共同关注、共同喜好、二度好友等功能,对上面的所有集合操作,你还可以使用不同的命令选择将结果返回给客户端还是存集到一个新的集合中。又比如 QQ 有一个社交功能叫做“好友标签”,大家可以给你的好友贴标签,比如“大美女”、“土豪”、“欧巴”等等,这里也可以把每一个用户的标签都存储在一个集合之中。

实现方式:

set 的内部实现是整数集合或者是 hashmap,因此 set 可以很快的实现查询用户是否在 set 中。

Sorted 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) 的时间复杂度类查询指定元素的分数。