条件竞争导致的支付安全问题
最近在整理关支付安全的内容,其中就是涉及到了一个在支付过程中的条件竞争问题。以下都是基于mysql的与php的架构来描述该问题,大佬勿喷。
0x01. 条件竞争
-
什么是条件竞争:
竞争条件
发生在多个线程同时访问同一个共享代码、变量、文件等没有进行锁操作或者同步操作的场景中。【Wikipedia-computer_science】 - 一个简单的购买的业务:
后台代码实现如下:
以上是一个购买商品的流程,看似并没有什么问题。
如果每次请求都是一个单线程的请求是没有什么问题的,但是如果采用多线程的并发请求就会出现问题。
因为每次的购买流程都是需要一定的时间去按照购买的流程执行,如果我们在第一次购买的流程3还没有结束时,就再去执行这一遍流程时,那么在第流程2时查询用户的余额就还是初始的余额,这是因为第一次购买的流程3还没有结束,没有结束也就意味着余额没有扣除,所以金额就是初始的值。
在上面的百年青花瓷购买的案例中,如果我们只有1000块钱,但是我们在多线程的情况下去购买青花瓷的时候就可能购买到10件以上的数目。
0x02. 实际测试
-
在数据库查看用户数据
-
打开购买页面
打开burp拦截购买商品的数据包,点击购买。
设置intruder发送50个数据包,线程调到25后发包
并发请求后查看购买页面,已拥有17件,余额成了-700。
我们调出mysql的查询日志。
从日志中可以看到我们的请求是并发的执行的,在一次查询还没有结束时就进行了下一次的查询,所以这也很容易产生两次查询的余额是相同的。
所以当我只剩100块的只能购买一件商品的时候,但是有可能两次查询余额都100
,是符合购买流程的操作的,后面也会对余额100进行两次扣除操作,所以最后余额变成了负数,购买的数量也大于10。
0x03. Solve the problem
mysql事务
- 在网上看到一篇关于mysql与php的条件竞争的分析中的解决方案是这样的:
这种解决的方案的意思是给mysql查询进行一个事务的处理,在mysql的查询前添加一个BEGIN
,开始一个事务,在结束时添加一个COMMIT
提交一个事务,完成一个查询操作。
本地测试一下:
设置好线程再次并发购买一次,结果发现还是失败了。并没有解决条件竞争带来的问题,所以解决方案是不行的。
- 什么是mysql的事务
事务是一组原子性sql查询语句,被当作一个工作单元。若mysql对改事务单元内的所有sql语句都正常的执行完,则事务操作视为成功,所有的sql语句才对数据生效,若sql中任意不能执行或出错则事务操作失败,所有对数据的操作则无效(通过回滚恢复数据)。
通过上面一句话差不多就知道了原因,只有在查询语句不能执行或出错则事务操作失败
, 所以我们添加事务并不能解决mysql竞争的问题,因为我们的查询是不会错误的,既然不会出错也就会照样执行并发的请求。
0x04. mysql锁
悲观锁
悲观并发控制(又名“悲观锁”,Pessimistic
Concurrency
Control,缩写“PCC”)是一种并发控制的方法。它可以阻止一个事务以影响其他用户的方式来修改数据。如果一个事务执行的操作对某行数据应用了锁,那只有当这个事务把锁释放,其他事务才能够执行与该锁冲突的操作。
悲观并发控制主要用于数据争用激烈的环境,以及发生并发冲突时使用锁保护数据的成本要低于回滚事务的成本的环境中。
当我们查询的数据随时可能会被其他操作修改时,我们对这个数据进添加一个悲观锁,如果想再次对这个数据进行操作时,只有这条查询的操作结束后释放这个悲观锁,其他查询才可以对这条数据进行操作,如果锁定没有结束时,其他查询会一直进行一个等待的状态。
mysql 悲观锁的实现
select * from goods where id = 1 for update;
for update仅适用于InnoDB引擎,且必须在事务块(BEGIN/COMMIT)中才能生效。在进行事务操作时,通过“for update”语句,MySQL会对查询结果集中每行数据都添加排他锁,其他线程对该记录的更新与删除操作都会阻塞。排他锁包含行锁、表锁。
使用悲观锁解决上述并发问题:
并发测试:
经过多次测试后发现商品购买正常。没有出现条件竞争的问题
我们这边来看下后端mysql查询的日志
我们吧mysql的查询分成了11组
前七组都没条件竞争的问题,所有操作都是有序执行的,但是在第八组的时候开始出现问题
在第八条数据查询的事务还没有结束时就开始查询第九条的数据了
但是由于我们使用了for update(悲观锁),对select语句进行锁定,所以在执行到第九条的时候发现第八条的事务还没有结束,所以他就只能等待第八条的更新完库存之后执行commit(提交事务)操作,第八条查询的锁才会进行释放,然后第九条查询才能获取到用户的余额进行下一步操作。所以通过悲观锁解决了条件竞争带来的问题。
但是悲观锁的弊端在每次查询都会对数据进行锁定,在高并发的请请求下会变得很慢。所以高并发的请求不建议使用悲观锁。
作者:0xchery