1

I have created a simple spring boot application which will demonstrate how the bank transactions happens.I have created one 'Account' entity and created one 'debit' rest endpoint.

Here I am calling 'debit' api two times concurrently but only one time amount is debited.I want to know how can I lock the account entity so that another thread will read updated balance and will debit second time too.

I tried to lock 'account' entity with lock mode type as PESSIMISTIC_WRITE but its not working.

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 +
                '}';
    }
}

Rest end point is

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

my database record is - click here to see the image

To test this scenario I have written on bash script debit.sh

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

run this with bash debit.sh So it can call same rest endpoint twice.

The output I am getting is

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

In both the transactions reading current balance as 100 and debiting same. What I want here is It should update balance as -100.

2 Answers2

0

Please have a read, on the following answer: Spring @Transactional - isolation, propagation

In your case, I feel that setting the transaction as Read Commited would do the trick, but if not, Serializable should fix your problem completely, but this would come with a performance cost.

Sofo Gial
  • 697
  • 1
  • 9
  • 20
  • I tried by adding @Transactional(isolation = Isolation.SERIALIZABLE) on method but still same problem. – R Karwalkar Mar 27 '19 at 10:51
  • Can you try updating your `CacheConcurrencyStrategy` to `TRANSACTIONAL` too and let us know of the result? The value you are using does the exact opposite of what you need. – Sofo Gial Mar 27 '19 at 11:03
  • No success with @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL) – R Karwalkar Mar 27 '19 at 11:31
0

I solved by making debit method in service to 'synchronized'.But it will decrease the performance as one request will wait for another to complete.I think The proper solution for this is to use locking mechanism -java-concurrent-locks

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