业务需求:数据库如何保证先查询后插入/更新 原子性?
一、业务需求:
当操作积分用户表时,如果accountId在表中没有数据,那么我们新增一条数据,设置用户积分。如果accountId在表中有数据,我们需要更新用户积分。
这个操作简单来说就是:
在单线程下 我们先查询后处理当然没有问题,但是在并发下问题就显而易见了,系统里可能同时插入两条一样的accountId数据。
二、问题解决:
解决方式一: ON DUPLICATE KEY UPDATE
数据库中account_id设置唯一索引,当发现account__id已经存在时,会执行update操作,不存在时会执行insert操作。
一行sql语句就能完成两种操作,保证了原子性。
sql语句如下:
添加单元测试,查看耗时以及查验数据库在并发下数据是否正确。
代码隐去业务代码,如下:
查看打印的日志,共耗时:22690ms
数据库数据能够保持正确性
解决方式二: 使用分布式锁
这个耗时比第一种方式差很多,所以没有测试完就放弃了。
因为高并发的情况下 锁的抢占很激烈,这里很多时间都耗费在锁的抢占上,没有抢占到锁的线程需要重试而不能失败,类似于CAS操作,所以这种方式不适合当前业务。
解决方式三: INSERT INTO SELECT
此种方式也是最优的,耗时:20010ms
sql语句如下:
查询accountId不存在时结果:
查询accountId存在时结果:
这里需要注意的是,此sql语句在Mapper.xml中是insert语句:
三、原理分析
1、ON DUPLICATE KEY UPDATE
mysql "ON DUPLICATE KEY UPDATE" 语法:
如果在INSERT语句末尾指定了ON DUPLICATE KEY UPDATE,并且插入行后会导致在一个UNIQUE索引或PRIMARY KEY中出现重复值,则在出现重复值的行执行UPDATE;如果不会导致唯一值列重复的问题,则插入新行。
2、 INSERT INTO SELECT
INSERT INTO SELECT 语句从一个表复制数据,然后把数据插入到一个已存在的表中。目标表中任何已存在的行都不会受影响。
其中使用到了dual虚拟表, 根据mysql的官方定义:
DUAL is purely for the convenience of people who require that all SELECT statements should have FROM and possibly other clauses. MySQL may ignore the clauses. MySQL does not require FROM DUAL if no tables are referenced.
官方的解释说:纯粹是为了满足select … from…这一习惯问题,mysql会忽略对该表的引用。所以上面的语句from dual可以去掉。
简言之,from dual完全是一个可有可无的东西。只是为了方便使用select 语句中喜欢带上from的开发者。
例如我们使用select 1 查询等价于select 1 from dual
四、总结
到了这里就分析完了,如果大家有更好的解决方案也可以拿出来学习下,文中如有问题恳请大家指正一下。
第一种方式会有一个id不是连续自增的问题,具体可以参考文章: https://segmentfault.com/a/1190000017268633