概括总结
既然Java同步之后,性能这么差,那么有没有办法可以不使用Java同步呢?有的,那就是利用数据库修改的行数来验证库存。另外,假设现在库存是10,需要减少1,推荐的做法是update Goods set stock=stock-1
,而不是update Goods set stock=9
,后面的写法有同步的情况下性能差,在未同步的情况下直接是错的。
011版本更新说明
更新的思路是这样的:
对于SQL语句update Goods as t set t.stock=stock-1 where t.id=1
来说,如果执行成功的话,一定会修改一条记录,也就是把库存减少1。对于JDBC来说,也就是说影响了结果的条数为1
。
对于SQL语句update Goods as t set t.stock=(stock-1) where t.id=1 and (stock-1) >= 0
来说,如果执行成功的话,不一定会修改一条记录,因为(stock-1)
可能会小于零。也就是说,如果库存充足,则影响了结果的条数就为1,如果库存不足,则影响了结果的条数就为0。
这里的关键点在于,对于同一条SQL语句中,虽然出现两次(stock-1)
,但是不会因为并发线程多而导致这两次计算的值不一样,换句话说:同一条SQL语句是线程安全的。 于是我们就可以把Java代码中的同步去掉了。
这次一共写了三个相似的类,分别是:
Version011Bad.java:性能差、结果正确的版本。它使用了Java代码同步,执行的SQL语句为:update Goods as t set t.stock=stock-1 where t.id=1
。
Version011Good.java:性能好、结果正确的版本。它没有使用Java代码同步,执行的SQL语句为:update Goods as t set t.stock=stock-1 where t.id=1 and (stock-1) >= 0
。
Version011Wrong.java:性能好、结果错误的版本。它没有使用Java同步,执行的SQL语句为:update Goods as t set t.stock=传来的参数 where t.id=1 and (stock-1) >= 0
。其中传来的参数
是在Java代码中计算出来的。
测试结果
统计10次测试的平均值之后:
第1种:性能差、结果正确的版本,每秒钟可以提交的订单数为:15
第2种:性能好、结果正确的版本,每秒钟可以提交的订单数为:220
第3种:性能好、结果错误的版本,每秒钟可以提交的订单数为:165
第1种:性能差、结果正确的版本,提交每个订单平均耗时的纳秒数:65095881
第2种:性能好、结果正确的版本,提交每个订单平均耗时的纳秒数:4551771
第3种:性能好、结果错误的版本,提交每个订单平均耗时的纳秒数:6029099
由于第3种的结果是错误的,没有比较的意义,因此这里只比较第1种和第2种的差别。
性能差别为:(65095881 - 4551771) / 4551771 = 13.30,即第二种方法比第一种方法的性能提升了13.3倍。 (由于差别太大,这里直接使用的是倍数,而没有使用百分比。)
【备注】:不同的机器上的测试结果会不一样,以上测试结果仅供参考。
测试结果说明
说明1:为什么第1种性能差?
因为使用了Java同步,即同一时间,只允许有一个线程查询商品,也只允许有一个线程修改库存。这就意味着多线程在查询商品和修改库存时变成了单线程。
说明2:为什么第2种性能好?
因为它没有使用Java同步,同一时间,允许有多个线程同时查询商品和修改库存。对库存的验证操作从Java代码中转移到了SQL语句中,也可以认为对同步的处理从Java中转移到了Mysql中。
说明3:为什么第3种是错的?
假设如下执行顺序:
线程A查询出商品,库存为10
线程B查询出商品,库存为10
线程A在自己查出的库存的基础上减1,得到9,修改数据库,数据库中的库存为9
线程B在自己查出的库存的基础上减1,得到9,修改数据库,数据库中的库存为9
也就是说,两个线程提交了两个订单,数据中的库存应该由10
变成8
,但实际上却是9
。
因此推荐的做法是update Goods set stock=stock-1
,而不是update Goods set stock=9
。
补充说明
关于Mysql相关的锁的知识,我目前还很欠缺,还没有开始研究,所以目前只是靠试探发现这么做是可以的,并没有严格的理论依据,希望日后可以补上。
另外,这个例子中,使用的线程数是32
,性能提升了13倍
。后来我又试过一次使用320
个线程,性能提升了41
倍。但是为了测试时间短一点,以后还是会使用32个线程,不过你心里应该有一个印象,即32
几乎肯定不是一个最佳的的线程数。