SQLite3数据库锁致ActiveRecord插入异常问题求助
解决SQLite数据库锁导致的消息重复插入问题
我明白这种“首次点击没反应、二次点击插两条”的问题有多闹心——尤其是试过各种现有方案还没解决的情况下。咱们从SQLite的锁特性入手,一步步拆解修复思路:
1. 先搞懂为什么会出现SQLite::BusyException
SQLite是文件级别的数据库锁,同一时间只能允许一个写操作执行。如果有未释放的数据库连接、长时间运行的读查询,或者多线程/进程同时写入,都会导致写操作被阻塞,抛出这个锁异常。
先排查基础配置
- 检查
config/database.yml里的连接池配置:如果用了Puma这类多线程服务器,pool的数值要和Puma的threads配置匹配(比如Puma设了threads 1:5,pool至少要设为5),避免因为连接不够用导致等待锁的情况。 - 关闭所有可能占用数据库的额外进程:比如开发环境的Spring、rails console,或者后台运行的Sidekiq任务,这些进程可能握着连接不释放,导致主服务的写操作被锁。
2. 代码层面加重试+幂等性,解决重复插入
既然首次点击因为锁失败,二次点击又会重复插入,我们需要从两个维度解决:
- 给写操作加有限次数的重试,应对临时锁;
- 加幂等性约束,确保即使重复调用也不会生成重复数据。
方案一:捕获锁异常并有限重试
把创建notification的代码包裹在重试块里,针对SQLite的BusyException做最多3次重试(次数可以调整):
def create_notification(content, user_id) retry_count = 0 begin Notification.create!(content: content, user_id: user_id) rescue SQLite3::BusyException => e retry_count += 1 if retry_count < 3 sleep 0.1 # 等待一小会儿再重试 retry else raise e # 超过重试次数再抛出异常,方便排查 end end end
方案二:给表加唯一索引实现幂等性
如果消息有唯一标识(比如前端传的消息UUID,或者内容+用户ID的组合),给notifications表加唯一索引,这样即使重复调用create,数据库也会拒绝插入重复数据:
- 生成迁移文件:
rails generate migration AddUniqueIndexToNotifications user_id:integer message_uuid:string
- 在迁移文件里添加唯一约束:
class AddUniqueIndexToNotifications < ActiveRecord::Migration[7.0] def change add_index :notifications, [:user_id, :message_uuid], unique: true end end
- 创建消息时传入唯一的message_uuid:
Notification.create!(content: content, user_id: user_id, message_uuid: SecureRandom.uuid)
这样重复调用时会触发ActiveRecord::RecordNotUnique异常,不会生成两条相同消息。
3. 排查并发写入场景
如果你的应用里有后台任务(比如Sidekiq)或者其他线程同时操作notifications表,需要确保这些操作不会互相抢锁:
- 用乐观锁:给notifications表加
lock_version字段(通过迁移add_column :notifications, :lock_version, :integer, default: 0),这样并发写入时会触发ActiveRecord::StaleObjectError,你可以在代码里捕获这个异常,提示用户“操作太频繁,请稍后重试”。 - 避免长时间占用连接:比如不要在写入notification之前执行耗时的查询(比如加载大量用户数据),尽量把读操作和写操作分开,减少连接占用时间。
4. 生产环境建议
如果这是生产环境的问题,SQLite本身并不适合高并发写入场景——它的文件锁机制在多进程/多线程写入时很容易出现锁等待。建议换成PostgreSQL或者MySQL,它们支持更细粒度的行级锁,能更好地处理并发写入。
内容的提问来源于stack exchange,提问作者Amer Bearat
相关产品推荐
相关产品推荐

