背景:
當我們需要對數據進行先讀取,滿足某條件再做新增,往往會面臨着線程不安全的問題,導致數據被重復插入。
下面分別舉例子來說明單實例與多實例(集群)下的保證數據安全。
需要用到的工具:
1、並發測試工具JMeter,模擬多用戶並發請求,也就是多個用戶在同一時刻同時情求該接口。
2、ngnix (1.12.2版本) 本地組建集群服務,通過配置完成,來自用戶的請求,從多個服務節點選擇一個來完成該請求響應。nginx會自動做分發,從而達到負載均衡。
需求:
插入80個項目實例到mysql 的project 表中。
一、單節點的情況
主要代碼如下:
service層 class ProjectServiceImpl中:
public void addProject2(Project project) {
int maxCount =100;
int count = 80;
public void addProject2(Project project) {
for (int i = 0; i < maxCount; i++) {
Date date = new Date();
List<Project> projectList = selectProjects(project);
if (projectList.size() >= count) {
return;
}
project.setId(UUID.randomUUID().toString().replaceAll("-", ""));
project.setProjectName(project.getId());
project.setCreateTime(date);
project.setModifyTime(date);
project.setModifier("1");
project.setCreator("1");
insertProject(project);
}
}
控制器層的代碼:
/**
* 新增項目
*/
@PostMapping("addProject")
public RestMessage addProject(@RequestBody Project project) {
service.addProject2(project);
return RestBuilders.successBuilder().build();
}
打開postman開多個tab快速的依次切換tab並點擊send,均成功返回:(此處也可以用JMeter測試)
去navicat 查看數據發現產生了81條數據,而不是80條。其原因就是線程不安全所致,查詢與插入的動作沒有在一個同步代碼塊中
單節點解決上述問題方案:方法加 synchronized 修飾,也可以加在代碼塊,還可以用同步鎖實現。
修改上述addProject2 方法如下:
synchronized public void addProject2(Project project) {
//xxx 省略相同部分
}
此處我們改用更專業的JMeter軟件來測試:設置200個線程(相當於上述步驟的postman開200個窗口同時點擊的效果)
htpp 請求設置如下:
去數據庫工具navicat 查看有多少條該數據:相同project_code共有80條整:
結論:
對於單機服務(單節點),加synchronized 修飾方法,或采用同步代碼塊,或同步鎖可以解決此類問題:代碼中線程不安全導致數據重復插入的問題。
一、集群多節點的情況
還是上述代碼不改,繼續保留synchronized 修飾方法,現在在idea中起兩個服務 LinkappApiApplication 服務端口是5042 LInkappApiApplication-B的服務端口是5043
nginx 集群配置如下,現在是兩個節點 127.0.0.1:5042 和127.0.0.1:5043
啟動nginx
打開JMeter請求,依然是設置200個線程(模擬200個用戶並發)
請求插入項目接口 http://localhost:5042/project/addProject 的請求參數如下:
{
"projectCode": "test_data_3"
}
待請求結束后去mysql 查看插入了多少條:結果卻顯示插入了81條:仍然出現了重復插入的問題:可以看到即使加了synchronized修飾方法仍然有重復插入
問題:如何解決多節點的情況下,數據重復插入的問題?
解答:這里需要用到分布式鎖,數字在做讀和寫的時候需要用分布式鎖將讀寫代碼鎖住。分布式鎖的實現方式有很多種,這里采用基於redis的redisson鎖
需要在pom中引入redisson依賴:
依賴注入對象:
對上面單節點的新增項目的代碼的改造:將addProject2 放在addProject方法中調用:
修改完成后測試:
依然采用JMeter 設置200個線程測試:
去數據庫查看插入的數據:不多不少剛好80條:有效果。
多次測試將JMeter進程數設置為500 ,仍然數據正確,剛好80條插入成功。
原創博文, 如需轉載請言明出處,謝謝!