筆者最近賦閑,以前公務繁忙,幾乎不怎麼寫博(主要還是懶),但考慮到現在什麼樣的公司都動不動就要求你有點部落格,開源,git啥的證明下自己,就随便班門弄斧折騰點啥吧,望各位讀者多多給予支援!
JPA方式下,使用version方式的樂觀鎖機制,是網上闡述比較多的資源,雖然也有部分資料提及Timestamp時間戳的方式,但很多最後其實也都使用了version的方式,采取version的樂觀鎖方式,固然不錯,但是資料庫表設計裡往往必須多餘出這一個單獨的version字段(筆者有點強迫症),未免不美,實際場景中,往往表列裡會有類似時間戳一樣的字段,比如gmt_modified(阿裡Java規範裡的常用資料庫字段規範),用來記錄行資料最後一次的更新時間,本文就是講述如何使用該列來實作高并發場景下的樂觀鎖的處理。(BTW,樂觀鎖是啥請自行百度)
本文所有示例都是在Spring boot架構下進行的,版本是2.1.X
相關依賴庫如下:
dependencies {
implementation('org.springframework.boot:spring-boot-starter-actuator')
implementation('org.springframework.boot:spring-boot-starter-cache')
implementation('org.springframework.boot:spring-boot-starter-thymeleaf')
implementation('org.springframework.boot:spring-boot-starter-web')
implementation('org.springframework.boot:spring-boot-starter-data-jpa')
implementation('org.springframework.boot:spring-boot-starter-test')
implementation('org.springframework.session:spring-session-core')
compile('com.alibaba:druid:1.1.9')
compile('com.alibaba:fastjson:1.2.47')
compile('com.google.guava:guava:25.0-jre')
compile('org.apache.commons:commons-text:1.6')
compile('io.springfox:springfox-swagger2:2.8.0')
compile('io.springfox:springfox-swagger-ui:2.8.0')
compile('org.springframework.data:spring-data-elasticsearch:3.1.3.RELEASE')
compile('com.aliyun.oss:aliyun-sdk-oss:3.3.0')
compile('commons-fileupload:commons-fileupload:1.3.3')
//compile('org.springframework.security.oauth:spring-security-oauth2:2.3.4.RELEASE')
compile('mysql:mysql-connector-java')
compileOnly('org.springframework.boot:spring-boot-configuration-processor')
}
資料表
以常用的商品表為例,表結構如下:
注意字段設定為根據目前實際戳更新,預設值為:CURRENT_TIMESTAMP,插入一條測試資料用于驗證我們的場景
商品是一本書《JPA高并發處理圖文詳細》,價格是30元,庫存3000本
Entity
/**
* @ClassName goods
* @Description 商品表
* @Author wangd
* @Create 2018-12-25 11:28
*/
@Entity
@Table(name = "goods")
@Proxy(lazy = false)
public class Goods {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(name = "name")
private String name;
@Column(name = "price")
private int price;
@Column(name = "stock")
private int stock;
@Version //版本控制注解
@Column(name = "gmt_modified",columnDefinition="timestamp")//注解列名和列類型
@Source(value = SourceType.DB) //注解值來自資料庫
private java.sql.Timestamp gmtModified;
public long getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
public Timestamp getGmtModified() {
return gmtModified;
}
public int getStock() {
return stock;
}
public void setStock(int stock) {
this.stock = stock;
}
}
DAO
因為示例簡單,可直接配置即可,不需要增加額外代碼
/**
* @InterFaceName GoodsDao
* @Description 商品DAO
* @Author wangd
* @Create 2018-12-25 11:32
*/
public interface GoodsDao extends JpaRepository<Goods, Long> {
}
Service
/**
* @InterFaceName GoodsService
* @Description 商品處理接口
* @Author wangd
* @Create 2018-12-25 11:34
*/
public interface IGoodsService {
/**
* 更改商品庫存
* @param id
* @param decStock 減去庫存數量
* @return
*/
Goods decreaseStock(long id,int decStock) throws InterruptedException;
}
service的接口實作類
/**
* @ClassName GoodsServiceImpl
* @Description 商品服務接口實作
* @Author wangd
* @Create 2018-12-25 11:36
*/
@Service
@Slf4j
public class IGoodsServiceImpl implements IGoodsService {
@Autowired
private GoodsDao goodsDao;
@Override
public Goods decreaseStock(long id, int decStock) throws InterruptedException{
//首先擷取商品資訊
Goods oldGood=goodsDao.getOne(id);
try {
//減庫存
if (oldGood.getStock() - decStock >= 0) {
oldGood.setStock(oldGood.getStock() - decStock);
return goodsDao.saveAndFlush(oldGood);
}
}catch (Exception e){
throw new InterruptedException("删減庫存"+decStock+"失敗!并發沖突!");
}
//傳回null值表示庫存已被清空,實際場景可自行處理,這裡隻是示例
return null;
}
}
基本的處理架構具備了,下面就需要驗證我們的樂觀鎖是否起到作用,這裡就要用到JDK1.5以後強大的一個java包concurrent,這個包提供了我們極為簡潔且極為強大的并發支援,具體這個包的介紹,請自行參悟,這裡不細講。
下面建構一個JunitTest測試類
/**
* @ClassName IGoodsServiceTest
* @Description 測試
* @Author wangd
* @Create 2018-12-25 12:09
*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
@Slf4j
public class IGoodsServiceTest {
// 請求總數
public static int clientTotal = 5000;
// 同時并發執行的線程數
public static int threadTotal = 200;
public static int count = 0;
@Autowired
IGoodsService goodsService;
@Test
public void testDecreaseStock(){
try {
ExecutorService executorService = Executors.newCachedThreadPool();
//信号量,此處用于控制并發的線程數
final Semaphore semaphore = new Semaphore(threadTotal);
//閉鎖,可實作計數器遞減
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal ; i++) {
//使用lambada方式執行線程
executorService.execute(() -> {
try {
//執行此方法用于擷取執行許可,當總計未釋放的許可數不超過200時,
//允許通行,否則線程阻塞等待,直到擷取到許可。
semaphore.acquire();
//調用goodService的修改庫存方法
Goods goods=goodsService.decreaseStock(1,25);
if(goods==null){
log.info("庫存已經清空!");
}
//釋放許可
semaphore.release();
} catch (InterruptedException e) {
//這裡捕獲IGoodsService抛出的中斷異常,實際場景中可執行資料復原操作
log.error("Exception", e);
e.printStackTrace();
}catch(Exception e){
log.error("exception", e);
e.printStackTrace();
}
//閉鎖減一
countDownLatch.countDown();
});
}
countDownLatch.await();//線程阻塞,直到閉鎖值為0時,阻塞才釋放,繼續往下執行
executorService.shutdown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
執行這個測試類,觀察控制台輸出,及資料庫記錄,可看到如下輸出:
跟蹤斷點,檢查是否是鎖沖突導緻的異常:
證明捕獲了異常ObjectOptimisticLockingFailureException異常,而這個異常就是因為鎖沖突造成的。而正是
goodsDao.saveAndFlush(oldGood);
這句觸發了鎖沖突彈出了我們訓示的異常,在庫存被删減到0後,雖然依然大量的并發還在執行,但是因為不在更新表資料,是以控制台訓示滾動如下資訊,不再觸發鎖沖突:
驗證此時資料庫表:
整個場景模拟完畢,本示例如有不妥當之處,歡迎大咖在下面讨論并斧正。