Hero Image of content
"先检查再执行"业务在分布式环境下的解决方案

业务开发中,我们会经常遇到先检查再执行的业务场景,比如:

  • 在发送短信验证码业务场景,我们会检查用户是否已经发送过验证码、是否已经过期、是否太频繁之类的,如果检查通过,再发送短信验证码。

这种业务场景存在并发安全问题。一般情况,可能想到的是通过加锁(sync.Lock)或者channel的方式来保证业务的并发安全,但是,如提到的发送短信验证码场景,如果在分布式环境中,我们是不能通过加锁的方式来处理并发安全的,加锁更多是针对单线程环境的。

解决方案

  1. 利用 Redis 是单线程的特性,通过 lua 脚本将我们要检查的业务逻辑封装成一个整体,实现原子性操作。
  2. 使用分布式锁。

还有一些其他方案,比如分布式事务。

选择哪种方法取决于具体的业务场景、及对性能、可靠性和复杂性的要求。

利用 Redis 的单线程特性例子

使用 lua 脚本实现发送短信验证码之前的检查逻辑:

--- set_code.lua
--- 验证码在 Redis 上的 key
--- eg: phone_code:login:15200000000
local key = KEYS[1]
--- 验证次数,一个验证码最多重复使用 3 次,记录了还可以验证几次
--- eg: phone_code:login:15200000000:cnt
local cntKey = key..":cnt"
--- 验证码 123456
local val = ARGV[1]
--- 过期时间
local ttl = tonumber(redis.call("ttl", key))
if ttl == -1 then
    --- key 存在,但没有过期时间
    return -2
elseif ttl == -2 or ttl < 540 then
    --- key 不存在,或者过期时间小于 9 分钟( 540: 600 - 60 )
    redis.call("set", key, val)
    redis.call("expire", key, 600)
    redis.call("set", cntKey, 3)
    redis.call("expire", cntKey, 600)
    return 0
else
    --- 发送太频繁
    return -1
end

在 Go 中使用 Redis 库 Eval 函数执行 lua 脚本:

//go:embed set_code.lua
var luaSetCode string
...
res, err := c.client.Eval(ctx, luaSetCode, []string{c.key(biz, phone)}, code).Int()
...