【Redis实战】Redis 简单实战

本文介绍了Redis在实战中的多种应用场景,包括导航Session、缓存常用数据、拼团活动管理、限流策略(计数器、列表、滑动窗口、漏斗限流)以及UV统计、实时消息和在线人数统计等。通过具体的例子展示了Redis如何高效地处理高并发和数据统计问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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 在新浪微博中的应用

Redis 在新浪微博中的应用

https://blog.youkuaiyun.com/book_zzmmwu/article/details/102961269

9、Redis 实现实时消息

利用Redis的订阅发布模式。相当于消息队列。

最通俗易懂的Redis发布订阅及代码实战

10、Redis 统计在线人数

https://blog.youkuaiyun.com/zwjyyy1203/article/details/80342874

11、统计用户在线时长

Redis计数器统计小程序用户停留时长

技巧

强一致性:Redis单线程很容易实现分布式锁。、

Redis 的瓶颈一般是在内存的大小,所以提高Redis内存的利用率可以优化Redis的性能。

//TODO 单独写一篇文章,提高Redis内存的利用率。
如何提高Redis 内存的利用率呢?

  • 善用Redis的数据结构,清楚各种数据结构的应用场景
  • 规范化命名,可以提高内存占用率。
  • 对内容进行压缩。
  • 使用SSD代替内存。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值