高併發秒殺專案隨手筆記

卡牌300發表於2024-05-09

1 資料庫基字符集為什麼選擇utf8mb4?

image-20240418162025413

2 在 MyBatis 中,JavaBean 屬性名和資料庫欄位名的對映非常關鍵,正確設定這一對映是保證資料正確封裝到 JavaBean 中的前提。以下是 MyBatis 對映機制的詳細解釋:

1. 預設對映行為

如果在 MyBatis 的 <resultMap> 中沒有明確指定對映關係,MyBatis 預設採用屬性名與欄位名相同的規則來自動匹配。這意味著,JavaBean 的屬性名必須與資料庫表的欄位名完全一致(包括字母的大小寫),否則這些欄位將無法被正確封裝,其對應的屬性值會是 null

例如,如果資料庫中的欄位名為 wife_name,而 JavaBean 中的屬性名為 wifeName,不使用 <result> 指定對映關係的話,wifeName 屬性將會得到 null 值。

2. 指定對映關係

為了解決欄位名與屬性名不一致的問題,可以在 <resultMap> 中使用 <result> 標籤明確指定對映關係。例如:

<resultMap id="resultWifeMap" type="Wife">
<result property="wifeName" column="wife_name"/>
</resultMap>

這樣配置後,wife_name 欄位的值就會正確地對映到 wifeName 屬性上。

3. MyBatis Plus 的自動對映擴充套件

MyBatis Plus 是 MyBatis 的一個擴充套件框架,它提供了更加強大的對映功能,包括自動的駝峰命名轉換(即自動將資料庫中的下劃線分隔命名轉換為 JavaBean 中的駝峰式命名)。這意味著,在 MyBatis Plus 中,即便不透過 <resultMap> 明確指定對映關係,wife_name 也能自動對映到 wifeName 屬性。

4. 總結

  • MyBatis: 需要屬性名和欄位名完全一致,或者透過 <resultMap> 明確指定對映關係。

  • MyBatis Plus: 提供自動的駝峰命名轉換,更加靈活。

在實際開發中,建議根據具體需求選擇合適的工具和配置。如果是在使用標準的 MyBatis,確保你的 <resultMap> 配置正確,或者調整資料庫欄位名和 JavaBean 屬性名使其一致。如果希望有更自動化的對映支援,可以考慮使用 MyBatis Plus。

3 關於在springboot+thymeleaf的專案中Controller層什麼情況下使用的是thymeleaf,返回HTML頁面,什麼情況下返回的是Json資料的說明

在使用 Spring Boot 構建的專案中,如果你想實現前後端分離並向前端返回 JSON 資料,這實際上與是否包含 spring-boot-starter-thymeleaf 依賴並沒有直接關係。spring-boot-starter-thymeleaf 是一個用於檢視模板渲染的依賴,主要用於服務端渲染頁面。如果你的專案是前後端分離的,即便保留了這個依賴,也可以輕鬆返回 JSON 資料。下面是如何做到這一點的詳細解釋:

1. 控制器方法返回 JSON

要使 Spring Boot 控制器返回 JSON 資料,確保你的控制器方法返回的是物件或物件集合,而不是檢視名稱(字串)。Spring Boot 使用 Jackson 庫自動將物件序列化為 JSON 格式。這是標準的做法,如下所示:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Arrays;

@RestController
public class UserController {

@GetMapping("/users")
public List<User> getAllUsers() {
return Arrays.asList(
new User(1, "Alice"),
new User(2, "Bob")
);
}
}

在這個例子中,使用 @RestController 註解確保所有響應都預設以 JSON 形式返回,無需特別指定響應型別。如果使用的是 @Controller 註解,那麼需要在方法上使用 @ResponseBody 註解來指示方法返回的是響應體內容。

2. 關於 spring-boot-starter-thymeleaf

你不需要移除 spring-boot-starter-thymeleaf 依賴來支援 JSON 資料的返回。這個依賴主要是用於處理 HTML 檢視的,如果你的控制器決定了返回型別為非檢視(如直接返回資料模型),Spring Framework 的 DispatcherServlet 將會根據上下文返回相應的內容。也就是說,即使 thymeleaf 在專案中,只要你的方法標記為返回資料(透過 @RestController@Controller+@ResponseBody),返回的就是 JSON 格式的資料。

3. 保留 Thymeleaf 的原因

如果你的專案中還需要渲染一些伺服器端頁面,或者需要生成電子郵件內容等,保留 Thymeleaf 是有意義的。只要適當管理你的控制器和返回型別,就可以在同一個應用中同時支援 HTML 檢視渲染和 JSON 資料響應。

4. 示例控制器同時支援 Thymeleaf 和 JSON

如果你想在同一個專案中同時使用 Thymeleaf 渲染頁面和返回 JSON 資料,可以如下配置控制器:

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class MixedController {

// 返回 Thymeleaf 頁面
@GetMapping("/greeting")
public String greeting(Model model) {
model.addAttribute("name", "World");
return "greeting"; // 返回的是檢視名
}

// 返回 JSON 資料
@GetMapping("/data")
@ResponseBody
public Map<String, String> getData() {
Map<String, String> data = new HashMap<>();
data.put("key", "value");
return data; // 返回的是 JSON 物件
}
}

總結

保留 spring-boot-starter-thymeleaf 對返回 JSON 沒有影響。選擇返回 JSON 或 HTML 檢視取決於你的控制器方法的配置,而不是依賴於是否包含某個特定的依賴。這種靈活性是 Spring Boot 提供的一大優點,使得開發者可以根據專案需要靈活選擇技術方案。

3.2 這裡的重點就是是否有@ResponseBody註解對嗎?也就是說如果返回即使是String型別的資料,只要有這個註解,也不會按照thymeleaf的方式返回?

是的,您完全正確。在 Spring Boot 中,決定響應型別(即是返回 HTML 頁面還是 JSON 資料)的關鍵是 @ResponseBody 註解的使用,以及控制器是否被標記為 @RestController(等同於在每個方法上使用 @Controller@ResponseBody)。讓我們具體解釋一下這兩者如何影響響應內容:

@ResponseBody 註解

當你在一個控制器方法上使用 @ResponseBody 註解時,你告訴 Spring Boot 你希望返回的資料應該直接寫入 HTTP 響應體中,而不是被解釋為檢視名稱。這意味著即使方法返回的是字串,這個字串也會被當作 HTTP 響應的內容返回,而不會去解析為檢視模板路徑。例如:

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class SimpleController {

@GetMapping("/text")
@ResponseBody
public String getText() {
return "Just a plain text"; // 直接返回文字字串
}
}

在上面的程式碼中,即使返回值是一個簡單的字串 "Just a plain text",這個字串也會直接作為響應體返回給客戶端,而不會被當作檢視名來處理。

@RestController 註解

@RestController 註解是 @Controller@ResponseBody 的簡寫。它表明:

  • 類中的所有方法都應該將其返回值直接寫入響應體中。

  • 不需要為每個方法單獨宣告 @ResponseBody

這適用於 RESTful 服務控制器,其中每個操作都是資源狀態的操作,通常需要返回 JSON 或 XML 等非頁面形式的資料。例如:

import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.GetMapping;

@RestController
public class ApiController {

@GetMapping("/number")
public Integer getNumber() {
return 42; // 返回數字,自動轉換為JSON格式的數字
}
}

在這個例子中,方法 getNumber() 返回的整數 42 會自動被轉換為 JSON 格式,並作為 HTTP 響應體返回。

使用 Thymeleaf

保留 spring-boot-starter-thymeleaf 依賴是完全無害的,因為是否使用 Thymeleaf 渲染檢視完全由控制器方法是否返回檢視名稱(且沒有 @ResponseBody)決定。如果你有某些頁面需要伺服器端渲染,你可以在相應的控制器方法中返回檢視名稱:

@Controller
public class ViewController {

@GetMapping("/welcome")
public String getWelcomePage(Model model) {
model.addAttribute("message", "Welcome to our site!");
return "welcome"; // 返回檢視名,由 Thymeleaf 處理並渲染
}
}

在這裡,getWelcomePage 方法返回一個檢視名 "welcome",Spring 將使用 Thymeleaf 或其他配置的檢視解析器來渲染這個檢視模板。

總結

因此,無論是返回 JSON 還是渲染檢視,關鍵在於方法的註解配置(@ResponseBody@RestController vs. @Controller),而不是依賴於是否包含 spring-boot-starter-thymeleaf。這允許 Spring Boot 應用同時支援 RESTful API 和服務端頁面渲染,靈活適應不同的應用場景。

4 在HTML中,當點選型別為button的按鈕時,預設行為是提交其父<form>表單(如果沒有指定type,則button預設為type="submit")。這意味著如果按鈕在一個表單中,點選它將會導致表單資料被提交到表單的action屬性指定的URL(如果未指定action,則提交到當前頁面)。

 <form class="more-padding" id="loginForm" method="post" autocomplete="off">
<input style="" id="mobile" name="mobile"
type="text" placeholder="手機號碼"
required="true"/>
<input style="" id="password" name="password"
type="password" placeholder="密碼" required="true"/>
<button class="button sumbit" onclick="login()">Login</button>
</form>


<script>
function login() {
doLogin();
}

function doLogin() {
//得到使用者在登入表單填寫的密碼
var inputPass = $("#password").val();
//客戶端鹽 Dz7Oyf9b
var salt = g_passsword_salt;
var str = "" + salt.charAt(0) + inputPass + salt.charAt(6);
var password = md5(str);

// console.log("inputPass-->",inputPass)
// console.log("salt-->",salt)
// console.log("password-->",password)


$.ajax({
url: "/login/doLogin",
type: "POST",
data: {
mobile: $("#mobile").val(),
password: password
},
success: function (data) {
if (data.code == 200) {
alert(data.message)
} else {
alert(data.message)
}
},
error: function () {
alert("失敗");
}
});
}
</script>

點選“Login”按鈕導致發出兩次請求的原因通常與表單提交的行為以及JavaScript事件處理有關。在您提供的程式碼示例中,<button>元素的預設行為(提交表單)與透過JavaScript顯式觸發的行為(呼叫login()函式)可能都在執行,導致請求被髮送兩次。下面是具體的分析和解決方法:

分析問題

  1. HTML表單提交:在HTML中,當點選型別為button的按鈕時,預設行為是提交其父<form>表單(如果沒有指定type,則button預設為type="submit")。這意味著如果按鈕在一個表單中,點選它將會導致表單資料被提交到表單的action屬性指定的URL(如果未指定action,則提交到當前頁面)。

  2. JavaScript事件觸發:在您的程式碼中,點選按鈕還會透過onclick="login()"觸發login()函式。這個函式進一步呼叫doLogin(),後者執行了另一個資料提交操作,即透過jQuery的$.ajax方法非同步提交資料。

問題結果

因為同時存在表單預設提交和$.ajax提交,所以每次點選“Login”按鈕時,會傳送兩個請求:

  • 一個是表單的預設提交。

  • 另一個是由$.ajaxdoLogin()函式中發起的非同步請求。

解決方法

為了防止表單的預設提交行為並確保只透過$.ajax傳送資料,您可以修改login()函式,使其在被呼叫時阻止預設行為。這可以透過幾種方法實現:

方法1: 修改按鈕型別

將按鈕的type屬性設定為button,這樣它就不會觸發表單提交:

<button class="button submit" type="button" onclick="login()">Login</button>

方法2: 在login()中阻止預設行為

login()函式中新增一個事件引數,並呼叫preventDefault()來阻止預設行為:

<button class="button submit" onclick="login(event)">Login</button>
function login(event) {
event.preventDefault(); // 阻止表單預設提交行為
doLogin();
}

方法3: 直接在<form>標籤上處理提交事件

移除按鈕的onclick事件,並在表單上設定onsubmit

<form class="more-padding" id="loginForm" method="post" autocomplete="off" onsubmit="return doLogin()">
<input style="" id="mobile" name="mobile" type="text" placeholder="手機號碼" required="true"/>
<input style="" id="password" name="password" type="password" placeholder="密碼" required="true"/>
<button class="button submit" type="submit">Login</button>
</form>
function doLogin() {
var inputPass = $("#password").val();
var salt = g_passsword_salt; // 確保這個變數是正確定義並可用的
var str = "" + salt.charAt(0) + inputPass + salt.charAt(6);
var password = md5(str);

$.ajax({
url: "/login/doLogin",
type: "POST",
data: {
mobile: $("#mobile").val(),
password: password
},
success: function (data) {
if (data.code == 200) {
alert(data.message);
} else {
alert(data.message);
}
},
error: function () {
alert("登入失敗");
}
});

return false; // 阻止表單預設提交行為
}

選擇以上任何一種方法都可以避免雙重提交的問題。通常,我推薦方法3,因為它保留了表單的語義性(使用<form>submit事件),同時清晰地管理了提交行為。

5 JavaBean中屬性引數校驗方式(兩種)

第一種透過自定義校驗器 + 自定義註解的方式完成校驗

image-20240420185041842

第二種 直接將引數校驗的邏輯寫在業務程式碼中 完成校驗

image-20240420185315630

6 您提到的問題關注於如何在Spring框架中處理資料返回流程,特別是在異常處理和模板渲染時如何“脫離”常規的Controller->Service->Controller流程。讓我們詳細探討這兩種情況:

1. 全域性異常處理的資料返回流程

在Spring框架中,全域性異常處理通常是透過@ControllerAdvice@RestControllerAdvice配合@ExceptionHandler實現的。這種處理機制可以“脫離”常規的業務邏輯流程(即Controller->Service->Controller),直接在異常發生時處理並返回響應。

工作原理

  • 當在服務層或控制器層丟擲異常時,而這些異常沒有在當前層次被捕獲處理,則會向上冒泡到Dispatcher Servlet。

  • 如果配置了全域性異常處理器(@ControllerAdvice類),Spring會查詢是否有匹配的@ExceptionHandler方法可以處理這個異常。

  • 一旦找到合適的處理方法,就會直接呼叫此方法來處理異常,並構建響應返回給客戶端,這個過程中不會回到原來的控制器方法。

示例解釋

考慮以下全域性異常處理器定義:

@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler({ArithmeticException.class, NullPointerException.class, AccessException.class})
public String handlerAritException(Exception e, Model model, HandlerMethod handlerMethod){
model.addAttribute("msg", e.getMessage());
return "error/global"; // 指向錯誤頁面的檢視名
}
}

這裡的方法handlerAritException直接返回檢視名,這意味著:

  • 異常資訊傳遞:異常資訊被放入Model,這樣錯誤頁面可以使用這些資訊。

  • 非傳統請求轉發:返回的是檢視名稱,而不是重定向或轉發路徑,Spring MVC會處理這個返回值,並渲染對應的檢視(HTML頁面),然後返回渲染後的HTML頁面給客戶端。

  • 攔截器不干預:這個處理過程中,常規的Spring MVC攔截器不會介入處理異常響應的生成,因為攔截器主要攔截正常的控制器執行流程。

2. Thymeleaf模板的請求轉發

當使用Thymeleaf或其他模板引擎時,檢視解析過程本質上也是一種請求內部處理,但它與傳統的Servlet請求轉發有所不同:

  • 檢視解析:當控制器返回一個檢視名時,檢視解析器(如Thymeleaf檢視解析器)會根據這個檢視名找到相應的模板檔案,然後執行模板渲染。

  • 直接返回渲染結果:模板引擎處理完畢後,生成的HTML內容直接作為響應體返回給客戶端,這個過程沒有新的HTTP請求發生,因此不同於傳統的HTTP重定向或請求轉發。

總結

這兩種“脫離”常規Controller->Service->Controller流程的方法各有特點:

  • 全域性異常處理:提供一種集中處理應用程式中所有異常的方式,能夠直接構造響應或定向到錯誤頁面,簡化了異常管理並提升了使用者體驗。

  • 模板渲染(如Thymeleaf):允許控制器直接返回檢視名稱,由框架內部處理模板渲染並返回HTML,適用於服務端渲染頁面的應用場景。

這些機制都是Spring框架提供的強大功能,旨在簡化開發流程、提升應用的靈活性和響應效率。

7 在Spring和Spring Boot應用中,@Valid@Validated 都用於觸發資料驗證,但它們在使用和支援的功能方面有所不同。下面是這兩個註解的主要用法和區別,幫助你選擇適合特定場景的註解。

@Valid

@Valid 是由 JSR-303 和 JSR-349 Bean Validation API 定義的標準驗證註解。它可以用於任何Java Bean物件上,以觸發對其屬性的驗證約束。

使用場景

  • 應用在方法引數上(如控制器方法中的引數),以確保傳入的引數滿足定義的約束。

  • 可以用在型別級別、屬性級別或方法引數級別。

  • 常用在Spring MVC的控制器或JAX-RS中。

@Validated

@Validated 是由Spring框架提供的,作為@Valid的一個增強版,支援分組功能。它可以在Spring管理的Bean中使用,不僅限於驗證控制器引數,還可以應用於Spring管理的任何Bean。

使用場景

  • 支援分組驗證,允許在驗證時指定一組特定的約束。例如,可以定義一些驗證規則僅在建立時應用,而另一些規則僅在更新時應用。

  • 可用於方法級別和型別級別。

  • 常用於Spring服務或元件中,特別是當你需要在不同的上下文中應用不同的驗證規則時。

示例解析

在你的示例中,使用了@Valid來確保傳入的LoginVo物件滿足驗證約束:

@RequestMapping("/doLogin")
@ResponseBody
public RespBean doLogin(@Valid LoginVo loginVo,
HttpServletRequest request,
HttpServletResponse response) {
log.info("{}", loginVo);
return userService.doLogin(loginVo, request, response);
}

這裡@Valid用於控制器方法引數loginVo,確保在執行doLogin方法體之前,loginVo已透過驗證(例如,檢查是否包含有效的手機號碼和密碼格式)。如果loginVo中的資料不符合@IsMobile等註解定義的規則,將自動拒絕處理請求,並生成一個響應表示輸入錯誤。

選擇 @Valid 還是 @Validated?

  • 如果你需要在控制器層簡單地驗證模型資料,且不需要分組功能,使用@Valid即可。

  • 如果你需要更復雜的驗證邏輯,例如分組驗證或在Spring元件之外進行驗證(如在服務層),應選擇@Validated

通常,在Spring MVC控制器中,@Valid 已經足夠用於處理大部分常規的資料驗證需求。當你的驗證需求超出這些基本場景,或者當你想要利用分組特性來執行更細緻的控制時,@Validated是一個更好的選擇。

2 需要分組,此時就不可以使用@Valid了對嗎

是的,正如你的示例所示,當你需要進行分組驗證時,應使用 @Validated 而不是 @Valid。這是因為 @Valid 註解不支援驗證分組功能,它將對所有應用了驗證約束的欄位進行驗證,而無法區分場景。而 @Validated 註解支援分組功能,使得你可以根據不同的場景應用不同的驗證規則。

分組驗證的使用場景

分組驗證通常在以下情況下非常有用:

  • 建立與更新操作:在建立新記錄(如新品牌)時,可能所有欄位都需要被驗證,但在更新記錄時,某些欄位(如品牌ID)可能不允許更改,因此不需要驗證。

  • 條件驗證:在不同的操作或業務邏輯分支中,可能需要對同一物件的不同欄位集進行驗證。

示例說明

在你的程式碼中:

@RequestMapping("/save")
public R save(@Validated(SaveGroup.class) @RequestBody BrandEntity brand) {
// 業務邏輯
}

這裡使用 @Validated(SaveGroup.class) 表明在處理 save 方法時,只對 BrandEntity 實體中屬於 SaveGroup 這一驗證組的屬性進行驗證。這種方式非常適合處理實體在不同操作(如新增與更新)中的驗證需求。

關於 @Valid 和 @Validated 的選擇

  • @Valid:適用於簡單的驗證場景,無需分組。它是標準JSR-303規範的一部分,與Spring的整合自然不如@Validated靈活。

  • @Validated:Spring特有的驗證註解,支援分組功能,更適合複雜的驗證場景,如同一個物件在不同的操作中需要不同的驗證規則。

因此,當你的驗證需求包括分組功能時,你應該選擇使用 @Validated。這使得在Spring框架中,你可以更靈活地控制驗證邏輯,確保根據具體的業務規則對資料進行正確的校驗。

8 校驗註解@Valid丟擲異常和不丟擲異常的兩種情況

總結如下:

在Spring MVC中,使用@Valid@Validated對方法引數進行校驗時,有兩種處理校驗失敗的方式:如果引數列表中包含BindingResult,所有校驗錯誤將被收集在此物件中,方法繼續執行,允許開發者在方法內部手動處理這些錯誤;如果不包含BindingResult,任何校驗錯誤將導致Spring自動丟擲MethodArgumentNotValidException,這是BindException的子類,通常需要在全域性異常處理器中被捕捉並處理。這樣的設計允許在控制器層靈活地決定如何響應校驗錯誤,無論是直接處理或透過異常機制統一管理。

當在Spring框架中使用@Valid@Validated註解進行資料校驗失敗時,通常不會直接丟擲一個執行時異常。相反,校驗失敗會被Spring的校驗機制捕捉,並且透過特定的異常處理流程來管理。讓我們詳細解析這一過程。

校驗過程與異常處理

  1. 校驗執行

    • 當一個請求到達Spring MVC控制器的方法,且該方法的引數前有@Valid@Validated註解時,Spring會自動執行繫結和校驗過程。

    • 如果引數校驗失敗(即isValid方法返回false),Spring構建一個包含校驗失敗資訊的BindingResultErrors物件。

  2. 異常觸發

    • 在Spring MVC中,如果方法引數後緊跟一個BindingResult,則所有的校驗錯誤都會被收集在這個BindingResult物件中,方法將正常執行,開發者需要手動檢查BindingResult有無錯誤並處理。

    • 如果沒有BindingResult引數跟隨,Spring MVC將會丟擲MethodArgumentNotValidException異常,該異常是BindException的子類,用來表示方法引數校驗失敗。

  3. 異常處理

    • 你已經在GlobalExceptionHandler類中定義了對BindException的處理邏輯。當MethodArgumentNotValidException(即一種BindException)被丟擲時,它會被這個異常處理器捕捉。

    • 異常處理器將校驗失敗的資訊轉換為你定義的錯誤格式,並返回給前端。這允許前端接收到具體的錯誤資訊,並據此顯示相應的使用者提示。

全域性異常處理器中的程式碼

你的全域性異常處理器程式碼如下:

@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public RespBean ExceptionHandler(Exception e) {
if (e instanceof GlobalException) {
GlobalException ex = (GlobalException) e;
return RespBean.error(ex.getRespBeanEnum());
} else if (e instanceof BindException) {
BindException ex = (BindException) e;
RespBean respBean = RespBean.error(RespBeanEnum.BIND_ERROR);
respBean.setMessage("引數校驗異常~:" + ex.getBindingResult().getAllErrors().get(0).getDefaultMessage());
return respBean;
}
return RespBean.error(RespBeanEnum.ERROR);
}
}

為什麼使用繫結異常處理非法輸入

  • @Validated@Valid註解在控制器引數中用於校驗並且校驗失敗時,由於你沒有在方法引數中提供BindingResult來手動處理錯誤,Spring會自動丟擲MethodArgumentNotValidException

  • 由於MethodArgumentNotValidExceptionBindException的一個特定形式,它被你的GlobalExceptionHandler中對BindException的處理邏輯捕獲並處理。

總結

因此,當前端輸入的手機號不合法,並且這一資訊沒有被一個隨後的BindingResult引數捕獲,Spring會丟擲MethodArgumentNotValidException。這個異常在你的全域性異常處理器中被識別為BindException,並相應地生成錯誤響應返回給前端。這是一個優雅的錯誤處理機制,確保使用者得到清晰的反饋而不會影響伺服器的穩定性。

2 如果有在隨後的BindingResult引數捕獲,就不會丟擲這個MethodArgumentNotValidException異常了?

正是如此,如果你在Spring MVC控制器方法的引數列表中,在使用了@Valid@Validated註解的引數後面緊跟一個BindingResult引數,Spring將不會丟擲MethodArgumentNotValidException異常。相反,所有的校驗錯誤將被收集在BindingResult物件中。這樣,你就可以在方法內部檢查是否存在錯誤,並根據錯誤存在與否進行適當的處理。

示例說明

假設你有以下的控制器方法:

@RequestMapping("/register")
public ResponseEntity<?> registerUser(@Valid @RequestBody UserRegistrationDto userDto, BindingResult result) {
if (result.hasErrors()) {
// 提取錯誤資訊,並構造響應體
List<String> errorMessages = result.getAllErrors()
.stream()
.map(ObjectError::getDefaultMessage)
.collect(Collectors.toList());
return ResponseEntity.badRequest().body(errorMessages);
}
// 如果沒有錯誤,繼續執行業務邏輯
userService.register(user);
return ResponseEntity.ok("User registered successfully");
}

在這個例子中:

  • @Valid @RequestBody UserRegistrationDto userDto:這是一個要驗證的DTO物件。

  • BindingResult result:這個引數緊跟在被驗證的物件後面,用於收集和訪問校驗過程中產生的錯誤。

校驗錯誤處理流程

  1. 校驗執行:當請求到達時,Spring會對UserRegistrationDto物件進行校驗。

  2. 錯誤收集:如果存在校驗錯誤,這些錯誤會被自動收集到BindingResult物件中。

  3. 錯誤檢查與響應:在方法內部,你可以使用result.hasErrors()來檢查是否有錯誤發生。如果有錯誤,可以從BindingResult中提取具體的錯誤資訊,並據此構造一個適當的響應返回給前端。

如果沒有BindingResult引數

  • 如果沒有BindingResult引數跟隨,且存在校驗錯誤,Spring會自動丟擲MethodArgumentNotValidException。這種情況下,你需要在全域性異常處理器中捕獲和處理這個異常。

優點與缺點

優點

  • 提供了一種靈活的方式來處理校驗錯誤,可以根據具體情況決定如何響應。

  • 允許在相同的請求處理流程中同時處理正常邏輯和錯誤邏輯,保持程式碼的連貫性。

缺點

  • 如果每個方法都要編寫錯誤處理程式碼,可能會導致程式碼重複,特別是當多個方法需要相似的錯誤處理邏輯時。

總結

透過在方法引數中使用BindingResult,你可以避免MethodArgumentNotValidException的丟擲,並且可以在控制器內部更靈活地處理校驗錯誤。這種方式特別適合需要在同一方法中處理多種結果的場景,允許開發者根據實際需要定製錯誤響應。

9 當你整合了spring-session-data-redis,Spring Session框架會接管應用的HTTP Session,其中Spring Session不使用傳統的JSESSIONID,而是使用SESSION作為Cookie名稱。

將Session儲存到Redis和使用spring-session-data-redis依賴是關鍵點。這種變化(JSESSIONID變成SESSION,以及Session值位數變多)是因為Spring Session的工作機制引起的。下面是詳細的解釋:

1. SESSION 替代 JSESSIONID

當你整合了spring-session-data-redis,Spring Session框架會接管應用的HTTP Session管理。預設情況下,Spring Session不使用傳統的JSESSIONID作為Cookie名稱,而是使用SESSION。這是為了避免和Servlet容器(如Tomcat)預設的Session管理衝突,特別是在分散式Session環境中。

2. Session ID 格式變化

你提到的Session ID格式變化(例如:"ODZiOTZmNzMtY2U3Mi00Y2MwLWE2NTMtZjExNmRiOTA1MzAx"),這種格式是一個基於UUID的字串。Spring Session預設使用這種更安全的UUID格式來生成Session ID,以增強安全性。UUID格式的Session ID更難被預測,這有助於防止Session劫持等安全問題。

3. 配置和使用

如果你想要回到使用JSESSIONID,或者自定義Session ID的生成策略,你可以在Spring的配置中進行調整。例如,要修改Cookie名稱為JSESSIONID,可以透過以下配置:

import org.springframework.session.web.http.DefaultCookieSerializer;

@Configuration
@EnableRedisHttpSession
public class SessionConfig {

@Bean
public DefaultCookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName("JSESSIONID"); // 設定為傳統的JSESSIONID
return serializer;
}
}

這段配置定義了一個Bean,用於自定義Cookie序列化器,將Session Cookie的名稱改回傳統的JSESSIONID

4. 結論

引入spring-session-data-redis是為了提高Session的可擴充套件性和可靠性,尤其是在分散式系統中。這種改變(使用SESSION和UUID格式的Session ID)雖然帶來了一些初步的困擾,但從長遠來看,為系統的擴充套件和安全提供了保障。如果需要,可以透過配置調整這些預設行為以適應特定的需求。

10 為什麼UserArgumentResolver解析器的supportsParameter方法中 aClass == User.class; 選擇使用 == 而非其他型別檢查方法



/**
* @author yangda
* @create 2024-04-23-19:20
* @description: springboot的解析器機制
* UserArgumentResolver 是我們自定義的一個 使用者引數解析器
* 用來解析瀏覽器到Controller層的方法之前,對引數
* 根據 Cookie、request、response、業務service方法 進行解析/封裝 User物件
* 在Controller層的方法形參位置,可以直接拿到User物件的具體資訊
*
* 注意:
* 1. 所有的請求在到達控制層方法之前,都會經過該解析器處理,進行判斷,控制層方法的形參中是否有
* 這裡需要解析的User.class 型別的引數,如果有則進行解析封裝
* 2. 需要將我們這裡自定義的 UserArgumentResolver 解析器
* 加入到WebMvcConfigurer介面的實現類WebConfig中的
* HandlerMethodArgumentResolver 列表中之後,該UserArgumentResolver解析器才生效!!!
*
*/
@Component
public class UserArgumentResolver implements HandlerMethodArgumentResolver {

// @Autowired
@Resource
private UserService userService;


//該方法 判斷你當前要解析的引數型別是不是你需要的
//如果這個方法返回 true 才會執行下面的 resolveArgument 方法
//返回 false 不執行下面的方法
@Override
public boolean supportsParameter(MethodParameter parameter) {
//獲取引數的型別
Class<?> aClass = parameter.getParameterType();
//判斷aClass是不是 user 型別,如果為 true,
// 就執行 resolveArgument,Dog Cat Person等型別都可以,根據實際情況進行封裝
return aClass == User.class;

// 為什麼 `aClass == User.class;` 選擇使用 == 而非其他型別檢查方法
// 使用 == 檢查引數型別是否嚴格為 User 型別
// 使用 == 是適當的,因為:
// 1. 型別標識:`==` 檢查兩個類物件是否指向同一個 Class 例項。
// 2. 類物件唯一性:對於任何給定的由特定類載入器載入的類,JVM 都保證只有一個 Class 物件。
// 3. 效能優勢:使用 `==` 比較記憶體地址比使用 `instanceof`(還考慮子類)效能更好。
// 4. 使用場景:此處需要確保引數型別不僅是 User 型別,而且不包括其子類。
}

/**
* 如果上面的supportsParameter返回true,就執行下面的resolveArgument方法
* 到底怎麼解析,是由程式設計師根據業務來編寫
* 這個方法,類似攔截器,將傳入的引數,取出 cookie 值,然後獲取對應的 User 物件
* 並把這個 User 物件作為引數繼續傳遞.
* @param parameter
* @param mavContainer
* @param webRequest
* @param binderFactory
* @return
* @throws Exception
*/
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer
mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory)
throws Exception {
HttpServletRequest request =
webRequest.getNativeRequest(HttpServletRequest.class);
HttpServletResponse response =
webRequest.getNativeResponse(HttpServletResponse.class);

String ticket = CookieUtil.getCookieValue(request, "userTicket");
if (!StringUtils.hasText(ticket)) {
return null;
}
//根據 cookie-ticket 到 Redis 獲取 User使用者資訊
User user = userService.getUserByCookie(ticket, request, response);

//這裡返回,相當於直接返回到/封裝到了Controller層的控制器方法形參位置上了,
//即如果Controller層的方法 如果使用到引數型別為User.class型別的引數,會直接
//被封裝為從Redis中根據cookie等獲取到的具體的user物件的資訊
return user;
}
}

在你的 UserArgumentResolver Java 類中使用 == 來比較引數的類與 User 類是正確的,這樣的用法能夠如你所預期地工作。

以下是為什麼可以這樣用的原因:

  1. 型別標識: 在 Java 中,使用 == 運算子比較類物件時,它檢查型別標識。這意味著它驗證兩個運算元是否引用記憶體中相同的 Class 物件。在 Java 環境中,由類載入器載入的每個類都由唯一的 Class 物件表示。因此,aClass == User.class 檢查引數的類是否確切地是 User,而不是子類或其他任何類。

  2. 類物件的唯一性: JVM 確保對於特定類載入器載入的任何給定類,將只有一個 Class 物件。這保證了使用 == 進行比較能有效地確定一個類是否為特定型別。

  3. 效能: 使用 == 進行類比較是高效的,因為它僅比較記憶體地址,而不進行更復雜的檢查,如 instanceof,後者還考慮了子類。

  4. 使用場景的具體性: 在你的方法中,你明確檢查引數是否確切地為 User 型別(不包括子類),在這種情況下使用 == 是適當的。如果你想檢查引數是否為 User 的例項或其子類,你應該使用 instanceof 運算子。

總之,在你的 supportsParameter 方法中使用 == 來檢查 parameter.getParameterType() 是否確切地為 User.class 既正確又適當,考慮到在 Java 類比較場景中的具體性和效能考慮。

11 在使用 MyBatis-Plus 的情況下,許多基本的 CRUD 操作和屬性到資料庫列的對映已經由框架自動處理,特別是當你使用了 @TableName@TableId 註解來明確指定了表名和主鍵列時。MyBatis-Plus 預設支援駝峰命名到下劃線命名的自動轉換,這可以覆蓋大多數日常需求。因此,你可能會覺得在這種情況下使用自定義 resultMap 有些多餘。

然而,編寫自定義的 resultMap 還是有其用處和優點的,尤其是在一些特定場景下:

  1. 複雜關聯和整合: 當你的查詢需要涉及到多表聯查、複雜的列到屬性的對映、或者當資料庫列和實體類屬性不能直接對應時(例如,繼承關係中的屬性,或者需要特定的型別處理器),resultMap 可以提供更大的靈活性和控制力。

  2. 非標準的對映需求: 如果某些欄位的對映不遵循常規的駝峰到下劃線規則,或者需要進行特殊處理(比如使用 SQL 函式處理資料後再對映到實體屬性上),resultMap 就顯得尤為重要。

  3. 最佳化和效能考慮: 在一些高效能需求的場景中,可能需要對查詢的結果集進行細粒度控制,包括但不限於指定載入的列、處理懶載入等。透過 resultMap,你可以精確地控制哪些列被載入,以及它們是如何被處理和轉換的。

  4. 明確的對映宣告: resultMap 提供了一種清晰明確的方式來描述資料庫列和 Java 實體類屬性之間的對映關係。這種顯式的對映宣告在維護大型專案或多人協作的專案時尤為有價值,因為它使得對映關係更加透明和易於理解。

在你的例子中,如果 Order 類和資料庫表 t_order 的對映是標準且直接的(即遵循 MyBatis-Plus 的預設對映策略),並且沒有涉及到上述複雜的情況,那麼自定義 resultMap 可能看起來有些多餘。但是,如果專案需求中包含了上述情況中的一種或多種,那麼編寫和使用自定義 resultMap 就非常有意義了。

示例:

當然,讓我們探討一下在使用自定義 resultMap 在 MyBatis 或 MyBatis-Plus 中可能遇到的幾種實用場景:

1. 複雜關聯和整合

場景: 假設你有一個電商應用,其中 Order 實體需要關聯 UserProduct 實體,以獲取使用者資訊和產品詳情。這種情況下,你的查詢可能需要從多個表中拉取資料。

<resultMap id="OrderDetailResultMap" type="com.hspedu.seckill.pojo.Order">
<id column="id" property="id" />
<result column="user_name" property="user.name" />
<result column="product_name" property="product.name" />
<result column="product_price" property="product.price" />
<association property="user" javaType="com.hspedu.seckill.pojo.User">
<result column="user_id" property="id" />
<result column="user_name" property="name" />
</association>
<association property="product" javaType="com.hspedu.seckill.pojo.Product">
<result column="product_id" property="id" />
<result column="product_name" property="name" />
<result column="product_price" property="price" />
</association>
</resultMap>

2. 非標準的對映需求

場景: 考慮一個使用者的積分系統,其中資料庫儲存的是使用者的生日,但你需要計算使用者的年齡來顯示。這需要在 SQL 查詢中使用資料庫函式來計算年齡,並將結果對映到實體類中的年齡屬性。

<resultMap id="UserWithAgeResultMap" type="com.hspedu.seckill.pojo.User">
<id column="id" property="id" />
<result column="name" property="name" />
<result column="birthday" property="birthday" />
<!-- 使用資料庫函式計算年齡 -->
<result column="DATEDIFF(CURDATE(), birthday) / 365" property="age" />
</resultMap>

3. 最佳化和效能考慮

場景: 如果你正在處理一個報表系統,需要從大量資料中提取彙總資訊。在這種情況下,你可能不需要載入所有列,只需特定的幾列來最佳化效能。

<resultMap id="OrderSummaryResultMap" type="com.hspedu.seckill.pojo.Order">
<id column="id" property="id" />
<result column="order_date" property="orderDate" />
<!-- 只載入訂單總金額和狀態,不載入其他詳情以提高查詢效率 -->
<result column="total_amount" property="totalAmount" />
<result column="status" property="status" />
</resultMap>

4. 明確的對映宣告

場景: 當資料庫表結構複雜或包含大量欄位時,明確地透過 resultMap 宣告對映關係可以幫助新成員更快理解資料結構和程式碼,尤其是在需要符合特定業務邏輯處理的大型企業應用中。

<resultMap id="ComplexUserResultMap" type="com.hspedu.seckill.pojo.User">
<id column="id" property="id" />
<result column="first_name" property="firstName" />
<result column="last_name" property="lastName" />
<result column="email" property="email" />
<!-- 明確對映可以減少誤解,特別是當列名和屬性名不直觀對應時 -->
</resultMap>

這些例子展示了在不同情況下,自定義 resultMap 如何提供額外的靈活性和控制,有助於處理特定需求,最佳化效能,並提高程式碼的可維護性和可理解性。

12 mybatis預設不支援駝峰法對映,但是mybatis-plus預設是支援駝峰法對映的

MyBatis 和 MyBatis-Plus 在處理屬性和資料庫列對映的預設行為上有所不同,這確實影響了是否需要額外配置 resultMap

MyBatis 的預設行為

  • 非自動對映:MyBatis 預設並不自動進行駝峰命名(camelCase)到下劃線(underscore)的轉換。如果資料庫的列名是下劃線風格,而Java實體屬性是駝峰風格,開發者通常需要手動配置對映關係,或者在 MyBatis 的配置檔案中設定 mapUnderscoreToCamelCasetrue 來啟用自動對映。

<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>

這個設定告訴 MyBatis 在執行 SQL 對映到 Java 實體時,自動把資料庫中的下劃線命名轉換成Java實體的駝峰命名。

MyBatis-Plus 的預設行為

  • 自動對映:MyBatis-Plus 預設支援駝峰到下劃線的自動轉換。這意味著在大多數常見場景下,你不需要手動寫 resultMap 來處理基本的屬性對映。此外,MyBatis-Plus 提供了許多方便的功能,如自動的 CRUD 操作和更簡單的查詢構造,這些都是基於約定優於配置的原則。

因此,如果你在使用 MyBatis-Plus,並且你的資料模型與資料庫列直接對應(遵循駝峰和下劃線自動對映規則),你可能不需要頻繁使用 resultMap。但如之前所述,對於複雜的SQL操作,如多表聯合、非標準列對映、複雜的聚合或特定的最佳化需求,自定義 resultMap 仍然是有用的。

總結一下,選擇使用 MyBatis 還是 MyBatis-Plus 取決於專案的需求以及你對框架提供的便利性和靈活性的需求。如果專案中有大量標準化的資料庫操作,並且希望儘可能減少配置的工作量,MyBatis-Plus 是一個很好的選擇。但對於需要精細控制SQL表達和資料對映處理的複雜業務場景,MyBatis 提供了更多的控制力和靈活性。

13 你問的是,當在Spring Boot專案中引入了spring-boot-starter-thymeleaf依賴後,為什麼在Controller層返回String型別的值會進入到對應的HTML模板頁面。這個現象的背後機制如下:

Thymeleaf 和 Spring Boot 的整合

  1. 自動配置spring-boot-starter-thymeleaf依賴啟用了Spring Boot的自動配置機制,這意味著Spring Boot會自動配置Thymeleaf模板引擎及其所需的所有元件(例如模板解析器和檢視解析器)。

  2. 檢視解析:在控制器中返回一個字串時,例如返回 "index",Spring MVC 預設將這個字串解釋為檢視的名稱。隨後,Spring的檢視解析器會尋找一個與之匹配的模板檔案,即在src/main/resources/templates目錄下查詢名為index.html的Thymeleaf模板。

控制器返回字串的處理

  • 當一個控制器方法返回型別為String的值時,Spring MVC 會將其視為要渲染的檢視的名稱。這是因為Spring MVC 的設計是以支援MVC(模型-檢視-控制器)架構為目的,其中控制器的職責是處理請求並返回一個檢視名稱,由檢視解析器處理並生成最終的HTML響應。

  • Thymeleaf模板引擎將這些檢視名稱與存放在特定目錄下的模板檔案關聯起來,然後渲染成HTML格式響應給客戶端。

如果只想返回字串

如果你的目的是返回一個簡單的字串而非檢視,可以透過以下方式:

  • 使用@ResponseBody註解:這個註解可以用於方法上,告訴Spring MVC直接將返回的字串作為響應體傳送給客戶端,而不是去解析檢視名稱。

  • 使用@RestController註解:這個註解是@Controller@ResponseBody的組合,適用於REST API服務,自動將所有方法的返回值作為響應體處理。

總之,透過這些配置,你可以根據需要靈活選擇返回HTML頁面或者純文字資料。

14 在springboot中Thymeleaf控制器返回的是一個物件會如何處理

在 Spring Boot 中使用 Thymeleaf 時,控制器如果返回一個物件(而不是字串代表的檢視名稱),處理方式主要依賴於方法上的註解和配置。以下是幾種可能的情況:

1. 使用 @ResponseBody@RestController

如果控制器方法使用了 @ResponseBody 註解或者控制器類使用了 @RestController 註解,Spring MVC 會將返回的物件自動轉換為 JSON 或其他的響應體格式。這是透過已註冊的 HttpMessageConverter 實現的,通常是 MappingJackson2HttpMessageConverter 負責轉換為 JSON。這種情況下,Thymeleaf 不會參與處理返回的物件。

@RestController
public class UserController {
@GetMapping("/user")
public User getUser() {
return new User("Alice", 25); // 返回JSON格式的User物件
}
}

2. 無檢視名返回(錯誤配置)

如果方法返回的是一個物件,且沒有透過 @ResponseBody 明確指示使用訊息轉換器,同時也沒有返回檢視名稱,Spring MVC 會嘗試找到對應的檢視來渲染這個物件。但由於沒有指定檢視名稱,這通常會導致異常,因為 Spring 不知道該如何渲染這個物件。

3. 新增到模型並指定檢視名稱

正確的做法是,在方法中使用 ModelModelMap 型別的引數新增物件,然後返回一個檢視名稱(字串)。這樣,返回的物件就被新增到模型中,Thymeleaf 會使用指定的模板檢視來渲染這個模型。

@Controller
public class UserController {
@GetMapping("/profile")
public String getUserProfile(Model model) {
User user = new User("Alice", 25);
model.addAttribute("user", user);
return "profile"; // 使用profile.html模板
}
}

在這個例子中,"profile" 是一個 Thymeleaf 模板的名稱(通常位於 src/main/resources/templates/profile.html),model.addAttribute("user", user)user 物件新增到模型中,Thymeleaf 會在渲染時使用這個模型。

總結

在使用 Thymeleaf 的 Spring Boot 應用中,控制器返回物件通常需要明確地透過 @ResponseBody 來處理 JSON 返回,或者透過新增物件到模型並返回一個檢視名稱來讓 Thymeleaf 渲染 HTML 頁面。如果返回的是物件本身而沒有適當的註解或檢視名稱,通常會導致錯誤。

15 在 MySQL 使用 InnoDB 儲存引擎的預設情況下,事務的隔離級別通常被設定為 REPEATABLE READ。在這種預設的設定下,以下操作會涉及加鎖:

會加鎖的操作

  1. UPDATE

    • 行鎖:UPDATE 操作會自動為它修改的每一行加上排他鎖(X鎖)。這意味著這些行在事務提交前不能被其他事務修改或刪除,且不能被未使用 LOCK IN SHARE MODE 的 SELECT 語句讀取。

  2. DELETE

    • 行鎖:DELETE 操作會為它刪除的每一行加上排他鎖。這阻止了其他事務對這些行的所有讀寫操作,直到原事務結束。

  3. INSERT

    • 行鎖:INSERT 操作會對插入的新行加上排他鎖。此外,如果涉及到自增列,還會對與自增主鍵相關的內部結構加鎖(通常是自增鎖),以保證ID的連續生成。

特定的 SELECT 操作

  • 在 REPEATABLE READ 和更高隔離級別下,常規的 SELECT 操作不會加鎖,但可以透過以下方式顯式加鎖:

    • SELECT ... FOR UPDATE:加排他鎖,防止其他事務修改或刪除這些資料。

    • SELECT ... LOCK IN SHARE MODE:加共享鎖,其他事務可以讀取這些資料,但不能修改或刪除。

自動加鎖的其他情況

  • 外來鍵約束:當一條記錄因為外來鍵約束而需要引用另一表中的記錄時,可能會在另一表的相關記錄上加鎖,以維護資料的完整性。

  • 間隙鎖(Gap Locks):在某些情況下,如當使用範圍條件但不觸及具體行時,InnoDB 可以使用間隙鎖來防止其他事務在這些間隙中插入資料。

  • Next-key 鎖:這是一種組合鎖,同時鎖定一個記錄和一個間隙。在預設隔離級別 REPEATABLE READ 下,為了防止幻讀,查詢會使用 next-key 鎖。

總結

在 MySQL 的 InnoDB 儲存引擎中,預設情況下,UPDATE、DELETE 和特定的 SELECT 操作會進行加鎖,以保證事務的隔離性和資料的一致性。

16 為什麼執行 queue_header01() 方法時,會將注入的物件從容器中取回來放入到這裡的 .bind() 方法中?

// 建立佇列 queue_header01
@Bean
public Queue queue_header01(){
return new Queue(QUEUE01);
}

// 完成佇列到交換機的繫結,同時宣告要匹配的k-v,以及要以什麼方式來匹配(all/any)
@Bean
public Binding binding_header01(){
Map<String, Object> map = new HashMap<>();
map.put("color","red");
map.put("speed","low");
return BindingBuilder.bind(queue_header01())
.to(headersExchange()).whereAny(map).match();
}

預設情況下,透過 @Configuration@Bean 註解注入的物件是單例的。這意味著 Spring 容器在啟動時會建立這些物件的唯一例項,並且在整個應用程式的生命週期內都會重用這些例項。

在你提供的 RabbitMQHeadersConfig 配置類中,使用了 Spring Framework 的依賴注入(DI)特性來管理和配置應用中的物件。特別是在定義 RabbitMQ 的佇列和交換機繫結時,Spring 的 @Bean 註解被用於宣告應用程式中的各種元件(如佇列、交換機和繫結),並將這些元件作為 Spring 容器管理的 beans。

這裡的 queue_header01() 方法透過 @Bean 註解定義了一個佇列。當 @Bean 註解的方法被呼叫時,Spring 容器會在首次需要時建立該方法所返回的物件例項,並將其儲存在容器中。此後,每當該物件需要被注入到其他部分(如繫結定義)時,容器會從其儲存中取出已存在的例項

binding_header01() 方法中,當你呼叫 queue_header01() 方法時,Spring 容器實際上並不會每次都建立一個新的 Queue 例項。相反,如果已經有一個由 queue_header01() 建立並由容器管理的 Queue 例項,容器將會返回這個已存在的例項。這是 Spring 的依賴注入特性的一個關鍵部分,即重用已經建立的元件,避免不必要的物件建立,從而提高效率和一致性。

在這段程式碼中,.bind(queue_header01()) 呼叫表示將已由 queue_header01() 方法建立和管理的佇列例項繫結到一個交換機。這是透過從 Spring 容器中獲取該佇列的例項來完成的,確保了佇列例項的單一性和一致性。這種方式允許整個應用中的不同部分共享和複用相同的物件例項,同時保持配置的集中管理和易於變更。

17 AtomicInteger failureCount = new AtomicInteger(0);用於在多執行緒環境中進行原子計數操作,確保即使在多個執行緒嘗試同時更新同一個變數的情況下,該變數的操作也是執行緒安全的。

AtomicInteger 是 Java 中的一個類,屬於 java.util.concurrent.atomic 包。它用於在多執行緒環境中進行原子操作,確保即使在多個執行緒嘗試同時更新同一個變數的情況下,該變數的操作也是執行緒安全的。

為什麼使用 AtomicInteger

在多執行緒程式中,簡單的整數增加操作(如 count++)並不是原子的。這個操作實際上包含三個步驟:

  1. 讀取 count 的當前值。

  2. 將值增加 1。

  3. 將新值寫回 count

在多執行緒環境下,如果兩個執行緒幾乎同時執行這個操作,它們可能讀取相同的初始值,都對其加 1,然後寫回,結果就是 count 被增加了 1 而不是預期的 2,這就是典型的競態條件(Race Condition)。

AtomicInteger 透過一種叫做 CAS(Compare-And-Swap)的機制來保證整數操作的原子性。CAS 操作包含三個運算元:記憶體位置(在這裡是 count 的值)、預期原值和新值。CAS 僅在記憶體位置的值與預期原值相匹配時,才將該位置的值更新為新值。這個過程是作為單個不可中斷的操作完成的,從而保證了原子性。

使用示例

incrementAndGet() 方法是 AtomicInteger 提供的一個實用方法,它的作用是原子地將當前值加 1,並返回更新後的值。這比手動實現原子加法要簡單和安全得多。

//使用了 static 修飾符將 AtomicInteger 變數宣告為靜態的,這意味著它屬於類級別,而不是物件級別。
//在 AtomicInteger 類的使用中,new AtomicInteger(0) 用於建立一個新的 AtomicInteger 例項,
//其中的 0 是這個 AtomicInteger 例項的初始值。這意味著計數器開始時的數值為 0。
private static AtomicInteger failureCount = new AtomicInteger(0);

// 在某個需要更新計數的地方
int count = failureCount.incrementAndGet(); // 原子地將計數加 1 並獲取新值

如何應用

在你的程式碼中,你可能要追蹤某個事件(如更新操作失敗)的發生次數。在高併發環境中,如果多個執行緒可能同時增加這個失敗計數,使用 AtomicInteger 就顯得非常有用。每次呼叫 incrementAndGet() 都會安全地增加計數器的值,無論多少執行緒同時進行這個操作。

這種方法特別適用於統計失敗次數、完成任務數量等場景,它簡化了執行緒安全問題的處理,使得你可以專注於業務邏輯的實現,而不用擔心底層的執行緒安全問題。

18 如果服務重啟導致自增長 ID 突然變大,這通常是因為在程式重新啟動時,消費者快速處理了大量積壓在 RabbitMQ 中的訊息。每次嘗試插入資料庫記錄(無論成功還是失敗)都可能導致自增 ID 增加。以下是詳細解釋:

自增 ID 機制

資料庫中的自增 ID(如 MySQL 中的 AUTO_INCREMENT)是用於唯一標識每條記錄的一種機制。當一條新記錄嘗試插入到帶有自增 ID 的表中時,資料庫管理系統(DBMS)會自動為新記錄分配下一個可用的 ID 值。關鍵點在於,這個 ID 一旦被分配給一個插入操作,即使這個插入操作因為某種原因(如資料驗證失敗、違反唯一性約束等)最終未能成功寫入資料,該 ID 通常也不會被回收或重用。

重啟時的訊息消費

當服務重啟後,RabbitMQ 的消費者可能會立即開始處理佇列中的所有積壓訊息。如果這些訊息涉及到資料庫的插入操作,如建立新的訂單等,那麼每個插入操作都會觸發自增 ID 的遞增。

訊息處理和自增 ID 的關係

  • 成功的插入操作:成功的資料庫插入會消耗一個自增 ID。

  • 失敗的插入操作:由於插入失敗不會回收自增 ID,因此即便插入未成功,所嘗試的插入操作仍然會導致 ID 的遞增。

大量積壓訊息導致的影響

如果在服務停止期間積累了大量的訊息,這些訊息在服務重啟時幾乎同時被處理,可能會導致大量的資料庫插入嘗試。每個嘗試都可能增加自增 ID,從而導致 ID 值突然變大。這種現象尤其在高併發的環境中更為明顯,如秒殺場景等。

解決方案

為了避免這種情況,可以採取以下措施:

  • 控制訊息消費的啟動時機:確保所有服務完全就緒後再開始訊息消費。

  • 訊息確認和錯誤處理:使用手動確認模式,並確保訊息在業務邏輯驗證完全透過後才確認。對於處理失敗的訊息,進行合理的錯誤處理,如重試或記錄到特定的死信佇列。

  • 監控和日誌:增加對訊息處理的監控和日誌記錄,確保可以追蹤每個訊息的處理狀態和結果。

透過這些方法,可以更有效地管理服務重啟時的訊息處理,避免由於處理大量積壓訊息而導致的資料庫自增 ID 值突然變大的問題。

你提出的問題很關鍵,這確實表明可能存在一些操作或配置上的矛盾。如果最終有一條記錄成功插入,但之前的嘗試均失敗且導致 ID 顯著增大,可能涉及幾個不同方面的問題,如資料庫約束、事務管理、訊息處理邏輯等。下面是幾個可能的解釋和需要檢查的地方:

1. 資料庫約束和驗證

  • 資料驗證失敗:之前的訊息可能由於不滿足資料庫約束(如唯一性約束、外來鍵約束等)或資料驗證(如欄位驗證不透過)而失敗。最終成功的訊息可能是第一個滿足所有條件的訊息。

  • 條件競爭:在高併發環境下,多個程序或執行緒可能幾乎同時嘗試執行相似的資料庫插入操作。由於條件競爭,一些操作可能因為如行鎖等問題而失敗或被阻塞,直到某個操作成功。

2. 事務回滾

  • 事務管理不當:可能存在部分操作在資料庫層面被視為獨立的事務處理,並在遇到錯誤時回滾,但自增 ID 由於其性質不會回滾。如果事務管理配置不當或每條訊息處理沒有正確管理事務的開始和結束,可能導致這種現象。

3. 訊息處理邏輯

  • 重複訊息處理:如果訊息佇列的消費者沒有正確處理訊息確認和拒絕,可能導致同一訊息被重複處理,嘗試多次插入,但由於業務邏輯或資料問題,只有最後一次嘗試成功。

4. 日誌和監控不足

  • 日誌記錄不完整:如果日誌記錄策略沒有覆蓋所有失敗的嘗試或者日誌級別設定不當,可能導致控制檯輸出看似只有一次成功處理,實際上有多次失敗嘗試。

解決方案建議

  • 審查資料庫約束:檢查資料庫中的約束定義,確保它們適應高併發的應用場景。

  • 最佳化事務處理:確保每個訊息的處理都在正確配置的事務管理之下。可能需要調整事務的隔離級別或確保每次操作前後都正確地開始和結束事務。

  • 完善訊息處理機制:確保訊息處理邏輯中包括了足夠的錯誤處理和回滾機制。使用訊息佇列的高階特性,如訊息拒絕、死信佇列等來處理無法立即成功消費的訊息。

  • 增強監控和日誌:增加對關鍵操作的監控和日誌記錄,特別是資料庫操作和訊息處理的每一步。

透過這些措施,你可以更準確地識別和解決導致自增 ID 異常增長的根本原因,同時最佳化系統的整體穩定性和可靠性。

對於你遇到的問題——服務重啟後自增 ID 值突然增大並且最終有一條記錄插入成功的現象,我們可以從幾個關鍵方面進行總結和分析:

1. 自增 ID 特性

  • 自增 ID 是資料庫用來保證記錄唯一性的一種機制。一旦分配給某個插入操作,即使該操作最終失敗,分配的 ID 也不會被回收或重用。

2. 訊息處理與併發

  • 在服務重啟時,RabbitMQ 的消費者可能會立即處理積壓的訊息,而這些訊息的處理可能涉及到資料庫的插入操作。

  • 如果訊息處理邏輯中缺乏有效的併發控制和事務管理,可能會導致大量插入嘗試,這些嘗試都會遞增自增 ID,即便它們沒有成功插入資料。

3. 資料庫操作的事務性

  • 每個資料庫操作(尤其是插入操作)理論上都應該在一個事務的保護之下執行。如果事務因為驗證失敗或其他資料庫約束而回滾,涉及的自增 ID 仍然會遞增。

4. 失敗的插入嘗試

  • 因為資料驗證失敗、違反資料庫約束或應用層的業務規則校驗不透過等原因,可能導致多次插入嘗試失敗,但每次嘗試都可能消耗一個自增 ID。

5. 最終成功的插入

  • 在多次嘗試後,可能有一條訊息的資料滿足了所有條件和約束,因此最終成功插入。這可能是由於某些併發條件在此之前沒有得到滿足,或者相關資源(如庫存)在之前的操作中被正確更新。

解決策略

  • 增強事務管理:確保資料庫操作特別是插入操作在嚴格的事務管理下進行,避免不必要的自增 ID 消耗。

  • 最佳化併發處理:在訊息處理邏輯中引入更嚴格的併發控制機制,避免多個執行緒或程序同時處理相同的訊息。

  • 改善訊息確認機制:採用手動訊息確認機制,確保只有當所有業務規則和資料驗證透過後,才確認訊息處理成功。

  • 增強日誌記錄和監控:確保所有操作尤其是失敗的操作都有詳盡的日誌記錄,方便問題追蹤和系統監控。

透過這些方法,可以更有效地管理和控制資料庫自增 ID 的使用,避免在服務重啟等特殊情況下出現 ID 異常增長的問題。同時,這也有助於提升系統的穩定性和資料的一致性。

19 是的,你的理解是正確的。在資料庫中,自增 ID 一旦被分配給一個插入操作,即使這個操作後來因為某些原因(如事務失敗)而導致整個事務被回滾,這個自增 ID 也不會被回收或重置。自增 ID 的這種特性確保了即使在併發操作中,每個插入嘗試都會得到一個唯一的識別符號。

關鍵點解釋

  • 自增 ID 分配:當執行插入操作時(例如,透過 orderMapper.insert(order)seckillOrderService.save(seckillOrder)),資料庫自動為新記錄分配一個自增 ID。這一分配是獨立於事務成功與否的。

  • 事務回滾:如果在插入操作後,事務因為任何原因(如業務邏輯失敗、資料驗證錯誤等)觸發了回滾,涉及的資料更改將會被撤銷,但自增 ID 不會回滾。這意味著儘管相關記錄沒有被成功儲存到資料庫中,其分配的 ID 依然被視為已使用。

  • ID 消耗:在高併發的場景下,如秒殺活動中,這可能導致大量的自增 ID 被“消耗”,儘管相應的記錄並沒有被成功建立。這會導致 ID 快速增長,而資料庫中實際的資料量並沒有相應的增加。

實際影響

在秒殺或其他需要處理大量併發寫入的應用中,這種自增 ID 的行為可能導致 ID 空間的快速耗盡。如果這成為一個問題,可能需要考慮以下策略:

  • 更改 ID 生成策略:考慮使用 UUID 或其他形式的 ID 生成策略,以避免依賴資料庫的自增 ID,尤其是在分散式系統中。

  • 最佳化事務處理:儘量在資料庫操作前完成所有必要的驗證,減少事務失敗的可能性,從而減少無用的 ID 分配。

  • 監控和調整:監控資料庫的效能和 ID 的使用情況,及時調整應用邏輯或資料庫配置以應對可能的問題。

透過這些措施,可以更有效地管理自增 ID 的使用,確保應用的穩定性和資料的一致性,特別是在處理高併發資料寫入時。

20 在資料庫中處理事務時,自增 ID 的處理與事務的提交或回滾行為密切相關,但它遵循自己獨特的規則。以下是關於自增 ID 自動分配和事務之間關係的總結,包括一些程式碼示例以便更好地理解。

自增 ID 的自動分配

自增 ID 是資料庫表中一種特殊的列,用於自動生成唯一的識別符號。當向表中插入新記錄時,自增列會自動賦予一個唯一的數字,通常是上一個記錄的 ID 加一。

CREATE TABLE Orders (
OrderID int NOT NULL AUTO_INCREMENT,
OrderNumber varchar(255) NOT NULL,
PRIMARY KEY (OrderID)
);

事務與自增 ID 的行為

事務是一組操作,它們作為一個整體一起執行,確保資料的完整性和一致性。事務具有原子性,意味著事務內的操作要麼全部成功,要麼全部失敗。

@Transactional
public void createOrder(Order order) {
orderMapper.insert(order); // 嘗試插入訂單
// 其他資料庫操作...
}

如果事務失敗,則進行回滾,撤銷所有操作。然而,自增 ID 的行為在此情境中稍有不同:

  • 自增 ID 分配:當記錄插入時,自增 ID 會被分配給新記錄。即使後續操作失敗,這個 ID 也已經被使用。

  • 事務回滾:如果事務中的某個操作失敗,導致事務回滾,那麼除了自增 ID 外,所有資料變動都將被撤銷。已分配的自增 ID 不會被回收,即使相關記錄未成功儲存到資料庫中。

示例說明

假設有一個簡單的訂單插入操作,如果插入操作在事務中失敗了,ID 仍然會遞增。

@Transactional
public void createOrder(Order order) {
try {
orderMapper.insert(order); // 嘗試插入訂單,ID 自增
} catch (DataAccessException e) {
throw new RuntimeException("Insert failed, transaction will rollback.");
// 即使回滾,已分配的 ID 不會復位
}
}

處理自增 ID 快速增長的策略

  • 最佳化業務邏輯:在進行資料庫寫入之前,儘可能在應用層完成所有必要的驗證,以減少失敗事務的發生。

  • 使用替代鍵生成策略:在高併發場景中,考慮使用 UUID 或其他分散式唯一鍵生成策略,以避免依賴自增 ID。

總結

自增 ID 在資料庫事務中的特殊行為需要特別注意,尤其是在設計需要處理大量資料插入的高併發系統時。理解其行為可以幫助開發者更好地設計資料模型和事務管理策略,避免潛在的問題,如 ID 耗盡或效能問題。

21 RabbitMQ 訊息被重複處理造成的死迴圈問題

從你提供的資訊來看,程式在處理從 RabbitMQ 接收到的秒殺請求時遇到了一個問題,這個問題導致了一個重複鍵異常(DuplicateKeyException)。這通常發生在嘗試插入一個已存在的唯一鍵組合到資料庫中時。在你的案例中,這個唯一鍵是由使用者 ID 和商品 ID 組成的,這表示你的系統試圖為同一個使用者對同一商品建立多個秒殺訂單。

分析問題

  1. 重複處理訊息

    • 如果queue方法中的seckill被呼叫多次,可能因為訊息被重複處理。

    • 重複處理可能是由於訊息佇列配置不當(如未正確確認訊息處理成功前就重新傳送了訊息)或者應用邏輯錯誤(如在處理中未能妥善檢查是否已處理過相同的訊息)。

  2. 事務和資料一致性

    • 看起來seckill方法確實在嘗試插入一個已存在的SeckillOrder,這可能是因為之前的操作已經插入了資料,但由於某種原因(如應用重啟或併發執行),相同的操作被重新執行。

  3. 訊息消費確認

    • 如果訊息一旦被消費就立即確認,而實際的業務邏輯執行還未完成或失敗,那麼訊息系統可能會再次投遞同一訊息。這要求業務邏輯處理必須是冪等的,即多次執行相同操作的結果應該和執行一次的結果相同。

解決策略

  1. 確保訊息處理冪等性

    • 修改seckill方法,確保即使它被多次呼叫,也不會重複建立訂單。這通常涉及到在插入之前檢查資料庫中是否已存在對應記錄。

    // 在嘗試建立訂單前檢查是否已存在
    SeckillOrder existingOrder = seckillOrderService.getOne(new QueryWrapper<SeckillOrder>()
    .eq("user_id", user.getId())
    .eq("goods_id", goodsVo.getId()));
    if (existingOrder != null) {
    return; // 如果訂單已存在,直接返回或處理邏輯
    }
  2. 改善訊息確認機制

    • 確保訊息只在完成所有業務邏輯且操作成功後才確認。這可以透過在訊息處理邏輯中適當使用 try-catch 塊,並在 catch 塊中處理錯誤,如重試或記錄錯誤而不是確認訊息。

    @RabbitListener(queues = "seckillQueue")
    public void queue(String message) {
    try {
    // 處理訊息
    processMessage(message);
    // 確認訊息
    } catch (Exception e) {
    // 記錄錯誤,可能的話進行重試
    // 不要確認訊息,讓它可以被重新處理
    }
    }
  3. 日誌和監控

    • 強化日誌記錄和監控,確保可以追蹤每次訊息的處理狀態和結果,尤其是在錯誤發生時。

透過這些改進,你可以更有效地管理和控制訊息的處理,避免因訊息重複處理導致的資料一致性問題。同時,這也有助於提升系統的整體穩定性和可靠性。

22 RabbitMQ 訊息被重複處理,導致嘗試插入重複資料至資料庫,結果觸發了 DuplicateKeyException

在你的系統中,RabbitMQ 訊息被重複處理,導致嘗試插入重複資料至資料庫,結果觸發了 DuplicateKeyException。這個問題表明訊息處理邏輯沒有妥善地處理重複訊息的情況,可能因此引起死迴圈,尤其是在訊息系統不斷地重新傳送那些未被確認消費的訊息。

問題分析

錯誤資訊

2024-05-03 22:45:11.383 ERROR 1084 --- [ntContainer#9-1] c.h.seckill.rabbitmq.MQReceiverMessage   : 處理訊息時發生錯誤

org.springframework.dao.DuplicateKeyException:
### Error updating database. Cause: java.sql.SQLIntegrityConstraintViolationException: Duplicate entry '13300000000-1' for key 'seckill_uid_gid'
### The error may exist in com/hspedu/seckill/mapper/SeckillOrderMapper.java (best guess)
### The error may involve com.hspedu.seckill.mapper.SeckillOrderMapper.insert-Inline
### The error occurred while setting parameters

這個錯誤指示嘗試建立一個已存在的唯一鍵組合(使用者ID和商品ID)的秒殺訂單。這說明相同的使用者對相同的商品重複進行了秒殺嘗試。

程式碼段

@RabbitListener(queues = "seckillQueue")
public void queue(String message) {
log.info("RabbitMQ接收者/消費者 MQReceiverMessage 接收到的訊息是-->" + message);
SeckillMessage seckillMessage = JSONUtil.toBean(message, SeckillMessage.class);
User user = seckillMessage.getUser();
Long goodsId = seckillMessage.getGoodsId();
GoodsVo goodsVo = goodsService.findGoodsVoByGoodsId(goodsId);
try {
orderService.seckill(user, goodsVo);
} catch (DuplicateKeyException e) {
log.error("處理訊息時發生錯誤", e);
// 可能需要處理邏輯,如記錄失敗或者傳送到另一個佇列處理等
}
}

解決策略

  1. 冪等性保證:確保 seckill() 方法能夠冪等地執行。即多次執行相同操作的結果應該和執行一次的結果相同。這通常涉及在進行操作前檢查資料庫中是否已存在相應記錄。

    if (seckillOrderService.exists(new QueryWrapper<SeckillOrder>()
    .eq("user_id", user.getId())
    .eq("goods_id", goodsId))) {
    return; // 如果訂單已存在,則不進行操作
    }
  2. 訊息確認機制:確保訊息在業務邏輯完全成功執行後再確認。這樣,如果處理過程中發生異常,訊息不會被確認,從而避免訊息被不斷重發。

    @RabbitListener(queues = "seckillQueue")
    public void queue(String message) {
    SeckillMessage seckillMessage = JSONUtil.toBean(message, SeckillMessage.class);
    try {
    // 嘗試執行業務邏輯
    orderService.seckill(seckillMessage.getUser(), seckillMessage.getGoodsId());
    // 業務邏輯成功執行後確認訊息
    } catch (Exception e) {
    log.error("處理訊息時發生錯誤", e);
    // 不確認訊息,讓它可以重新入佇列
    throw e; // 丟擲異常,確保訊息重新入隊或移至死信佇列
    }
    }
  3. 錯誤處理和監控:增強錯誤處理和監控能力,尤其是在訊息處理失敗時。考慮設定死信佇列來處理那些無法成功消費的訊息。

    @Bean
    public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(ConnectionFactory connectionFactory) {
    SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
    factory.setConnectionFactory(connectionFactory);
    factory.setDefaultRequeueRejected(false); // 失敗訊息不重新入佇列
    factory.setDeadLetterExchange("some.dlx.exchange");
    factory.setDeadLetterRoutingKey("some-routing-key");
    return factory;
    }

透過上述方法,你可以確保訊息在處理過程中的冪等性、訊息的確認機制和錯誤處理,從而避免了訊息被重複處理的問題。如果訊息處理過程中發生異常,你可以選擇將訊息重新入隊或者移至死信佇列,以便後續處理或監控。這樣可以有效地避免死迴圈問題,並保證系統的穩定性和可靠性。

23 RabbitMQ訊息處理陷阱:重試機制背後的死迴圈陷阱

根據你提供的配置資訊,似乎你在使用RabbitMQ作為訊息佇列,並配置了一些消費者和重試機制。但是,即使你設定了重試機制,但在程式執行過程中如果出現了 "seckill" 中提到的錯誤,會一直執行下去,進入死迴圈。這可能是因為重試機制並沒有針對特定的錯誤進行處理,或者是因為重試次數過多導致的死迴圈。

在你的配置中,重試機制被啟用了,但並沒有指定特定的錯誤型別進行重試。預設情況下,重試會針對所有的消費錯誤,包括臨時的和持久的。如果 "seckill" 錯誤是臨時的,可能會導致無限重試,因為每次都會重新嘗試處理。另外,如果 "seckill" 錯誤是持久的,那麼在達到最大重試次數後,會繼續失敗並重新排隊,導致死迴圈。

要解決這個問題,你可以考慮以下幾點:

  1. 檢查錯誤型別:確定 "seckill" 錯誤是臨時的還是持久的。臨時錯誤可能會透過重試得到解決,但持久錯誤可能需要其他的處理方式。

  2. 指定重試錯誤型別:在重試模板中,可以指定重試的錯誤型別。這樣可以避免對於不應重試的錯誤型別進入死迴圈。

  3. 限制重試次數:確保重試次數不會無限增加,避免因為達到最大重試次數而陷入死迴圈。可以設定一個合理的最大重試次數,或者在達到一定次數後進行人工介入處理。

  4. 記錄錯誤日誌:在處理 "seckill" 錯誤時,記錄詳細的錯誤日誌,包括錯誤型別、發生時間等資訊,以便後續排查和處理。

透過以上方法,你可以更好地管理訊息佇列中的錯誤,並避免因錯誤處理不當而導致的死迴圈。

在訊息佇列中,臨時的和持久的錯誤是指訊息處理過程中可能遇到的不同型別的錯誤,它們的性質和影響有所不同:

  1. 臨時錯誤:臨時錯誤通常是暫時性的,可能是由於網路問題、資源不足或者服務暫時不可用等原因引起的錯誤。這些錯誤在稍後的重試中可能會自行解決,因為它們通常是暫時性的,不會持續影響訊息的處理。例如,暫時無法連線到資料庫或者外部服務的情況。

  2. 持久錯誤:持久錯誤是指訊息處理過程中遇到的持續性錯誤,這些錯誤通常是由於邏輯錯誤、資料錯誤或者環境問題引起的。持久錯誤不太可能透過簡單的重試來解決,因為它們通常需要特定的處理或者人工干預。例如,由於邏輯錯誤導致的無法處理的訊息,或者由於訊息內容格式錯誤而無法處理的情況。

區分臨時錯誤和持久錯誤對於訊息佇列的穩定性和可靠性至關重要。對於臨時錯誤,可以透過重試來嘗試解決,而持久錯誤可能需要更多的處理,可能需要修復程式碼邏輯或者修復資料。在設計訊息處理系統時,需要考慮如何處理這兩種型別的錯誤,以確保系統能夠在各種情況下保持穩定和可靠。

達到最大重試次數後,會重新進行排隊,然後又失敗,又重試,又到了最大嘗試次數,重新排隊...這樣進入的死迴圈?

是的,如果在訊息處理過程中發生永續性錯誤,導致達到了最大重試次數後仍然無法成功處理訊息,而系統又不做其他處理,那麼就可能陷入死迴圈。

在這種情況下,訊息處理失敗後會重新進入佇列,然後又被消費者重新獲取並嘗試處理。如果永續性錯誤依然存在,那麼處理過程會再次失敗,重試次數又會增加。這樣反覆進行直到達到最大重試次數,然後訊息再次進入佇列,週而復始,形成了死迴圈。

為了避免這種情況,你可以考慮以下幾種策略:

  1. 限制重試次數:確保設定了一個合理的最大重試次數,避免無限增加重試次數。

  2. 處理失敗訊息:當訊息達到最大重試次數後,不再將其重新放回佇列,而是將其標記為失敗,並進行後續處理,比如記錄錯誤日誌、傳送警報或者進行人工干預處理。

  3. 指定死信佇列:使用死信佇列來處理無法成功處理的訊息,將失敗訊息轉移到死信佇列中,以便後續分析和處理。

  4. 回退策略:考慮實現一種回退策略,在重試次數達到一定閾值後,可以選擇將訊息傳送到另一個佇列或者延遲佇列中,以便稍後重新嘗試處理,或者進行手動處理。

透過以上策略,可以有效避免陷入由永續性錯誤導致的死迴圈,並提高訊息佇列系統的穩定性和可靠性。

24 秒殺場景中,冪等性的概念

在秒殺場景中,冪等性的概念意味著無論使用者發起多少次秒殺請求,最終都只會成功秒殺一次商品,即使用者最終只會獲得一件商品。這是因為在秒殺系統中,每個使用者只能成功秒殺一次,而不會因為重複請求而導致多次秒殺成功,或者多次扣減庫存。

舉例來說,假設某個使用者正在參與秒殺活動,他發起了一次秒殺請求。如果系統具有冪等性,那麼無論這個請求被系統處理了多少次(可能由於網路問題或者其他原因),最終結果都應該是使用者只成功秒殺了一次,即系統只會扣減一次庫存,並且使用者只會收到一件商品。

冪等性在秒殺系統中非常重要,因為秒殺活動通常會引發大量的請求併發,而且使用者可能會多次嘗試秒殺同一件商品。如果系統沒有實現冪等性,可能會導致使用者多次秒殺成功或者扣減多次庫存,從而引發商品超賣等問題,嚴重影響系統的穩定性和使用者體驗。

因此,在設計和實現秒殺系統時,保證秒殺介面的冪等性是至關重要的,可以透過各種手段來實現,如使用唯一標識來標識每個秒殺請求、限制使用者只能秒殺一次等措施,以確保系統在高併發場景下仍能正確處理使用者的秒殺請求。

冪等性的普遍概念

1 冪等性是指對於同一個操作,無論執行多少次,其結果都是相同的。在電腦科學和工程中,冪等性通常用來描述系統或者操作的行為。具體來說,一個冪等操作是指在多次執行後產生的效果與執行一次時的效果相同。

舉個例子,考慮銀行轉賬操作。無論你執行一次還是多次轉賬操作,最終的結果應該是相同的:從一個賬戶中扣除一定金額,然後將該金額轉入另一個賬戶。如果轉賬操作是冪等的,那麼無論執行多少次,最終的結果都應該是相同的金額變化,而且不會出現額外的或者意外的變化。

在分散式系統中,冪等性尤其重要,因為在網路通訊中可能會發生丟包、超時、重傳等問題,導致訊息或者請求被重複傳送。如果操作是冪等的,那麼即使訊息或者請求被重複傳送,系統也能夠正確地處理,而不會產生額外的副作用或者錯誤。

因此,冪等性是設計和實現分散式系統和網路服務時需要考慮的一個重要概念,可以確保系統在面對重複請求或者訊息時能夠保持一致性和可靠性。

2 讓我們用一個更簡單的例子來解釋冪等性。

想象你是一位快遞員,要將包裹送到客戶家裡。你收到一個送貨任務後,按照地址將包裹送到客戶家裡。現在,我們來看看冪等性的概念如何應用在這個例子中:

  • 冪等性的例子:你拿到了一個送貨任務,按照地址將包裹送到客戶家裡。如果你拿到同一個任務兩次,無論是因為系統錯誤或者其他原因,你都會發現你要做的事情是一樣的:還是按照相同的地址將包裹送到客戶家裡。這就是一個冪等操作,因為無論你執行多少次,最終結果都是相同的——客戶家裡收到了包裹。

  • 非冪等性的例子:相反,如果你的任務是每天都要給客戶送一份新的報紙,而不是單純的送貨。如果你每天都給客戶送一份新的報紙,這個操作就不是冪等的了,因為即使你執行多次,結果也會不同——客戶會收到多份報紙。

所以,冪等性就是說,無論你執行多少次同樣的操作,最終的結果都是一樣的。

25 解釋下為什麼這裡使用happyCaptcha,後端直接將驗證碼影像作為二進位制資料流直接寫入HTTP響應,前端的img中就可以顯示圖片了?

image-20240504180921453

image-20240504181015623

好的,這裡涉及到HTTP協議和瀏覽器如何處理影像資料的基本工作原理。當後端將驗證碼影像作為二進位制資料流直接寫入HTTP響應時,前端的 <img> 標籤能夠顯示影像的過程如下:

  1. HTTP 響應

    • 當後端生成驗證碼影像後,它使用 HTTP 響應直接傳送這個影像的二進位制資料。這通常涉及設定正確的響應頭,尤其是 Content-Type。對於影像,Content-Type 應該設定為對應影像格式的MIME型別,如 image/jpeg, image/png 等。

    • 這樣做告訴瀏覽器這個響應包含的是影像資料,並說明了影像的格式。

  2. 瀏覽器處理

    • 瀏覽器在接收到包含影像資料的HTTP響應時,會檢視響應的 Content-Type,從而知道接收到的資料是什麼型別。

    • <img> 標籤的 src 屬性被設定為請求驗證碼的URL(例如 /seckill/captcha?goodsId=...)。當這個URL被載入時,瀏覽器自動傳送HTTP請求到這個URL。

  3. 影像顯示

    • 一旦瀏覽器接收到影像資料,它就開始解析這些二進位制資料,根據 Content-Type 指示的格式將其轉換為影像。

    • 解析完成後,瀏覽器會在 <img> 標籤所在的位置渲染這個影像。因為這個 <img> 標籤的 src 屬性指向了驗證碼的生成連結,所以這個影像就是生成的驗證碼。

透過這種方式,驗證碼的生成、傳送和顯示是一個連續的流程,能夠確保每次請求都能得到一個新的驗證碼影像,並且這個影像是在使用者需要進行操作(如提交表單或開始秒殺)時實時生成的。這增加了驗證碼的安全性,因為它減少了驗證碼被預先抓取或重用的可能性。

確實,這個過程可以透過以下簡要的步驟和程式碼示例來總結:

步驟概述

  1. 後端生成影像

    • 後端接收到請求驗證碼的API呼叫。

    • 生成驗證碼影像並將其以二進位制流的形式傳送,同時設定合適的 Content-Type

  2. 前端顯示影像

    • <img> 標籤的 src 屬性設定為指向驗證碼生成的URL。

    • 瀏覽器請求該URL,接收響應並在 <img> 標籤中顯示影像。

程式碼示例

後端程式碼 (Java/Spring Boot)

@RequestMapping("/captcha")
public void generateCaptcha(HttpServletResponse response) throws IOException {
// 生成驗證碼
BufferedImage captchaImage = captchaService.createCaptchaImage();

// 設定響應型別為影像
response.setContentType("image/png");

// 將影像寫入響應輸出流
ImageIO.write(captchaImage, "PNG", response.getOutputStream());
}

前端HTML和JavaScript

<img id="captchaImg" src="" style="display:none;">

<script>
function showCaptcha() {
var img = document.getElementById('captchaImg');
img.src = '/captcha'; // 設定請求驗證碼的URL
img.style.display = 'block'; // 顯示影像
}
</script>

在這個示例中,後端處理函式 generateCaptcha() 負責生成影像並透過HTTP響應傳送。前端透過修改 <img> 標籤的 src 屬性並顯示該標籤來請求和顯示影像。這種機制確保了驗證碼影像是按需生成和顯示的,增強了安全性和互動性。

這裡的重點為給img設定了src後就會自動的傳送一次請求對嗎?

是的,正是如此。在HTML中,當你為<img>標籤設定src屬性後,瀏覽器會自動向該src屬性所指向的URL傳送HTTP請求以獲取影像資料。這個機制是瀏覽器內建的,用於載入並顯示影像資源。

這種自動請求的特性使得在需要時動態改變<img>src屬性成為了一種常見且有效的方式來更新頁面上的影像。例如,在驗證碼的場景中,每次使用者需要新的驗證碼時,後端可以生成新影像,而前端僅需更新<img>標籤的src屬性來重新觸發請求,從而獲取並顯示新的驗證碼影像。

這也是為什麼在很多動態互動的Web應用中,驗證碼更新或影像輪播等功能會利用修改<img>標籤的src屬性來實現影像的更新和管理。

26 ajax 在data中攜帶資料會根據請求型別不同get/post,攜帶資料的形式也不同

當發出的是get請求時,在data中的資料,會被放到url後面

GET /seckill/path?goodsId=1&captcha=14803

 $.ajax({
url: "/seckill/path",

type: "GET",

// captcha: captcha 中的第一個 captcha 需要和後端方法形參位置要封裝的引數名對應才可以封裝成功
data: {
goodsId: goodsId,
captcha: captcha,
},

image-20240504184430493

image-20240504184626700

當發出的是get請求時,在data中的資料,會被放到url後面

POST /seckill/path

 $.ajax({
url: "/seckill/path",

type: "POST",

// captcha: captcha 中的第一個 captcha 需要和後端方法形參位置要封裝的引數名對應才可以封裝成功
data: {
goodsId: goodsId,
captcha: captcha,
},

請求體中的資料形式為:

image-20240504185003151

goodsId=1&captcha=222

image-20240504184903874

27 在自定義的攔截器中,可以得到要訪問的目標方法的資訊嗎?如要訪問的是哪個方法,要訪問的方法的定義形式,形參之類的?

在Spring框架中使用HandlerInterceptor介面實現的攔截器,可以在某種程度上獲取到要訪問的目標方法的資訊,但這取決於handler物件的具體型別。通常,handler引數是一個處理器(Controller中的方法)的表示,可以是HandlerMethod物件的例項,如果請求對應的是一個由Spring MVC處理的方法。

解析HandlerMethod

handlerHandlerMethod的例項時,你可以透過它獲取到很多關於目標方法的詳細資訊,包括方法的名稱、所屬類、引數型別等。以下是如何在攔截器中獲取這些資訊的示例程式碼:

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 檢查handler的型別
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;

// 獲取方法名
String methodName = handlerMethod.getMethod().getName();

// 獲取方法所在的類
Class<?> controllerClass = handlerMethod.getBeanType();

// 獲取方法的引數型別
Method method = handlerMethod.getMethod();
Class<?>[] paramTypes = method.getParameterTypes();

log.info("即將執行的方法名: {}", methodName);
log.info("方法所在的類: {}", controllerClass.getSimpleName());
log.info("方法的引數型別: {}", Arrays.toString(paramTypes));

// 更多的詳細操作...
}

return true;
}

限制和注意事項

  • 訪問許可權:雖然你可以獲取方法的名稱、引數型別等資訊,但你不能訪問方法引數的實際執行時值,除非這些值被作為請求引數傳遞或以某種方式儲存(例如,在請求屬性或會話中)。

  • 靜態資源和直接的Servlet呼叫:如果請求是針對靜態資源或不經過Spring的直接Servlet呼叫,handler可能不是HandlerMethod的例項,而是其他型別,如ResourceHttpRequestHandler等,在這種情況下你將無法獲取到方法級的資訊。

實際應用

在實際應用中,使用攔截器獲取目標方法的資訊非常有用,尤其是在執行許可權檢查、日誌記錄、或者進行特定的業務邏輯處理前驗證方法的特定屬性時。這使得攔截器成為Spring MVC中功能強大且靈活的工具,可以用來增強應用的控制流和業務邏輯處理能力。

28 對於"訪問許可權:雖然你可以獲取方法的名稱、引數型別等資訊,但你不能訪問方法引數的實際執行時值",自定義攔截器中的這三個方法都是這樣嗎?都不能訪問方法引數的實際執行時值?

在使用Spring框架的HandlerInterceptor介面實現的攔截器中,確實有限制訪問控制器方法引數的實際執行時值。這是因為HandlerInterceptor介面的設計主要是用來攔截請求的處理過程,在處理請求之前(preHandle)、之後(postHandle)以及請求完成後(afterCompletion)進行操作,但它不提供直接訪問方法引數實際值的功能。

分析攔截器的三個方法:

  1. preHandle

    • 執行於控制器方法之前。

    • 此時還沒有呼叫目標方法,因此無法訪問到方法的引數值。

    • 最常用於許可權檢查、日誌記錄或請求預處理。

  2. postHandle

    • 執行於控制器方法之後,檢視渲染之前。

    • 此時雖然目標方法已執行,但HandlerInterceptor介面並不提供直接訪問控制器方法引數的機制。

    • 可以訪問並修改ModelAndView物件,但這不包括方法引數的值。適用於修改檢視層資料或進一步處理請求(如新增額外模型資料)。

  3. afterCompletion

    • 請求完成後執行,即檢視渲染之後。

    • 用於資源清理或記錄整體請求處理的日誌。

    • 此時無法訪問控制器方法的引數值,因為請求已經處理完畢。

如何訪問方法引數的值?

要在攔截器中訪問方法引數的實際值,可以考慮以下替代方案:

  • 使用AOP(Aspect-Oriented Programming):Spring AOP可以在方法執行前後織入邏輯,允許訪問方法的引數值。透過定義切面(Aspect),可以在方法呼叫前後獲取引數值進行操作。

  • 透過Request物件:如果方法引數是作為請求引數傳遞的,可以透過HttpServletRequest物件從請求中直解析這些引數。例如,使用request.getParameter("paramName")獲取。

總結來說,HandlerInterceptor設計的重點在於提供在請求處理過程中的攔截點,並不直接支援訪問控制器方法引數的實際值。如果需要這種功能,可能需要考慮使用Spring AOP或其他機制來達成。

29 注入攔截器的方式分析

在Spring框架中,當你在配置類中直接例項化攔截器並將其註冊到InterceptorRegistry時,實際上並不需要在攔截器類上使用@Component註解來注入到Spring的IoC容器中。這是因為在這種情況下,攔截器的例項是在配置類中直接建立的,而不是透過容器自動掃描和管理。

解析兩種注入攔截器的方式:

  1. 直接例項化並註冊 在你的配置類中,你直接使用new LoginInterceptor()來建立攔截器的例項,並透過addInterceptor()方法註冊它。這種方法不依賴於Spring的自動元件掃描,因此不需要@Component或其他相關注解。

    @Bean
    public WebMvcConfigurer webMvcConfigurer(){
    return new WebMvcConfigurer() {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new LoginInterceptor())
    .addPathPatterns("/**")
    .excludePathPatterns("/", "/login", "/images/**");
    }
    };
    }
  2. 透過元件掃描 如果你選擇使用元件掃描來管理攔截器,那麼你可以在攔截器類上使用@Component註解。這樣,Spring會自動將攔截器類作為一個Bean例項化並管理。但在這種情況下,通常還需要在配置類中引用這個Bean,而不是直接建立新例項。

    @Component
    public class LoginInterceptor implements HandlerInterceptor {
    // 實現方法
    }

    然後在配置類中這樣引用:

    @Autowired
    private LoginInterceptor loginInterceptor;

    @Bean
    public WebMvcConfigurer webMvcConfigurer(){
    return new WebMvcConfigurer() {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(loginInterceptor)
    .addPathPatterns("/**")
    .excludePathPatterns("/", "/login", "/images/**");
    }
    };
    }

總結

  • 當你在配置類中直接新建攔截器例項時,不需要使用@Component註解。

  • 如果你希望Spring管理攔截器的生命週期,比如當攔截器中需要注入其他Bean或使用Spring的功能(如自動注入,生命週期回撥等),則可以使用@Component註解,並透過自動注入(如@Autowired)來獲取攔截器例項。

在你的示例中,第一種方法(直接例項化並註冊)是合適的,不需要@Component註解,因為攔截器是在配置類中顯式建立的。

關鍵點是確保Spring IoC容器中存在攔截器的一個例項,這樣才能在Spring MVC的配置中引用並正確地註冊這個攔截器。

是的,你的理解是正確的。無論是使用@Bean還是@Component註解,關鍵點是確保Spring IoC容器中存在攔截器的一個例項,這樣才能在Spring MVC的配置中引用並正確地註冊這個攔截器。這兩種方法都可以達到這個目的,但它們在使用場景和控制程度上有所不同:

使用@Bean在配置類中註冊攔截器

  • 控制度高:你可以在建立攔截器例項時進行更詳細的配置,比如設定特定的屬性或依賴。

  • 明確指定:在配置類中直接例項化和註冊攔截器,可以明確地看到攔截器的使用和配置過程,適合於需要特定配置的情況。

  • 區域性應用:適合於只在特定的配置中需要使用攔截器的場景。

使用@Component進行元件掃描

  • 自動管理:Spring將自動處理攔截器的生命週期和依賴注入等,適用於攔截器內部需要依賴其他Spring管理的Bean的情況。

  • 全域性應用:攔截器作為元件被Spring管理,可以在多個地方被重用,無需在每個配置類中重複建立例項。

  • 便於維護:對於大型專案,使用元件掃描可以減少配置的複雜性,使專案結構更清晰。

選擇使用哪種方式

選擇@Bean還是@Component取決於你的具體需求:

  • 如果攔截器的配置和使用非常特定,並且希望避免全域性影響,使用@Bean可能更合適。

  • 如果攔截器將在應用中廣泛使用,或者需要Spring管理其生命週期和依賴,那麼使用@Component更為合適。

最終,確保IoC容器中存在攔截器的一個例項,並且在Spring MVC的配置中正確註冊,是使攔截器生效的關鍵。這可以透過以上任一方法實現,選擇哪種方法取決於你對專案的架構和管理的偏好。

30 自定義註解,屬性的宣告方式 int maxCount();和int maxCount = 5;有什麼不同之處?

package com.hspedu.seckill.config;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* @author yangda
* @create 2024-05-06-11:45
* @description:
* 自定義註解,在實現springmvc底層機制時學過
* 自定義註解@AccessLimit
*/
@Retention(RetentionPolicy.RUNTIME) //RetentionPolicy.RUNTIME:編譯器將把註解記錄在 class 檔案中. 當執行 Java 程式時, JVM 會保留註解. 程式可以透過反射獲取該註解
@Target(ElementType.METHOD) //指定自定義註解可以用來修飾那些程式元素
public @interface AccessLimit {

int second();//時間範圍
int maxCount();//訪問的最大次數
// int maxCount = 5;//這種定義方式是不合法的

/**
* 在Java註解中,所謂的“屬性”實際上透過無引數的方法來定義。
* 這些方法可以定義預設值,但它們的行為與Java類中的欄位或屬性不同。註解中的這些方法:
*
* 必須沒有引數。
* 不能有throws子句。
* 返回值必須是基本資料型別、字串、類、註解或這些型別的陣列。
* 在使用註解時,這些方法的返回值表現為註解的屬性。
*/


boolean needLogin() default true;//是否需要登入

}

在Java註解定義中,int maxCount();int maxCount = 5;表示的是完全不同的概念,且int maxCount = 5;在註解定義中實際上是不合法的。

解析

  1. int maxCount();

    • 這是一個註解方法宣告。在Java註解中,這樣的宣告用於定義一個註解的屬性,這裡定義了一個名為maxCount的屬性,其型別是int

    • 當使用此註解時,除非為maxCount提供了預設值,否則使用此註解的地方必須顯式指定maxCount的值。

  2. int maxCount = 5;

    • 這個語法在Java註解定義中是不合法的。Java註解不允許定義具有初始化值的欄位,因為註解僅用於宣告一些屬性(可以帶預設值),而不是儲存具體的資料。

    • 在Java註解中,如果你想為某個屬性提供一個預設值,應該使用如下語法:

      int maxCount() default 5;
    • 上述程式碼正確地定義了maxCount屬性,併為它提供了預設值5。如果使用註解時未顯式指定maxCount的值,那麼它將自動採用預設值5

正確的註解定義示例

基於你的描述和需求,正確的AccessLimit註解定義應該如下:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AccessLimit {

int seconds(); // 定義一個時間範圍屬性,需要使用註解時指定
int maxCount() default 5; // 定義最大訪問次數,提供預設值為5
boolean needLogin() default true; // 定義是否需要登入,提供預設值為true
}

在這個修改後的定義中:

  • 每個屬性都是一個方法宣告,可能帶有預設值。

  • 這使得使用註解時更加靈活,可以只指定部分屬性,未指定的屬性將自動採用其預設值。

  • seconds()需要在使用註解時顯式指定,因為它沒有預設值。

為什麼在自定義註解中不可以像在Java類中一樣定義一個屬性?

在Java中,註解的設計是為了提供一種後設資料的宣告方式,而不是作為常規類使用。註解的設計意圖是用來標記程式碼和注入後設資料資訊,這些資訊可以在編譯時或執行時被讀取和處理。註解的語法和常規Java類有以下關鍵區別,這些區別也解釋了為什麼在註解中不能像在Java類中一樣定義屬性:

1. 屬性與方法定義

在Java註解中,所謂的“屬性”實際上透過無引數的方法來定義。這些方法可以定義預設值,但它們的行為與Java類中的欄位或屬性不同。註解中的這些方法:

  • 必須沒有引數。

  • 不能有throws子句。

  • 返回值必須是基本資料型別、字串、類、註解或這些型別的陣列。

  • 在使用註解時,這些方法的返回值表現為註解的屬性。

2. 預設值限制

註解的方法可以有預設值,這透過使用default關鍵字來指定。這樣的設計允許在不提供某些註解屬性時使用預設的值,簡化了註解的使用。這與Java類中欄位的預設值有相似之處,但在語法和使用上更嚴格。

3. 編譯時處理

註解的定義旨在編譯時就確定其結構。這意味著註解的屬性必須在編譯時就完全定義好,不允許在執行時修改。相比之下,Java類的例項欄位可以在執行時動態改變。

4. 用途和使用場景

註解通常用於配置和後設資料描述,如標記測試方法、配置資料驗證或提供配置資訊。這些用途需要註解在編譯時就完全確定,以便編譯器和執行時環境能夠利用這些資訊進行最佳化和配置。

5. 簡潔和目標明確

註解的設計強調簡潔和專一的目的。在註解中引入類似Java類的屬性定義會增加語言的複雜性,並可能引起誤用。例如,註解不是用來儲存狀態資訊的,它們的主要作用是影響程式的行為,而不是作為資料的容器。

總結來說,註解中的“屬性”透過定義特定的方法來實現,這與Java類的欄位不同。這種設計符合註解的使用目的和限制,確保註解提供一種高效、明確且易於理解的方式來新增程式碼後設資料。

31 @ResponseBody 加 RespBean 的形式給前端返回資料,響應體中返回json字串形式

image-20240506133222924

image-20240506133406351

32 以流的形式給前端返回資料,響應體中返回json字串形式

 render(response,RespBeanEnum.SESSION_ERROR);



//方法: 構建返回物件,以流的形式返回
private void render(HttpServletResponse response,RespBeanEnum respBeanEnum) throws IOException {

response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
PrintWriter out = response.getWriter();
//構建RespBean
RespBean error = RespBean.error(respBeanEnum);
//把RespBean轉成 json 字串-使用工具類 jackson
//然後以流的形式返回給前端
out.write(new ObjectMapper().writeValueAsString(error));
out.flush();
out.close();


}

image-20240506150144670

image-20240506150002026

結論: "以流的形式給前端返回資料,響應體中返回json字串形式" 與 "@ResponseBody 加 RespBean 的形式給前端返回資料,響應體中返回json字串形式" 前端接收到的形式是相同的,都是在響應體中json格式接收到資料的

33 ThreadLocal 問題

1在多使用者多執行緒的環境中,ThreadLocal提供了一種極其有效的方式來維護每個執行緒(通常對應一個使用者請求)的資料隔離,即使所有執行緒都訪問相同的ThreadLocal靜態例項。

每個執行緒(處理一個請求)都在其自己的ThreadLocalMap中運算元據,保證了資料的隔離性。

下面將詳細分析這一過程,並提供相應的核心程式碼示例。

資料隔離的工作原理

ThreadLocal利用每個執行緒持有自己的ThreadLocalMap來實現資料隔離。雖然ThreadLocal變數是靜態的(即類級別的,所有例項共享),每個執行緒透過ThreadLocal例項存取的資料實際上儲存在它自己的ThreadLocalMap中。這保證了即使多個執行緒訪問同一個ThreadLocal物件,它們也只能訪問各自獨立儲存的資料。

核心程式碼示例

假設我們有一個ThreadLocal用於儲存每個HTTP請求的使用者資訊(例如,當前登入的使用者物件):

public class UserContext {
private static final ThreadLocal<User> currentUser = new ThreadLocal<>();

public static void setUser(User user) {
currentUser.set(user); // 將當前使用者儲存在當前執行緒的ThreadLocalMap中
}

public static User getUser() {
return currentUser.get(); // 從當前執行緒的ThreadLocalMap中獲取使用者資訊
}

public static void clear() {
currentUser.remove(); // 清除當前執行緒的ThreadLocalMap中的使用者資訊,防止記憶體洩漏
}
}

應用場景

在一個典型的Web應用中,每個HTTP請求由一個單獨的執行緒處理。你可以在處理請求的初始階段設定使用者資訊,並在請求處理完畢後清除這些資訊:

public class UserAuthenticationInterceptor implements HandlerInterceptor {

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 假設已經有某種方式獲取到了使用者資訊
User user = authenticate(request);
UserContext.setUser(user); // 在進入處理器之前設定使用者
return true;
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
UserContext.clear(); // 確保在請求完成後清除ThreadLocal中的資料
}

private User authenticate(HttpServletRequest request) {
// 認證邏輯,返回使用者物件
return new User(); // 示例
}
}

總結

  • 隔離性:每個執行緒(處理一個請求)都在其自己的ThreadLocalMap中運算元據,保證了資料的隔離性。

  • 安全性:由於資料是執行緒區域性的,不同執行緒之間的資料操作不會互相影響,從而避免了同步問題。

  • 清理必要性:確保在適當的時間呼叫ThreadLocal.remove()來清理儲存的資料,防止記憶體洩漏,特別是在使用執行緒池的情況下。

透過使用ThreadLocal,可以高效且安全地在多執行緒環境中管理與每個請求相關的資料,而無需擔心多個請求之間的資料衝突或需要複雜的同步機制。

2 建立了兩個ThreadLocal物件來儲存不同的資料

是的,你的觀察是正確的。在你的示例中,建立了兩個ThreadLocal物件來儲存不同的資料(一個儲存Dog物件,另一個儲存Pig物件)。這是因為每個ThreadLocal例項只能維護一個執行緒本地變數的副本。如果你希望在同一個執行緒中關聯多種型別的資料,每種型別的資料都需要一個單獨的ThreadLocal物件。

為什麼需要多個ThreadLocal物件?

  1. 資料隔離:每個ThreadLocal例項為其儲存的資料提供隔離,確保資料不會在不同的ThreadLocal例項之間產生衝突。

  2. 型別安全:每個ThreadLocal可以儲存不同型別的物件。在你的例子中,一個ThreadLocal用於儲存Dog型別,另一個用於儲存Pig型別。這樣做可以保持程式碼的型別安全和清晰性。

  3. 執行緒安全:由於每個執行緒只能訪問自己的ThreadLocal副本,因此在多執行緒環境中操作ThreadLocal是安全的。

示例解析

在你的示例中,當新執行緒開始執行Task時,它將一個Dog物件和一個Pig物件分別設定到兩個不同的ThreadLocal例項中。這意味著這兩個物件被儲存在獨立的執行緒本地儲存區域中,並且在整個執行緒的生命週期內,只有這個執行緒可以訪問這些物件。

使用場景

使用ThreadLocal是解決多執行緒程式中資料隔離問題的常見方法。它廣泛用於:

  • 使用者會話管理:在Web應用中,經常使用ThreadLocal來儲存使用者的會話資訊。

  • 事務管理:資料庫事務的管理,可以透過ThreadLocal來維護與每個執行緒關聯的資料庫連線物件。

  • 效能最佳化:在需要頻繁建立和銷燬的物件(如日期格式化工具SimpleDateFormat)上,使用ThreadLocal可以減少物件建立的開銷。

注意事項

雖然ThreadLocal提供了方便的執行緒區域性變數管理,但使用它時也需要注意:

  • 記憶體洩漏:如果ThreadLocal儲存的物件生命週期過長或執行緒一直不結束,它們可能不會被垃圾收集器回收,從而導致記憶體洩漏。這通常在使用執行緒池時發生。

  • 清理資源:在不再需要儲存在ThreadLocal中的物件時,應該呼叫ThreadLocal.remove()方法來清理資源,防止記憶體洩漏。

透過理解這些原則和最佳實踐,你可以更有效地在多執行緒環境中使用ThreadLocal來管理執行緒區域性變數。

3 我想知道這裡每一個請求都會有對應的一個ThreadLocal物件嗎?但是在上面確實static的啊

在Java中,ThreadLocal雖然被宣告為static,這確實意味著ThreadLocal例項本身是屬於類的,而不是屬於類的某個物件。然而,這並不意味著所有執行緒共享同一個ThreadLocal中的資料。實際上,ThreadLocal的設計目的就是為每個執行緒提供一個獨立的變數副本。這意味著每個執行緒訪問該ThreadLocal變數時,都只能看到和修改自己執行緒的資料副本。

如何理解ThreadLocal的工作機制?

  • 執行緒隔離性:儘管ThreadLocal物件作為類的靜態成員被所有物件共享,但每個執行緒訪問這個靜態ThreadLocal變數時,都只是訪問其為當前執行緒維護的、獨立的、執行緒區域性的資料副本。

  • 儲存機制ThreadLocal內部使用一個名為ThreadLocalMap的內部類來儲存資料,每個執行緒都有自己的ThreadLocalMap例項。ThreadLocalMap的鍵是ThreadLocal物件本身,而值是儲存在ThreadLocal變數中的資料副本。

每個請求對應一個ThreadLocal物件嗎?

每個HTTP請求通常由一個單獨的執行緒處理(尤其是在使用執行緒池的Web伺服器中)。如果在處理請求的程式碼中使用了ThreadLocal,那麼:

  • 每個請求的執行緒:每個請求都會在其對應的執行緒中處理,並且這個執行緒將擁有自己的ThreadLocal變數副本。不同的請求即使同時訪問同一個ThreadLocal靜態例項,也不會互相干擾,因為它們實際操作的是不同的資料副本。

  • 重用和清理:在伺服器環境中,執行緒通常會被重用來處理多個請求。因此,非常重要的一點是在請求完成後清理ThreadLocal中的資料(使用ThreadLocal.remove()方法),以防止舊資料在新請求中被誤用,這也有助於防止記憶體洩漏。

示例

假設有一個Web應用,每個使用者請求都需要進行身份驗證,並將使用者資訊儲存在ThreadLocal中。即使ThreadLocalstatic的,每個使用者請求(由不同或重用的執行緒處理)仍然能安全地儲存和管理自己的使用者資訊,而不會與其他請求衝突。

透過這種方式,ThreadLocal提供了一種非常有效的機制,以保證資料在併發環境下的隔離性和執行緒安全性。

4 ThreadLocalMap的鍵是ThreadLocal物件本身,而值是儲存在ThreadLocal變數中的資料副本。就像上面的dog和pig的例子ThreadLocalMap的鍵是ThreadLocal物件本身,值是dog物件對嗎?但是這裡在多個請求的情況下的ThreadLocal物件是同一個啊,不會替換ThreadLocalMap中的值嗎?比如不同的使用者資訊user

理解ThreadLocal及其與ThreadLocalMap的關係確實很重要,尤其是在多使用者環境下。首先,確實如你所說,ThreadLocalMap的鍵是ThreadLocal物件本身,而值是與該ThreadLocal相關聯的執行緒特定資料。然而,這裡的關鍵是理解每個執行緒都擁有自己的ThreadLocalMap

如何工作?

  • ThreadLocal的靜態性:儘管ThreadLocal例項是靜態的,這意味著所有的執行緒都訪問相同的ThreadLocal例項,但每個執行緒呼叫ThreadLocal.get()ThreadLocal.set()方法時,實際上是在自己的ThreadLocalMap中運算元據。

  • 鍵的角色:在每個執行緒的ThreadLocalMap中,ThreadLocal例項作為鍵。由於每個執行緒都有自己的ThreadLocalMap,因此同一個ThreadLocal例項在不同執行緒的ThreadLocalMap中可以關聯不同的值。

多請求和使用者資訊示例

假設你有一個Web應用,其中用ThreadLocal來儲存每個請求的使用者資訊。儘管所有請求處理執行緒共享相同的ThreadLocal例項,每個執行緒仍然能獨立地儲存和管理自己的使用者資訊,因為:

  • 執行緒獨立性:每個HTTP請求通常由不同的執行緒處理(或執行緒池中的執行緒輪流處理新的請求)。每個這樣的執行緒將會在它自己的ThreadLocalMap中用共享的ThreadLocal例項作為鍵,儲存其獨特的使用者資訊作為值。

  • 避免資料替換:即使多個請求使用相同的ThreadLocal例項,也不會互相替換ThreadLocalMap中的值,因為每個請求都是在不同的執行緒中處理的。每個執行緒的ThreadLocalMap是獨立的。

清理ThreadLocal

為了防止記憶體洩漏,尤其是在使用執行緒池的情況下,重要的是在請求處理完成後清除ThreadLocal中的資料。這可以透過在適當的時候呼叫ThreadLocal.remove()來完成。例如,在Web應用中,可以在請求的響應階段後或在服務請求後立即清理ThreadLocal

public void someMethod() {
try {
// 處理請求
} finally {
myThreadLocal.remove(); // 清理操作,確保不留下舊資料
}
}

結論

ThreadLocal提供了一種確保資料隔離的方式,在多執行緒環境中每個執行緒可以安全地存取自己的資料副本,而不會與其他執行緒發生衝突。正確管理ThreadLocal使用(包括及時清理)可以有效防止記憶體洩漏並確保應用的健壷執行。

5 這裡是每個執行緒都有自己的threadLocals 屬性,即自己的ThreadLocalMap,因此即使是同一個ThreadLocal物件,也可以保證操作的是不同的Map對嗎?是對不同的Map進行操作的

是的,你的理解是正確的。每個執行緒都持有自己的ThreadLocalMap,這是一個專門為ThreadLocal設計的內部資料結構,用來儲存執行緒區域性變數。即便是同一個ThreadLocal例項,不同執行緒透過它設定或獲取值時,實際上操作的是各自執行緒的ThreadLocalMap。這確保了不同執行緒間資料的隔離,即使它們都使用相同的ThreadLocal物件。

如何理解ThreadLocalThreadLocalMap的關係?

  • ThreadLocal 是作為鍵儲存在每個執行緒自己的ThreadLocalMap中。每個執行緒都有一個獨立的ThreadLocalMap例項,這個對映儲存了所有使用ThreadLocal變數的鍵值對。

  • ThreadLocal物件本身, 是使用ThreadLocal.set(value)設定的具體資料。

實際操作示例

考慮兩個執行緒A和B,它們都使用相同的ThreadLocal物件threadLocal來儲存資料:

ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
  • 執行緒A 設定值:

    threadLocal.set(1);

    線上程A的ThreadLocalMap中,threadLocal作為鍵,整數1作為值。

  • 執行緒B 設定值:

    threadLocal.set(2);

    線上程B的ThreadLocalMap中,相同的threadLocal例項作為鍵,但整數2作為值。

即使兩個執行緒使用的是同一個ThreadLocal物件,它們也無法訪問或修改對方的資料。每次呼叫threadLocal.get()時,都只會從當前執行緒關聯的ThreadLocalMap中根據ThreadLocal物件(作為鍵)檢索值。

為何要這樣設計?

這種設計主要是為了提供執行緒封閉(Thread Confinement),這是併發程式設計中一種避免共享資料的方法,可以大大減少執行緒間的競爭和同步需求,從而提高效能,並減少錯誤發生的機會。

注意事項

雖然ThreadLocal提供了很好的執行緒封閉功能,但使用它時需要注意:

  • 記憶體洩漏:長時間執行的執行緒(如Web伺服器中的執行緒池執行緒)如果不及時清除ThreadLocal中的資料,可能會導致記憶體洩漏。這是因為ThreadLocalMap的生命週期與執行緒一樣長,如果不顯式清除,設定的值會一直保持到執行緒結束。

  • 及時清除:在不需要ThreadLocal儲存的資料時,應呼叫ThreadLocal.remove()來清除執行緒區域性變數,這有助於防止記憶體洩漏並保持資料的正確性。

透過這種方式,即使多個執行緒使用同一個ThreadLocal物件,也可以確保它們各自的資料互不干擾。

33 在Spring框架中,控制生產環境和測試環境中使用不同的Bean可以透過使用profiles來實現。Profiles提供了一種方式來分組相關的Bean定義,只有在特定的profile啟用時,這些Bean才會被註冊到Spring容器中。這樣,你可以為生產環境和測試環境分別建立不同的profiles,從而控制哪些Bean應該在特定環境下被注入。

使用@Profile註解定義環境

你可以使用@Profile註解來指定某個Bean只在特定的環境中啟用。比如,你可以定義一個Bean只在生產環境中啟用,另一個只在測試環境中啟用。

示例程式碼:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

@Configuration
public class AppConfig {

@Bean
@Profile("production")
public MyBean productionBean() {
return new MyBean("Production Bean");
}

@Bean
@Profile("test")
public MyBean testBean() {
return new MyBean("Test Bean");
}
}

在這個示例中,productionBean只在啟用了production profile時建立,而testBean只在啟用了test profile時建立。

啟用Profile

你可以透過多種方式啟用特定的profiles:

  1. 透過環境變數 設定SPRING_PROFILES_ACTIVE環境變數來指定啟用的profiles。

    export SPRING_PROFILES_ACTIVE=production
  2. 在application.properties或application.yml檔案中

    # application.properties
    spring.profiles.active=production

    或者

    # application.yml
    spring:
    profiles:
    active: production
  3. 程式啟動時設定 在Spring Boot應用中,你可以在啟動時透過命令列引數來指定啟用的profiles:

    java -jar myapp.jar --spring.profiles.active=production

總結

使用profiles來管理不同環境下的Bean配置是一個非常有效和常見的做法。這不僅可以幫助你清晰地分隔生產和測試環境的配置,還可以避免在錯誤的環境中部署不適當的配置。透過@Profile註解與外部配置結合使用,Spring提供了一套強大的工具來支援不同環境下的應用部署。

34

相關文章