Redis是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。今天我们来看下利用Python或php结合redis的一些应用场景,希望对大家有所帮助。

打开网易新闻 查看更多图片

一、缓存

项目场景:

1.1 用户登录或注册时的验证码存储,用户名

set Code:1:code 1232 EX 100 NX

OK

get Code:1:code

“1232”

set User:1:name bob EX 100 NX

OK

get User:1:name

“bob”

1.2 全页面缓存

如果你使用的是服务器端内容渲染,你又不想为每个请求重新渲染每个页面,就可以使用 Redis 把常被请求的内容缓存起来,能够大大的降低页面请求的延迟,已经有很多框架用Redis来缓存页面,这就是页面静态化的一种方式。

// Set the page that will last 1 minute
SET key "..." EX 60
// Get the page
GET key

缓存是 redis 出镜率最高的一种使用场景,仅仅使用 set/get 就可以实现,不过也有一些需要考虑的点
1、如何更好的设置缓存
2、如何保持缓存与上游数据的一致性
3、如何解决缓存雪崩,缓存击穿问题

二:消息队列

lpush UserEmailQueue 1 2 3 4

lpop UserEmailQueue

rpop UserEmailQueue
1
rpop UserEmailQueue
2

可以把redis的队列看为分布式队列,作为消息队列时,生产者在一头塞入数据。消费者另一条取出数据:(lpush/rpop,rpush/lpop),不过也有一些不足,而这些不足有可能是致命的,不过对于一些丢几条消息也没关系的场景还是可以考虑的
1、没有ack(消息确认机制),有可能丢消息
2、需要做redis的持久化配置

三:过滤器(dupefilter)

sadd UrlSet http://1
(integer) 1
sadd UrlSet http://2
(integer) 1
sadd UrlSet http://2
(integer) 0
smembers UrlSet

1. “http://1”

2. “http://2”
scrapy-redis作为分布式的爬虫框架,便是使用了 redis 的 Set 这个数据结构来对将要爬取的 url 进行去重处理。

def request_seen(self, request):
"""Returns True if request was already seen.
Parameters
----------
request : scrapy.http.Request
Returns
-------
bool
"""
fp = self.request_fingerprint(request)
added = self.server.sadd(self.key, fp)
return added == 0

不过当 url 过多时,会有内存占用过大的问题

四、频率控制

项目的社区功能里,不可避免的总是会遇到垃圾内容,一觉醒来你会发现首页突然会被某些恶意的帖子和广告刷屏了,如果不采取适当的机制来控制就会导致用户体验受到严重的影响
控制广告垃圾贴的策略很多,高级一点的可以通过AI,最简单的方式是通过关键词扫描,还有比较常用的一种方式是频率控制,限制单个用户内容的生产速度,不通等级的用户会有不同的频率控制参数
频率控制就可以使用redis来实现,我们将用户的行为理解为一个时间序列,我们要保证在一定的时间内限制单个用户的时间序列的长度,超过这个长度就禁止用户的行为,它可以是用redis的zset(有序集合,zset详解)来实现

图中绿色的部门就是我们要保留的一个时间段的时间序列信息,灰色的段会被砍掉。统计绿色段中时间序列记录的个数就知道是否超过了频率的阈值。

下面的代码控制用户的ugc行为为每小时最对N次
hist_key:"ugc:${user_id}"
with redis.pipeline() as pipe:
# 记录当前的行为
pipe.zadd(hist_key,ts,uuid)
#保留1小时内的行为序列
pipe.zremrangebyscore(hist_key, 0, now_ts -3600)
# 获取这1小时的行为数量
pipe.zcard(hist_key)
# 设置过期时间,节约内存
pipe.expire(hist_key, 3600)
# 批量执行
_ , _ , count, _ =pipe.exec()
return count > N

五、位图

项目里需要做一个球队成员的签到系统,当用户量比较少的时候,设计上比较简单,就是将用户的签到状态用redis的hash结构来存储,签到一次就再hash结构里记录一条,签到有三种状态:未签到,已签到和部签到,分别是0,1,2三个整数值

hset sign:$(user_id) 2019-08-11 1
hset sign:$(user_id) 2019-08-12 0
hset sign:$(user_id) 2019-08-14 2

这个其实非常浪费用户空间,后来想做全部用户的签到,技术leader指出,这时候的再用hash就有问题了,他讲到当用户过千万的时候,内存可能会飚到 30G+,我们线上实例通常过了 20G 就开始报警,30G 已经属于严重超标了。
这时候我们就开始着手解决这个问题,去优化存储。我们选择使用位图来记录签到信息,一个签到状态需要两个位来记录,一个月的存储空间只需要 8 个字节。这样就可以使用一个很短的字符串来存储用户一个月的签到记录。

打开网易新闻 查看更多图片

但是位图也有一个缺点,它的底层是字符串,字符串是连续存储空间,位图会自动扩展,比如一个很大的位图 8m 个位,只有最后一个位是 1,其它位都是零,这也会占用1m 的存储空间,这样的浪费非常严重。
所以呢就有了咆哮位图这个数据结构,它对大位图进行了分段存储,全位零的段可以不用存。
另外还对每个段设计了稀疏存储结构,如果这个段上置 1 的位不多,可以只存储它们的偏移量整数。这样位图的存储空间就得到了非常显著的压缩。

六、 模糊计数

上面提到的签到系统,如果产品经理需要知道这个签到的日活月活怎么办呢?
通常我们会直接甩锅——请找数据部门。
但是数据部门的数据往往不是很实时,经常前一天的数据需要第二天才能跑出来,离线计算是通常是定时的一天一次。那如何实现一个实时的活跃计数?
最简单的方案就是在 Redis 里面维护一个 set 集合,来一个用户,就 sadd 一下,最终集合的大小就是我们需要的 UV 数字。
但是这个空间浪费严重怎么办?这时候就需要使用redis提供的HyperLogLog模糊计数功能,它是一种概率计数,有一定的误差,大约是0.81%。
但是空间占用很小,其底层是一个位图,它最多只会占用12k的存储空间,而且在计数值比较小的时候,位图使用稀疏存储,空间占用就更小了。

#记录用户
pfadd sign_uv_${day} user_id
#获取记录数量
p count sign_uv_${day}
微信公众号文章的阅读数可以使用它,网页的 UV 统计它都可以完成。但是如果产品经理非常在乎数字的准确性,比如某个统计需求和金钱直接挂钩,那么你可以考虑一下前面提到的咆哮位图。

它使用起来会复杂一些,需要提前将用户 ID 进行整数序列化。Redis 没有原生提供咆哮位图的功能,但是有一个开源的 Redis Module 可以拿来即用。

七、布隆过滤器

如果系统即将会有大量的新用户涌入时,布隆过滤器就会非常有价值,可以显著降低缓存的穿透率,降低数据库的压力
这个新用户的涌入不一定是业务系统的大规模铺开,也可能是因为来自外部的缓存穿透攻击。

def get_user_state(user_id):
state = cache.get(user_id)
if not state:
state = db.get(user_id) or{}
cache.set(user_id,state)
return state
def save_user_state(user_id,state):
cache.set(user_id,state)
db.set_async(user_id,state)
比如就上面这个业务系统的用户状态查询接口代码,现在一个新用户过来,会先去缓存里查询有没有这个用户的状态数据
因为是新用户,所以肯定缓存里没有,然后它就要去数据库里查,结过数据库也没有,如果这样的新用户大批量瞬间涌入,那么可以遇见数据库的压力会比较大,会存在大量的空查询
我们非常希望redis里面有这样一个set,它存放了所有的用户id,这样通过查询这个set集合就知道是不是新用户来了
当用户量非常庞大的时候,维护这样的一个集合需要的存储空间是很大的
这时候就可以使用布隆过滤器,它相当于一个set,但是又不同于set,它需要的存储空间要小的多
比如存储一个用户id需要64个字节,而布隆过滤器存储一个用户ID只需要1个字节多点,其实它存的不是用户id,而是用户id的指纹,所以会存在一定的小概率误判,它是一个具备模糊过滤能力的容器。

当它说用户 id 不在容器中时,那么就肯定不在。当它说用户 id 在容器里时,99% 的概率下它是正确的,还有 1% 的概率它产生了误判。

不过在这个案例中,这个误判并不会产生问题,误判的代价只是缓存穿透而已。
相当于有 1% 的新用户没有得到布隆过滤器的保护直接穿透到数据库查询,而剩下的 99% 的新用户都可以被布隆过滤器有效的挡住,避免了缓存穿透。

布隆过滤器的原理有一个很好的比喻,那就是在冬天一片白雪覆盖的地面上,如果你从上面走过,就会留下你的脚印。如果地面上有你的脚印,那么就可以大概率断定你来过这个地方,但是也不一定,也许别人的鞋正好和你穿的一模一样。可是如果地面上没有你的脚印,那么就可以 100% 断定你没来过这个地方。

八. 排行榜

Redis 基于内存,可以非常快速高效的处理增加和减少的操作,相比于使用 SQL 请求的处理方式,性能的提升是非常巨大的。

Redis 的有序集合可以轻松实现“从一个大型列表中取得排名最高的N个元素”,毫秒级,而且非常简单。

// Add an item to the sorted set
ZADD sortedSet 1 "one"
// Get all items from the sorted set
ZRANGE sortedSet 0 -1
// Get all items from the sorted set with their score
ZRANGE sortedSet 0 -1 WITHSCORES

例子:

id 为6001 的新闻点击数加1:zincrby hotNews:20190926 1 n6001

获取今天点击最多的15条:zrevrange hotNews:20190926 0 15 withscores

打开网易新闻 查看更多图片

九. Session 存储

这可能是应用最广的点了,相比较于类似 memcache 的 session 存储,Redis 具有缓存数据持久化的能力,当缓存因出现问题而重启后,之前的缓存数据还在那儿,这个就比较实用,避免了因为session突然消失带来的用户体验问题。

// Set session that will last 1 minute
SET randomHash "{userId}" EX 60
// Get userId
GET randomHash

十. 发布/订阅

pub/sub 是 Redis 内置的一个非常强大的特性,例如可以创建一个实时的聊天系统、社交网络中的通知触发器等等。

// Add a message to a channel
PUBLISH channel message
// Recieve messages from a channel
SUBSCRIBE channel