spring-boot 项目中的版本更改时不会抛出 OptimisticLockException
OptimisticLockException not thrown when version has changed in spring-boot project
模型结构:
@MappedSuperclass
public class BaseModel<K extends Comparable> implements Serializable, Comparable<Object> {
private static final long serialVersionUID = 1L;
@Id
private K id;
@Version
private Integer version;
// getter/setter
}
@Entity
public class MyEntity extends BaseModel<String> {
// some fields and it's getter/setter
}
在我的数据库中记录 my_entity
:
编号:1
版本:1
...
下面是我的更新方法:
void update(String id, Integer currentVersion, ....) {
MyEntity myEntity = myRepository.findOne(id);
myEntity.setVersion(currentVersion);
// other assignments
myRepository.save(myEntity);
}
下面是调用此方法时触发的查询。
update my_entity set version=?, x=?, y=?, ...
where id=? and version=?
当传入上述方法的 currentVersion
不是 1
.
时,我期待 OptimisticLockException
任何人都可以帮助我为什么我没有得到 OptimisticLockException?
我正在为我的 webmvc 项目使用 spring-boot。
JPA 规范第 11.1.54 节指出:
In general, fields or properties that are specified with the Version
annotation should not be updated by the application.
根据经验,如果您尝试手动更新版本字段,我可以建议某些 JPA 提供程序(OpenJPA 就是其中之一)实际上会抛出异常。
虽然不能严格回答您的问题,但您可以按以下方式重构以确保 JPA 提供程序之间的可移植性并严格遵守 JPA 规范:
public void update(String id, Integer currentVersion) throws MyWrappedException {
MyEntity myEntity = myRepository.findOne(id);
if(currentVersion != myEntity.getVersion()){
throw new MyWrappedException();
}
myRepository.save(myEntity);
//still an issue here however: see below
}
假设您的 update(...)
方法在事务中是 运行 但是您仍然有上述问题,如 JPA 规范第 3.4.5 节所述:
3.4.5 OptimisticLockException Provider implementations may defer writing to the database until the end of the transaction, when
consistent with the lock mode and flush mode settings in effect. In
this case, an optimistic lock check may not occur until commit time,
and the OptimisticLockException may be thrown in the "before
completion" phase of the commit. If the OptimisticLockException must
be caught or handled by the application, the flush method should be
used by the application to force the database writes to occur. This
will allow the application to catch and handle optimistic lock
exceptions.
那么基本上,2 个用户可以为同一个实体提交并发修改。两个线程都可以通过初始检查,但是当更新刷新到数据库时,一个线程将失败,这可能是在事务提交时,即在您的方法完成之后。
为了您可以捕获并处理 OptimisticLock 异常,您的代码应如下所示:
public void update(String id, Integer currentVersion) throws MyWrappedException {
MyEntity myEntity = myRepository.findOne(id);
if(currentVersion != myEntity.getVersion()){
throw new MyWrappedException();
}
myRepository.save(myEntity);
try{
myRepository.flush()
}
catch(OptimisticLockingFailureException ex){
throw new MyWrappedException();
}
}
使用 JPA 时在更新之前使用 EVICT。我也没有让@Version 工作。 属性 增加了,但在更新具有错误版本的对象时没有抛出异常 - 属性。
我唯一要做的就是先 EVICT 对象,然后保存它。如果 Version 属性不匹配,则抛出 HibernateOptimisticLockingException。
将休眠 ShowSQL 设置为 'true' 以验证实际更新 sql 以 "where id=? and version=?" 结束。如果对象没有先被逐出,更新语句只有 "where id=?",并且(出于显而易见的原因)将不起作用。
乐观休眠锁开箱即用(您不必为实体设置版本):
@Entity
@Table(name = "product")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long quantity;
private Long likes;
@Version
private Long version;
public Product() {
}
//setter and getter
//equals and hashcode
- 存储库
public interface ProductRepository extends JpaRepository<Product, Long> {}
- 服务
@Service
public class ProductOptimisticLockingService {
private final ProductRepository productRepository;
public ProductOptimisticLockingService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@Transactional(readOnly = true)
public Product findById(Long id, String nameThread){
Product product =
productRepository
.findById(id)
.get();
System.out.printf(
"\n Select (%s) .... " +
"(id:) %d | (likes:) %d | (quantity:) %d | (version:) %d \n",
nameThread,
product.getId(),
product.getLikes(),
product.getQuantity(),
product.getVersion()
);
return product;
}
@Transactional(isolation = Isolation.READ_COMMITTED)
public void updateWithOptimisticLocking(Product product, String nameThread) {
try {
productRepository.save(product);
} catch (ObjectOptimisticLockingFailureException ex) {
System.out.printf(
"\n (%s) Another transaction is already working with a string with an ID: %d \n",
nameThread,
product.getId()
);
}
System.out.printf("\n--- Update has been performed (%s)---\n", nameThread);
}
}
- 测试
@SpringBootTest
class ProductOptimisticLockingServiceTest {
@Autowired
private ProductOptimisticLockingService productService;
@Autowired
private ProductRepository productRepository;
@Test
void saveWithOptimisticLocking() {
/*ID may be - 1 or another. You must put the ID to pass in your methods. You must think how to write right your tests*/
Product product = new Product();
product.setLikes(7L);
product.setQuantity(5L);
productRepository.save(product);
ExecutorService executor = Executors.newFixedThreadPool(2);
Lock lockService = new ReentrantLock();
Runnable taskForAlice = makeTaskForAlice(lockService);
Runnable taskForBob = makeTaskForBob(lockService);
executor.submit(taskForAlice);
executor.submit(taskForBob);
executorServiceMethod(executor);
}
/*------ Alice-----*/
private Runnable makeTaskForAlice(Lock lockService){
return () -> {
System.out.println("Thread-1 - Alice");
Product product;
lockService.lock();
try{
product = productService
.findById(1L, "Thread-1 - Alice");
}finally {
lockService.unlock();
}
setPause(1000L); /*a pause is needed in order for the 2nd transaction to attempt
read the line from which the 1st transaction started working*/
lockService.lock();
try{
product.setQuantity(6L);
product.setLikes(7L);
update(product,"Thread-1 - Alice");
}finally {
lockService.unlock();
}
System.out.println("Thread-1 - Alice - end");
};
}
/*------ Bob-----*/
private Runnable makeTaskForBob(Lock lockService){
return () -> {
/*the pause makes it possible to start the transaction first
from Alice*/
setPause(50L);
System.out.println("Thread-2 - Bob");
Product product;
lockService.lock();
try{
product = findProduct("Thread-2 - Bob");
}finally {
lockService.unlock();
}
setPause(3000L); /*a pause is needed in order for the 1st transaction to update
the string that the 2nd transaction is trying to work with*/
lockService.lock();
try{
product.setQuantity(5L);
product.setLikes(10L);
update(product,"Thread-2 - Bob");
}finally {
lockService.unlock();
}
System.out.println("Thread-2 - Bob - end");
};
}
private void update(Product product, String nameThread){
productService
.updateWithOptimisticLocking(product, nameThread);
}
private Product findProduct(String nameThread){
return productService
.findById(1L, nameThread);
}
private void setPause(long timeOut){
try {
TimeUnit.MILLISECONDS.sleep(timeOut);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void executorServiceMethod(ExecutorService executor){
try {
executor.awaitTermination(10L, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
executor.shutdown();
}
}
模型结构:
@MappedSuperclass
public class BaseModel<K extends Comparable> implements Serializable, Comparable<Object> {
private static final long serialVersionUID = 1L;
@Id
private K id;
@Version
private Integer version;
// getter/setter
}
@Entity
public class MyEntity extends BaseModel<String> {
// some fields and it's getter/setter
}
在我的数据库中记录 my_entity
:
编号:1 版本:1 ...
下面是我的更新方法:
void update(String id, Integer currentVersion, ....) {
MyEntity myEntity = myRepository.findOne(id);
myEntity.setVersion(currentVersion);
// other assignments
myRepository.save(myEntity);
}
下面是调用此方法时触发的查询。
update my_entity set version=?, x=?, y=?, ...
where id=? and version=?
当传入上述方法的 currentVersion
不是 1
.
任何人都可以帮助我为什么我没有得到 OptimisticLockException? 我正在为我的 webmvc 项目使用 spring-boot。
JPA 规范第 11.1.54 节指出:
In general, fields or properties that are specified with the Version annotation should not be updated by the application.
根据经验,如果您尝试手动更新版本字段,我可以建议某些 JPA 提供程序(OpenJPA 就是其中之一)实际上会抛出异常。
虽然不能严格回答您的问题,但您可以按以下方式重构以确保 JPA 提供程序之间的可移植性并严格遵守 JPA 规范:
public void update(String id, Integer currentVersion) throws MyWrappedException {
MyEntity myEntity = myRepository.findOne(id);
if(currentVersion != myEntity.getVersion()){
throw new MyWrappedException();
}
myRepository.save(myEntity);
//still an issue here however: see below
}
假设您的 update(...)
方法在事务中是 运行 但是您仍然有上述问题,如 JPA 规范第 3.4.5 节所述:
3.4.5 OptimisticLockException Provider implementations may defer writing to the database until the end of the transaction, when consistent with the lock mode and flush mode settings in effect. In this case, an optimistic lock check may not occur until commit time, and the OptimisticLockException may be thrown in the "before completion" phase of the commit. If the OptimisticLockException must be caught or handled by the application, the flush method should be used by the application to force the database writes to occur. This will allow the application to catch and handle optimistic lock exceptions.
那么基本上,2 个用户可以为同一个实体提交并发修改。两个线程都可以通过初始检查,但是当更新刷新到数据库时,一个线程将失败,这可能是在事务提交时,即在您的方法完成之后。
为了您可以捕获并处理 OptimisticLock 异常,您的代码应如下所示:
public void update(String id, Integer currentVersion) throws MyWrappedException {
MyEntity myEntity = myRepository.findOne(id);
if(currentVersion != myEntity.getVersion()){
throw new MyWrappedException();
}
myRepository.save(myEntity);
try{
myRepository.flush()
}
catch(OptimisticLockingFailureException ex){
throw new MyWrappedException();
}
}
使用 JPA 时在更新之前使用 EVICT。我也没有让@Version 工作。 属性 增加了,但在更新具有错误版本的对象时没有抛出异常 - 属性。
我唯一要做的就是先 EVICT 对象,然后保存它。如果 Version 属性不匹配,则抛出 HibernateOptimisticLockingException。
将休眠 ShowSQL 设置为 'true' 以验证实际更新 sql 以 "where id=? and version=?" 结束。如果对象没有先被逐出,更新语句只有 "where id=?",并且(出于显而易见的原因)将不起作用。
乐观休眠锁开箱即用(您不必为实体设置版本):
@Entity
@Table(name = "product")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long quantity;
private Long likes;
@Version
private Long version;
public Product() {
}
//setter and getter
//equals and hashcode
- 存储库
public interface ProductRepository extends JpaRepository<Product, Long> {}
- 服务
@Service
public class ProductOptimisticLockingService {
private final ProductRepository productRepository;
public ProductOptimisticLockingService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@Transactional(readOnly = true)
public Product findById(Long id, String nameThread){
Product product =
productRepository
.findById(id)
.get();
System.out.printf(
"\n Select (%s) .... " +
"(id:) %d | (likes:) %d | (quantity:) %d | (version:) %d \n",
nameThread,
product.getId(),
product.getLikes(),
product.getQuantity(),
product.getVersion()
);
return product;
}
@Transactional(isolation = Isolation.READ_COMMITTED)
public void updateWithOptimisticLocking(Product product, String nameThread) {
try {
productRepository.save(product);
} catch (ObjectOptimisticLockingFailureException ex) {
System.out.printf(
"\n (%s) Another transaction is already working with a string with an ID: %d \n",
nameThread,
product.getId()
);
}
System.out.printf("\n--- Update has been performed (%s)---\n", nameThread);
}
}
- 测试
@SpringBootTest
class ProductOptimisticLockingServiceTest {
@Autowired
private ProductOptimisticLockingService productService;
@Autowired
private ProductRepository productRepository;
@Test
void saveWithOptimisticLocking() {
/*ID may be - 1 or another. You must put the ID to pass in your methods. You must think how to write right your tests*/
Product product = new Product();
product.setLikes(7L);
product.setQuantity(5L);
productRepository.save(product);
ExecutorService executor = Executors.newFixedThreadPool(2);
Lock lockService = new ReentrantLock();
Runnable taskForAlice = makeTaskForAlice(lockService);
Runnable taskForBob = makeTaskForBob(lockService);
executor.submit(taskForAlice);
executor.submit(taskForBob);
executorServiceMethod(executor);
}
/*------ Alice-----*/
private Runnable makeTaskForAlice(Lock lockService){
return () -> {
System.out.println("Thread-1 - Alice");
Product product;
lockService.lock();
try{
product = productService
.findById(1L, "Thread-1 - Alice");
}finally {
lockService.unlock();
}
setPause(1000L); /*a pause is needed in order for the 2nd transaction to attempt
read the line from which the 1st transaction started working*/
lockService.lock();
try{
product.setQuantity(6L);
product.setLikes(7L);
update(product,"Thread-1 - Alice");
}finally {
lockService.unlock();
}
System.out.println("Thread-1 - Alice - end");
};
}
/*------ Bob-----*/
private Runnable makeTaskForBob(Lock lockService){
return () -> {
/*the pause makes it possible to start the transaction first
from Alice*/
setPause(50L);
System.out.println("Thread-2 - Bob");
Product product;
lockService.lock();
try{
product = findProduct("Thread-2 - Bob");
}finally {
lockService.unlock();
}
setPause(3000L); /*a pause is needed in order for the 1st transaction to update
the string that the 2nd transaction is trying to work with*/
lockService.lock();
try{
product.setQuantity(5L);
product.setLikes(10L);
update(product,"Thread-2 - Bob");
}finally {
lockService.unlock();
}
System.out.println("Thread-2 - Bob - end");
};
}
private void update(Product product, String nameThread){
productService
.updateWithOptimisticLocking(product, nameThread);
}
private Product findProduct(String nameThread){
return productService
.findById(1L, nameThread);
}
private void setPause(long timeOut){
try {
TimeUnit.MILLISECONDS.sleep(timeOut);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void executorServiceMethod(ExecutorService executor){
try {
executor.awaitTermination(10L, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
executor.shutdown();
}
}