SpringBoot+Mybatis保證讀寫事務隔離性的三種實現方式
實際開發中經常會有這樣的需求,注冊用戶,如果用戶名存在則失敗,否則注冊成功。
在單線程下,邏輯很簡單,但是高並發下需要保證事務隔離性,這里舉一個簡化版的例子來講述自己的實現方法。
問題
在實際開發的時候,我們經常會做這種事情:
- 先查詢數據庫中的數據,得到一些臨時結果
- 根據一些臨時結果做判斷,進行增刪改查操作
也就是說,第二個階段的增刪改查操作依賴於在第一個階段的結果
舉個例子,我們的表結構很簡單
查詢是否存在dname=TEST的部門,如果不存在插入一個部門名為TEST,如果存在則不操作。
要求必須不能使數據庫中存在兩個dname相同的行
我們知道SpringBoot中的Controller、Service都是單例的,在實際環境下,面對高並發量的請求,每個請求會起一個線程來進行操作,那么就會發生一些問題
舉一個類似“臟讀”的例子
@Service
public class DeptServiceImpl implements DeptService{
@Autowired
DeptDAO deptDAO;
@Override
public int concurrent() {
Dept dept = deptDAO.queryByName("TEST");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (dept == null) {
System.out.println("不存在TEST,插入");
Dept nDept = new Dept().setDName("TEST");
deptDAO.addDept(nDept);
} else {
System.out.println("存在");
}
return 1;
}
}
如果不加以處理,我們對以下接口連着發兩個請求調用這個方法試試
@PostMapping("/concurrent")
public int concurrent(){
return deptService.concurrent();
}
結果明顯是有問題的
方法一:加synchronized鎖
最簡單方法,犧牲一些性能,加synchronized鎖即可
@Override
public synchronized int concurrent() {
Dept dept = deptDAO.queryByName("TEST");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (dept == null) {
System.out.println("不存在TEST,插入");
Dept nDept = new Dept().setDName("TEST");
deptDAO.addDept(nDept);
} else {
System.out.println("存在");
}
return 1;
}
執行結果沒問題
注意,這里為了實驗使用了Thread.sleep(3000);
,切記不能使用object.wait(3000);
,因為wait會釋放鎖
方法二:使用dual表寫sql
出現這個問題的原因在於,我們使用了兩次mapper,是否能在一個語句里實現需求呢?
其實是可以的,使用dual表
修改Mybatis語句
<insert id="addDept" parameterType="com.cpaulyz.PO.Dept">
insert into dept(dname, db_source) select #{dName},DATABASE() from dual where not exists(
select * from dept where dname = #{dName}
);
</insert>
實際上就是
insert into dept(dname, db_source) select "TEST",DATABASE() from dual where not exists(
select * from dept where dname = "TEST"
);
然后直接插入即可
@Override
public synchronized int concurrent() {
Dept nDept = new Dept().setDName("TEST");
deptDAO.addDept(nDept);
return 1;
}
方法三:行鎖+@Transactional
分析一下出現問題的原因,主要在於Dept dept = deptDAO.queryByName("TEST");
時,默認使用的是快照讀,即select * from dept where xxxx;
我們可以進行當前讀,類似MySQL的鎖策略
修改mybatis映射
<select id="queryByName" resultType="com.cpaulyz.PO.Dept" resultMap="DeptMap">
select * from dept where dname=#{name} for update;
</select>
在方法頭上加上@Transactional
注解(該注解還可以進行隔離級別的配置,這里不再贅述)
@Override
@Transactional
public int concurrent() {
Dept dept = deptDAO.queryByName("TEST");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (dept == null) {
System.out.println("不存在TEST,插入");
Dept nDept = new Dept().setDName("TEST");
deptDAO.addDept(nDept);
} else {
System.out.println("存在");
}
return 1;
}
測試,成功