學習Spring-Data-Jpa(十六)---@Version與@Lock


1、問題場景

  以用戶賬戶為例,如果允許同時對某個用戶的賬戶進行修改的話,會導致某些修改被覆蓋,使最后的結果不正確。

  如:1.1、張三的賬戶中有100元。

    1.2、張三的賬戶消費了50元。

    1.3、張三的賬戶充值了100元。

  我們希望的張三賬戶最終的結果是150元。如果1.2、1.3是並發執行的,按下面的方式執行的話,回事怎樣的呢?

賬戶實體:

/**
 * 賬戶實體
 *
 * @author caofanqi
 */
@Slf4j
@Data
@EqualsAndHashCode(callSuper = true)
@Entity
@Builder
@Table(name = "jpa_account")
@NoArgsConstructor
@AllArgsConstructor
public class Account extends AbstractID {

    /**
     *  簡單代表一下賬戶所屬人
     */
    private String accountName;

    @Column(columnDefinition = "DECIMAL(19, 2)")
    private BigDecimal balance;

}

Repository接口:

/**
 * @author caofanqi
 */
public interface AccountRepository extends JpaRepositoryImplementation<Account,Long> {

    Account findByAccountName(String accountName);

}

Service:

/**
 *
 * @author caofanqi
 */
@Service
public class AccountServiceImpl implements AccountService {

    @Resource
    private AccountRepository accountRepository;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public String addAccountMoney(String accountName, BigDecimal money){

        System.out.println(Thread.currentThread().getName() + ",addAccountMoney start...");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Account account = accountRepository.findByAccountName(accountName);
        System.out.println(Thread.currentThread().getName() + ",find balance : " + account.getBalance());
        account.setBalance(account.getBalance().add(money));

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Account result = accountRepository.save(account);
        System.out.println(Thread.currentThread().getName() + ", update balance end ,balance : " + result.getBalance());

        System.out.println(Thread.currentThread().getName() + ",addAccountMoney sleep...");
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName() + ",addAccountMoney end...");

        return "success";
    }

}

數據庫表中數據:

  

 

測試用例:

    @Test
    void addAccountMoney() throws InterruptedException {

        CountDownLatch count = new CountDownLatch(2);

        ExecutorService executorService = Executors.newFixedThreadPool(2);

        executorService.execute(() -> {
            String result = accountService.addAccountMoney("張三的賬戶", BigDecimal.valueOf(-50));
            System.out.println(Thread.currentThread().getName() + ",result : " + result);
            count.countDown();
        });

        TimeUnit.SECONDS.sleep(1);

        executorService.execute(() -> {
            String result = accountService.addAccountMoney("張三的賬戶", BigDecimal.valueOf(100));
            System.out.println(Thread.currentThread().getName() + ",result : " + result);
            count.countDown();
        });

        count.await(10, TimeUnit.SECONDS);

        Account endAccount = accountRepository.findByAccountName("張三的賬戶");
        System.out.println("final balance :" + endAccount.getBalance());

    }

控制台打印及數據庫結果:

  

 

  這明顯不是我們想要的正確答案,那怎么解決呢?這里提供幾個方法,①如果是單JVM的話,可以使用Java的同步機制和Lock(估計這種情況很少見吧...)。②使用JPA為我們提供的樂觀鎖@Version。

③使用JPA為我們提供的@Lock中的悲觀鎖。

2、@Version

  JPA提供的樂觀鎖,指定實體中的字段或屬性作為樂觀鎖的version,該version用於確保並發操作的正確性。每個實體只能使用一個version屬性或字段。version支持(int, Integer, short, Short, long, Long, java.sql.Timestamp)類型的屬性或字段。

  使用起來非常方便,我們只需要在實體中添加一個字段,並添加@Version注解就可以了。加了@Version后,insert和update的SQL語句都會帶上version的操作。當樂觀鎖更新失敗的時候,會拋出異常org.springframework.orm.ObjectOptimisticLockingFailureException。我們自己進行業務處理。

 實體修改如下:

/**
 * 賬戶實體
 *
 * @author caofanqi
 */
@Slf4j
@Data
@EqualsAndHashCode(callSuper = true)
@Entity
@Builder
@Table(name = "jpa_account")
@NoArgsConstructor
@AllArgsConstructor
public class Account extends AbstractID {

    /**
     *  簡單代表一下賬戶所屬人
     */
    private String accountName;

    @Column(columnDefinition = "DECIMAL(19, 2)")
    private BigDecimal balance;

    /**
     * 樂觀鎖version
     */
    @Version
    private Integer version;

}

重新插入一條數據,可以看到數據庫中如下

  

修改Service方法如下:

    @Override
    @Transactional(rollbackFor = Exception.class)
    public String addAccountMoney(String accountName, BigDecimal money){

        try {
            updateAccount(accountName, money);
            return "success";
        }catch (ObjectOptimisticLockingFailureException e){
            //記錄日志,重新操作...
            return "fail";
        }

    }

    @Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class)
    public void updateAccount(String accountName, BigDecimal money) {
        System.out.println(Thread.currentThread().getName() + ",addAccountMoney start...");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Account account = accountRepository.findByAccountName(accountName);
        System.out.println(Thread.currentThread().getName() + ",find balance : " + account.getBalance());
        account.setBalance(account.getBalance().add(money));

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Account result = accountRepository.save(account);
        System.out.println(Thread.currentThread().getName() + ", update balance end ,balance : " + result.getBalance());


        System.out.println(Thread.currentThread().getName() + ",addAccountMoney sleep...");
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName() + ",addAccountMoney end...");
    }

重新運行測試用例:

 

  這樣只有和我們上次版本一樣的時候才會更新,就不會出現互相覆蓋的問題,保證了數據的原子性。但是如果我們的業務就是需要讓兩次都必須成功,那么可以使用下面的悲觀鎖來實現。

 3、@Lock

       spring-data-jpa為我們提供了@Lock注解,指定查詢方法要使用的鎖定模式。可以添加在派生查詢上,也可以重寫父類CRUD的方法,添加該注解。@Lock只有一個value屬性,為LockModeType枚舉類型,我們主要看以下里面的悲觀鎖PESSIMISTIC_WRITE。

   修改Repository如下:

/**
 * @author caofanqi
 */
public interface AccountRepository extends JpaRepositoryImplementation<Account,Long> {

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    Account findByAccountName(String accountName);

}

  恢復數據庫表數據為100,並將@Version注解去掉,運行測試用例控制台打印如下:

pool-1-thread-1,addAccountMoney start...
pool-1-thread-2,addAccountMoney start...
Hibernate: select account0_.id as id1_0_, account0_.account_name as account_2_0_, account0_.balance as balance3_0_, account0_.version as version4_0_ from cfq_jpa_account account0_ where account0_.account_name=? for update
pool-1-thread-1,find balance : 100.00
Hibernate: select account0_.id as id1_0_, account0_.account_name as account_2_0_, account0_.balance as balance3_0_, account0_.version as version4_0_ from cfq_jpa_account account0_ where account0_.account_name=? for update
pool-1-thread-1, update balance end ,balance : 50.00
pool-1-thread-1,addAccountMoney sleep...
pool-1-thread-1,addAccountMoney end...
Hibernate: update cfq_jpa_account set account_name=?, balance=?, version=? where id=?
pool-1-thread-2,find balance : 50.00
pool-1-thread-1,result : success
pool-1-thread-2, update balance end ,balance : 150.00
pool-1-thread-2,addAccountMoney sleep...
Hibernate: select account0_.id as id1_0_, account0_.account_name as account_2_0_, account0_.balance as balance3_0_, account0_.version as version4_0_ from cfq_jpa_account account0_ where account0_.account_name=? for update
pool-1-thread-2,addAccountMoney end...
Hibernate: update cfq_jpa_account set account_name=?, balance=?, version=? where id=?
final balance :150.00
2019-12-08 17:20:43.915  INFO 4160 --- [           main] o.s.t.c.transaction.TransactionContext   : Committed transaction for test: [DefaultTestContext@7674f035 testClass = AccountServiceImplTest, testInstance = cn.caofanqi.study.studyspringdatajpa.service.impl.AccountServiceImplTest@46d69ca4, testMethod = addAccountMoney@AccountServiceImplTest, testException = [null], mergedContextConfiguration = [WebMergedContextConfiguration@69e153c5 testClass = AccountServiceImplTest, locations = '{}', classes = '{class cn.caofanqi.study.studyspringdatajpa.StudySpringDataJpaApplication}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@9353778, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@1700915, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@31c88ec8, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@20ce78ec], resourceBasePath = 'src/main/webapp', contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.web.ServletTestExecutionListener.activateListener' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.populatedRequestContextHolder' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.resetRequestContextHolder' -> true]]

  可以看到查詢語句通過for update進行加鎖。得到了我們想要的150結果。

  注意:for update ,如果不通過索引條件檢索數據,那么InnoDB將對表中的所有記錄加鎖,實際效果跟鎖表一樣。我們進行測試,在數據庫中在添加一條記錄,如下:

  

 

  執行下面測試用例:

 

    /**
     *  for update ,如果不通過索引條件檢索數據,那么InnoDB將對表中的所有記錄加鎖,實際效果跟鎖表一樣
     */
    @Test
    void addAccountMoney2() throws InterruptedException {

        CountDownLatch count = new CountDownLatch(2);

        ExecutorService executorService = Executors.newFixedThreadPool(2);

        executorService.execute(() -> {
            String result = accountService.addAccountMoney("張三的賬戶", BigDecimal.valueOf(-50));
            System.out.println(Thread.currentThread().getName() + ",result : " + result);
            count.countDown();
        });

        TimeUnit.SECONDS.sleep(1);

        executorService.execute(() -> {
            String result = accountService.addAccountMoney("李四的賬戶", BigDecimal.valueOf(100));
            System.out.println(Thread.currentThread().getName() + ",result : " + result);
            count.countDown();
        });

        count.await(20, TimeUnit.SECONDS);

    }

控制台打印結果:

 

 可以看到並不是並行進行的更新,我們就該實體類,重新生成數據庫表,並插入數據(或直接修改數據庫)

/**
 * 賬戶實體
 *
 * @author caofanqi
 */
@Slf4j
@Data
@EqualsAndHashCode(callSuper = true)
@Entity
@Builder
@Table(name = "jpa_account")
@NoArgsConstructor
@AllArgsConstructor
public class Account extends AbstractID {

    /**
     *  簡單代表一下賬戶所屬人
     */
    @Column(unique = true,nullable = false)
    private String accountName;

    @Column(columnDefinition = "DECIMAL(19, 2)")
    private BigDecimal balance;

    /**
     * 樂觀鎖version
     */
//    @Version
    private Integer version;

}

 

   

 

 重新運行測試用例:

 

 

   我們在使用的過程中要根據自己的業務進行選擇。

 

 

參考連接:https://blog.csdn.net/u014316026/article/details/78726459

       https://blog.csdn.net/loophome/article/details/79867174

 

源碼地址:https://github.com/caofanqi/study-spring-data-jpa

 

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM