在我們用excel表進行插入導出的時候,通常使用easyexcel或者FastExcel,而fastexcel是easy的升級版本,今天我們就對使用FastExcel時往數據庫插入數據的業務場景做出一個詳細的剖析
場景1
現在我們數據庫有一張組織表,組織表的字段如下
package com.example.tabledemo.pojo.entity;import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.example.tabledemo.pojo.BaseEntity;
import lombok.Data;/*** @Author: wyz* @Date: 2025-03-25-10:18* @Description:*/
@TableName("organization")
@Data
public class OrganizationEntity extends BaseEntity {/*** 組織代碼* <p>* 組織的唯一代碼,用于標識不同的組織,不能為空。* </p>*/@TableField("org_code")private String orgCode;/*** 學院/組織名稱* <p>* 組織的名稱,用于描述組織的具體名稱,不能為空。* </p>*/@TableField("org_name")private String orgName;/*** 組織類型* <p>* 組織的類型,用于描述組織的分類或性質,可以為空。* </p>*/@TableField("org_type")private String orgType;
}
現在我們業務要求是,組織code和組織name在插入的過程中是唯一性,也就是說這兩個字段的數據是唯一的,那我們對這種情況有兩種處理方式
方式1
我們應該最先想到的是在業務層進行重復值的判斷,具體的流程如下
?然后我們按照此流程進行插入,但是這樣會出現一個典型的多線程問題,就是我再查詢結束之后,進行插入的時候,有另外一個線程也插入了,這時候我又插入成功,不是出現了問題,那么解決這個問題的方法也很簡單,對資源上鎖就行了
方式2
我們為org_name 和org_code分別在數據庫中設置一個唯一性約束
create table organization
(id bigint auto_increment comment '序號,主鍵,自增'primary key,org_code varchar(50) not null comment '組織代碼',org_name varchar(100) not null comment '學院/組織名稱',org_type varchar(50) null comment '類型',status int default 0 null comment '狀態,默認為0(可用)',create_time datetime default CURRENT_TIMESTAMP null comment '創建時間,插入時自動填充',update_time datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '更新時間,插入和更新時自動填充',is_deleted int default 0 null comment '邏輯刪除標志,0表示未刪除,1表示已刪除',constraint org_codeunique (org_code),constraint org_nameunique (org_name)
)comment '組織信息表';
這樣的話,在我們后臺我們只需要關注插入的問題就行了,甚至修改的時候都不需要關心數據重復性的問題,因為在mysql底層,他會為每一個設置唯一性約束的字段創建一個索引,索引是b+樹結構的,每次插入的時候會查詢是否有這個索引,沒有就插入,有就會報錯
對應的java代碼如下,我們不需要加事務是因為 這是對單表進行的純插入刪除操作,無需回滾,插入不成功我們數據庫有唯一性約束數據庫會自動禁止插入,而且在 mybatisplus的saveOrupdate方法中也有事務管理
@Override
// @Transactional(rollbackFor = Exception.class)無需事務public Result add(OrganizationRequest.addOrganization addOrganization) {OrganizationEntity organizationEntity = new OrganizationEntity();BeanUtil.copyProperties(addOrganization,organizationEntity);try {boolean b = saveOrUpdate(organizationEntity);return Result.success(b);}catch (Exception e){if (e.getCause() instanceof SQLException) {SQLException sqlException = (SQLException) e.getCause();if (sqlException.getErrorCode() == 1062) { // MySQL 唯一性約束錯誤碼return Result.fail("組織名稱或代碼已存在,請勿重復插入!");}}return Result.fail("數據庫操作失敗:" + e.getMessage());}}
問題1
當我們組織表信息量大了以后,我們每一次數據的插入都會使得mysql底層的索引的b+樹結構改變,這種IO帶來的開銷無疑是越來越大的,所以,根據這個延申出來的解決方案也有幾種
對mysql進行分庫分表,然后讓name和code做一次hash,根據不同的hash找到不同的表,然后進行數據的插入等這樣能減少重建索引帶來的IO開銷。但是無論是哪種方法,都有一定的優缺點,看我們如何選擇了吧
場景二
現在做的是一個excel表,我們填充完數據之后,需要批量導入,這時候org_name 和org_code也是需要唯一的,同樣的也有兩種方式,就是我們上文所說的,只是問題從 單個插入變成了批量插入。
而批量插入在數據庫中的事務也同樣延申出來的許多的問題
問題1
我在使用數據庫 原始的sql進行批量插入的時候,假如有3條數據ABC,B數據和C數據一樣,這時候如果我加了唯一性約束,會不會導致A插入成功,B,C兩條數據沒有插入成功下面我們來測試一下
我們現在拿到的是最新的數據
我們插入一下看看
我們再次查詢一下數據庫看一下
數據并沒有變化,說明了在我們用values的時候,如果加了唯一性約束,這些批量插入的后面是同一個事務的,只要有一個失敗,就會回滾所有的數據。
那我們再看同一個事務下,三條數據分批次插入的情況
顯而易見,分批次插入的話,只有出現異常的數據不會被插入。
那么我們再來分析,假如說 現在 我們批量插入上面三條數據,那么第一條成功了,那么第二條還沒有插入的時候,這時候這個字段的唯一索引變化是怎么樣的,這時候唯一索引會帶來額外的額外的io開銷嗎?
我們看下面一張圖
我是按照紅字的順序進行事務的數據插入操作的,當我進行到4的時候,我5沒有提交事務,這時候4會一直阻塞,原因是?REPEATABLE READ
?隔離級別下,事務會持有插入的行的排他鎖(X Lock
),直到事務提交或回滾。??
我們再回來看索引的問題,當我們事務沒有提交的時候,也就是步驟進行到3的時候,其實mysql已經為我們插入的這條數據加了唯一性索引了,假如這時候出現了異常,導致了事務回滾,那么索引就會重新取消,這也時帶來io開銷
其實解決情況已經很明了了,如果不想讓數據庫有多的索引的io開銷,那么我們就要在代碼層面控制,先查詢所有數據,然后比對唯一性,要么就是 數據庫層面控制,
如果是在數據庫層面控制,要注意 插入的時候不要用for循環單條插入,而是saveBacth批量插入,如果非用for循環單挑插入,記得使用spring的事務注解,就跟我們前面說的一樣,如果是設計多條數據的改變,而且需要回滾所有,這時候記得加事務
@Override
// @Transactional(rollbackFor = Exception.class)public void doAfterAllAnalysed(AnalysisContext context) {log.info("所有數據解析完成!");// 字段唯一性約束 可以 用mysql 自己的 也可用 代碼邏輯判斷List<OrganizationEntity> organizationEntities = BeanUtil.copyToList(list, OrganizationEntity.class);
// boolean b = organizationService.saveBatch(organizationEntities);
// log.info("保存成功");try {boolean b = organizationService.saveBatch(organizationEntities);log.info("保存成功");}catch (Exception e){if (e.getCause() instanceof SQLException) {SQLException sqlException = (SQLException) e.getCause();if (sqlException.getErrorCode() == 1062) { // MySQL 唯一性約束錯誤碼throw new RuntimeException("組織名稱或代碼已存在,請勿重復插入!");}}throw new RuntimeException("數據庫操作失敗:" + e.getMessage());}}
而在我的代碼中為什么我把事務注解注釋掉了,因為再mybatisplus中,他的saveBatch方法默認加了事務