discord.py并发指令场景下如何实现指定异步函数串行执行及解决数据库竞态
Discord机器人注册指令的并发处理问题
功能需求
我开发了Discord机器人,设置了apply指令,用户输入该指令时就会触发apply函数运行。其中apply函数的大部分代码需要支持多用户同时触发时的并发执行,但函数内调用的register异步函数需要保证同一时间仅运行一个实例。
对应的示例代码如下:
@client.command() async def apply(ctx): # 需要支持并发执行的代码段 await register(ctx.author) async def register(user): # 数据库访问逻辑全部封装在独立的db.py文件中 data = await db.execute_query('SELECT Total_Reg from Data WHERE rowid = 1') total = data[0][0] # 取出查询结果中的当前总注册数,存入total变量 if total >= max_reg: # 注册数达到上限时直接关闭注册通道 print("Registrations are closed!") return # 注册未达上限时,总注册数自增1,为用户分配唯一的注册号 await db.execute_query('UPDATE Data SET Total_Reg = Total_Reg + 1 WHERE rowid = 1') reg_no = total + 1 registrations[reg_no] = user await close_registration() """额外要求:即使对register函数做了队列或单实例执行限制,也不希望这行代码阻塞当前流程,要让close_registration()在后台运行,不卡住当前register的代码执行。这种情况是不是应该用asyncio.ensure_future(close_registration())替代原来的await调用?"""
现存的问题
apply指令是由Discord客户端在用户输入时触发的,很容易出现多用户同时执行指令的并发场景:多个register协程会同时执行查询语句,所有协程拿到完全相同的总注册数值,最终分配的注册号会出现重复。
我知道SQLite在有连接执行数据库更新或查询时会对数据库加锁,但这种锁的作用范围仅覆盖单条语句的执行过程:第一条SELECT查询执行完释放锁后,第二条查询会立刻启动,这个时机完全发生在UPDATE自增语句执行之前,根本没办法避免读到旧的注册总数。
预期的执行逻辑
我要求注册流程严格按顺序串行执行:
- 先通过
data = await db.execute_query('SELECT Total_Reg from Data WHERE rowid = 1')获取当前的总注册数 - 检查注册数是否达到上限
- 如果未达上限,完成当前用户的注册,再通过
await db.execute_query('UPDATE Data SET Total_Reg = Total_Reg + 1 WHERE rowid = 1')将数据库内的总注册数加1
举个实际的并发场景:两个用户同时触发注册请求,程序必须先走完第一个用户的全注册流程,再执行第二个用户的注册逻辑,两人拿到的注册号必须唯一。
待解决的疑问
- 这个功能应该怎么实现?是否需要使用专门的队列?
- SQLite有没有内置的原子操作特性,可以把「判断当前注册数是否小于
max_reg、满足条件就将注册数加1」的逻辑整合成单条查询完成? - 如果使用队列或者其他方式实现
register函数的单实例运行,要让await close_registration()不阻塞当前流程、在后台运行,是不是使用asyncio.ensure_future(close_registration())替代await调用就可以?
内容的提问来源于stack exchange,提问作者Shubham
相关产品推荐
相关产品推荐

