Как сделать метод в Java атомарным при использовании Spring Boot?

У меня есть приложение REST, написанное с использованием Spring Boot, в котором у меня есть кошелек. В кошельке есть такие методы, как addAmount, deductAmount и так далее. Вот код:

WalletController.java

public class WalletController {
    public final LoadDatabase loadDatabase;
    private final WalletRepository repository;

    @Autowired
    public WalletController(LoadDatabase loadDatabase, WalletRepository repository) {
        this.loadDatabase = loadDatabase;
        this.repository = repository;
    }

    @GetMapping("/addAmount")
    @ResponseBody
    public void addAmount(@RequestParam Long custId, @RequestParam Long amount){
        try{
            Wallet wallet = repository.findWalletsByCustId(custId).get(0);
            wallet.balance = wallet.balance+amount;
            repository.save(wallet);
        }catch(IndexOutOfBoundsException e){
            //handle exception
        }
    }

    @GetMapping("/deductAmount")
    @ResponseBody
    public boolean deductAmount(@RequestParam Long custId, @RequestParam Long amount){
        try{
            Wallet wallet = repository.findWalletsByCustId(custId).get(0);
            if (wallet.balance < amount)
                return false;
            wallet.balance = wallet.balance-amount;
            repository.save(wallet);
            return true;
        }catch(IndexOutOfBoundsException e){
            return false;
        }
    }

    // some other methods.
 

Доступ к нему будет осуществляться одновременно, и, следовательно, я хочу сделать как addAmount, так и deductAmount атомарными по своей природе.

Чтобы проверить это, я написал сценарий оболочки, который одновременно добавляет и вычитает некоторую сумму.

wallet_test.sh

#! /bin/sh

# Get the balance of Customer 201 before.
balanceBefore=$(curl -s "http://localhost:8082/getBalance?custId=201")

echo "Balance Before:" $balanceBefore

sh wa1 & sh wa2
wait    

# Get the balance of Customer 201 afterwards.
balanceAfter=$(curl -s "http://localhost:8082/getBalance?custId=201")

echo "Balance After" $balanceAfter

где wa1 и wa2 следующие:

for i in {0..10};
do
#   echo "Shell 1:" $i
    resp=$(curl -s "http://localhost:8082/addAmount?custId=201&amount=100")
done
for i in {0..10};
do
#   echo "Shell 2:" $i
    resp=$(curl -s "http://localhost:8082/deductAmount?custId=201&amount=100")
done

Результат, как и ожидалось из-за одновременного доступа, имеет вид:

Balance Before: 10000
Balance After 9900

Balance Before: 10000
Balance After 10300

Balance Before: 10000
Balance After 9600

Ожидаемый результат для меня заключается в том, что баланс до и после должен оставаться неизменным, то есть 10000.

Я прочитал, что, чтобы сделать его атомарным, мы можем использовать аннотацию @Transactional и добавить ее к обоим методам или ко всему классу. Я пробовал делать и то, и другое, но все же не получаю желаемых результатов.

Я добавил его на уровне метода, т.е.

    @GetMapping("/deductAmount")
    @ResponseBody
    @Transactional(isolation = Isolation.SERIALIZABLE)
    public boolean deductAmount(@RequestParam Long custId, @RequestParam Long amount){

и то же самое для deductAmount, который не сработал.

Я пробовал добавить его на уровне класса, т.е.

@Controller
@Transactional(isolation = Isolation.SERIALIZABLE)
public class WalletController {
    public final LoadDatabase loadDatabase;
    private final WalletRepository repository;

и это тоже не сработало.

Разве @Transactional не предназначен для использования таким образом? Должен ли я использовать какой-либо другой механизм блокировки, чтобы достичь того, что я хочу?

Обновлено:

Как уже упоминалось, я также попытался добавить пессимистические блокировки.

    import static javax.persistence.LockModeType.PESSIMISTIC_WRITE;

    @PersistenceContext
    private EntityManager em;


    @GetMapping("/addAmount")
    @ResponseBody
    @Transactional
    synchronized public void addAmount(@RequestParam Long custId, @RequestParam Long amount){
        try{
            Wallet wallet = repository.findWalletsByCustId(custId).get(0);
            em.lock(wallet, PESSIMISTIC_WRITE);
            wallet.balance = wallet.balance+amount;
            repository.save(wallet);
        }catch(IndexOutOfBoundsException e){
            //handle exception
        }
    }

    @GetMapping("/deductAmount")
    @ResponseBody
    @Transactional
    synchronized public boolean deductAmount(@RequestParam Long custId, @RequestParam Long amount){
        try{
            Wallet wallet = repository.findWalletsByCustId(custId).get(0);
            em.lock(wallet, PESSIMISTIC_WRITE);
            if (wallet.balance < amount)
                return false;
            wallet.balance = wallet.balance-amount;
            repository.save(wallet);
            return true;
        }catch(IndexOutOfBoundsException e){
            return false;
        }
    }

Я помню, как использовал его в методах со значением по умолчанию (я имею в виду: только @Transactional, а не @Transactional(isolation=...)). Кроме того, я использовал его на уровне обслуживания, а не в контроллерах. но похоже, что вы вызываете репозитории прямо в контроллерах ... это все равно должно работать. Если вы хотите проверить это проще, вы можете сделать запрос, а затем выбросить исключение и посмотреть, был ли результат зафиксирован.

Arnaud Denoyelle 09.04.2021 15:47

@ArnaudDenoyelle Я пробовал это делать и получаю те же результаты.

Gokul 09.04.2021 15:55
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
0
2
56
1

Ответы 1

Недостаточно установить уровень изоляции. Вы должны использовать оптимистичную или пессимистичную блокировку, чтобы добиться желаемого поведения. Красивое и краткое описание этой стратегии можно найти в этом отвечать.

Я сделал это, но мои результаты все еще неверны. Я добавил свой отредактированный код в исходный вопрос.

Gokul 09.04.2021 19:57

Я думаю, вам следует изменить свой подход к блокировке, например, используя аннотацию @Lock в классе репозитория кошелька. В вашем примере кода чтение и блокировка - это отдельные операции, которые могут допускать проблемы с параллелизмом, такие как: поток T1 считывает значение кошелька 1000, и в то же время поток T2 считывает значение кошелька 1000, когда T1 блокирует и обновляет кошелек с 1000-100, когда T2 разблокирует и обновляет свою версию кошелька на 1000 + 100. Чтобы избежать этого, эта строка должна быть заблокирована во время операции выбора. На уровне базы данных это может быть достигнуто с помощью SELECT * ... FOR UPDATE

Sergey Vasnev 09.04.2021 20:48

Другие вопросы по теме