天天看點

Spring boot使用Timestamp樂觀鎖方式的高并發場景處理完整示例

筆者最近賦閑,以前公務繁忙,幾乎不怎麼寫博(主要還是懶),但考慮到現在什麼樣的公司都動不動就要求你有點部落格,開源,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')
}
           

資料表

以常用的商品表為例,表結構如下:

Spring boot使用Timestamp樂觀鎖方式的高并發場景處理完整示例

注意字段設定為根據目前實際戳更新,預設值為:CURRENT_TIMESTAMP,插入一條測試資料用于驗證我們的場景

Spring boot使用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();
        }
    }
}

           

執行這個測試類,觀察控制台輸出,及資料庫記錄,可看到如下輸出:

Spring boot使用Timestamp樂觀鎖方式的高并發場景處理完整示例

跟蹤斷點,檢查是否是鎖沖突導緻的異常:

Spring boot使用Timestamp樂觀鎖方式的高并發場景處理完整示例

證明捕獲了異常ObjectOptimisticLockingFailureException異常,而這個異常就是因為鎖沖突造成的。而正是

goodsDao.saveAndFlush(oldGood);

這句觸發了鎖沖突彈出了我們訓示的異常,在庫存被删減到0後,雖然依然大量的并發還在執行,但是因為不在更新表資料,是以控制台訓示滾動如下資訊,不再觸發鎖沖突:

Spring boot使用Timestamp樂觀鎖方式的高并發場景處理完整示例

驗證此時資料庫表:

Spring boot使用Timestamp樂觀鎖方式的高并發場景處理完整示例

整個場景模拟完畢,本示例如有不妥當之處,歡迎大咖在下面讨論并斧正。

繼續閱讀