Одновременно выполняющиеся транзакции Spring Framework обновляют неверные данные - PullRequest
1 голос
/ 27 марта 2019

Я создал простое приложение с весенней загрузкой, которое продемонстрирует, как происходят банковские транзакции. Я создал одну сущность «Счет» и создал одну конечную точку «дебет».

Здесь я вызываю api-код «debit» два раза одновременно, но списывается только одна сумма времени. Я хочу знать, как я могу заблокировать сущность счета, чтобы другой поток считывал обновленный баланс и также дебетовал во второй раз.

Я попытался заблокировать объект «учетная запись» с типом режима блокировки как PESSIMISTIC_WRITE, но он не работает.

Account.java

package hello;

import org.hibernate.annotations.CacheConcurrencyStrategy;

import javax.persistence.*;

@Table(name = "account")
@Entity // This tells Hibernate to make a table out of this class
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
public class Account {


    //@Version
    @Column(name="version")
    private Integer version;

    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private Integer userId;

    @Column(name = "name")
    private String name;

    @Column(name="balance")
    private int balance;


    public Integer getUserId() {
        return userId;
    }

    public void setUserId(Integer userId) {
        this.userId = userId;
    }

    public int getBalance() {
        return balance;
    }

    public void setBalance(int balance) {
        this.balance = balance;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }


    @Override
    public String toString() {
        return "Account{" +
                "userId=" + userId +
                ", name='" + name + '\'' +
                ", balance=" + balance +
                '}';
    }
}

Конечная точка покоя

    @GetMapping(path = "/debit")
    public ResponseEntity<String>  debit() {
        Integer withdrawAmount = 100;
        Integer userId = 1;
        log.debug("debit  {} from account id {} ",withdrawAmount,userId);
        accountService.debit(userId,withdrawAmount);
        return ResponseEntity.badRequest().body("debited");
    }

AccountService.java

package hello.service;

import hello.Account;
import hello.AccountRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.EntityManager;
import javax.persistence.LockModeType;
import java.util.Optional;

@Service
public class AccountService {

    final private Logger log = LoggerFactory.getLogger(AccountService.class);
    @Autowired
    private AccountRepository accountRepository;


    @Autowired
    private EntityManager entityManager;



    @Transactional
    public void debit(Integer id,int balance){
        Optional<Account> accountOptional = accountRepository.findById(id);
        Account account = accountOptional.get();
        entityManager.refresh(account, LockModeType.PESSIMISTIC_WRITE);

        final int oldBalance = account.getBalance();
        log.debug("current balance {}",oldBalance);
        account.setBalance(oldBalance-balance);
        accountRepository.save(account);
        log.debug("debited");
    }
}

AccountRepository.java

package hello;

import org.springframework.data.repository.CrudRepository;

// This will be AUTO IMPLEMENTED by Spring into a Bean called userRepository
// CRUD refers Create, Read, Update, Delete

public interface AccountRepository extends CrudRepository<Account, Integer> {
        Account findOneByUserId(Integer userId);
}

моя запись в базе данных - нажмите здесь, чтобы увидеть изображение

Для проверки этого сценария я написал на bash script debit.sh

curl -I 'http://localhost:8080/demo/debit' &
curl -I 'http://localhost:8080/demo/debit' &

запустите это с помощью bash debit.sh Таким образом, он может вызывать одну и ту же конечную точку покоя дважды.

Вывод, который я получаю

2019-03-27 14:17:36.375 DEBUG 11191 --- [nio-8080-exec-3] hello.MainController                     : debit  100 from account id 1 
2019-03-27 14:17:36.376 DEBUG 11191 --- [nio-8080-exec-4] hello.MainController                     : debit  100 from account id 1 
2019-03-27 14:17:36.394 DEBUG 11191 --- [nio-8080-exec-4] hello.service.AccountService             : current balance 100
2019-03-27 14:17:36.394 DEBUG 11191 --- [nio-8080-exec-3] hello.service.AccountService             : current balance 100
2019-03-27 14:17:36.395 DEBUG 11191 --- [nio-8080-exec-4] hello.service.AccountService             : debited
2019-03-27 14:17:36.396 DEBUG 11191 --- [nio-8080-exec-3] hello.service.AccountService             : debited

В обеих транзакциях текущий баланс равен 100 и дебетуется одинаково. Здесь я хочу обновить баланс до -100.

Ответы [ 2 ]

0 голосов
/ 27 марта 2019

Я решил, сделав метод дебетования в сервисе «синхронизированным». Но это снизит производительность, так как один запрос будет ждать завершения другого. Я думаю, что правильным решением для этого является использование механизма блокировки - java-одновременные блокировки

@Transactional
    synchronized  public void debit(Integer id,int balance){



        Optional<Account> accountOptional = accountRepository.findById(id);
        Account account = accountOptional.get();
       // entityManager.refresh(account, LockModeType.PESSIMISTIC_WRITE);

        final int oldBalance = account.getBalance();
        log.debug("current balance {}",oldBalance);
        account.setBalance(oldBalance-balance);
        accountRepository.save(account);
        log.debug("debited");
    }
0 голосов
/ 27 марта 2019

Пожалуйста, прочитайте следующий ответ: Spring @Transactional - изоляция, распространение

В вашем случае, я чувствую, что установка транзакции как Read Commited поможет, но если нет, Serializable полностью решит вашу проблему, но это приведет к снижению производительности.

...