3

Need some Guru advice.

Our system checks if the current client's total Debt amount exceeds allowed Credit amount and if true, adds new Debt entry

if (additionalDebtAllowed(clientId, amount)) {
    deptRepository.saveAndFlush(new Debt(clientId, amount));
}

In additionalDebtAllowed() we get all active debt rows by client id and compare with the credit limit, that we get from another system.

The problem is that REST calls might be concurrent and we can run in following situation:

  1. Current client debt is 50, his credit limit is 100 and he asks for another 50.
  2. Both threads get current debt (50).
  3. Both threads check towards credit limit (50 + 50 <= 100)
  4. Both threads create new debt rows
  5. Now client debt is 150, which is more, than credit limit.

The easiest way would be before reading and persisting data try lock some row in database by client id. If success - proceed and unlock. If fail - retry until success. But I assume there might be more beautiful ways.

Thought about SERIALIZABLE Isolation Level, but it will lock whole table, while I need synchronizations only per client.

Alex Kartishev
  • 1,836
  • 3
  • 18
  • 25
  • I am assuming that you are going for MSA . Please have a look at [this](https://stackoverflow.com/questions/36948775/managing-data-store-concurrency-as-microservices-scale) for architectural option. If you service is monolith then you can implement a check or flag where before going to db for any client id you will go to that variable/collection . ( basically what you said but in app instead of db). If its MSA then you might look into redis dist lock . or [here](https://dzone.com/articles/data-consistency-in-microservices-architecture). Basically what you want but not in DB – RAHUL ROY Apr 09 '19 at 12:55

1 Answers1

5

I'll try to do it in a simple way instead of complicating things.

I will focus on the real problem and not on the beauty of the code.

My approach, which I have tested, would be as follows:

I created a main class, in which two CompletableFuture simulate two simultaneous calls for the same clientId.

//Simulate lines of db debts per user
static List<Debt> debts = new ArrayList<>();

static Map<String, Object> locks = new HashMap<String, Object>();

public static void main(String[] args) {

    String clientId = "1";

    //Simulate previous insert line in db per clientId
    debts.add(new Debt(clientId,50));

    //In a operation, put in a map the clientId to lock this id
    locks.put(clientId, new Object());

    final ExecutorService executorService = Executors.newFixedThreadPool(10);

    CompletableFuture.runAsync(() -> {
        try {
            operation(clientId, 50);
        } catch (Exception e) {
        }
    }, executorService);

    CompletableFuture.runAsync(() -> {
        try {
            operation(clientId, 50);
        } catch (Exception e) {
        }
    }, executorService);

    executorService.shutdown();
}

The method operation is the key. I have synchronized the map by clientId, which means that for other clientId it will not be locked, for each clientId it will let pass a thread simultaneously.

private static void operation(String clientId, Integer amount) {
    System.out.println("Entra en operacion");
    synchronized(locks.get(clientId)) {
        if(additionalDebtAllowed(clientId, 50)) {
            insertDebt(clientId, 50);
        }
    }
}

The following methods simulate insertions, db searches and remote searches, but I think the concept is understood, I could do it with repositories but that is not the focus.

private static boolean additionalDebtAllowed(String clientId, Integer amount) {

    List<Debt> debts = debtsPerClient(clientId);

    int sumDebts = debts.stream().mapToInt(d -> d.getAmount()).sum();

    int limit = limitDebtPerClient(clientId);

    if(sumDebts + amount <= limit) {
        System.out.println("Debt accepted");
        return true;
    }

    System.out.println("Debt denied");

    return false;
}

//Simulate insert in db
private static void insertDebt(String clientId, Integer amount) {
    debts.add(new Debt(clientId, amount));
}

//Simulate search in db
private static List<Debt> debtsPerClient(String clientId) {

    return debts;
}

//Simulate rest petition limit debt
private static Integer limitDebtPerClient(String clientId) {

    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    return 100;
}

You can test it with another clientId and another CompletableFuture more and you will see that it works for each client separately in a correct way.

I hope it helps you.

Francesc Recio
  • 2,187
  • 2
  • 13
  • 26
  • How can we scales this app horizontally. The synchronized block will work within the context of one node only. – Vasco Apr 17 '19 at 05:17