分享一个保证数据库操作原子性的简单方法
发布于 6 年前 作者 jysperm 9863 次浏览 最后一次编辑是 5 年前 来自 分享

在涉及对用户的金钱和积分等数值进行增减操作的地方,如果没有特殊对待,就会导致有可能受到攻击,多个请求之间并发地对数据进行了修改。

如果是使用 MongoDB 的 save 方法会导致增减操作只发生一次;即使使用了 $inc 这样的原子操作符,也会导致余额等数值跳过检查,被错误地降低到了 0 以下;就算你用了 findAndModify, 也只是能在一个文档内实现原子性而已,没法跨文档。

MongoDB 是不提供跨文档的原子性的,MySQL 的 MyISAM 引擎也不提供事务支持,还有时虽然使用了支持事务的数据库,但因为偷懒也没有用事务的功能。

通常在一个简单的应用中,只有某几项操作需要事务,所以我分享一个简单的中间件用来实现事务:

lock = (require 'lock')()
onFinished = require 'on-finished'

transaction = (req, res, next) ->
  lock "account:#{req.account.id}", (releaseLock) ->
	onFinished req, ->
  	  releaseLock()()
    next()

这个中间件用到了两个库,lock 是一个简单的锁,可以锁住一个字符串(在这里是用户 ID)所代表的资源,一次只能有一个请求获得这个锁。 on-finished 用于在请求结束时释放掉这个请求得到的锁。

使用方式:

app.post '/recharge', transaction, (req, res) ->
  # ...

app.post '/withdraw', transaction, (req, res) ->
  # ...

这样即可实现一个用户同时只能进入一个与金钱相关的接口,其余请求会进行排队,直到前一个完成。 这是一种简单的实现,可能对同时涉及多个用户的操作支持较弱,但关键在于简单。

5 回复

怎么回滚呢?

@yakczh 没法回滚。。。叫事务不大合适,正在想一个更好的说法

靠谱的办法还是换用支持事务的数据库。如果要用独占锁把多个请求排队,也得用分布式锁(例如,基于ZooKeeper实现的分布式锁)才有保证。试想,如果还有另外一个应用也同时写入这个数据库,不还是破坏了你的应用的数据库操作的原子性吗?

mongodb不支持ACID事物,可考虑用tokumx替代。tokumx支持ACID和document级别锁,另外有很多创新功能,数据存储时经过压缩,大小差不多是mongodb占用空间的20%左右,写入数据超过mongo很多,读的速度基本慢15%左右

你这个只能单机锁, 碰到分布式你肿么锁, 你可以选择用支持事务的数据库, 也可以选择用memcache/redis之类的做计数器, 也可以实现你需要的效果

回到顶部