Database and transaction control in operations with values

12

In my project I need to develop a system of transactions where the user will put credit and the services that he has contracted will consume these credits.

I built the following structure but I do not know if it's the best way to do it:

Usuario
id | Nome

Transacoes
id | usuario_id | valor | data

When the user enters credits I will feed the transacoes table; when there is an automatic debit for the use of the services, I will feed the table transacoes with negative values (for example, -12 monthly value); when I was to generate the cash (balance) of the user I will count all the values to reach the current balance.

The problem I found is that as this table grows, the performance to generate the balance will decrease.

Anyway, I can not think of a framework to encompass this business rule without stress: (

    
asked by anonymous 27.11.2014 / 16:00

4 answers

6

Let's say that constructing the balance from reading all transactions is actually a performance constraint (you still do not know, but let's say you already know), you can create a balance table (a record only for each user):

Usuário
id | Nome

Transação
id | usuario_id | valor | data

Usuário_Saldo
id | usuario_id | saldo_atual

With each transaction (credit or debit in the Transaction table), you update the user's balance in the Usuário_Saldo table, so you will have to read a single record when you want to check a user's balance.

So far, of course. The care you need to have is to ensure the integrity of the data. If you first create the record in the Transaction table, when you calculate the balance it may have already been changed by another Credit or Debit transaction.

Then you first need to update the balance, to launch the credit or debit transaction. Of course, these two commands must be an atomic operation, that is, they must be within the same database transaction. So:

-- abre transação de banco de dados
UPDATE Usuário_Saldo
SET saldo_atual = saldo_atual + @valor_transação
WHERE usuario_id = @usuario_id;

INSERT INTO Transação (usuario_id, valor, data)
VALUES (@usuario_id, @valor_transação, @data)
-- commita transação

If the system sends "two" credit or debit transactions at the same time, the second will have to wait for the first one to complete because the Usuário_Saldo registry will be locked.

Update: What if I want to ensure that a debit greater than the available balance is not made?

The solution described above ensures balance integrity without having to worry about the level of transaction isolation. But if I add a SELECT before updating the balance in order to first check if there is available balance for a new debt, I would break the solution because the select could read an earlier version of the balance, or a version not yet committed, depending on the level transaction isolation.

In this solution, then the available balance last is checked, causing a rollback if the operation resulted in a negative balance. Just add a check check for the balance before the end of the transaction. The complete solution looks like this:

-- abre a transação de banco de dados

-- atualiza o saldo
UPDATE Usuário_Saldo
SET saldo_atual = saldo_atual + @valor_transação
WHERE usuario_id = @usuario_id;

-- registra a operação de débito/crédito
INSERT INTO Transação (usuario_id, valor, data)
VALUES (@usuario_id, @valor_transação, @data);

-- verifica se a operação resultou em um saldo negativo
SELECT saldo_atual
FROM Usuário_Saldo
WHERE usuario_id = @usuario_id

-- se saldo_atual < 0, faz rollback
-- senão, commita a transação

This is a simple solution for not worrying about transaction isolation level (which depends on database settings or additional policies when executing SQL commands); and is performative because it allows the lowest level of isolation because the higher the level of isolation, the more resources are used by the database server, and the lower the concurrency of query operations.

More complex solutions can emerge depending on specific needs. For example, if you carry out the whole transaction before you know that the balance was insufficient, it causes too much cost, I can do a pre-check of the balance and not trigger the update if it is insufficient - this reduces the occurrences of attempting debits that do not will be completed successfully. Of course I will not rely on this prior checking to secure my balance - the final check that determines whether the transaction can be committed remains.

Note 1: These commands are considering Id auto-increment in tables. Note 2: The "@" is symbolizing the values passed by parameter to the query. Note 3: This code assumes that User_Over already has a record for each user, but you can have variations of that.

    
27.11.2014 / 18:03
4

There are several approaches, it depends a lot on the scenario you are thinking of, the investment in hardware / software and the speed you want to achieve.

The transaction log template you described is not bad. There are many systems today based on it. It is called Event Sourcing . link

The Event Sourcing is a technique that will save the events one by one from the first, and to know the current result (the balance for example), you will add up register by registry.

There is a big advantage in it: in relational databases, you have a very short response time to insert new records and can greatly increase the parallelization capacity of the system by adding new hardware / software. There are also techniques for you to

30.11.2014 / 20:34
2

I read excellent answers. Just to show another approach I would like to suggest the following:

I would model this way:

Usuário
id | Nome

Transação
id | usuario_id | valor | data | data_processamento

Consolidado
competencia | usuario_id | saldo_atual

What changes? I find it more interesting to work on a cash flow style solution: I have a current balance , inputs and outputs . Anytime every day of the month I would run a routine that would consolidate the balance of current month .

In addition, I've included a data_processing field in the Transaction entity where it would be possible to know when the transaction was actually consolidated.

With the above approach I get the following benefits :

  • A smaller data mass to work with, since I would only work with 1 month information.
  • It is much easier to look at the past, for nothing will be lost.
  • If database size is a problem, it would be easy to create a routine to archive very old values (type with 5 years old)
  • As consolidated would be the responsibility of a job, it would not be very difficult to control transactions.

Now as not everything is flowers, I think some disadvantages would be

  • Your logic would be a bit more complex (I say this by imagining a basic system, I do not know what your system is exactly about)
  • I can take advantage of the parentheses above to complement that this solution is probably not exactly what you are looking for.

Well, I hope it helps.

    
02.12.2014 / 17:00
1

In transaction you can put valor acumulado which will always be the valor acumulado previous + valor current, with the first valor acumulado being the first valor itself. At the time you access the balance, you access the last valor acumulado without having to make any transactions.

    
01.12.2014 / 03:38