V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
• 请不要在回答技术问题时复制粘贴 AI 生成的内容
sduoduo233
V2EX  ›  程序员

可暂停加速器的计费逻辑/数据库设计疑问

  •  
  •   sduoduo233 · 2024-02-11 10:51:37 +08:00 via Android · 6168 次点击
    这是一个创建于 368 天前的主题,其中的信息可能已经有所发展或是发生改变。

    正在设计一个类似雷神/Golink 的加速器系统,没想好怎么实现暂停时长

    目前的想法是有一个用户表记录剩余时长和是否暂停:

    id int
    ...
    paused bool
    time_remain int
    

    然后再开一个定时任务每五分钟把没有暂停的用户时间-5

    求教各位老哥有没有更优雅的方法

    27 条回复    2024-02-12 11:21:34 +08:00
    ThirdFlame
        1
    ThirdFlame  
       2024-02-11 10:55:49 +08:00
    更好的做法肯定是,暂停时,将用户状态修改为暂停。同时记录暂停开始的时间。
    在申请恢复时,给用户的有效期+(恢复时间-暂停时间),同时将状态改为正常
    vituralfuture
        2
    vituralfuture  
       2024-02-11 11:21:20 +08:00 via Android
    通用操作系统不保证实时性,例如你要五分钟后执行某个任务,或者 sleep 五分钟,都只是保证间隔时间大于等于五分钟,长期下来误差累积,结果相去甚远
    zhensjoke
        3
    zhensjoke  
       2024-02-11 11:42:40 +08:00
    设计 2 个,一个按使用时间扣除。一个按照暂停时间扣除。
    双重保障。
    ntedshen
        4
    ntedshen  
       2024-02-11 11:51:11 +08:00
    雷神不是即点即结算的么。。。
    就不说误差的问题哪有每五分钟-5 的,二游签个到光速下线的岂不是往死里亏。。。
    vituralfuture
        5
    vituralfuture  
       2024-02-11 11:55:28 +08:00 via Android
    可以动态计算出总使用时间,也可以定期计算,更新数据库。例如记录一个人的年龄,可以只记录出生时间,用当前时间减去出生时间就得到了年龄,也可以只存储今年的年龄,每年定期把所有年龄+1

    在这个场景下,可以拿一张表记录用户使用的情况,包括用户 id ,取消暂停的时间,暂停的时间,需要计算总时长,就根据用户 id 查到所有记录,把每个记录的两个时间相减,再所有记录求和

    计算可能比较花费时间,可以想办法把之前计算的结果记录下来避免重复计算,例如记录一下每个用户前 n 条记录的时间总和,也就是定期更新计算的中间结果
    Kinnice
        6
    Kinnice  
       2024-02-11 12:00:13 +08:00 via Android
    暂停恢复时加个 hook 计算
    IlllIlllIlIIl
        7
    IlllIlllIlIIl  
       2024-02-11 12:01:17 +08:00
    在用户状态为非暂停时,记录用户到期的时间点`time_expire`。
    当用户将状态从非暂停改为暂停时,用到期时间点-现在的时间计算出时长`time_remain`保存。
    当用户从暂停改为非暂停时,用现在的时间+时长算出新的到期时间点保存。
    所以表里加个`time_expire datetime`的数值,用于非暂停状态下的计算.
    GooMS
        8
    GooMS  
       2024-02-11 13:33:55 +08:00 via Android
    暂停是常态,启用是偶尔,所以应该记录使用的时常
    leaflxh
        9
    leaflxh  
       2024-02-11 13:38:58 +08:00
    remain_seconds BIGINT COMMENT '剩余秒数'
    last_start_time TIMESTAMP COMMENT '上次开始加速的时间戳'
    is_accelerating TINYINT COMMENT '是否正在使用加速服务'

    开始加速时,判断当前是否正在加速。

    是,则将当前时间戳 - last_start_time 得到上次使用时长,然后减掉 remain_seconds 。然后如果没欠费,更新 last_start_time 为当前时间,否则停止加速

    否,则更新 last_start_time ,设置 is_accelerating = 1

    ---

    暂停加速时

    将当前时间戳 - last_start_time 得到上次使用时长,然后减掉 remain_seconds ,设置 is_accelerating = 1

    ---

    充值时,直接按照充的时长,转换成秒数加到 remain_seconds
    leaflxh
        10
    leaflxh  
       2024-02-11 13:41:59 +08:00
    需要处理的是用户没暂停

    如果不想挣这个钱,设置个心跳检测客户端是否在运行,如果没响应超过一定阈值,停止加速
    leaflxh
        11
    leaflxh  
       2024-02-11 13:43:26 +08:00
    MySQL TIMESTAMP 类型可能会有 2038 的问题,改成 BIGINT 也行
    leaflxh
        12
    leaflxh  
       2024-02-11 13:44:25 +08:00
    《声明》
    以上仅是技术探讨,他要是搞机场业务,本人并不知情,与本人无关
    ryd994
        13
    ryd994  
       2024-02-11 14:45:54 +08:00 via Android
    表里记录当前 session 开始时间,然后根据余额计算当前 session 最晚结束时间,先不改余额
    然后每五分钟查表,如果有 session 超时就踢下线

    用户暂停/下线时,根据开始时间和当前时间计算消耗量,从余额中减去。并把当前 session 的开始时间和结束时间设为最大值,这样定期扫描就不会扫到

    如果考虑性能的话可以分表,分为活跃 session 表和用户余额表两个表。用户上线时加入活跃表,下线时结算并更新余额表
    DeWjjj
        14
    DeWjjj  
       2024-02-11 16:36:42 +08:00
    我感觉启动时间很明显是要记到 redis 里面的,id+启动时间。
    用户不点结束就一直让他存活下去,他扣的是钱还不能让他在 redis 长活?
    ccde8259
        15
    ccde8259  
       2024-02-11 17:51:51 +08:00
    Database is Log 的思路去做这个事情
    第一张表记录充值的分钟数
    第二张表记录暂停的时间段
    那剩余分钟数=充值分钟数-区间暂停分钟数
    如果你要提升性能就减少回溯长度,定每月/每周活跃用户归档表,三表联查来加快查询性能。
    Session 是否存活判断就分钟级确认一下是否存在生效暂停记录且剩余分钟数>0
    whileFalse
        16
    whileFalse  
       2024-02-11 22:35:54 +08:00 via Android
    没太明白需求,这是点卡吗?
    IlllIlllIlIIl
        17
    IlllIlllIlIIl  
       2024-02-11 22:47:21 +08:00
    @ccde8259 你这样子用户的分钟数只会越来越多。

    我前面提到的方法只适用于 hobby project , 如果要按照这位 v 友的方法来做的话可以直接在现有的充值系统上改。

    创建一张充值记录表,包含充值 ID ,用户 ID ,充值时间,充值时长,和新的到期时间(需要根据上一次的到期时间来计算,如果已经到期了的话就根据现在时间算,如果没有就在之前的到期时间上加)

    再创建一张暂停记录表,记录暂停开始,暂停结束的事件。每次暂停开始前要判断一下用户是否到期,如果到期不能给暂停。暂停结束后把暂停时长按照一次充值事件加入充值记录里,计算出新的到期时间。
    IlllIlllIlIIl
        18
    IlllIlllIlIIl  
       2024-02-11 22:55:01 +08:00
    @IlllIlllIlIIl #17 突然发现 1 楼好像就是这个意思,补充一下暂停事件作为一个特殊的充值事件,不受上一次到期时间的影响,只管我到期时间上加就行了
    IlllIlllIlIIl
        19
    IlllIlllIlIIl  
       2024-02-11 23:08:18 +08:00
    @IlllIlllIlIIl #18 所以前面那张充值记录表应该叫时长变化表,再加一个叫“变化原因” 的值,里面可以写一些像是“用户充值“,”暂停“,”活动赠送“之类的
    IlllIlllIlIIl
        20
    IlllIlllIlIIl  
       2024-02-11 23:15:17 +08:00
    @IlllIlllIlIIl #19 , 考虑到一致性的话,那张暂停表也就不要了, 可以像一楼说的把暂停状态和暂停开始时间记录在用户表下
    IlllIlllIlIIl
        21
    IlllIlllIlIIl  
       2024-02-11 23:19:06 +08:00
    @IlllIlllIlIIl #20 再补充一点,不能允许让用户在暂停状态下充值,
    listen2wind
        22
    listen2wind  
       2024-02-12 00:07:50 +08:00 via Android
    @leaflxh 求告知 2038 是什么问题
    yidinghe
        23
    yidinghe  
       2024-02-12 00:58:25 +08:00
    这个设计的核心要素是用户的每次使用时段,所以必须设计一个使用时段表。使用时段表需要详细记录用户每次的使用时段,当用户有疑问时,你要拿这些记录出来进行核对。

    使用时段表包含下面的主要属性:用户 ID 、开始时间、计费权重(如果有的话)、结束时间(为空表示正在计费中)、时长(结束时间为空时这个字段也为空)

    开始计时:使用时段表新增一条记录,记录用户 ID 和开始时间。

    结束计时:在使用时段表中找到未结束的记录,填入当前时间作为结束时间,以及时长。

    如何实时计算用户的使用时长:选取所有已结束的记录,用 union all 拼接上那条未结束的记录(其时长为当前时间减去开始时间),然后对时长求和即可。

    如果你不想扫描太多记录,那么可以给用户添加冗余字段:每次结束计时后,将所有已结束的记录合计一下,存入用户表。那么实时计算用户的使用时长,就只要从用户表取一条用户记录,再从使用时段表取一条或零条未结束记录即可。
    yidinghe
        24
    yidinghe  
       2024-02-12 01:09:11 +08:00
    @yidinghe 计算用户使用时长可以简化,不用 union all ,下面是一个例子:

    select
    sum(
    case
    when end_time is null then TIMESTAMPDIFF(SECOND, start_time, now())
    else then TIMESTAMPDIFF(SECOND, start_time, end_time)
    end
    ) as duration
    from acc_records
    where user_id=?
    Terminl
        25
    Terminl  
       2024-02-12 01:38:01 +08:00
    我思考了一下,一楼方案可以改一改采纳。在用户点击暂停时,将用户状态修改为暂停,服务端对有效期结算并备份上一次暂停的有效期后下发给客户端暂停时间计数指令。下一次点击恢复的时候读取最新的有效期,如此循环即可。
    Terminl
        26
    Terminl  
       2024-02-12 01:50:14 +08:00
    还有一个是服务端过滤低于多少秒的账户进入心跳检测,账户时间为 0 时强行断开连接。
    sduoduo233
        27
    sduoduo233  
    OP
       2024-02-12 11:21:34 +08:00
    感谢各位老哥的回复

    看了一下感觉 1 楼的方法是最简单的,总结一下就是这几个 SQL

    CREATE TABLE users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    time_left INTEGER DEFAULT(0), 剩余时长 单位是秒
    paused INTEGER,
    resume_time INTEGER 上一次恢复计时的时间戳
    )

    剩余 60 秒的时长
    INSERT INTO users (time_left, paused, resume_time) VALUES (60, TRUE, 0)

    恢复
    UPDATE users SET paused = FALSE, resume_time = unixepoch() WHERE id = 1

    暂停
    UPDATE users SET paused = TRUE, time_left = time_left - (unixepoch() - resume_time) WHERE id = 1

    定时任务 更新未暂停用户的剩余时间
    UPDATE users SET time_left = time_left - (unixepoch() - resume_time), resume_time = unixepoch() WHERE paused = FALSE

    9 楼的应该也是差不多的思路
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5364 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 47ms · UTC 01:21 · PVG 09:21 · LAX 17:21 · JFK 20:21
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.