Discord.py 2.0:并行使用同一命令时交互处理失败求助
Discord机器人多用户同时调用游戏命令时交互失败问题排查与修复
问题描述
我正在开发一款搭载多款小游戏的Discord机器人(一个命令对应一款游戏),游戏采用交互按钮操作。当前遇到异步处理问题:当多个用户同时使用同一命令(比如两人同时玩带方向按钮的迷宫游戏),会出现交互失败,进而影响每个命令实例的正常运行。
迷宫命令的代码结构如下:
@bot.command(brief="...",help="...") @commands.cooldown(1, 60*60, commands.BucketType.member) async def maze(ctx): #PostreSQL check cur = conn.cursor() req = "..." cur.execute(req) rows = cur.fetchall() if len(rows)>0 : m = Maze(13,13) #Generate a 13x13 maze board brd = m.saveEmojiBoard(True) #str representation of the board (unicode emojis) class myButton(ui.Button): def __init__(self,label,style): super().__init__(label=label,style=style) async def callback(self,interaction): nonlocal rows if interaction.user.id==ctx.author.id : moved = False if str(self.label)=="\U000025C0": moved = m.left() elif str(self.label)=="\U000025B2": moved = m.up() elif str(self.label)=="\U000025BC": moved = m.down() elif str(self.label)=="\U000025B6": moved = m.right() if moved: brd = m.saveEmojiBoard(True) #Update board if position changed await self.view.msg.edit(content=brd) await interaction.response.defer() if m.goalreached() : #Game ends when exit found, then update database req = "..." cur.execute(req) conn.commit() self.view.stop() await ctx.send("...") else : await interaction.response.defer() else : await interaction.response.defer() class myView(ui.View): def __init__(self): super().__init__(timeout=120) self.add_item(myButton("\U000025C0",style=discord.ButtonStyle.primary)) self.add_item(myButton("\U000025B2",style=discord.ButtonStyle.primary)) self.add_item(myButton("\U000025BC",style=discord.ButtonStyle.primary)) self.add_item(myButton("\U000025B6",style=discord.ButtonStyle.primary)) async def on_timeout(self): await ctx.send("...") async def on_error(self,error, item, interaction) : print("...") traceback.print_exc() v = myView() await ctx.send(view=v) else : await ctx.send("...")

问题根源
- 嵌套类的上下文污染:在
maze命令函数内部定义myButton和myView类,每次调用命令都会重新定义类,但Python闭包特性会导致不同实例意外共享外部作用域变量(比如m、ctx),多用户操作时上下文直接混乱。 - 全局数据库游标共享:用全局的
conn.cursor(),多用户并发操作时游标冲突,数据库操作直接异常。 - View未绑定消息实例:代码里
self.view.msg.edit但msg属性没在myView中初始化,会触发属性错误,直接导致交互失败。 - 非线程安全的游戏实例:
Maze实例在命令函数内创建,但按钮回调直接操作这个实例,多实例共享上下文时状态会乱。
修复方案
1. 把View和Button类移到命令函数外,通过初始化传递上下文
抽离嵌套类,用构造函数传入游戏实例、用户ID、消息对象等,彻底避免闭包共享问题:
class MazeButton(ui.Button): def __init__(self, label, style, maze_instance, author_id, view): super().__init__(label=label, style=style) self.maze = maze_instance self.author_id = author_id self.parent_view = view async def callback(self, interaction): if interaction.user.id != self.author_id: await interaction.response.defer() return moved = False if self.label == "\U000025C0": moved = self.maze.left() elif self.label == "\U000025B2": moved = self.maze.up() elif self.label == "\U000025BC": moved = self.maze.down() elif self.label == "\U000025B6": moved = self.maze.right() if moved: new_board = self.maze.saveEmojiBoard(True) await self.parent_view.msg.edit(content=new_board) await interaction.response.defer() if self.maze.goalreached(): # 数据库操作改用局部游标 async with conn.cursor() as cur: req = "..." await cur.execute(req) await conn.commit() self.parent_view.stop() await interaction.channel.send("恭喜你走出迷宫!") else: await interaction.response.defer() class MazeView(ui.View): def __init__(self, maze_instance, author_id, ctx): super().__init__(timeout=120) self.maze = maze_instance self.author_id = author_id self.ctx = ctx self.msg = None # 后续绑定发送的消息 self.add_item(MazeButton("\U000025C0", discord.ButtonStyle.primary, maze_instance, author_id, self)) self.add_item(MazeButton("\U000025B2", discord.ButtonStyle.primary, maze_instance, author_id, self)) self.add_item(MazeButton("\U000025BC", discord.ButtonStyle.primary, maze_instance, author_id, self)) self.add_item(MazeButton("\U000025B6", discord.ButtonStyle.primary, maze_instance, author_id, self)) async def on_timeout(self): await self.ctx.send("游戏超时结束!") self.stop() async def on_error(self, error, item, interaction): print(f"交互出错: {error}") traceback.print_exc() await interaction.response.defer()
2. 修改命令函数,用局部数据库游标并绑定View的消息实例
@bot.command(brief="迷宫游戏", help="游玩13x13的迷宫,用方向按钮移动") @commands.cooldown(1, 3600, commands.BucketType.member) async def maze(ctx): # 用局部游标,避免全局共享冲突 async with conn.cursor() as cur: req = "..." await cur.execute(req) rows = await cur.fetchall() if len(rows) > 0: m = Maze(13, 13) brd = m.saveEmojiBoard(True) v = MazeView(m, ctx.author.id, ctx) # 发送消息并绑定到View的msg属性 v.msg = await ctx.send(content=brd, view=v) else: await ctx.send("你暂时无法游玩这个游戏!")
3. 确保数据库连接异步安全
如果用asyncpg这类异步PostgreSQL库,确保每个操作使用独立游标或连接;如果是同步库,要用线程池包装,避免阻塞事件循环。
4. 严谨验证用户交互权限
在按钮回调里,除了检查用户ID,还可以检查交互消息是否属于当前View实例,进一步避免跨实例干扰。
内容的提问来源于stack exchange,提问作者S3NI
相关产品推荐
相关产品推荐

