You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何在Sequelize中检测查询与更新间的数据变更并解决事务竞态条件问题

处理Sequelize事务中竞态条件的最佳实践

我来帮你梳理下这两个问题的解决方案,结合你给出的Bilbo年龄计算的竞态场景,咱们一步步来看可行的处理方式:

问题1:已读取数据发生UPDATE时如何中止事务?

你可以通过乐观锁悲观锁或者在UPDATE时加入校验条件的方式,在应用层检测到数据被修改后主动回滚事务。核心思路就是确认你要更新的数据还是你之前读取的状态,一旦发现不一致,就终止当前事务。

问题2:Sequelize中能否检测SELECT与UPDATE间数据被修改?

完全可以!Sequelize支持两种主流的检测方式:基于版本号的乐观锁,以及数据库层面的悲观行锁,另外你也可以手动在更新语句中校验原始数据状态,来判断是否有其他事务修改过数据。

针对你的场景的具体解决方案

方案1:乐观锁(推荐高并发低冲突场景)

乐观锁的核心是给数据表加一个版本字段(比如version),每次更新时必须带上你读取到的版本号——只有数据库里的版本号和你带的一致,更新才会生效;如果不一致,说明已经有其他事务修改过数据了。

步骤很简单:

  1. hobbits表加一个version字段(整数类型,默认值设为1)
  2. 在Sequelize模型里开启乐观锁配置:
const Hobbit = sequelize.define('hobbit', {
  name: DataTypes.STRING,
  age: DataTypes.INTEGER,
  estimate: DataTypes.INTEGER,
  version: {
    type: DataTypes.INTEGER,
    defaultValue: 1,
    allowNull: false
  }
}, {
  version: true, // 开启乐观锁,自动使用version字段
  timestamps: false
});
  1. 修改你的function1逻辑:
async function function1() {
  const t1 = await sequelize.transaction();
  try {
    // 读取数据时同时获取当前的version值
    const bilbo = await Hobbit.findOne({
      where: { name: 'Bilbo' },
      transaction: t1
    });
    const yearsRemaining = 131 - bilbo.age;
    
    // 执行更新,Sequelize会自动带上version条件,且更新后version自增
    const [updatedRows] = await Hobbit.update(
      { estimate: yearsRemaining },
      {
        where: { name: 'Bilbo', version: bilbo.version },
        transaction: t1
      }
    );
    
    // 如果受影响行数为0,说明数据已经被其他事务修改了,直接回滚
    if (updatedRows === 0) {
      await t1.rollback();
      console.log('数据已被其他事务修改,当前事务已中止');
      return;
    }
    
    await t1.commit();
  } catch (err) {
    await t1.rollback();
    throw err;
  }
}

这种方式不需要加锁,性能很好,适合并发高但冲突少的场景,只有当冲突发生时才会回滚重试。

方案2:悲观锁(适合冲突频繁场景)

悲观锁的思路是“先锁定再操作”——在SELECT数据的时候就把该行锁住,其他事务想要修改这行数据必须等当前事务完成(提交或回滚)。

修改function1的查询部分即可:

async function function1() {
  const t1 = await sequelize.transaction();
  try {
    // 使用lock: true等价于SQL的FOR UPDATE,直接锁定该行
    const bilbo = await Hobbit.findOne({
      where: { name: 'Bilbo' },
      transaction: t1,
      lock: true,
      skipLocked: false // 设为true的话会直接跳过锁定行,这里我们选择等待锁释放
    });
    const yearsRemaining = 131 - bilbo.age;
    
    await Hobbit.update(
      { estimate: yearsRemaining },
      { where: { name: 'Bilbo' }, transaction: t1 }
    );
    
    await t1.commit();
  } catch (err) {
    await t1.rollback();
    throw err;
  }
}

这样事务1在读取Bilbo数据时就会锁住该行,事务2的UPDATE会被阻塞,直到事务1提交后才能执行,从根源上避免了基于旧值计算的错误。

方案3:UPDATE时校验原始读取值(快速临时方案)

如果你不想加版本字段或者锁,可以在UPDATE语句里带上你之前读取的age值作为条件——只有数据库里的age还是你读取的那个值时,更新才会生效:

async function function1() {
  const t1 = await sequelize.transaction();
  try {
    const bilbo = await Hobbit.findOne({
      where: { name: 'Bilbo' },
      transaction: t1
    });
    const originalAge = bilbo.age;
    const yearsRemaining = 131 - originalAge;
    
    const [updatedRows] = await Hobbit.update(
      { estimate: yearsRemaining },
      {
        where: { name: 'Bilbo', age: originalAge },
        transaction: t1
      }
    );
    
    if (updatedRows === 0) {
      await t1.rollback();
      console.log('数据已被修改,事务中止');
      return;
    }
    
    await t1.commit();
  } catch (err) {
    await t1.rollback();
    throw err;
  }
}

这种方式简单直接,但如果age字段本身可能被多次合法修改,或者需要校验多个字段,就不如乐观锁灵活。

方案4:数据库层面原子计算(最优解,如果业务允许)

其实最彻底的解决方式是把计算逻辑直接放到SQL里,这样不需要在应用层读取数据,所有操作都是数据库层面的原子操作,从根源上消除竞态条件:

async function function1() {
  const t1 = await sequelize.transaction();
  try {
    // 直接在UPDATE语句里计算yearsRemaining,不需要先读取age
    await sequelize.query(
      `UPDATE hobbits SET estimate = 131 - age WHERE name='Bilbo'`,
      { transaction: t1 }
    );
    await t1.commit();
  } catch (err) {
    await t1.rollback();
    throw err;
  }
}

这种方式没有中间状态,完全不会被其他事务干扰,是最安全的解决方案——只要你的业务逻辑允许把计算放到数据库端,优先选这个。

总结一下选择建议

  • 高并发低冲突场景:用乐观锁或者UPDATE带原始值校验的方式,性能最优
  • 冲突频繁场景:用悲观锁,避免频繁回滚重试
  • 业务允许的话:直接用数据库原子UPDATE,彻底消除竞态

内容的提问来源于stack exchange,提问作者friartuck

火山引擎 最新活动