前言
相信用过MySQL的都知道on duplicate key update这个关键字吧,这是MySQL用来防止重复插入的,前提是你的表中加了唯一索引,表的结构和插入语句如下,唯一索引不懂的请自行百度.
语句的意思就是当发生了冲突时不报错而自行更新修改时间
CREATE TABLE `test` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`user_name` varchar(20) DEFAULT NULL COMMENT '用户名称',
`create_time` bigint NOT NULL DEFAULT '0' COMMENT '创建时间',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '最后更新时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `user_name` (`user_name`) USING BTREE
) ENGINE=InnoDB COMMENT='test过滤表';
dao 层
int saveOnDuplicate(UserRoleFilter userRoleFilter);
插入语句
<insert id="saveOnDuplicate" keyColumn="id" keyProperty="id" useGeneratedKeys="true">
insert into ac_user_role_filter (user_name, create_time)
values (#{userName}, #{createTime})
on duplicate key update update_time = curtime()
</insert>
问题
dao层当发生冲突时应该返回2才对,本地测试的时候返回的是2,但是到了生产环境一直返回1.
把具体的sql拿到生产环境进行执行也是返回了2,这就很奇怪了
后来通过找资料的时候才知道原来如果并发过高的时候可能不会返回2,而是返回了1
自己本地模拟测试过并发也确实是返回了1,测试代码为
public void threadTestFilter() throws Exception{
String userName = "zxc";
Test test = new Test();
test.setUserName(userName);
CountDownLatch countDownLatch = new CountDownLatch(1);
Thread r1 = new Thread(() -> {
try {
countDownLatch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
int i = zxcDao.saveOnDuplicate(userRoleFilter);
System.out.println("当前线程名为" + Thread.currentThread().getName() + ",执行后的结果为===" + i);
});
Thread r2 = new Thread(() -> {
try {
countDownLatch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
int i = zxcDao.saveOnDuplicate(userRoleFilter);
System.out.println("当前线程名为" + Thread.currentThread().getName() + ",执行后的结果为===" + i);
});
Thread r3 = new Thread(() -> {
try {
countDownLatch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
int i = zxcDao.saveOnDuplicate(userRoleFilter);
System.out.println("当前线程名为" + Thread.currentThread().getName() + ",执行后的结果为===" + i);
});
r1.start();
r2.start();
r3.start();
//模拟统一发起保存
TimeUnit.SECONDS.sleep(5);
countDownLatch.countDown();
}
参考文章: update和insert...on duplicate key update返回值 - 简书
on duplicate key update 冲突时也不会为2
上面已经说了,如果高并发的时候其实是有很大概率不会返回2,那么要如何解决呢,可以参考下面这篇文章并在结合spring的事务来做这个事
MySQL中INSERT INTO ... ON DUPLICATE KEY UPDATE浅析 - 烟沙九洲 - 博客园
@Transactional(rollbackFor = Exception.class)
public void testSave(ZxcFilter zxcFilter) {
int i = ZxcFilterDao.saveOnDuplicate(zxcFilter);
UserRoleFilter roleFilter = zxcFilterDao.getById(zxcFilter.getId());
System.out.println("当前线程名为" + Thread.currentThread().getName() + ",执行后的结果为===" + i + ",appNumber为---" + roleFilter.getAppNumber());
}
也就是说更新完立马去查询一下数据,然后再依赖字段去判断是否发生了冲突来解决
注意:必须要有事务,否则很大概率又区分不了,因为会查到最新的那个值,只有事务才能查到当前事务里的值
存在问题
由于主键或唯一索引出现重复的时候,在更新该调存在的数据的时候,会为该条数据加一个共享锁和一个排它锁,最后才会写入该条数据,如果并发比较高,两个线程几乎同时来操作这条数据的话,如果两个线程同时给一条数据加上共享锁和排它锁的话,就会出现死锁的情况。所以要根据你的业务来定是否要使用
如果你要使用到返回值来做判断并且很高并发那么就不能用这种方式了,如果只是为了避免重复插入那就无所谓了
替代方案
如果你的业务并发比较高,那也可以考虑用Redis的setnx来解决,只不过就得考虑会不会存在过多的key问题了
key设置的时间可以在几分钟之内即可,不要影响你的业务就行,要么就是放到hash里面,每一天都生成一个新的用来存储数据,过期时间1天即可
但是就得考虑hash里面的内容会不会过大从而造成Bigkey的问题,如果会可能就得用分段锁来存储hash片断
总结
其实最好还是用redis来做,比较快,mysql也可以,就是在有数据保存的前提下进行保存咯