Java Spring、JPA 和 Hibernate - 如何在没有并发问题的情况下增加计数器

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/30143594/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-11 09:13:29  来源:igfitidea点击:

Spring, JPA, and Hibernate - how to increment a counter without concurrency issues

javaspringhibernatejpatransactions

提问by Johny19

I'm playing around a bit with Spring and JPA/Hibernate and I'm a bit confused on the right way to increment a counter in a table.

我正在尝试使用 Spring 和 JPA/Hibernate,但我对在表中增加计数器的正确方法有些困惑。

My REST API needs to increment and decrement some value in the database depending on the user action (in the example bellow, liking or disliking a tag will make the counter increment or decrement by one in the Tag Table)

我的 REST API 需要根据用户操作增加和减少数据库中的某些值(在下面的示例中,喜欢或不喜欢标签将使标签表中的计数器增加或减少 1)

tagRepositoryis a JpaRepository(Spring-data) and I have configured the transaction like this

tagRepository是一个JpaRepository(Spring-data),我已经像这样配置了事务

<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);
    }
}

As you can see I have put on purpose a sleep of 20 second on increment JUST before the .save()to be able to test a concurrency scenario.

正如您所看到的,我.save()在能够测试并发场景之前故意在增量上休眠 20 秒。

initial tag counter = 10;

初始标签计数器 = 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()

1)用户调用increaseTag,代码进入睡眠状态,因此实体的值=11,DB中的值仍然是10

2)用户调用reduceTag并遍历所有代码。值是数据库现在是 = 9

3) sleeps 完成并点击 .save 实体的计数为 11 然后点击 .save()

When I check the database, the value for that tag is now equal to 11.. when in reality (at least what I would like to achieve) it would be equal to 10

当我检查数据库时,该标签的值现在等于 11.. 而实际上(至少是我想要实现的)它将等于 10

Is this behaviour normal? Or the @Transactionalannotation is not doing is work?

这种行为正常吗?或者@Transactional注释不是在做是工作?

采纳答案by Vlad Mihalcea

The simplest solution is to delegate the concurrency to your databaseand simply rely on the database isolation levellock on the currently modified rows:

最简单的解决方案是将并发委托给您的数据库,并简单地依赖当前修改行上的数据库隔离级别锁:

The increment is as simple as this:

增量就像这样简单:

UPDATE Tag t set t.count = t.count + 1 WHERE t.id = :id;

and the decrement query is:

递减查询是:

UPDATE Tag t set t.count = t.count - 1 WHERE t.id = :id;

The UPDATE query takes a lock on the modified rows, preventing other transactions from modifying the same row, before the current transaction commits(as long as you don't use READ_UNCOMMITTED).

UPDATE 查询对修改的行进行锁定,防止其他事务在当前事务提交之前修改同一行(只要您不使用READ_UNCOMMITTED)。

回答by mh-dev

For example use Optimistic Locking. This should be the easiest solution to solve your problem. For more details see -> https://docs.jboss.org/hibernate/orm/4.0/devguide/en-US/html/ch05.html

例如使用乐观锁定。这应该是解决您的问题的最简单的方法。有关更多详细信息,请参阅-> https://docs.jboss.org/hibernate/orm/4.0/devguide/en-US/html/ch05.html

回答by krishna Prasad

It's worth to mention here about spring-boot-mongodbJPA - how to increment a counter without concurrency issues:

这里值得一提的是spring-boot-mongodbJPA - 如何在没有并发问题的情况下增加计数器:

In MongoDB as per official documentation:

根据官方文档在 MongoDB 中:

When modifying a single document, both db.collection.findAndModify()and the update() method atomically update the document. See Atomicity and Transactionsfor more details about interactions and order of operations of these methods.

修改单个文档时,db.collection.findAndModify()和 update() 方法都会自动更新文档。有关这些方法的交互和操作顺序的更多详细信息,请参阅原子性和事务

In the case of Mongo shell, we can simply run the findAndModify as below:

对于 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()always increment/decrements and return the actually value previously present for the counter.

findAndModify()始终递增/递减并返回之前为counter.

In the term of JPA, first, create a Model and then Repository and Service class to get unique ids as below:

在 JPA 术语中,首先创建一个 Model,然后创建 Repository 和 Service 类以获得唯一的 id,如下所示:

// Model 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 Repository class:

// Mongo 存储库类:

import org.springframework.data.mongodb.repository.MongoRepository;
import sample.data.mongo.models.IdGenerator;


public interface IdGeneratorRepository extends MongoRepository<IdGenerator, String> {

}

// Id Generator Service class

// ID生成器服务类

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();
  }
}

By using the above method generateIdit will return always an incremented number for the specific keys.

通过使用上述方法generateId,它将始终为特定键返回一个递增的数字。