Spring、JPA 和 Hibernate - 如何在没有并发问题的情况下递增计数器
Spring, JPA, and Hibernate - how to increment a counter without concurrency issues
我正在研究 Spring 和 JPA/Hibernate,我对在 table.[=17 中递增计数器的正确方法有点困惑=]
我的 REST API 需要根据用户操作增加和减少数据库中的某些值(在下面的示例中,喜欢或不喜欢标签将使标签中的计数器增加或减少 1 Table)
tagRepository
是一个 JpaRepository
(Spring-数据)
我已经像这样配置了交易
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager"/>
@Controller
public class TestController {
@Autowired
TagService tagService
public void increaseTag() {
tagService.increaseTagcount();
}
public void decreaseTag() {
tagService.decreaseTagcount();
}
}
@Transactional
@Service
public class TagServiceImpl implements TagService {
public void decreaseTagcount() {
Tag tag = tagRepository.findOne(tagId);
decrement(tag)
}
public void increaseTagcount() {
Tag tag = tagRepository.findOne(tagId);
increment(tag)
}
private void increment(Tag tag) {
tag.setCount(tag.getCount() + 1);
Thread.sleep(20000);
tagRepository.save(tag);
}
private void decrement(Tag tag) {
tag.setCount(tag.getCount() - 1);
tagRepository.save(tag);
}
}
如您所见,我在 .save()
之前故意增加了 20 秒的睡眠,以便能够测试并发场景。
初始标记计数器 = 10;
1) A user calls increaseTag and the code hits the sleep so the value
of the entity = 11 and the value in the DB is still 10
2) a user calls the decreaseTag and goes through all the code. the
value is the database is now = 9
3) The sleeps finishes and hits the .save with the entity having a
count of 11 and then hits .save()
当我检查数据库时,该标签的值现在等于 11.. 而在现实中(至少我想要实现的)它会等于 10
这种行为正常吗?或者 @Transactional
注释不起作用?
例如使用乐观锁。
这应该是解决您的问题的最简单方法。
有关详细信息,请参阅 -> https://docs.jboss.org/hibernate/orm/4.0/devguide/en-US/html/ch05.html
最简单的解决方案是将并发委托给您的数据库,并简单地依赖当前修改行上的数据库隔离级别锁:
增量就这么简单:
UPDATE Tag t set t.count = t.count + 1 WHERE t.id = :id;
减量查询为:
UPDATE Tag t set t.count = t.count - 1 WHERE t.id = :id;
UPDATE 查询锁定修改后的行,防止其他事务在当前事务提交之前修改同一行(只要您不使用 READ_UNCOMMITTED
)。
这里值得一提的是 spring-boot-mongodb JPA - 如何在没有并发问题的情况下增加计数器:
根据官方文档在 MongoDB 中:
When modifying a single document, both db.collection.findAndModify() and the update() method atomically update the document. See Atomicity and Transactions for more details about interactions and order of operations of these methods.
在Mongo shell的情况下,我们可以简单地运行 findAndModify 如下:
> db.idGenerator.findAndModify({query: {identifier: "identified_by_Id"}, update: {$inc: {counter:1}} })
{
"_id" : ObjectId("5e115560ff14992f34fd18c6"),
"identifier" : "identified_by_Id",
"counter" : 1
}
> db.idGenerator.find()
{ "_id" : ObjectId("5e115560ff14992f34fd18c6"), "identifier" : "identified_by_Id", "counter" : 2 }
> db.idGenerator.findAndModify({query: {identifier: "identified_by_Id"}, update: {$inc: {counter:1}} })
{
"_id" : ObjectId("5e115560ff14992f34fd18c6"),
"identifier" : "identified_by_Id",
"counter" : 2
}
>
findAndModify()
始终 increment/decrements 和 return 之前为 counter
提供的实际值。
在 JPA 术语中,首先创建一个模型,然后创建存储库和服务 class 以获取唯一 ID,如下所示:
// 模型 class
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
@Document
public class IdGenerator {
@Id
private String identifier;
private long counter;
public String getIdentifier() {
return identifier;
}
public void setIdentifier(String identifier) {
this.identifier = identifier;
}
public long getCounter() {
return counter;
}
public void setCounter(long counter) {
this.counter = counter;
}
}
// Mongo 存储库 class:
import org.springframework.data.mongodb.repository.MongoRepository;
import sample.data.mongo.models.IdGenerator;
public interface IdGeneratorRepository extends MongoRepository<IdGenerator, String> {
}
// ID 生成器服务 class
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.FindAndModifyOptions;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Service;
import sample.data.mongo.models.IdGenerator;
import sample.data.mongo.repository.IdGeneratorRepository;
@Service
public class IdGeneratorService {
@Autowired
private IdGeneratorRepository idGeneratorRepository;
@Autowired
private MongoTemplate mongoTemplate;
public long generateId(String key) {
Query query = new Query();
// key = identified_by_Id;
Criteria criteria = new Criteria("identifier").is(key);
query.addCriteria(criteria);
Update update = new Update();
update.inc("counter", 1);
FindAndModifyOptions options = new FindAndModifyOptions();
options.upsert(true);
options.returnNew(true);
IdGenerator idGenerator = mongoTemplate
.findAndModify(query, update, options, IdGenerator.class);
return idGenerator.getCounter();
}
}
通过使用上述方法 generateId
它将 return 始终为特定键增加一个数字。
有3种方法可以处理并发问题-
1.等到锁空闲时获取 -
如果你使用的是redis锁,并且锁被其他线程获取了,那么等到锁被释放后再执行increment/decrement操作,这样可以保证数据的正确性。
2。 Increment/Decrement查询本身的计数器-
Increment/Decrement 查询本身的计数器,让数据库处理并发和锁定。
3。启用重试机制 -
如果锁被其他线程获取,而新线程无法获取锁以执行所需的操作,则将消息推送到某个延迟队列(比方说 rabbitMQ)并延迟一些时间(比方说 10 秒) ), 以便在 10 秒后线程将检查锁是否可用或是否被其他线程释放然后 increment/decrement 计数器。
第一个和第三个解决方案非常相似,但在第一个中我们可以使用 while 循环和线程睡眠来延迟,在第三个解决方案中我们可以使用一些延迟队列和重试机制。
我正在研究 Spring 和 JPA/Hibernate,我对在 table.[=17 中递增计数器的正确方法有点困惑=]
我的 REST API 需要根据用户操作增加和减少数据库中的某些值(在下面的示例中,喜欢或不喜欢标签将使标签中的计数器增加或减少 1 Table)
tagRepository
是一个 JpaRepository
(Spring-数据)
我已经像这样配置了交易
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager"/>
@Controller
public class TestController {
@Autowired
TagService tagService
public void increaseTag() {
tagService.increaseTagcount();
}
public void decreaseTag() {
tagService.decreaseTagcount();
}
}
@Transactional
@Service
public class TagServiceImpl implements TagService {
public void decreaseTagcount() {
Tag tag = tagRepository.findOne(tagId);
decrement(tag)
}
public void increaseTagcount() {
Tag tag = tagRepository.findOne(tagId);
increment(tag)
}
private void increment(Tag tag) {
tag.setCount(tag.getCount() + 1);
Thread.sleep(20000);
tagRepository.save(tag);
}
private void decrement(Tag tag) {
tag.setCount(tag.getCount() - 1);
tagRepository.save(tag);
}
}
如您所见,我在 .save()
之前故意增加了 20 秒的睡眠,以便能够测试并发场景。
初始标记计数器 = 10;
1) A user calls increaseTag and the code hits the sleep so the value of the entity = 11 and the value in the DB is still 10
2) a user calls the decreaseTag and goes through all the code. the value is the database is now = 9
3) The sleeps finishes and hits the .save with the entity having a count of 11 and then hits .save()
当我检查数据库时,该标签的值现在等于 11.. 而在现实中(至少我想要实现的)它会等于 10
这种行为正常吗?或者 @Transactional
注释不起作用?
例如使用乐观锁。 这应该是解决您的问题的最简单方法。 有关详细信息,请参阅 -> https://docs.jboss.org/hibernate/orm/4.0/devguide/en-US/html/ch05.html
最简单的解决方案是将并发委托给您的数据库,并简单地依赖当前修改行上的数据库隔离级别锁:
增量就这么简单:
UPDATE Tag t set t.count = t.count + 1 WHERE t.id = :id;
减量查询为:
UPDATE Tag t set t.count = t.count - 1 WHERE t.id = :id;
UPDATE 查询锁定修改后的行,防止其他事务在当前事务提交之前修改同一行(只要您不使用 READ_UNCOMMITTED
)。
这里值得一提的是 spring-boot-mongodb JPA - 如何在没有并发问题的情况下增加计数器:
根据官方文档在 MongoDB 中:
When modifying a single document, both db.collection.findAndModify() and the update() method atomically update the document. See Atomicity and Transactions for more details about interactions and order of operations of these methods.
在Mongo shell的情况下,我们可以简单地运行 findAndModify 如下:
> db.idGenerator.findAndModify({query: {identifier: "identified_by_Id"}, update: {$inc: {counter:1}} })
{
"_id" : ObjectId("5e115560ff14992f34fd18c6"),
"identifier" : "identified_by_Id",
"counter" : 1
}
> db.idGenerator.find()
{ "_id" : ObjectId("5e115560ff14992f34fd18c6"), "identifier" : "identified_by_Id", "counter" : 2 }
> db.idGenerator.findAndModify({query: {identifier: "identified_by_Id"}, update: {$inc: {counter:1}} })
{
"_id" : ObjectId("5e115560ff14992f34fd18c6"),
"identifier" : "identified_by_Id",
"counter" : 2
}
>
findAndModify()
始终 increment/decrements 和 return 之前为 counter
提供的实际值。
在 JPA 术语中,首先创建一个模型,然后创建存储库和服务 class 以获取唯一 ID,如下所示:
// 模型 class
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
@Document
public class IdGenerator {
@Id
private String identifier;
private long counter;
public String getIdentifier() {
return identifier;
}
public void setIdentifier(String identifier) {
this.identifier = identifier;
}
public long getCounter() {
return counter;
}
public void setCounter(long counter) {
this.counter = counter;
}
}
// Mongo 存储库 class:
import org.springframework.data.mongodb.repository.MongoRepository;
import sample.data.mongo.models.IdGenerator;
public interface IdGeneratorRepository extends MongoRepository<IdGenerator, String> {
}
// ID 生成器服务 class
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.FindAndModifyOptions;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Service;
import sample.data.mongo.models.IdGenerator;
import sample.data.mongo.repository.IdGeneratorRepository;
@Service
public class IdGeneratorService {
@Autowired
private IdGeneratorRepository idGeneratorRepository;
@Autowired
private MongoTemplate mongoTemplate;
public long generateId(String key) {
Query query = new Query();
// key = identified_by_Id;
Criteria criteria = new Criteria("identifier").is(key);
query.addCriteria(criteria);
Update update = new Update();
update.inc("counter", 1);
FindAndModifyOptions options = new FindAndModifyOptions();
options.upsert(true);
options.returnNew(true);
IdGenerator idGenerator = mongoTemplate
.findAndModify(query, update, options, IdGenerator.class);
return idGenerator.getCounter();
}
}
通过使用上述方法 generateId
它将 return 始终为特定键增加一个数字。
有3种方法可以处理并发问题-
1.等到锁空闲时获取 -
如果你使用的是redis锁,并且锁被其他线程获取了,那么等到锁被释放后再执行increment/decrement操作,这样可以保证数据的正确性。
2。 Increment/Decrement查询本身的计数器-
Increment/Decrement 查询本身的计数器,让数据库处理并发和锁定。
3。启用重试机制 -
如果锁被其他线程获取,而新线程无法获取锁以执行所需的操作,则将消息推送到某个延迟队列(比方说 rabbitMQ)并延迟一些时间(比方说 10 秒) ), 以便在 10 秒后线程将检查锁是否可用或是否被其他线程释放然后 increment/decrement 计数器。
第一个和第三个解决方案非常相似,但在第一个中我们可以使用 while 循环和线程睡眠来延迟,在第三个解决方案中我们可以使用一些延迟队列和重试机制。