1.背景描述

应用框架:Spring + SpringMVC + Hibernate

数据库:Oracle11g

一家文学网站向我系统推多线程低并发推送数据,我这边观察日志和数据库,发现有一个作者被存储了2次到数据库中。按照程序的编写逻辑,重复的数据是会被判断出来不被存储的。

2.原因分析

由于网络原因,客户可能连续推送了两条重复的数据,两条数据时间间隔非常小,因此导致了我们的

if(用户不存在){ xxxxx 存储用户到数据库}else{ 重复推送,不采取任何措施}这个操作还没有执行完毕,第二条拥有相同数据的线程已经进入并通过了if的检验,导致数据库存储了两条相同的数据。后来我自己写了个100并发的多线程测试程序,发现100条相同数据中有40条被插入到了数据库里!天啦噜!!!因此确定了是多线程的并发导致了程序的判断逻辑失效。

3.解决思路:

1) 在Author主表中对 身份证号 添加了唯一索引,现在Author主表不会出现重复数据了,如果连续推送客户会收到一条推送失败的提示信息。 2) 但是AuthorOrg表(Author与AuthorOrg是一对多关系)依然会出现重复数据,想过添加siteId + userUniqueId的 联合唯一索引来解决,但是想到work表也会出现同样的问题,添加过多索引会导致DB占用空间无限增大,因此不采用。 3) 考虑使用synchronized对方法添加同步锁,但是这样会导致其他正常数据的推送线程也被阻塞,影响效率。因此不采用。 4) 使用对数据库添加行锁,实验发现还是会出现2条重复数据 分析: 理论上的结果应该是1条成功,149条失败。 对数据库的select语句添加行锁必须作用于某条记录,但是第一次报送时,数据库中并没有这条数据,因此行锁根本没有加上,导致第二条数据成功异步使用select语句。 第一次报送成功以后,数据库中有了这条数据,select语句成功的对这条记录添加了行锁,所以后边不会出现重复数据。因此此法不可用。 5) 即想提高效率不对方法添加synchronized,又想保证数据准确性,最后使用synchronized(siteId + uid) 在Controller层加锁(保证了只有重复数据被加锁,在Controller使用的原因是因为事务会在Service调用完毕才被提交,我实验过在Service同步,150并发会出现2条重复数据,因为事务还没来得及提交) 测试结果:测试了3次150并发 不到一秒的时间全部返回,结果1条登记成功,149条返回该作者已登记。下一步: 针对所有可能出现高并发问的接口进行调整。

4.提示

这种加同步锁的方法在负载均衡下的多台应用服务器会失效!因为就算Spring保证了对象是单例的,但是多台服务器肯定是多个对象!因此synchronized将无效。解决方法是在数据库层对该对接公司的唯一记录加select锁,这样就能保证数据的不重复性,但是会降低该公司推送数据的效率(相当于逐条推送),但是公司与公司之间还是并行推送的。还有一个方法就是将业务逻辑写入存储过程,然后对存储过程加锁,这种方法太麻烦了,需求有变动就必须去修改存储过程,但是效率要比前者高得多。