问个 Redis 的问题 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
myd
V2EX    Redis

问个 Redis 的问题

  •   myd 2021-02-26 15:51:51 +08:00 4673 次点击
    这是一个创建于 1750 天前的主题,其中的信息可能已经有所发展或是发生改变。

    需求:

    记录商品的购买用户和数量,实现:

    • 购买限制:例如每个用户 24 小时内只能购买 2 次
    • 清空并重新计算指定产品的购买限制:删除指定产品的全部用户的购买记录。

    方案一

    使用哈希:

    hset product_{product} {user} {qty} 

    优点:清空限制很方便,del product_{product}
    缺点:hash 貌似不能针对指定 field(用户)设置缓存过期时间, 相关讨论: https://github.com/redis/redis/issues/1042 。只能在 qty 加一个过期时间 /购买,维护起来麻烦,也不太优雅。

    方案二

    使用字符串:

    set product_{product}_{user} {qty} 

    优点:可以针对产品 /用户设置过期时间,实现购买限制简单。
    缺点:清空指定产品的购买限制复杂度是 O(N),类似于 keys product_{product}_* |xargs redis-cli DEL

    有没有比较优雅的实现方式?

    31 条回复    2021-02-26 22:42:24 +08:00
    junan0708
        1
    junan0708  
       2021-02-26 15:55:55 +08:00
    product_{product}_{user}_{Ymd}
    also24
        2
    also24  
       2021-02-26 15:58:10 +08:00
    『 24 小时内』 指的是滚动的 24 小时,还是固定的 24 小时( 1 天)。

    前者:昨天 8 点 1 次,18 点 1 次;今天 0 点不能购买。
    后者:昨天 8 点 1 次,18 点 1 次;今天 0 点可以购买。
    kiracyan
        3
    kiracyan  
       2021-02-26 16:02:04 +08:00
    @also24 应该是 前者 后者直接给 hash 设过期时间就可以了
    myd
        4
    myd  
    OP
       2021-02-26 16:10:09 +08:00
    @also24 是的,后者,从购买时间开始计算的 24 小时。


    @junan0708 限制时间不一定是 24 小时。
    myd
        5
    myd  
    OP
       2021-02-26 16:10:22 +08:00
    打错,前者。
    beryl
        6
    beryl  
       2021-02-26 16:11:01 +08:00
    如果是固定 24 小时(绝对时间):
    set product_{product}_{user} count expire_time(固定时间)

    这个有个问题是,会在固定时间清除大量的 key, redis 会有压力。


    如果是滚动 24 小时,从第一次购买算为原点:
    set product_{product}_{user} count expire_time(当前时间+24 小时)

    这种压力就分散多了
    also24
        7
    also24  
       2021-02-26 16:18:06 +08:00
    @beryl #6
    如果按照:
    set product_{product}_{user} count expire_time(当前时间+24 小时)

    那么距离第一次购买 24 小时后,仍然无法购买了。


    可以改成

    set product_{product}_{user}_{timestamp} timestamp expire_time(当前时间+24 小时)

    取数量的时候 keys product_{product}_{user}_* 就好
    junan0708
        8
    junan0708  
       2021-02-26 16:33:55 +08:00
    hset product_{product}_{user} count 值:购买次数
    hset product_{product}_{user} expire 值:第一次购买时间 + 86400

    expire < now 可以购买
    expire >= now && count < 2 可以购买
    beryl
        9
    beryl  
       2021-02-26 16:43:21 +08:00
    @also24 #7

    如果只是第一次购买,24 小时后,被自动清掉了,key 不存在可以认为 0 次,可以购买的。

    但确实有问题,因为『第二次购买后,24 小时时间被重置了』(-

    set product_{product}_{user}_{timestamp} timestamp expire_time(当前时间+24 小时)
    这个同样会有『第二次购买后的过期时间是根据第二次的当前时间+24 小时』这样第一次购买 24 小时后,购买次数并没有被重置


    可以在你的这个思路上,第二个 key 拿到第一个的时间戳
    also24
        10
    also24  
       2021-02-26 16:46:16 +08:00
    @beryl #9
    我说的不能购买,就是指第二次购买后覆盖了过期时间。


    我的方法里,key 是包含了 timestamp 的,第二次购买的时候设置的是另一个 key,不存在覆盖问题。
    beryl
        11
    beryl  
       2021-02-26 16:50:14 +08:00
    @also24
    嗯,那我的方案的问题理解一致

    『 key 是包含了 timestamp 的,第二次购买的时候设置的是另一个 key,不存在覆盖问题』
    如果第一次购买的时候是:2021-02-26 18:00,过期时间 2021-02-27 18:00
    第二次购买是:2021-02-26 20:00 , 过期时间 2021-02-27 20:00

    如果在 2021-02-27 18:00-20:00 理论上可以购买一次,但是其实只有一次机会了
    myd
        12
    myd  
    OP
       2021-02-26 16:53:22 +08:00
    @also24 可以把设置 key 和设置过期时间分开。如果 key 存在就不设置过期时间。主要是使用字符串,需要遍历用户,变成 O(N)了。
    also24
        13
    also24  
       2021-02-26 16:53:33 +08:00
    @beryl #11
    2021-02-27 18:00-20:00 的时候,第一次购买的 key 已经过期了。

    此时 keys product_{product}_{user}_* 只能查出 1 条购买记录,没啥问题啊。
    thet
        14
    thet  
       2021-02-26 16:56:07 +08:00
    hset product_{product} {user} "qty {qty} expire {expire}"

    值序列化一下
    also24
        15
    also24  
       2021-02-26 16:59:21 +08:00
    这个方法的缺点还是 keys product_{product}_{user}_* 的效率比较低,性能差。

    有一个优化方案是做一次剪枝,大部分用户不存在 24 小时内购买过商品,那么我们设置一个 『用户是否在 24 小时内购买过商品』的 key 就好了。

    也就是每次用户产生购买行为,都 set product_{product}_{user} timestamp expire_time (不论原 key 是否存在,都续期 24 小时)

    这样对于大部分用户,只需要查询 product_{product}_{user} 不存在,就可以认为不存在限制了。

    而对于 product_{product}_{user} 存在的用户,再进行 keys product_{product}_{user}_* 查询,确认具体是否超过了限制。
    dreamstart
        16
    dreamstart  
       2021-02-26 17:06:47 +08:00
    我觉得可以按照 key 值顺序来删除吧 毕竟购买记录是按顺序写进去的(就是个队列),每次只看队列头的时间是否满足了 24 小时就可以的
    beryl
        17
    beryl  
       2021-02-26 17:11:07 +08:00
    @also24 #13

    2021-02-27 18:00-20:00 我理解需求这个时候理论应该可以买两次,但是只可以买一次。
    beryl
        18
    beryl  
       2021-02-26 17:11:28 +08:00
    题外话 qty 是什么的缩写
    thet
        19
    thet  
       2021-02-26 17:13:24 +08:00
    @beryl quantity,数量把
    k9982874
        20
    k9982874  
       2021-02-26 17:18:13 +08:00
    我在项目里使用的方案二,起初也是使用方案一,后来发现很难维护内容时效性就切到了方案二。
    方案一的问题在于,如果 qty 增加时间戳,在删除时必须把 qty 的内容一起取回来,当 qty 本身是个很大的对象时,成本就很高。
    如果像楼上说的增加一个 expired set,在查找 qty 时,还需要再请求一次 redis 取出来 expire 值,然后校验数据有效性,一次请求硬变成两次。
    hw93
        21
    hw93  
       2021-02-26 17:27:40 +08:00   1
    方案二再维护一个 `set product_{product} [user1, user2]`方便去`删除指定产品的全部用户的购买记录`
    also24
        22
    also24  
       2021-02-26 17:33:24 +08:00
    @beryl #17
    当你再下第三个订单的时候,keys product_{product}_{user}_* 就变成 2 了,
    此时你再想下第四个订单,就需要等第二个订单超过 24 小时。
    adamwong
        23
    adamwong  
       2021-02-26 17:47:48 +08:00
    每个用户存两个时间戳不就完了?代码里做判断
    参考 golang 的令牌桶算法 time/rate.Limiter
    yuankui
        24
    yuankui  
       2021-02-26 17:58:32 +08:00
    删除的动作不频繁就 2 方案了
    PiersSoCool
        25
    PiersSoCool  
       2021-02-26 18:03:12 +08:00
    这就是个限流器的问题 可参考开源方案
    whileFalse
        26
    whileFalse  
       2021-02-26 18:29:39 +08:00 via iPhone   1
    为产品指定版本号,product-{product}.{product.v}-{user}
    清空产品时不做清空操作,而是产品版本号+1
    palmers
        27
    palmers  
       2021-02-26 20:08:37 +08:00
    第一次插入的时候设置 key 的过期时间 第二使用 incr 不会清理过期时间, 通过 key 获取值如果没有了一定是可以购买的 否则累加一
    palmers
        28
    palmers  
       2021-02-26 20:09:59 +08:00
    哦 可以购买的时候 还有判断 2
    night98
        29
    night98  
       2021-02-26 20:57:09 +08:00
    redisson,有个分布式对象你可以参考下
    rocky114
        30
    rocky114  
       2021-02-26 21:31:45 +08:00   1
    存储俩份 hset product_{product} {user} {qty},set product_{product} expire_time
    lldld
        31
    lldld  
       2021-02-26 22:42:24 +08:00
    如果“清空并重新计算指定产品的购买限制” 是实际需求, 而"删除指定产品的全部用户的购买记录"只是你想的实现方法的话, 我觉得没必要删除购买记录 key.

    新增一个 key "product_reset_<product_id>", value 存重置的时间. service 这边可以定时更新(比如每 5 分钟)这个数据存在内存里面, 因为这个行为应该是运营有计划的, 所以可以要求(或者告之)他们至少要提前 10 分钟在系统设置重置.

    用户购买记录, key "product_buy_<product_id>_<user_id>", value 存 购买时间,数量, ... , 购买时间,数量, 每次购买刷新过期时间为 24 小时


    用户购买时, 读其购买记录, 并过滤时间早于重置时间的数量, 然后计算是否可以购买, 能购买几个
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2730 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 38ms UTC 02:48 PVG 10:48 LAX 18:48 JFK 21:48
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86