直接用 time() 加数据库字段做60秒限制会因并发请求导致重复发送,redis原子操作更可靠;须用lua脚本封装ttl获取与键设置逻辑以保证原子性,避免竞态。

为什么直接用 time() 加数据库字段做60秒限制会出问题
因为并发请求下,两个几乎同时到达的请求都读到“上一次发送时间是 10 秒前”,都会判定为可发,结果验证码发了两次。数据库行锁或乐观锁能缓解但不彻底,且增加 DB 压力。
Redis 的原子性操作天然适合这类短时频控场景,关键是:不能只存时间戳,得把“是否已发”和“剩余等待秒数”状态一并管理。
-
SET key value EX 60 NX是最简方案,但无法返回剩余时间,前端无法提示“还剩 X 秒” - 必须用
GET+SET组合?不行——中间有竞态窗口 - 正确做法是用 Lua 脚本封装判断+写入逻辑,保证原子性
用 Lua 脚本实现“取剩余时间 or 设置新有效期”
核心逻辑:尝试获取当前 key 的 TTL,若存在则返回剩余秒数;若不存在或已过期,则设置新值并返回 0(表示可发)。
PHP 中调用示例:
$script = 0 then
return ttl
else
redis.call("SET", KEYS[1], "1", "EX", 60)
return 0
end
LUA;
$remaining = $redis->eval($script, [$phone]);
if ($remaining === 0) {
// 可发送验证码
} else {
// 提示“请 {$remaining} 秒后重试”
}
- 脚本里用
TTL而不是GET+ 计算差值,避免时间戳精度与服务器时钟偏差问题 -
SET ... EX 60确保键最多存活 60 秒,不依赖客户端传入时间戳 - 不要用
INCR+ 过期时间组合——INCR不带过期,需额外EXPIRE,非原子
手机号作为 Redis key 时的编码注意事项
直接用 $phone = "13812345678" 当 key 看似简单,但容易踩坑:
- 国际号码格式不统一(+86、86、无前缀),导致同一号码多个 key
- Redis key 中含
+或空格可能被某些客户端误处理 - 建议标准化:统一去 +、空格、横线,再加业务前缀,如
"sms:verify:8613812345678" - 避免用用户输入原始值拼接 key,防止注入式 key 泛滥(比如手机号里塞控制字符)
测试时发现“明明没发过,却提示 59 秒后重试”怎么办
大概率是本地开发环境或测试脚本里,多次执行时复用了同一个 Redis 连接,而连接中残留了前一次的 key(比如没清库,或 key 过期时间被意外延长)。
- 先手动执行
redis-cli --raw keys "sms:verify:*"确认 key 是否真实存在 - 用
TTL查具体剩余时间:redis-cli TTL "sms:verify:8613812345678" - 注意 PHP Redis 扩展默认使用连接池或长连接,
flushDB()不一定生效,建议用SELECT切库或改用临时 db(如select(15))做隔离测试 - 线上环境别依赖
FLUSHDB清理,应靠 key 自带过期自动回收
return 或错用 GET,就可能让限制形同虚设。php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











