discord.py中tasks.loop与sqlite3配合使用出现数据库锁问题如何解决
问题根因分析
- 你使用的SQLite是文件级数据库,同一时间仅支持一个写入操作,你当前每秒执行的定时任务中,每次单条数据更新就调用一次
connection.commit(),高频的提交操作会大幅提升锁冲突概率 - 同步数据库操作和异步Discord框架混用:全局共享的
cursor、connection对象没有并发保护,Discord事件循环中的其他协程(比如用户指令触发的数据库写操作)和定时任务同时操作数据库时,会直接触发锁竞争 - 遍历逻辑缺陷:遍历
playersTime列表的同时执行删除操作,会导致索引偏移、部分数据漏处理,异常被捕获后可能未正确释放数据库连接持有的锁 - 高频提交导致的性能损耗:每次commit都会触发磁盘IO,高流量场景下IO堆积直接放大锁冲突概率
优化方案
- 改用异步数据库驱动:将同步的sqlite3替换为
aiosqlite,适配discord.py的异步执行模型,支持协程安全的数据库操作 - 降低提交频率:同一个游戏的所有玩家计时更新完成后再执行一次commit,避免单条更新就提交
- 加协程锁:全局添加
asyncio.Lock,保证同一时间只有一个协程执行数据库写操作,彻底避免锁竞争 - 使用参数化查询:替代直接拼接SQL的写法,避免SQL注入风险同时提升查询执行效率
- 优化遍历逻辑:倒序遍历玩家列表避免删除元素导致的索引偏移问题
- 长期高流量场景建议替换为支持行级锁的关系型数据库(如MySQL、PostgreSQL),从根本上解决文件锁带来的并发限制
优化后代码示例
import asyncio import aiosqlite import discord from discord.ext import tasks # 全局协程锁,保护数据库写操作 db_lock = asyncio.Lock() nl = "\n" # 保持你原来的换行符变量 @tasks.loop(seconds = 1) async def secondLoop(): async with db_lock: # 拿到锁之后再操作数据库 async with aiosqlite.connect("你的数据库文件路径.db") as conn: # 先查询所有待处理的游戏 async with conn.execute("SELECT players, joinedTimePlayers, channelId, id FROM games WHERE onPick = 0") as cursor: games = await cursor.fetchall() guild = bot.guilds[0] for game in games: playersTime = game[1].split() players = game[0].split() updated = False # 标记当前游戏是否有修改,避免无意义提交 # 倒序遍历避免删除元素导致索引偏移 for playerTimeIndex in reversed(range(len(playersTime))): try: current_time = int(playersTime[playerTimeIndex]) if current_time > 1: playersTime[playerTimeIndex] = str(current_time - 1) updated = True elif current_time == 1: # 发送提醒消息 channel = bot.get_channel(game[2]) embedToChannel = discord.Embed( description = f"Пользователь <@!{players[playerTimeIndex]}> автоматически вышел из лобби, по причине бездействия", colour = discord.Color.red() ) await channel.send(embed = embedToChannel) # 删除超时玩家 del players[playerTimeIndex] del playersTime[playerTimeIndex] updated = True except Exception as e: # 不要吞异常,打印日志方便排查问题 print(f"处理玩家超时出错: {e}") # 整个游戏的所有修改完成后,一次性提交更新 if updated: await conn.execute( "UPDATE games SET joinedTimePlayers = ?, players = ? WHERE id = ?", (nl.join(playersTime), nl.join(players), game[3]) ) # 所有游戏处理完成后一次性提交事务 await conn.commit()
内容的提问来源于stack exchange,提问作者Argon-cell
相关产品推荐
相关产品推荐

