很多人在做项目时都会遇到一个实际问题:用NoSQL数据库,比如MongoDB或者Redis,能不能像MySQL那样保证数据的一致性?特别是在下单、转账这种场景下,一步出错就得全部回滚,这时候就离不开事务。
NoSQL和事务的关系没那么简单
传统印象里,NoSQL为了性能和扩展性,牺牲了事务支持。早期的MongoDB、Cassandra确实不支持多文档或多行事务,只能保证单条记录的原子性。比如你往购物车加一件商品,这条操作不会一半成功一半失败,但如果你要同时扣库存、改订单状态、更新用户积分,这就跨了多个文档,老版本的MongoDB干不了这事。
但情况早就变了。从MongoDB 4.0开始,已经支持多文档ACID事务,而且在副本集里就能用。到了4.2版本,还支持了分片集群上的事务。也就是说,现在你在MongoDB里也能写类似下面的代码:
session.startTransaction();
try {
db.orders.insertOne(order, { session });
db.inventory.updateOne(
{ sku: order.sku },
{ $inc: { qty: -order.qty } },
{ session }
);
db.users.updateOne(
{ _id: order.userId },
{ $inc: { points: 10 } },
{ session }
);
session.commitTransaction();
} catch (error) {
session.abortTransaction();
throw error;
}
这段逻辑就像银行转账,要么全做,要么全不,不会出现订单生成了但库存没扣的情况。
不同NoSQL产品差别挺大
不是所有NoSQL都跟上了这一步。比如Redis,虽然支持MULTI/EXEC命令来打包执行,看起来像事务,但实际上不具备回滚能力。如果中间某个命令出错,前面的命令已经执行了,没法自动撤销。所以你在用Redis做账户余额变更时得特别小心,不能完全依赖它的“事务”。
再看Cassandra,它一直主打高可用和分区容忍,原生不支持传统事务。虽然可以通过轻量级事务(Lightweight Transactions)实现CAS(比较并设置),比如INSERT IF NOT EXISTS,但性能代价高,不适合高频场景。
DynamoDB倒是提供了事务支持,有TransactWriteItems和TransactGetItems,能跨多表做原子操作,但限制也不少,比如最多25条语句,总大小不超过4MB,超了就报错。
实际开发中怎么选
如果你的应用需要强一致性,比如金融、订单系统,那得优先考虑支持完整事务的NoSQL,比如新版本MongoDB或DynamoDB。但如果只是做缓存、会话存储、日志记录这类对一致性要求不高的场景,Redis那种弱事务也够用。
另外,别忘了事务是有代价的。开启事务后,性能下降、锁竞争增加、响应变慢都是常见问题。有些团队宁愿用“最终一致性+补偿机制”来代替事务,比如通过消息队列重试失败步骤,而不是强行上分布式事务。
所以说,NoSQL支不支持事务,不能一概而论。得看你用的是哪种数据库、什么版本,还有你的业务到底能不能容忍中间状态。技术选型从来都不是非黑即白的事。