一、解决的场景
开发中,经常遇到这样的场景,数据库中存在记录,则需要更新这条记录,不存在这条记录,则插入这条记录
比如:给用户加积分,加道具,存在则直接字段加值,不存在这条记录需要插入初始化的一条数据; 统计每天的参与数(每天生成一条记录) 等等。
二、优化过程
1、先查再插
最常见的编码方式:
Entity entity
=
service
.
findById
(
10
);
if
(
null
==
entity)
service
.
insert
(
obj
);
}
else
{
service
.
update
(
obj
);
}
问题: (1)要2次访问数据库。(2)有并发问题。
2、加同步锁
先根据条件查询,存在就更新,不存在添加,但是往往我们是集群、多列的状态下,会再你正在判断null == entity
的时候,另外一个线程已经插入完毕了,导致你以为是不存在,重复插入。
在单实例中,可以用java锁解决, 在多实例中,可以通过分布式锁解决。
以单实例为例:
synchronized(对象){
Entity entity
=
service
.
findById
(
10
);
if
(
null
==
entity)
service
.
insert
(
obj
);
}
else
{
service
.
update
(
obj
);
}
}
问题:(1)要访问2次数据库,(2)如果加分布式锁,还需要多1次访问1次Redis,但性能很低。
扩展:synchronized 粒度的问题,如:锁粒度一定要小,synchronized (("baiShi_lock" + activityId + currentPage).intern())
3、 INSERT 中 ON DUPLICATE KEY UPDATE的使用
在Mysql的语法中其实提供了这么一种操作,能够完成这样的任务。语法如下:
INSERT … ON DUPLICATE KEY UPDATE field1=value1, field2=value2,…;
这样的语句在执行时作为一个原子操作执行,如果数据库中没有该条记录,则插入,如果有,则更新这些字段(field1=value1, field2=value2,…, fieldn=valuesn);需要说明的是,数据库表必须含有一个唯一的索引,通过该索引来标记是否存在该条记录。
如:
INSERT INTO table (a,b,c) VALUES (1,2,3) ON DUPLICATE KEY UPDATE c=c+1;
不存在记录,则插入, 存在记录,则 UPDATE table SET c=c+1 WHERE a=1;
问题:有可能会死锁。 在5.7.30-log没有死锁问题。 在5.7.18-log REPEATABLE-READ隔离级别下有死锁问题,在READ-COMMITTED隔离级别下没有死锁问题。
死锁问题:
https://www.pudn.com/news/628f8426bf399b7f351edd71.html
https://cloud.tencent.com/developer/article/1797058
https://www.likecs.com/show-305633666.html
https://blog.csdn.net/LS7011846/article/details/124548283
https://blog.csdn.net/agonie201218/article/details/125265266
4、唯一索引的方式
Boolean success = this.updateBackup(playerId,propId);
if(!success){
try {
// playerId 和 propId是组合唯一索引
this.insertBackup(playerId,propId);
}catch (Exception ex){
this.updateBackup(playerId,propId);
}
}
需要注意的问题:(1)必须加唯一索引,通常情况下,只需要访问1次数据库,但插入时需要执行2到3次SQL。
(2) 如果在事务的情况下,性能较低(以下隔离级别为已提交读),还有可能导致死锁。
事务导致的死锁问题:
参考:https://blog.csdn.net/weixin_44146398/article/details/125705284
以云逛店为例,公司的框架,隔离级别是已提交读, 如果加事务的话,主键重复可以捕获异常,更新成功了,但也会异常回滚,如下图。如果是死锁,连Exception和Error也捕获不到。
5、高并发下,有事务导致回滚的解决方式
解决方式:分布式锁 或 队列。
(1)分布式锁
加锁范围小的情况下:
问题:(1)有可能使分布式锁无法unlock。
(2) 依然有索引冲突和死锁的问题。
索引冲突:隔离级别导致的
死锁:第一个updateBackup与insertBackup导致的。
加锁范围大的情况下:
问题:(1)有可能使分布式锁无法unlock。
(2)性能很低。
(2)异常后发MQ,重新执行。
public Boolean saveBackup(Integer playerId, Integer propId){
//事务完成后提交
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
//doSomething
}
@Override
public void afterCompletion(int status) {
if (status == TransactionSynchronization.STATUS_ROLLED_BACK) {
// 发MQ
}
});
// 更新背包
Boolean success = this.updateBackup(playerId,propId);
if(!success){
this.insertBackup(playerId, propDO);
}
return Boolean.TRUE;
}
复杂版本:
public Boolean saveBackup(Integer playerId, Integer propId){
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
//doSomething
}
@Override
public void afterCompletion(int status) {
if (status == TransactionSynchronization.STATUS_ROLLED_BACK) {
// 发MQ
}
}
});
Boolean success = this.updateBackup(playerId,propId);
if(!success){
PropDO propDO = propService.getById(propId);
try {
// playerId 和 propId是组合唯一索引
this.insertBackup(playerId, propDO);
}catch (Exception ex){
// 没有以下代码,在索引冲突的情况下,也不会发MQ,因为异常已经被捕获了。但有以下代码的话,就会发MQ,因为又接着抛出了死锁异常
//(奇怪的点:多次重启服务复测都是这个结果:9个主键冲突导致了8个死锁,触发了8次回滚,1次success为true,另外1次的死锁是刚进来执行updateBackup导致的)。
if(cause instanceof SQLIntegrityConstraintViolationException){
// log.info("重复主键");
success = this.updateBackup(playerId, propId);
// log.info("重复主键1,{}",success);
// }
}
}
}
优化版本1:
public class TransactionUtil {
private static final ThreadPoolExecutor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(5,
20, 30, TimeUnit.SECONDS, new ArrayBlockingQueue<>(20), new ThreadPoolExecutor.AbortPolicy());
public static void afterCommit(Runnable runnable) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
try {
THREAD_POOL_EXECUTOR.execute(runnable);
} catch (Exception e) {
// 记录日志
}
}
});
}
}
// 调用就变成了
TransactionUtil.afterCommit(() -> System.out.println("测试事务提交后的逻辑处理"));
优化版本2:
利用Spring 4.2版本的新特性@TransactionalEventListener注解的事件机制来完成事务提交后的逻辑处理
@Autowired
private ApplicationEventPublisher publisher;
@Override
@Transactional(rollbackFor = Exception.class)
public void add(AdvanceChargeApplyAddInput input) {
// 业务逻辑TODO
// 发送事件
publisher.publishEvent(advanceChargeApply);
}
// 响应事件, 事务提交后执行
@TransactionalEventListener
public void handle(PayloadApplicationEvent<AdvanceChargeApply> event) {
System.out.println("TransactionalEventListener 事务提交后执行");
}