บทความนี้จะอธิบายการจัดการธุรกรรมใน Spring พร้อมตัวอย่าง แบ่งปันกับทุกคนสำหรับการอ้างอิงของคุณ การวิเคราะห์เฉพาะมีดังนี้:
แนะนำธุรกิจ:
การจัดการธุรกรรมเป็นเทคโนโลยีที่จำเป็นในการพัฒนาแอปพลิเคชันระดับองค์กรเพื่อให้มั่นใจในความสมบูรณ์และความสม่ำเสมอของข้อมูล
ธุรกรรมคือชุดของการดำเนินการที่ถือเป็นหน่วยงานเดียว การกระทำทั้งหมดนี้ได้ผลหรือไม่ได้ผลเลย
คุณลักษณะที่สำคัญสี่ประการของการทำธุรกรรม (ACID)
1 ความเป็นอะตอม: การดำเนินการของอะตอมในห้องธุรกรรมประกอบด้วยชุดของการกระทำ ความเป็นอะตอมมิกของธุรกรรมช่วยให้มั่นใจได้ว่าการกระทำต่างๆ จะเสร็จสมบูรณ์หรือไม่มีผลกระทบใดๆ เลย 2 ความสอดคล้อง: เมื่อการกระทำของธุรกรรมทั้งหมดเสร็จสมบูรณ์ ธุรกรรมจะถูกสร้างขึ้น ข้อมูลและทรัพยากรอยู่ในสถานะที่สอดคล้องกันซึ่งเป็นไปตามกฎเกณฑ์ทางธุรกิจ 3 การแยก: อาจมีธุรกรรมจำนวนมากที่ประมวลผลข้อมูลเดียวกันในเวลาเดียวกัน ดังนั้นแต่ละสิ่งควรแยกออกจากธุรกรรมอื่น ๆ เพื่อป้องกันข้อมูลเสียหาย ④ ความทนทาน: เมื่อธุรกรรมเสร็จสมบูรณ์ ผลลัพธ์ของมันไม่ควรได้รับผลกระทบไม่ว่าระบบจะเกิดข้อผิดพลาดก็ตาม โดยทั่วไป ผลลัพธ์ของธุรกรรมจะถูกเขียนไปยังที่เก็บข้อมูลถาวร
การจัดการธุรกรรมใน Spring
ในฐานะที่เป็นเฟรมเวิร์กแอปพลิเคชันระดับองค์กร Spring กำหนดเลเยอร์นามธรรมที่อยู่ด้านบนของ API การจัดการธุรกรรมต่างๆ นักพัฒนาแอปพลิเคชันไม่จำเป็นต้องเข้าใจ API การจัดการธุรกรรมพื้นฐานเพื่อใช้กลไกการจัดการธุรกรรมของ Spring
Spring รองรับทั้งการจัดการธุรกรรมแบบเป็นโปรแกรมและการจัดการธุรกรรมแบบประกาศ
การจัดการธุรกรรมแบบเป็นโปรแกรม: ฝังรหัสการจัดการธุรกรรมไว้ในวิธีการทางธุรกิจเพื่อควบคุมการส่งธุรกรรมและการย้อนกลับ ในธุรกรรมแบบเป็นโปรแกรม จะต้องรวมรหัสการจัดการธุรกรรมเพิ่มเติมในการดำเนินธุรกิจแต่ละรายการ
การจัดการธุรกรรมที่เปิดเผย: ใช้งานได้ดีกว่าการจัดการธุรกรรมแบบเป็นโปรแกรมในกรณีส่วนใหญ่ แยกรหัสการจัดการธุรกรรมออกจากวิธีการทางธุรกิจ และใช้การจัดการธุรกรรมในลักษณะที่เปิดเผย การจัดการธุรกรรมถือเป็นข้อกังวลแบบข้ามขั้นตอน สามารถทำให้เป็นโมดูลผ่านวิธี AOP Spring รองรับการจัดการธุรกรรมที่ประกาศผ่านกรอบงาน Spring AOP
คุณสมบัติการขยายพันธุ์ของธุรกรรม Spring:
เมื่อวิธีธุรกรรมถูกเรียกโดยวิธีธุรกรรมอื่น คุณต้องระบุวิธีกระจายธุรกรรม ตัวอย่างเช่น: วิธีการอาจทำงานต่อไปในธุรกรรมที่มีอยู่ หรืออาจเริ่มธุรกรรมใหม่และทำงานในธุรกรรมของตนเอง
พฤติกรรมการเผยแพร่ของธุรกรรมสามารถระบุได้ด้วยแอตทริบิวต์การเผยแพร่ Spring กำหนดพฤติกรรมการสื่อสาร 7 ประการ:
พฤติกรรมการสื่อสาร | ความหมาย |
การเผยแพร่_บังคับ | บ่งชี้ว่าต้องเรียกใช้เมธอดภายในธุรกรรม หากไม่มีธุรกรรมปัจจุบัน ข้อยกเว้นจะถูกส่งออกไป |
การขยายพันธุ์_NESTED | บ่งชี้ว่าหากมีธุรกรรมอยู่ในปัจจุบัน วิธีการนี้จะถูกเรียกใช้ในธุรกรรมที่ซ้อนกัน ธุรกรรมที่ซ้อนกันสามารถกระทำหรือย้อนกลับได้โดยอิสระจากธุรกรรมปัจจุบัน หากไม่มีธุรกรรมปัจจุบัน ลักษณะการทำงานจะเหมือนกับ PROPAGATION_REQUIRED โปรดทราบว่าการสนับสนุนของผู้ผลิตแต่ละรายสำหรับพฤติกรรมการเผยแพร่นี้แตกต่างกัน คุณสามารถดูเอกสารประกอบของผู้จัดการทรัพยากรของคุณเพื่อยืนยันว่าพวกเขารองรับธุรกรรมที่ซ้อนกันหรือไม่ |
การเผยแพร่_ไม่เคย | บ่งชี้ว่าไม่ควรรันเมธอดปัจจุบันในบริบทธุรกรรม หากมีการทำธุรกรรมอยู่ในขณะนี้ ข้อยกเว้นจะถูกส่งออกไป |
PROPAGATION_NOT_SUPPORTED | บ่งชี้ว่าไม่ควรเรียกใช้วิธีนี้ภายในธุรกรรม หากมีธุรกรรมปัจจุบันอยู่ ธุรกรรมดังกล่าวจะถูกระงับในขณะที่วิธีนี้กำลังทำงานอยู่ หากคุณใช้ JTATransactionManager คุณจะต้องเข้าถึง TransactionManager |
PROPAGATION_REQUIRED | บ่งชี้ว่าต้องเรียกใช้วิธีการปัจจุบันภายในธุรกรรม หากธุรกรรมปัจจุบันมีอยู่ วิธีการจะถูกเรียกใช้ในธุรกรรมนั้น มิฉะนั้นธุรกรรมใหม่จะเริ่มต้นขึ้น |
PROPAGATION_REQUIRED_NEW | บ่งชี้ว่าวิธีการปัจจุบันต้องรันในธุรกรรมของตัวเอง ธุรกรรมใหม่จะเริ่มขึ้น หากมีธุรกรรมปัจจุบันอยู่ ธุรกรรมนั้นจะถูกระงับระหว่างการดำเนินการตามวิธีนี้ หากคุณใช้ JTATransactionManager คุณจะต้องเข้าถึง TransactionManager |
PROPAGATION_SUPPORTS | ระบุว่าวิธีการปัจจุบันไม่ต้องการบริบทธุรกรรม แต่หากมีธุรกรรมปัจจุบัน วิธีการจะทำงานในธุรกรรมนี้ |
หนึ่งในนั้นคือ PROPAGATION_REQUIRED ที่เป็นแอตทริบิวต์การเผยแพร่เริ่มต้น
ปัญหาที่เกิดจากการทำธุรกรรมที่เกิดขึ้นพร้อมกัน
ปัญหาที่ไม่คาดคิดมากมายอาจเกิดขึ้นเมื่อธุรกรรมหลายรายการในแอปพลิเคชันเดียวกันหรือในแอปพลิเคชันที่แตกต่างกันดำเนินการพร้อมกันบนชุดข้อมูลเดียวกัน
ปัญหาที่เกิดจากการทำธุรกรรมที่เกิดขึ้นพร้อมกันสามารถแบ่งออกเป็น 3 ประเภทดังนี้
1 การอ่านแบบ Dirty: การอ่านแบบ Dirty เกิดขึ้นเมื่อธุรกรรมหนึ่งอ่านข้อมูลที่ถูกเขียนใหม่โดยธุรกรรมอื่น แต่ยังไม่ได้ดำเนินการ หากการเขียนซ้ำถูกย้อนกลับในภายหลัง ข้อมูลที่ได้รับจากธุรกรรมแรกจะไม่ถูกต้อง
② การอ่านที่ไม่สามารถทำซ้ำได้: การอ่านที่ไม่สามารถทำซ้ำได้เกิดขึ้นเมื่อธุรกรรมดำเนินการค้นหาเดียวกันสองครั้งขึ้นไป แต่ได้รับข้อมูลที่แตกต่างกันในแต่ละครั้ง ซึ่งมักเป็นเพราะธุรกรรมที่เกิดขึ้นพร้อมกันอีกรายการหนึ่งได้อัปเดตข้อมูลระหว่างแบบสอบถาม
3 การอ่านแบบ Phantom: การอ่านแบบ Phantom นั้นคล้ายคลึงกับการอ่านแบบไม่สามารถทำซ้ำได้ มันเกิดขึ้นเมื่อธุรกรรมหนึ่ง (T1) อ่านข้อมูลสองสามแถว จากนั้นธุรกรรมที่เกิดขึ้นพร้อมกันอีกรายการหนึ่ง (T2) แทรกข้อมูลบางส่วน ในการสืบค้นครั้งต่อๆ ไป ธุรกรรมแรก (T1) จะพบว่ามีเรกคอร์ดเพิ่มเติมบางอย่างที่ไม่มีอยู่ตั้งแต่แรก
ตัวอย่างรหัส
อย่างแรกคือตารางฐานข้อมูล:
รวมถึงหนังสือ (isbn, book_name, ราคา), บัญชี (ชื่อผู้ใช้, ยอดคงเหลือ), book_stock (isbn, หุ้น)
จากนั้นคลาสที่ใช้:
ร้านหนังสือดาว
คัดลอกรหัสรหัสดังต่อไปนี้:
แพ็คเกจ com.yl.spring.tx;
อินเทอร์เฟซสาธารณะ BookShopDao {
//รับราคาต่อหน่วยของหนังสือตามหมายเลขหนังสือ
int สาธารณะ findBookPriceByIsbn (สตริง isbn);
//อัปเดตสินค้าคงคลังของหนังสือเพื่อให้สินค้าคงคลังที่สอดคล้องกับหมายเลขหนังสือเป็น -1
โมฆะสาธารณะ updateBookStock (สตริง isbn);
//อัปเดตยอดคงเหลือในบัญชีของผู้ใช้: สร้างยอดคงเหลือของชื่อผู้ใช้
โมฆะสาธารณะ updateUserAccount (ชื่อผู้ใช้สตริง, ราคา int);
-
ร้านหนังสือDaoImpl
คัดลอกรหัสรหัสดังต่อไปนี้:
แพ็คเกจ com.yl.spring.tx;
นำเข้า org.springframework.beans.factory.annotation.Autowired;
นำเข้า org.springframework.jdbc.core.JdbcTemplate;
นำเข้า org.springframework.stereotype.Repository;
@Repository("bookShopDao")
BookShopDaoImpl ระดับสาธารณะใช้งาน BookShopDao {
@สายอัตโนมัติ
JdbcTemplate ส่วนตัว JdbcTemplate;
@แทนที่
int สาธารณะ findBookPriceByIsbn (สตริง isbn) {
String sql = "เลือกราคาจากหนังสือ WHERE isbn = ?";
กลับ JdbcTemplate.queryForObject (sql, Integer.class, isbn);
-
@แทนที่
โมฆะสาธารณะ updateBookStock (สตริง isbn) {
//ตรวจสอบว่าสินค้าคงคลังในหนังสือเพียงพอหรือไม่ หากไม่เพียงพอ ให้ส่งข้อยกเว้น
String sql2 = "เลือกหุ้นจาก book_stock WHERE isbn = ?";
int stock = JdbcTemplate.queryForObject (sql2, Integer.class, isbn);
ถ้า (หุ้น == 0) {
โยน BookStockException ใหม่ ("สต็อกไม่เพียงพอ!");
-
String sql = "UPDATE book_stock SET stock = stock - 1 WHERE isbn = ";
JdbcTemplate.update(sql, isbn);
-
@แทนที่
โมฆะสาธารณะ updateUserAccount (ชื่อผู้ใช้สตริง ราคา int) {
//ตรวจสอบว่ายอดคงเหลือไม่เพียงพอหรือไม่ หากไม่เพียงพอ ให้ส่งข้อยกเว้น
String sql2 = "เลือกยอดคงเหลือจากบัญชี WHERE ชื่อผู้ใช้ = ";
int balance = JdbcTemplate.queryForObject (sql2, Integer.class, ชื่อผู้ใช้);
ถ้า (ยอดคงเหลือ < ราคา) {
โยน UserAccountException ใหม่ ("ยอดเงินไม่เพียงพอ!");
-
String sql = "อัปเดตบัญชี SET balance = balance - ? WHERE ชื่อผู้ใช้ = ?";
JdbcTemplate.update (sql, ราคา, ชื่อผู้ใช้);
-
-
บริการร้านหนังสือ
คัดลอกรหัสดังต่อไปนี้: แพคเกจ com.yl.spring.tx;
อินเทอร์เฟซสาธารณะ BookShopService {
การซื้อโมฆะสาธารณะ (ชื่อผู้ใช้ String, String isbn);
-
ร้านหนังสือบริการImpl
คัดลอกรหัสรหัสดังต่อไปนี้:
แพ็คเกจ com.yl.spring.tx;
นำเข้า org.springframework.beans.factory.annotation.Autowired;
นำเข้า org.springframework.stereotype.Service;
นำเข้า org.springframework.transaction.annotation.Isolation;
นำเข้า org.springframework.transaction.annotation.Propagation;
นำเข้า org.springframework.transaction.annotation.Transactional;
@Service("บริการร้านหนังสือ")
BookShopServiceImpl ระดับสาธารณะใช้ BookShopService {
@สายอัตโนมัติ
BookShopDao ส่วนตัว bookShopDao;
-
* 1. เพิ่มคำอธิบายประกอบธุรกรรม
* ใช้การแพร่กระจายเพื่อระบุพฤติกรรมการแพร่กระจายของธุรกรรม นั่นคือ วิธีใช้ธุรกรรมเมื่อมีการเรียกวิธีธุรกรรมปัจจุบันโดยวิธีธุรกรรมอื่น
* จำเป็นต้องมีค่าเริ่มต้น ซึ่งหมายถึงการใช้ธุรกรรมที่เรียกเมธอด
* REQUIRES_NEW: ใช้ธุรกรรมของคุณเอง และธุรกรรมของวิธีธุรกรรมที่เรียกว่าถูกระงับ
-
* 2. ใช้การแยกเพื่อระบุระดับการแยกของธุรกรรม ค่าที่ใช้บ่อยที่สุดคือ READ_COMMITTED
* 3. ตามค่าเริ่มต้น ธุรกรรมที่ประกาศของ Spring จะย้อนกลับข้อยกเว้นรันไทม์ทั้งหมด ซึ่งสามารถตั้งค่าผ่านคุณสมบัติที่เกี่ยวข้องได้ โดยปกติ ค่าเริ่มต้นก็เพียงพอแล้ว
* 4. ใช้ readOnly เพื่อระบุว่าธุรกรรมเป็นแบบอ่านอย่างเดียวหรือไม่ บ่งชี้ว่าธุรกรรมนี้อ่านเฉพาะข้อมูลแต่ไม่ได้อัปเดตข้อมูล ซึ่งสามารถช่วยกลไกฐานข้อมูลเพิ่มประสิทธิภาพธุรกรรมได้ หากเป็นวิธีที่อ่านเฉพาะฐานข้อมูลจริงๆ ควรตั้งค่า readOnly=true
* 5. ใช้ timeOut เพื่อระบุเวลาที่ธุรกรรมสามารถทำได้ก่อนที่จะบังคับให้ย้อนกลับ
-
@ธุรกรรม (การเผยแพร่=การเผยแพร่.REQUIRES_NEW,
แยก = แยก READ_COMMITTED,
noRollbackFor={UserAccountException.class},
อ่านอย่างเดียว=จริง หมดเวลา=3)
@แทนที่
การซื้อโมฆะสาธารณะ (ชื่อผู้ใช้สตริง, สตริง isbn) {
//1. รับราคาต่อหน่วยของหนังสือ
ราคา int = bookShopDao.findBookPriceByIsbn(isbn);
//2. อัพเดตรายการหนังสือ
bookShopDao.updateBookStock (isbn);
//3. อัปเดตยอดผู้ใช้
bookShopDao.updateUserAccount(ชื่อผู้ใช้, ราคา);;
-
-
BookStockException
คัดลอกรหัสรหัสดังต่อไปนี้:
แพ็คเกจ com.yl.spring.tx;
BookStockException คลาสสาธารณะขยาย RuntimeException {
-
-
-
serialVersionUID ยาวสุดท้ายแบบคงที่ส่วนตัว = 1L;
BookStockException สาธารณะ () {
ซุปเปอร์();
// TODO ต้นขั้วคอนสตรัคเตอร์ที่สร้างขึ้นอัตโนมัติ
-
BookStockException สาธารณะ (สตริง arg0, Throwable arg1, บูลีน arg2,
บูลีน arg3) {
ซุปเปอร์(arg0, arg1, arg2, arg3);
// TODO ต้นขั้วคอนสตรัคเตอร์ที่สร้างขึ้นอัตโนมัติ
-
BookStockException สาธารณะ (สตริง arg0, Throwable arg1) {
ซุปเปอร์(arg0, arg1);
// TODO ต้นขั้วคอนสตรัคเตอร์ที่สร้างขึ้นอัตโนมัติ
-
BookStockException สาธารณะ (สตริง arg0) {
ซุปเปอร์(arg0);
// TODO ต้นขั้วคอนสตรัคเตอร์ที่สร้างขึ้นอัตโนมัติ
-
BookStockException สาธารณะ (โยนได้ arg0) {
ซุปเปอร์(arg0);
// TODO ต้นขั้วคอนสตรัคเตอร์ที่สร้างขึ้นโดยอัตโนมัติ
-
-
UserAccountException
คัดลอกรหัสรหัสดังต่อไปนี้:
แพ็คเกจ com.yl.spring.tx;
UserAccountException คลาสสาธารณะขยาย RuntimeException {
-
-
-
serialVersionUID ยาวสุดท้ายแบบคงที่ส่วนตัว = 1L;
UserAccountException สาธารณะ () {
ซุปเปอร์();
// TODO ต้นขั้วคอนสตรัคเตอร์ที่สร้างขึ้นอัตโนมัติ
-
UserAccountException สาธารณะ (สตริง arg0, Throwable arg1, บูลีน arg2,
บูลีน arg3) {
ซุปเปอร์(arg0, arg1, arg2, arg3);
// TODO ต้นขั้วคอนสตรัคเตอร์ที่สร้างขึ้นอัตโนมัติ
-
UserAccountException สาธารณะ (สตริง arg0, Throwable arg1) {
ซุปเปอร์(arg0, arg1);
// TODO ต้นขั้วคอนสตรัคเตอร์ที่สร้างขึ้นอัตโนมัติ
-
UserAccountException สาธารณะ (สตริง arg0) {
ซุปเปอร์(arg0);
// TODO ต้นขั้วคอนสตรัคเตอร์ที่สร้างขึ้นอัตโนมัติ
-
UserAccountException สาธารณะ (โยนได้ arg0) {
ซุปเปอร์(arg0);
// TODO ต้นขั้วคอนสตรัคเตอร์ที่สร้างขึ้นอัตโนมัติ
-
-
แคชเชียร์
คัดลอกรหัสรหัสดังต่อไปนี้:
แพ็คเกจ com.yl.spring.tx;
นำเข้า java.util.List;
แคชเชียร์อินเทอร์เฟซสาธารณะ {
การชำระเงินเป็นโมฆะสาธารณะ (ชื่อผู้ใช้ String, List<String>isbns);
-
แคชเชียร์Impl. CashierImpl.checkout และ bookShopService.purchase ร่วมกันทดสอบพฤติกรรมการเผยแพร่ธุรกรรม
คัดลอกรหัสรหัสดังต่อไปนี้:
แพ็คเกจ com.yl.spring.tx;
นำเข้า java.util.List;
นำเข้า org.springframework.beans.factory.annotation.Autowired;
นำเข้า org.springframework.stereotype.Service;
นำเข้า org.springframework.transaction.annotation.Transactional;
@บริการ("แคชเชียร์")
CashierImpl ระดับสาธารณะดำเนินการแคชเชียร์ {
@สายอัตโนมัติ
บริการร้านหนังสือส่วนตัว บริการร้านหนังสือ;
@ธุรกรรม
@แทนที่
การชำระเงินเป็นโมฆะสาธารณะ (ชื่อผู้ใช้ String, รายการ <String> isbns) {
สำหรับ (สตริง isbn : isbns) {
bookShopService.purchase(ชื่อผู้ใช้, isbn);
-
-
-
ชั้นเรียนทดสอบ:
คัดลอกรหัสรหัสดังต่อไปนี้:
แพ็คเกจ com.yl.spring.tx;
นำเข้า java.util.Arrays;
นำเข้า org.junit.Test;
นำเข้า org.springframework.context.ApplicationContext;
นำเข้า org.springframework.context.support.ClassPathXmlApplicationContext;
SpringTransitionTest คลาสสาธารณะ {
ApplicationContext ส่วนตัว ctx = null;
BookShopDao ส่วนตัว bookShopDao = null;
BookShopService ส่วนตัว bookShopService = null;
แคชเชียร์ส่วนตัว แคชเชียร์ = null;
-
ctx = ClassPathXmlApplicationContext ใหม่ ("applicationContext.xml");
bookShopDao = ctx.getBean(BookShopDao.class);
bookShopService = ctx.getBean (BookShopService.class);
แคชเชียร์ = ctx.getBean(แคชเชียร์.คลาส);
-
@ทดสอบ
โมฆะสาธารณะ testBookShopDaoFindPriceByIsbn () {
System.out.println(bookShopDao.findBookPriceByIsbn("1001"));
-
@ทดสอบ
โมฆะสาธารณะ testBookShopDaoUpdateBookStock () {
bookShopDao.updateBookStock("1001");
-
@ทดสอบ
โมฆะสาธารณะ testBookShopDaoUpdateUserAccount () {
bookShopDao.updateUserAccount("AA", 100);
-
@ทดสอบ
โมฆะสาธารณะ testBookShopService(){
bookShopService.purchase("AA", "1001");
-
@ทดสอบ
โมฆะสาธารณะ testTransactionPropagation(){
cashier.checkout("AA", Arrays.asList("1001", "1002"));
-
-
ฉันหวังว่าบทความนี้จะเป็นประโยชน์กับการออกแบบโปรแกรม Spring ของทุกคน