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

问个 Redis 的问题

  •  
  •   myd · 54 天前 · 2854 次点击
    这是一个创建于 54 天前的主题,其中的信息可能已经有所发展或是发生改变。

    需求:

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

    • 购买限制:例如每个用户 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   54 天前
    product_{product}_{user}_{Ymd}
    also24
        2
    also24   54 天前
    『 24 小时内』 指的是滚动的 24 小时,还是固定的 24 小时( 1 天)。

    前者:昨天 8 点 1 次,18 点 1 次;今天 0 点不能购买。
    后者:昨天 8 点 1 次,18 点 1 次;今天 0 点可以购买。
    kiracyan
        3
    kiracyan   54 天前
    @also24 应该是 前者 后者直接给 hash 设过期时间就可以了
    myd
        4
    myd   54 天前
    @also24 是的,后者,从购买时间开始计算的 24 小时。


    @junan0708 限制时间不一定是 24 小时。
    myd
        5
    myd   54 天前
    打错,前者。
    beryl
        6
    beryl   54 天前
    如果是固定 24 小时(绝对时间):
    set product_{product}_{user} count expire_time(固定时间)

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


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

    这种压力就分散多了
    also24
        7
    also24   54 天前
    @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   54 天前
    hset product_{product}_{user} count 值:购买次数
    hset product_{product}_{user} expire 值:第一次购买时间 + 86400

    expire < now 可以购买
    expire >= now && count < 2 可以购买
    beryl
        9
    beryl   54 天前
    @also24 #7

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

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

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


    可以在你的这个思路上,第二个 key 拿到第一个的时间戳
    also24
        10
    also24   54 天前
    @beryl #9
    我说的不能购买,就是指第二次购买后覆盖了过期时间。


    我的方法里,key 是包含了 timestamp 的,第二次购买的时候设置的是另一个 key,不存在覆盖问题。
    beryl
        11
    beryl   54 天前
    @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   54 天前
    @also24 可以把设置 key 和设置过期时间分开。如果 key 存在就不设置过期时间。主要是使用字符串,需要遍历用户,变成 O(N)了。
    also24
        13
    also24   54 天前
    @beryl #11
    2021-02-27 18:00-20:00 的时候,第一次购买的 key 已经过期了。

    此时 keys product_{product}_{user}_* 只能查出 1 条购买记录,没啥问题啊。
    thet
        14
    thet   54 天前
    hset product_{product} {user} "qty {qty} expire {expire}"

    值序列化一下
    also24
        15
    also24   54 天前
    这个方法的缺点还是 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   54 天前
    我觉得可以按照 key 值顺序来删除吧 毕竟购买记录是按顺序写进去的(就是个队列),每次只看队列头的时间是否满足了 24 小时就可以的
    beryl
        17
    beryl   54 天前
    @also24 #13

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

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

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


    用户购买时, 读其购买记录, 并过滤时间早于重置时间的数量, 然后计算是否可以购买, 能购买几个
    关于   ·   帮助文档   ·   FAQ   ·   API   ·   我们的愿景   ·   广告投放   ·   感谢   ·   实用小工具   ·   2497 人在线   最高记录 5497   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 17ms · UTC 12:07 · PVG 20:07 · LAX 05:07 · JFK 08:07
    ♥ Do have faith in what you're doing.