如何解决多线程数据库重复插入、更新问题

基础概念

幂等性 : 在编程中.一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。
简单来说:幂等就是一个操作,不论执行多少次,产生的效果和返回的结果都是一样的。
幂等性操作:
1、查询操作:查询一次和查询多次,在数据不变的情况下,查询结果是一样的。select是天然的幂等操作;

2、删除操作:删除操作也是幂等的,删除一次和多次删除都是把数据删除。(注意可能返回结果不一样,删除的数据不存在,返回0,删除的数据多条,返回结果多个) ;
3.插入操作:插入情况下默认主键唯一,所以多次插入同一个数据不是幂等的
4.更新操作: 这里分两种情况:

1、update t set money=100  where id=1
2、update t set money=money+100  where id=1

第一种则是幂等的,第二种则是不幂等的

总结:
幂等与你是不是分布式高并发还有JavaEE都没有关系。关键是你的操作是不是幂等的。
要做到幂等性,从接口设计上来说不设计任何非幂等的操作即可。譬如说需求是:当用户点击赞同时,将答案的赞同数量+1。改为:当用户点击赞同时,确保答案赞同表中存在一条记录,用户、答案。赞同数量由答案赞同表统计出来。在设计系统时,是首要考虑的问题,尤其是在像支付宝,银行,互联网金融公司等涉及的都是钱的系统,既要高效,数据也要准确,所以不能出现多扣款,多打款等问题,这样会很难处理,用户体验也不好。

这里可参考我的另一篇文章:
幂等问题 8种方案解决重复提交

原因分析

如下这种操作很常见:

if(用户不存在)
{
    xxxxx
    存储用户到数据库
}
else
{
    重复推送,不采取任何措施
}

这个操作还没有执行完毕,第二条拥有相同数据的线程已经进入并通过了if的检验,导致数据库存储了两条相同的数据。这就是是多线程的并发导致了程序的判断逻辑失效。

解决方案

单机模式下,简单采用sync即可,这里讨论分布式情景下避免重复插入问题
这里主要说下数据库解决多线程,其余方案可参考上文中提到的:

幂等问题 8种方案解决重复提交

多线程插入解决:

1、 insert时带有where条件的写法(类似于索引)
1.1 、插入单条记录

普通的 INSERT INTO 插入:
INSERT INTO card(cardno, cardnum) VALUES('1111', '100');

对于普通的 INSERT 插入,如果想要保证不插入重复记录,我们只有对某个字段创建唯一约束实现(比如:cardno卡号不能重复);
如果要保证多个字段不会重复,可以考虑联合唯一索引!
创建索引后程序处理:

if (该cardno在数据库表中存在) {  
    update();  
} else {   
    try {  
         insert();  
         //违反唯一性约束会报异常:InvocationTargetException 
         } catch (InvocationTargetException e) {  
         //如果重复插入已经有数据,则进行更新
         update();  
     }   
}  

这里还有个问题,就是如果表中记录逻辑删除的时候采用唯一索引会出现BUG

重点在这里
那有没有不创建唯一约束,仅通过 INSERT INTO 一条语句实现的方案呢?

答案:有的, INSERT INTO IF EXISTS 具体语法如下:
INSERT INTO table(field1, field2, fieldn) SELECT 'field1', 'field2', 'fieldn' FROM DUAL WHERE NOT EXISTS(SELECT field FROM table WHERE field = ?)
其中的 DUAL 是一个临时表,不需要物理创建,这么用即可。

针对上面的card示例的改造如下:
INSERT INTO card(cardno, cardnum) SELECT '111', '100' FROM DUAL WHERE NOT EXISTS(SELECT cardno FROM card WHERE cardno = '111')
1.2、插入多条记录

INSERT INTO user  (id, no,add_time,remark)
select * from (
SELECT 1 id, 1 no, NOW() add_time,'1,2,3,1,2' remark FROM DUAL
UNION ALL
SELECT 1 no, 2 no, NOW() add_time,'1,2,3,1,2' remark FROM DUAL
UNION ALL
SELECT 1 no, 3 no, NOW() add_time,'1,2,3,1,2' remark FROM DUAL
) a where not exists (select no from user b where a.no = b.no)

上述是实现user表的no字段不重复,插入三条记录。
另外,附上mybatis批量写入no字段不重复的实现语句。

INSERT INTO user (id, no,add_time,result)
select * from (
<foreach collection="list" item="obj" separator=" UNION ALL " >
SELECT #{obj.id} id, #{obj.no} no, #{obj.addTime} add_time,#{obj.result} result FROM DUAL
</foreach>
) a where not exists (select no from user b where a.no = b.no)

多线程更新解决

1、update使用 乐观锁 version版本法
情景:
 比如两个用户同时购买一件商品,库存只有1件了!在数据库层面实际操作应该是库存进行减2操作,但是由于高并发的情况,第一个用户购买完成进行数据读取当前库存并进行减1操作,由于这个操作没有完全执行完成,这样就会出现商品超卖!

select goods_num,version from goods where goods_name = "小本子";
update goods set goods_num = goods_num -1,version =查询的version值自增 where goods_name ="小本子" and version=查询出来的version;

为什么加个version字段就满足了呢,因为数据库本身特性的帮忙,update语句执行的时候,如果更新的时候update语句不走索引就会将表锁住保证了一个时刻只有一个线程能进入更新,等这次更新释放锁以后才会执行下一次的update操作,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据,这样就能保证了程序的安全性。
这种在大数据量和高并发下效率依赖数据库硬件能力,可针对非核心业务
2.使用select … for update 悲观锁
这种和 synchronized 锁住先查再insert or update一样,但要避免死锁,效率也较差,针对单体 请求并发不大 可以推荐使用

获取数据的时候加锁获取:select * from table_xxx where id='xxx' for update;
注意:id字段一定是主键或者唯一索引,不然是锁表,会死人的;悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,根据实际情况选用;
这种在大数据量和高并发下效率依赖数据库硬件能力,可针对非核心业务

参考文章:
https://www.cnblogs.com/ganhaiqiang-20130831/articles/4478472.html
https://www.cnblogs.com/lihuanliu/p/6764048.html

幂等性解决:
https://www.cnblogs.com/baizhanshi/p/10449306.html
https://www.cnblogs.com/aspirant/p/11628654.html

发布了107 篇原创文章 · 获赞 14 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/belongtocode/article/details/103587176
今日推荐