Параллельное изменение одной строки опасно не само по себе: опасно потерять чужой результат. Версия и блокировка дают два разных способа это заметить.
Optimistic lock через версию
@Entity('products')
export class Product {
@PrimaryGeneratedColumn()
id: number;
@VersionColumn()
version: number;
}
@VersionColumn увеличивает версию при обновлении и проверяет старое значение в условии. Если другая транзакция обновила строку раньше, текущее обновление не проходит как ни в чем не бывало: TypeORM выбрасывает ошибку optimistic lock.
Приложение должно повторить сценарий: прочитать свежую строку, заново проверить инвариант и только потом записать. Повторять без перечитывания бессмысленно.
Pessimistic lock для критического участка
pessimistic_write использует SELECT ... FOR UPDATE: другие транзакции ждут снятия блокировки строки.
await dataSource.transaction(async manager => {
const product = await manager.getRepository(Product)
.createQueryBuilder('product')
.setLock('pessimistic_write')
.where('product.id = :id', { id: 42 })
.getOne();
product.stock -= 1;
await manager.save(product);
});
Блокировка действует внутри транзакции. setLock вне нее не заменяет атомарную операцию и создает ложную уверенность. Для простого уменьшения остатка часто лучше один SQL stock = stock - 1 WHERE stock > 0 без чтения в память.
Коротко
- Версия обнаруживает конкурирующее обновление, не скрывает его.
- После optimistic lock ошибки сценарий читают и выполняют заново.
pessimistic_writeприменяют внутри транзакции.- Для атомарного изменения предпочтителен один SQL с условием.
- Любую гонку проверяют двумя параллельными сценариями.
Что почитать дальше
- Транзакции и isolation - граница блокировки.
- Интеграционные тесты - как воспроизвести гонку.