一、解决的场景

     开发中,经常遇到这样的场景,数据库中存在记录,则需要更新这条记录,不存在这条记录,则插入这条记录

     比如:给用户加积分,加道具,存在则直接字段加值,不存在这条记录需要插入初始化的一条数据; 统计每天的参与数(每天生成一条记录) 等等。

二、优化过程

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 事务提交后执行");
}

 

posted on 2023-07-20 15:42  毛会懂  阅读(1105)  评论(0编辑  收藏  举报