学习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)
spring roll怎么读@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() + ",");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
Account account = accountRepository.findByAccountName(accountName);
System.out.println(Thread.currentThread().getName() + ",find balance : " + Balance());
account.Balance().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 : " + Balance());
System.out.println(Thread.currentThread().getName() + ",");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ",");
return "success";
}
}
数据库表中数据:
测试⽤例:
@Test
void addAccountMoney() throws InterruptedException {
CountDownLatch count = new CountDownLatch(2);
ExecutorService executorService = wFixedThreadPool(2);
String result = accountService.addAccountMoney("张三的账户", BigDecimal.valueOf(-50));
System.out.println(Thread.currentThread().getName() + ",result : " + result);
});
TimeUnit.SECONDS.sleep(1);
String result = accountService.addAccountMoney("张三的账户", BigDecimal.valueOf(100));
System.out.println(Thread.currentThread().getName() + ",result : " + result);
});
count.await(10, TimeUnit.SECONDS);
Account endAccount = accountRepository.findByAccountName("张三的账户");
System.out.println("final balance :" + Balance());
}
控制台打印及数据库结果:
 这明显不是我们想要的正确答案,那怎么解决呢?这⾥提供⼏个⽅法,①如果是单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的操作。当乐观锁更新失败的时候,会抛出异常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() + ",");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
Account account = accountRepository.findByAccountName(accountName);
System.out.println(Thread.currentThread().getName() + ",find balance : " + Balance());
account.Balance().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 : " + Balance());
System.out.println(Thread.currentThread().getName() + ",");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ",");
}
重新运⾏测试⽤例:
  这样只有和我们上次版本⼀样的时候才会更新,就不会出现互相覆盖的问题,保证了数据的原⼦性。但是如果我们的业务就是需要让两次都必须成功,那么可以使⽤下⾯的
悲观锁来实现。
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,
pool-1-thread-2,
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,
pool-1-thread-1,
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,
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,
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] ansaction.TransactionContext  : Commit
ted transaction for test: [DefaultTestContext@7674f035 testClass = AccountServiceImplTest, testInstance = cn.caofanqi.study.studyspring   可以看到查询语句通过for update进⾏加锁。得到了我们想要的150结果。
  注意:for update ,如果不通过索引条件检索数据,那么InnoDB将对表中的所有记录加锁,实际效果跟锁表⼀样。我们进⾏测试,在数据库中在添加⼀条记录,如下:
  执⾏下⾯测试⽤例:
/**
*  for update ,如果不通过索引条件检索数据,那么InnoDB将对表中的所有记录加锁,实际效果跟锁表⼀样
*/
@Test
void addAccountMoney2() throws InterruptedException {
CountDownLatch count = new CountDownLatch(2);
ExecutorService executorService = wFixedThreadPool(2);
String result = accountService.addAccountMoney("张三的账户", BigDecimal.valueOf(-50));
System.out.println(Thread.currentThread().getName() + ",result : " + result);
});
TimeUnit.SECONDS.sleep(1);
String result = accountService.addAccountMoney("李四的账户", BigDecimal.valueOf(100));
System.out.println(Thread.currentThread().getName() + ",result : " + result);
});
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;
}
重新运⾏测试⽤例:
  我们在使⽤的过程中要根据⾃⼰的业务进⾏选择。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。