4.9 KiB
4.9 KiB
name, overview, todos
| name | overview | todos | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 修复 redismodel/redisdao 语义与健壮性问题(P1+P2) | 修复 redismodel/redisdao 内部的日期类型不一致、Quota 缓存语义不清,以及扣减/读取的健壮性边界问题,不修改 middleware。 |
|
产品概述
针对 redismodel 与 redisdao 二次校验后发现的 P1(语义/一致性)与 P2(健壮性边界)问题进行修复,仅限这两层内部改动,不动 middleware。
核心修复内容
- P1.1 日期类型统一:
quota模块每日计数 key 的日期由%s(string)统一为%d(int64 YYYYMMDD),与horoscope及mysqlmodel的StatDate int64对齐。 - P1.2 Quota 缓存语义澄清:将
KeyQuotaTotal/QuotaTotalSet/Get/Decr重命名为KeyQuotaRemain/QuotaRemainSet/Get/Decr,明确 Redis 存的是「剩余可用配额」而非「总配额」,消除与mysqlmodel.Quota(Total + Used)的语义错位。 - P2.1 扣减原子化与防负:
QuotaRemainDecr改用 Lua 脚本原子扣减,key 不存在时不产生负数脏 key,剩余不足时不扣减。 - P2.2 每日计数未命中返回 0:
quotaDailyValue对redis.Nil返回0, nil,与NotificationUnreadGet行为一致。 - P2.3 未读计数扣减防负:
NotificationUnreadDecr改用 Lua 脚本保证结果不低于 0。
技术栈
- 语言:Go
- Redis 客户端:
github.com/redis/go-redis/v9 - 仅修改 3 个文件,沿用现有
redismodel(key 常量)+redisdao(操作函数)分层与redis.NewScriptLua 脚本模式。
实现方案
P1.1 日期类型统一为 int64
internal/redismodel/quota.go 三个每日 key 的 %s 改为 %d,注释标注 date(YYYYMMDD);internal/redisdao/quota.go 中 6 个 QuotaDaily* 函数签名 date string 改为 date int64。fmt.Sprintf 的 %d 直接接收 int64,无其它改动。
P1.2 配额语义重命名为「剩余配额」
redismodel/quota.go:KeyQuotaTotal重命名为KeyQuotaRemain,key 由quota:total:%d:%d改为quota:remain:%d:%d,注释改为「剩余可用配额」。redisdao/quota.go:QuotaTotalSet/Get/Decr重命名为QuotaRemainSet/Get/Decr,QuotaRemainSet形参total改为remain,函数注释同步说明存的是「剩余可用配额」。
P2.1 配额扣减原子化(Lua)
新增 quotaRemainDecrScript,QuotaRemainDecr 通过脚本执行,单次 RTT、Redis 端原子,避免并发超扣与脏 key:
local v = redis.call('GET', KEYS[1])
if not v then return -1 end
v = tonumber(v)
local d = tonumber(ARGV[1])
if v < d then return -2 end
return redis.call('DECRBY', KEYS[1], d)
返回值契约:
-1:配额未初始化(key 不存在)-2:剩余配额不足>= 0:扣减成功后的剩余值
P2.2 每日计数未命中返回 0
quotaDailyValue 改为:
v, err := global.RedisEngine.Get(ctx, key).Int64()
if err == redis.Nil {
return 0, nil
}
return v, err
P2.3 未读数扣减防负(Lua)
NotificationUnreadDecr 改用脚本,保证结果不低于 0:
local v = redis.call('GET', KEYS[1])
if v and tonumber(v) > 0 then return redis.call('DECRBY', KEYS[1], 1) end
return 0
目录结构
internal/
├── redismodel/
│ └── quota.go # [MODIFY] 每日 key %s→%d;KeyQuotaTotal→KeyQuotaRemain(key quota:remain:%d:%d),注释澄清语义
└── redisdao/
├── quota.go # [MODIFY] 6 个 QuotaDaily* date→int64;QuotaTotal*→QuotaRemain* 重命名;QuotaRemainDecr 用 Lua;quotaDailyValue 未命中返回 0
└── notification.go # [MODIFY] NotificationUnreadDecr 用 Lua 防负
实施要点
- 影响范围可控:已确认
QuotaDaily*/QuotaTotal*及KeyQuotaDaily*/KeyQuotaTotal无业务层调用方,重命名与签名变更自包含、无连带改动。 - 兼容性:这些 key 均为新增未上线代码,key 字符串变更(
quota:total→quota:remain、日期%s→%d)不涉及存量数据迁移。 - 性能:Lua 脚本均为单次往返、Redis 端原子执行,与既有
quotaDailyIncrScript/unlockScript风格一致,无额外开销。 - 日志:本次改动不引入日志;错误沿现有
error返回值上抛,由上层统一处理。