我有一个简单的数据库表(PostgreSql),其中包含以下数据
在Spring Boot JPA应用中,该表对应的Entity类定义如下
import java.time.LocalDateTime;
import org.hibernate.annotations.UpdateTimestamp;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Entity
@Table(name = "counter", schema = "demo")
public class CounterRow {
@Id
private int row_id;
private int counter;
private String last_user;
@UpdateTimestamp
private LocalDateTime last_updated;
}
存储库定义如下
import org.ravi.entity.CounterRow;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
public interface CounterRepository extends JpaRepository<CounterRow, Integer> {
}
现在,我已经初始化了两个线程来同时增加计数器,我的目标是在数据库级别而不是应用程序级别实现两个线程之间的同步(即不使用
synchronized
关键字或任何其他分布式锁机制)
如果每个线程将计数器增加 5 倍(在循环中),我希望计数器的最终值与其当前值相比增加 10。但是,我发现它仅增加了 5 倍,假设这可能是由于数据库同步不当造成的。
这是运行更新线程的类
package org.ravi;
import org.ravi.entity.CounterRow;
import org.ravi.repository.CounterRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
@Component
@Slf4j
public class DbSyncTester {
@Autowired
private CounterRepository repository;
@PostConstruct
public void start() {
final Thread thread1 = getThread("Updater_Thread_1");
final Thread thread2 = getThread("Updater_Thread_2");
thread1.start();
thread2.start();
}
private Thread getThread(String threadName) {
return new Thread(getDbTask(), threadName);
}
private Runnable getDbTask() {
return () -> {
int loopCounter = 0;
while (loopCounter++ < 5) {
updateDb();
}
};
}
@Transactional(readOnly = false, isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
private void updateDb() {
log.info("Fetching the counter from the DB");
CounterRow counterRow = repository.findById(1).get();
log.info("Counter value: {}", counterRow.getCounter());
counterRow.setCounter(counterRow.getCounter() + 1);
counterRow.setLast_user(Thread.currentThread().getName());
repository.save(counterRow);
log.info("Updated the counter in the DB to {}", counterRow.getCounter());
}
}
我在方法上添加了以下代码行,希望实现数据库级别的同步,但没有成功。
@Transactional(只读= false,隔离= Isolation.READ_COMMITTED,传播= Propagation.REQUIRED)
我也尝试过
Isolation.SERIALIZABLE
,还是不行。
请帮助我理解使用上述代码的数据库级锁的问题。
有2个问题:
@Transactional
实际上不起作用。您可以设置一个断点,在调试期间停止并查看堆栈跟踪中没有事务代理。首先,
@Transactional
的工作方式是创建一个包装类(代理)。 Spring 不是使用实际的类/对象,而是注入此代理。因此,当调用事务方法时,就会调用代理并启动事务。之后它会调用您的实际代码。
但是因为您从类内部调用
@Transactional
方法,所以该调用不会通过代理。因此没有开始任何交易。您需要将您的 bean 注入到其他类中,然后才调用事务方法。
现在关于锁..简单的 SELECT 语句永远不会遇到任何锁。由于多版本控制 (MVCC),数据库会保留记录的多个版本。如果并行事务更新了该行,则 SELECTing 事务将仅读取旧版本。因此,即使并行事务更新了记录,您也会读取旧版本,在 Java 中递增它,并覆盖之前写入的值。
当
UDPATE
/DELETE
发生或在 SELECT ... FOR SHARE/UPDATE
期间发生锁定。因此,如果您想正确编写利用锁定的代码,您需要在 1 个 SQL 语句中完成此操作:
update table_name set counter=counter+1 where ...