Обсуждение: Locking question

Поиск
Список
Период
Сортировка

Locking question

От
"Frank Millman"
Дата:
Hi all
 
I am designing an inventory application, and I want to ensure that the stock level of any item cannot go negative.
 
Instead of maintaining a running balance per item, I store the original quantities received in one table (call it ‘inv_rec’), and any amounts removed in another table (call it ‘inv_alloc’).
 
CREATE TABLE inv_rec
    (row_id SERIAL PRIMARY KEY,
    product_id INT REFERENCES inv_products,
    qty INT);
 
CREATE TABLE inv_alloc
    (row_id SERIAL PRIMARY KEY,
    rec_id INT REFERENCES inv_rec,
    qty INT);
 
To get the balance of a particular item -
 
SELECT SUM(
    a.qty + COALESCE(
      (SELECT SUM(b.qty) FROM inv_alloc b
      WHERE b.rec_id = a.row_id), 0))
FROM inv_rec a
WHERE a.product_id = 99;
 
To remove a quantity from a particular item -
 
INSERT INTO inv_alloc (rec_id, qty)
  VALUES (23, -1);
 
I want the application to check that there is sufficient quantity before attempting to execute the INSERT command.
 
If ok, it will look for a suitable row in ‘inv_rec’ to allocate against.
 
The danger of course is that, in a multi-user system, another user might have removed an additional quantity from the same item in between the SELECT and the INSERT.
 
I *think* that the solution is to BEGIN the transaction, then perform SELECT ... WITH UPDATE, then proceed with INSERT and COMMIT if ok, else ROLLBACK.
 
Is this the correct approach, or am I missing something?
 
Thanks
 
Frank Millman
 

Re: Locking question

От
hubert depesz lubaczewski
Дата:
On Wed, Oct 26, 2016 at 10:42:29AM +0200, Frank Millman wrote:
> Hi all
>
> I am designing an inventory application, and I want to ensure that the stock level of any item cannot go negative.
>
> Instead of maintaining a running balance per item, I store the original quantities received in one table (call it
‘inv_rec’),and any amounts removed in another table (call it ‘inv_alloc’). 
>
> CREATE TABLE inv_rec
>     (row_id SERIAL PRIMARY KEY,
>     product_id INT REFERENCES inv_products,
>     qty INT);
>
> CREATE TABLE inv_alloc
>     (row_id SERIAL PRIMARY KEY,
>     rec_id INT REFERENCES inv_rec,
>     qty INT);
>
> To get the balance of a particular item -
>
> SELECT SUM(
>     a.qty + COALESCE(
>       (SELECT SUM(b.qty) FROM inv_alloc b
>       WHERE b.rec_id = a.row_id), 0))
> FROM inv_rec a
> WHERE a.product_id = 99;
>
> To remove a quantity from a particular item -
>
> INSERT INTO inv_alloc (rec_id, qty)
>   VALUES (23, -1);
> Is this the correct approach, or am I missing something?

What I would do, is to add trigger on inv_alloc, than when you
insert/update/delete row there, it updates appropriate row in inv_rec by
correct number.

Then, I'd add check on inv_rec to make sure qty is never < 0.

Best regards,

depesz

--
The best thing about modern society is how easy it is to avoid contact with it.
                                                             http://depesz.com/


Re: Locking question

От
"Frank Millman"
Дата:
 
Sent: Wednesday, October 26, 2016 10:46 AM
Subject: Re: [GENERAL] Locking question
 
On Wed, Oct 26, 2016 at 10:42:29AM +0200, Frank Millman wrote:
> Hi all
> >
> > I am designing an inventory application, and I want to ensure that the stock level of any item cannot go negative.
> >
[...]
>
> What I would do, is to add trigger on inv_alloc, than when you
> insert/update/delete row there, it updates appropriate row in inv_rec by
> correct number.
>
> Then, I'd add check on inv_rec to make sure qty is never < 0.
>
Thanks, depesz

I can see how that would work, but I have two comments.
 
1. I am writing my application to support 3 databases – PostgreSQL, sqlite3, and MS SQL Server. Because they are all so different when it comes to triggers and procedures, I am trying to avoid using them, and do as much within the application as possible.
 
2. I think you are suggesting maintaining a ‘balance’ column on inv_rec. This raises the question of whether or when you should create and maintain a column if the same information could be derived from other sources. I realise that this is a judgement call, and sometimes I struggle to get the balance right. Is this a situation where people would agree that it is warranted?
 
I would still appreciate some feedback as to whether my proposed solution would work.
 
Thanks
 
Frank
 

Re: Locking question

От
Gary Evans
Дата:
Hi,

Personally, I like to make the database responsible for the integrity of the data within it as much as possible.  And therefore would favour Depsesz's solution to trying to manage it within the application.

Cheers
Gary

On Wed, Oct 26, 2016 at 8:18 PM, Frank Millman <frank@chagford.com> wrote:
 
Sent: Wednesday, October 26, 2016 10:46 AM
Subject: Re: [GENERAL] Locking question
 
On Wed, Oct 26, 2016 at 10:42:29AM +0200, Frank Millman wrote:
> Hi all
> >
> > I am designing an inventory application, and I want to ensure that the stock level of any item cannot go negative.
> >
[...]
>
> What I would do, is to add trigger on inv_alloc, than when you
> insert/update/delete row there, it updates appropriate row in inv_rec by
> correct number.
>
> Then, I'd add check on inv_rec to make sure qty is never < 0.
>
Thanks, depesz

I can see how that would work, but I have two comments.
 
1. I am writing my application to support 3 databases – PostgreSQL, sqlite3, and MS SQL Server. Because they are all so different when it comes to triggers and procedures, I am trying to avoid using them, and do as much within the application as possible.
 
2. I think you are suggesting maintaining a ‘balance’ column on inv_rec. This raises the question of whether or when you should create and maintain a column if the same information could be derived from other sources. I realise that this is a judgement call, and sometimes I struggle to get the balance right. Is this a situation where people would agree that it is warranted?
 
I would still appreciate some feedback as to whether my proposed solution would work.
 
Thanks
 
Frank
 

Re: Locking question

От
btober@broadstripe.net
Дата:



From: "Frank Millman" <frank@chagford.com>
To: pgsql-general@postgresql.org
Sent: Wednesday, October 26, 2016 4:42:29 AM
Subject: [GENERAL] Locking question

Hi all
 
I am designing an inventory application, and I want to ensure that the stock level of any item cannot go negative.
 
Instead of maintaining a running balance per item, I store the original quantities received in one table (call it ‘inv_rec’), and any amounts removed in another table (call it ‘inv_alloc’).
 
CREATE TABLE inv_rec
    (row_id SERIAL PRIMARY KEY,
    product_id INT REFERENCES inv_products,
    qty INT);
 
CREATE TABLE inv_alloc
    (row_id SERIAL PRIMARY KEY,
    rec_id INT REFERENCES inv_rec,
    qty INT);
 
To get the balance of a particular item -
 
SELECT SUM(
    a.qty + COALESCE(
      (SELECT SUM(b.qty) FROM inv_alloc b
      WHERE b.rec_id = a.row_id), 0))
FROM inv_rec a
WHERE a.product_id = 99;
 
To remove a quantity from a particular item -
 
INSERT INTO inv_alloc (rec_id, qty)
  VALUES (23, -1);
 
I want the application to check that there is sufficient quantity before attempting to execute the INSERT command.
 
If ok, it will look for a suitable row in ‘inv_rec’ to allocate against.
 
The danger of course is that, in a multi-user system, another user might have removed an additional quantity from the same item in between the SELECT and the INSERT.
 
I *think* that the solution is to BEGIN the transaction, then perform SELECT ... WITH UPDATE, then proceed with INSERT and COMMIT if ok, else ROLLBACK.
 
Is this the correct approach, or am I missing something?
 
Thanks
 
Frank Millman
 


Is it necessary to have the two separate tables for received and allocated? I would record the receipt and allocation transactions in a single table.

Also, and then if there is no need for the high concurrency performance of SERIAL (which there probably is not, I'm guessing, since you are considering locking), I would make a keyed sequence by recording the last-used row_id as a column in the inv_products table.

Then, apply a strategy such as described in



That pattern employs a trigger for convenience, but you could do without. Then, the first step in your BEGIN ... COMMIT block is to update the last-used value in the corresponding inv_products row to compute the next-to-be-used row_id value (i.e., UPDATE first, then SELECT it back out, or use UPDATE ...RETURNING).

That initial UPDATE transaction will block other transactions attempting to initiate inventory updates on that particular inventory item and effectively serialize your concurrent inventory activity, per inventory item.

After you add the inventory transaction, then check the net balance and throw an exception if negative. That rolls back everything back to the initial row_id update in inv_products for the product

Assuming a well-managed inventory organization, the exception throwing should be relatively infrequent.

Me personally, depending on application specifics, might make the deliberate de-normalization decision and layout the transaction table to model an accounting balance sheet, having separate columns for inventory additions, subtractions, and a running total.

--B


Re: Locking question

От
rob stone
Дата:
On Wed, 2016-10-26 at 12:18 +0200, Frank Millman wrote:
>  
> From: hubert depesz lubaczewski
> Sent: Wednesday, October 26, 2016 10:46 AM
> To: Frank Millman
> Cc: pgsql-general@postgresql.org
> Subject: Re: [GENERAL] Locking question
>  
> On Wed, Oct 26, 2016 at 10:42:29AM +0200, Frank Millman wrote:
> > Hi all
> > > 
> > > I am designing an inventory application, and I want to ensure
> that the stock level of any item cannot go negative.
> > > 
> [...]
>  
> 1. I am writing my application to support 3 databases – PostgreSQL,
> sqlite3, and MS SQL Server. Because they are all so different when it
> comes to triggers and procedures, I am trying to avoid using them,
> and do as much within the application as possible.
>  
> 2. I think you are suggesting maintaining a ‘balance’ column on
> inv_rec. This raises the question of whether or when you should
> create and maintain a column if the same information could be derived
> from other sources. I realise that this is a judgement call, and
> sometimes I struggle to get the balance right. Is this a situation
> where people would agree that it is warranted?
>  
> I would still appreciate some feedback as to whether my proposed
> solution would work.
>  
> Thanks
>  
> Frank
>  

Hello Frank,


Stock on-hand quantity = goods inwards  +
                         goods returned from customers -
                         goods invoiced  -
                         goods returned to suppliers due to defects.

Available stock quantity = stock on-hand -
                           goods ordered.

The hassle you have in a multi-user environment is when User "A" takes
an order from Customer "A" for Product "A" and at the same time User
"B" takes an order from Customer "B" also for Product "A".
Both users will quote the same value for quantity available (or
quantity on-hand if you don't keep track of orders).
Most companies rank their customers according to internal policy. For
example, customers who pay their bills late could be given a lower rank
than those that pay on time. Some customers won't accept partial
deliveries. Etc.
So, if you just take orders you can run a batch process to convert
orders into invoices and read your orders according to customer ranking
criteria.
By running a batch process, with a suitable "locking" mechanism to
avoid the process being run twice at the same time, the maintenance of
stock on-hand quantities is run in a single instance, you won't over
ship and it won't become negative.

I think this method will work on all three databases cited.

HTH,
Rob



Re: Locking question

От
Kevin Grittner
Дата:
On Wed, Oct 26, 2016 at 3:42 AM, Frank Millman <frank@chagford.com> wrote:

> I am designing an inventory application, and I want to ensure
> that the stock level of any item cannot go negative.

One way to do this is to use only transactions at the SERIALIZABLE
transaction isolation level to maintain and query this data.  When
you do that, you can write the transactions as though each would
only ever be run by itself, and if a concurrent transaction would
cause incorrect behavior you will get an error with a SQLSTATE
starting with "40", and you can retry the transaction from the
start.  For applications like you describe, this often performs
better than approaches which use blocking locks (assuming proper
configuration and reasonable indexes).

https://www.postgresql.org/docs/current/static/transaction-iso.html

Logically, the problem is similar to the overdraft protection
example here:

https://wiki.postgresql.org/wiki/SSI#Overdraft_Protection

Basically, you need some way to catch serialization failure errors
and retry the failed transaction from the start, and that frees you
from worrying about where race conditions exist and covering each
one individually.

If you want to use a less strict isolation level, you need to
either promote the conflict from read-write to write-write by using
SELECT FOR UPDATE or you need to materialize the conflict.  The
latter could be accomplished by maintaining a total within any
transactions modifying the detail (either from triggers or
application code), which will cause a write conflict if two
transactions try to update the same total at the same time, or by
using explicit locking controlled from the application.

--
Kevin Grittner
EDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company


Re: Locking question

От
"Frank Millman"
Дата:
 
Sent: Wednesday, October 26, 2016 10:42 AM
Subject: [GENERAL] Locking question
 
> I am designing an inventory application, and I want to ensure that the stock level of any item cannot go negative.
 
Thanks to all for some really great replies. Much food for thought there.
 
As mentioned previously, I am trying to avoid using PostgreSQL-specific techniques, as I need to support sqlite3 and SQL Server as well.
 
There is an additional complication that I forgot to mention in my original post.
 
For costing purposes, I want to run a FIFO system. This means I have to maintain separate entries for each receipt of stock, and allocate any sales of stock against the receipts ‘oldest first’.
 
Assume the following purchases -
 
2016-06-01  qty 5  Unit price $5.00
2016-06-02  qty 10  Unit price $5.50
2016-06-03  qty 15  Unit price $6.00
 
Quantity on hand after the third purchase is 30. Whether this should be maintained as a total somewhere, or derived from totalling the receipts, is a matter for debate, but I think that it is not relevant for this discussion.
 
Then assume the following sales -
 
2016-06-11  qty 8
2016-06-12  qty 12
2016-06-13  qty 16
 
The first sale will succeed, and will record a ‘cost of  sale’ of (5 x $5.00) + (3 x $5.50).
The second sale will succeed, and will record a ‘cost of  sale’ of (7 x $5.50) + (5 x $6.00).
The third sale must be rejected, as there is insufficient stock.
 
This is how I propose to achieve this -
 
CREATE TABLE inv_rec
    (row_id SERIAL PRIMARY KEY,
    product_id INT REFERENCES inv_products,
    rec_date DATE,
    qty INT
    unit_price DEC(15, 2));
 
CREATE TABLE inv_alloc
    (row_id SERIAL PRIMARY KEY,
    rec_id INT REFERENCES inv_rec,
    qty INT);
 
INSERT INTO inv_rec (product_id, rec_date, qty, unit_price)
  VALUES (99, ‘2016-06-01’, 5, 5.00);
INSERT INTO inv_rec (product_id, rec_date, qty, unit_price)
  VALUES (99, ‘2016-06-02’, 10, 5.50);
INSERT INTO inv_rec (product_id, rec_date, qty, unit_price)
  VALUES (99, ‘2016-06-03’, 15, 6.00);
 
The sales will be handled at application level. Here is some pseudo code -
 
qty_to_allocate = sale_qty
cost_of_sale = 0
 
BEGIN TRANSACTION
 
SELECT a.row_id, a.unit_price,
      a.qty + COALESCE((SELECT SUM(b.qty) FROM inv_alloc b
      WHERE b.rec_id = a.row_id), 0) AS balance
FROM inv_rec a
WHERE a.product_id = 99
AND
      a.qty + COALESCE((SELECT SUM(b.qty) FROM inv_alloc b
      WHERE b.rec_id = a.row_id), 0)
  > 0
ORDER BY a.rec_date
FOR UPDATE
 
for row in rows:
    if row.balance >= qty_to_allocate:
        INSERT INTO inv_alloc (rec_id, qty)
            VALUES (row.row_id, –qty_to_allocate)
        cost_of_sale += (qty_to_allocate * unit_price)
        qty_to_allocate = 0
    else:
        INSERT INTO inv_alloc (rec_id, qty)
            VALUES (row.row_id, –row.balance)
        cost_of_sale += (row.balance * unit_price)
        qty_to_allocate –= row.balance
 
if qty_to_allocate:  # i.e. insufficient stock
    raise exception and ROLLBACK
else:
    COMMIT
 
My main concern is that this should be robust.
 
A secondary concern is that it should be reasonably efficient, but that is not a priority at this stage. If it became a problem, I would look at maintaining a ‘balance’ column on each ‘inv_rec’.
 
Comments welcome.
 
Frank
 

Re: Locking question

От
Kevin Grittner
Дата:
On Thu, Oct 27, 2016 at 1:37 AM, Frank Millman <frank@chagford.com> wrote:

> As mentioned previously, I am trying to avoid using PostgreSQL-specific
> techniques, as I need to support sqlite3 and SQL Server as well.

The SERIALIZABLE transaction isolation level is portable.  It it
part of the SQL standard (and has been since the beginning), and is
supported by just about every database product, including SQLite
and SQL Server.  (In fact, you have to go well out of your way for
SQLite transactions *not* to be SERIALIZABLE --
https://www.sqlite.org/isolation.html )

> For costing purposes, I want to run a FIFO system. This means I have to
> maintain separate entries for each receipt of stock, and allocate any sales
> of stock against the receipts ‘oldest first’.

The two ways of doing this which spring to mind are window
functions (supported by PostgreSQL and SQL Server, but not SQLite)
and cursors (supported by most database products, including the
three you mention).

--
Kevin Grittner
EDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company