Redis 实战
1、Navigation Session
需求:用户60秒内访问的N个网站页面,或许包含当前 TA 正在看或者感兴趣的东西。由此可以推荐对应的广告,使得用户更容易对投放的广告感兴趣。
利用Redis可以简单实现:
MULTI
RPUSH pagewviews.user:<userid> http://.....
EXPIRE pagewviews.user:<userid> 60
EXEC
2、Redis 缓存常用数据
图片:String,hash 数据类型
3、拼团活动缓存
需求:热门商品会有拼团,进行促销或者引流。特点是:推广阶段会有大量的人去创建拼团,等团的人数满了,才算商品购买成功,这一期间会频繁查询数据库,看团是否满。
为了减轻数据库的压力,可以将拼团信息存放在Redis 缓存中。
- activityId 拼团活动id
- activityCount 设置的成团人数
- memberId 用户id
开团会创建一个拼团活动,这时会去数据库把拼团商品的成团人数activityCount设定查出来,同时生成一个activityId,使用哈希表的数据结构将拼团活动id和人数存放在groupPurchasing哈希表中。
如何确定某个用户是否参加了某个拼团活动呢?以 activityId 作为Set集合的key,用户的memberId 作为value。当用户创建了一个拼团的活动时,利用集合的数据结构:以活动id为key,memberId为value,利用集合的特性防止用户重复参团。这样有新的用户参团的时候,检查拼团活动的状态就直接去缓存中查找就行。
- groupPurchasing 哈希表:
hset groupPurchasing activityId activityCount
- Set 集合:
SADD activityId memberId
判断该用户是否重复参团:
SISMEMBER activityId memberId
成功参团后,将剩余所需参团人数减1:
HINCBY groupPurchasing activityId -1
当团满人,所有人都成功付款的时候,将拼团信息从groupPurchasing哈希表中删除
HDEL groupPurchasing activityId
上述只是讲了核心实现方法,还有其他的细枝末节没有讲到。
//TODO 拼团的核心逻辑
4、次数限制器 Rate Limiter
需求:限制单个IP地址一秒内访问服务器API资源的次数为10次
应用场景:当系统的能力有限,无法对外界提供更多服务的时候,限流就是一个很好的解决方法。
比如活动抢票的时候,有时候返回的界面是:“活动太火爆,请稍后再试”,这种可以通过在前端简单设置随机算法就可以实现。
在一些UCG社区,一些风控策略,比如用户单位时间的点赞次数,转发次数等会受到限制。
在一些裂变活动中,为了防止羊毛党,也会设置参与活动次数相关的限制等。
总结就是:控制用户行为。
4.1、实现方法一:利用计数器,
利用计数器对每个IP在访问的那一秒内的访问次数进行计数。
使用MULTI 和EXEC 组合(原子操作),保证在每一次API访问的时候设置自增和过期时间。
缺点:依赖Redis实例的时间戳。
/**
* @方法: LIMIT_API_CALL
* @param ip 访问者的ip地址
*/
FUNCTION LIMIT_API_CALL(ip)
// 当前时间
ts = CURRENT_UNIX_TIME()
// 生成 key
keyname = ip+":"+ts
current = GET(keyname)
//判断当前ip在ts时的访问次数
IF current != NULL AND current > 10 THEN
ERROR "too many requests per second"
ELSE
MULTI
// 访问次数自增
INCR(keyname,1)
// 设置过期时间 10s
EXPIRE(keyname,10)
EXEC
PERFORM_API_CALL()
END
4.2、实现方法二: 利用计数器
缺陷:如果因为并发的原因,导致 value == 1 后面的设置过期时间没有执行到,那么该ip就成功逃过了检查。
FUNCTION LIMIT_API_CALL(ip):
current = GET(ip)
IF current != NULL AND current > 10 THEN
ERROR "too many requests per second"
ELSE
value = INCR(ip)
IF value == 1 THEN
EXPIRE(ip,1)
END
PERFORM_API_CALL()
END
改进:将自增和设置过期时间写成一个 lua 脚本,使之成为一个原子操作,lua脚本如下:执行lua脚本,传入参数即可。
local current
current = redis.call("incr",KEYS[1])
if tonumber(current) == 1 then
redis.call("expire",KEYS[1],1)
end
4.3、实现方法三:使用 列表list,
设置过期时间为1秒,列表长度就是该ip在1秒内的访问次数。
注意:RPUSHX 只有 key 存在的时候,才会生效。RPUSH 在 key 为空的时候会创建一个空的 list,在进行操作。
FUNCTION LIMIT_API_CALL(ip)
current = LLEN(ip)
IF current > 10 THEN
ERROR "too many requests per second"
ELSE
IF EXISTS(ip) == FALSE
MULTI
RPUSH(ip,ip)
EXPIRE(ip,1)
EXEC
ELSE
RPUSHX(ip,ip)
END
# API调用
PERFORM_API_CALL()
END
注意:EXISTS(ip) 可能在返回 FALSE 的同时,被另外的 Redis 客户端在 MULTI-EXEC 中创建了 key,这会导致统计少了一次API 调用,但是不影响主要的功能。
4.4、zset 滑动窗口限流
利用 zset 数据结构中的 score 在时间维度上建立一个滑动窗口,key 设置为表示某一个用户的某项行为,需要保证 zset value 的唯一性,这里就简单使用时间戳作为其 value。
每次目标行为出现,就维护这个时间窗口,去除时间窗口之外的记录。
如果时间窗口内的记录超过了最大记录限制,就是非法行为
# 定义时间间隔:period ,最大访问次数:maxCount,即在时间间隔period内,最大访问次数为maxCount;
# 获取当前微秒数 now_timestamp
EVAL "local a=redis.call('TIME') ;return {a[1]*1000000+a[2], a[1],a[2]}" 0
# 以用户id与事件的组合为 key,当前时间戳为 value 和 score
ZADD "userId:actionKey" now_timestamp now_timestamp
# (1)去除时间窗口之外的记录[0, now_timestamp - period * 1000000],然后统计其中的访问次数
ZREMRANGEBYSCORE "userId:actionKey" 0 (now_timestamp - period * 1000000)
ZCARD "userId:actionKey"
# (2)直接统计[now_timestamp - period * 1000000,now_timestamp]之间的访问次数
ZCOUNT "userId:actionKey" -inf +inf
4.5、漏斗限流
需要安装 Redis-cell 模块。如何安装和使用请参见:https://github.com/brandur/redis-cell
性能:Redis-cell 的执行命令的速度与执行两天 Redis基本命令 SET 的速度差不多快。(0.1ms)
使用非常简单,只涉及到一条命令:
CL.THROTTLE <key> <max_burst> <count per period> <period> [<quantity>]
# 限制用户 user123 的访问速率为 30/60 次/秒,最大访问量为0
CL.THROTTLE user123 15 30 60 1
▲ ▲ ▲ ▲ ▲
| | | | └───── 访问 1 次 (参数为空则默认1)
| | └──┴─────── 访问速率 30 次 / 60 秒
| └───────────── 最大访问量 15
└─────────────────── key "user123"
# 返回值
127.0.0.1:6379> CL.THROTTLE user123 15 30 60
1) (integer) 0 # 0:行为被允许 1:禁止
2) (integer) 16 # 最大访问量限制 + 1
3) (integer) 15 # 剩余可以访问的量 15(本次访问完之后剩余)
4) (integer) -1 # -1:允许本次行为,整数N:N秒之后尝试
5) (integer) 2 # 剩余N秒之后重置访问次数
5、Redis 统计UV
需求:以一天为单位,统计 index.html 页面打开的UV。需根据 UserId 对访问的用户去重。(假设所有用户已经登录)
UV 和 PV 在本需求中的定义:
- UV 的定义:以UserId 为唯一标识,统计当天用户唯一访问量。比如:你在今天一共访问 index.html 10次,但是统计 UV 的时候,只算作 UV + 1
- PV定义:登录用户访问该页面的次数。
代码如下:
这里使用了Redis 中的哈希表hash数据结构,创建一个哈希表,以URL+日期(年-月-日)
作为 key,以 userId 作为 field,PV 次数作为 value。
除重逻辑:
1、用户访问页面,产生页面PV
2、去缓存中查询是否有该用户的访问记录:hget URL+今天日期 userId
有:此人PV次数加一
没有:UV加一,添加该用户的访问记录 hset URL+今天日期 userId 1
注意:
- 需要设置过期时间,过期时间必须大于1天。
- 日期的获取需要注意时区。如果是System.milliSeconds获取时间戳,需要转换为不带有时区信息的日期,否则当使用EXPIREAT 或者 PEXPIREAT 会对过期时间产生影响,如果使用EXPIRE 或者 PEXIPRE命令设置过期秒数 24 * 60 * 60,就不需要关注这一点。
public static final String INIT_COUNT = "1";
public static final int TwoDaySeconds = 2 * 24 * 60 * 60;
/**
* 上报页面UV
*/
@Override
public void reportOpenPage(String userId) {
String date = new SimpleDateFormat("yyyy-MM-dd").format(new Date()).toString();
// 统计 www.baidu.com/index.html 的页面访问UV
String key = String.format("%s:%s","wwww.baidu.com/index.html", date);
//打开页面次数 (PV)
String count = redisClient.hget(key, userId);
//为空:没有该用户的访问记录
if (StringUtils.isEmpty(count)) {
// redis 增加用户访问记录
redisClient.hset(key, userId, INIT_COUNT);
redisClient.expire(key, TwoDaySeconds);
log.info("打开页面用户访问uv增加,userId:{}",userId);
// 上报用户PV
report(xxxxxx);
} else {
//不为空:该用户今天已经访问过该页面:访问次数+1
log.info("打开页面用户访问uv不变,userId:{}",userId);
redisClient.hincrBy(key, userId, 1L);
}
}
6、得到最近1小时广告点击量实时统计并写入到redis
https://blog.youkuaiyun.com/qq_16146103/article/details/108051997
7、火影忍者活动服务使用Redis实现
和拼团的案例差不多。比如有个活动,需要5个人参加。发起人创建活动,加入活动的时候需要上锁,上锁成功的玩家才能成功加入活动,
队伍存在后,队伍成员使用set结构
https://mp.weixin.qq.com/s/yzpUPQ5HGc7e41YnuxVehQ
8、Redis 在新浪微博中的应用
https://blog.youkuaiyun.com/book_zzmmwu/article/details/102961269
9、Redis 实现实时消息
利用Redis的订阅发布模式。相当于消息队列。
10、Redis 统计在线人数
https://blog.youkuaiyun.com/zwjyyy1203/article/details/80342874
11、统计用户在线时长
技巧
强一致性:Redis单线程很容易实现分布式锁。、
Redis 的瓶颈一般是在内存的大小,所以提高Redis内存的利用率可以优化Redis的性能。
//TODO 单独写一篇文章,提高Redis内存的利用率。
如何提高Redis 内存的利用率呢?
- 善用Redis的数据结构,清楚各种数据结构的应用场景
- 规范化命名,可以提高内存占用率。
- 对内容进行压缩。
- 使用SSD代替内存。