PostgreSQL中的行级锁及Odoo中的应用

PosgreSQL行级锁可以是排他的或者是共享的。特定行上的排他行级锁是在行被更新的时候自动请求的。 该锁一直保持到事务提交或者回滚。行级锁不影响对数据的查询,它们只阻塞对同一行的写入。

行级锁命令语法

SELECT ... FOR lock_strength [OF table_name [, ...]] [ NOWAIT ]

其中lock_strenth 行级锁强度参数值为下列之一,其影响SELECT如何从表中锁定行:

  • UPDATE

  • NO KEY UPDATE

  • SHARE

  • KEY SHARE

    FOR UPDATE

    令那些被SELECT检索出来的行被锁住,就像在更新一样。这样就避免它们在当前事务结束前被其它事务修改或者删除;也就是说, 其它企图UPDATE,DELETE,SELECT FOR UPDATE,SELECT FOR NO KEY UPDATE,SELECT FOR SHARE或SELECT FOR KEY SHARE这些行的事务将被阻塞,直到当前事务结束。同样, 如果一个来自其它事务的UPDATE,DELETE,SELECT FOR UPDATE已经锁住了某个或某些选定的行,SELECT FOR UPDATE将等到那些事务结束,并且将随后锁住并返回更新的行(或者不返回行,如果行已经被删除)。但是,在REPEATABLE READ或SERIALIZABLE事务内部,如果在事务开始时要被锁定的行已经改变了,那么将抛出一个错误。

    FOR NO KEY UPDATE

    的行为类似于FOR UPDATE,只是获得的锁比较弱:该锁不阻塞尝试在相同的行上获得锁的SELECT FOR KEY SHARE命令。该锁模式也可以通过任何不争取FOR UPDATE锁的UPDATE获得。

    FOR SHARE

    的行为类似于FOR NO KEY UPDATE,只是它在每个检索出来的行上获得一个共享锁,而不是一个排它锁。一个共享锁阻塞其它事务在这些行上执行UPDATE,DELETE,SELECT FOR UPDATE或SELECT FOR NO KEY UPDATE,却不阻止他们执行SELECT FOR SHARE或SELECT FOR KEY SHARE。

    FOR KEY SHARE

    的行为类似于FOR SHARE,只是获得的锁比较弱: 阻塞SELECT FOR UPDATE但不阻塞SELECT FOR NO KEY UPDATE。一个共享锁阻塞其他事务执行DELETE或任意改变键值的UPDATE, 但是不阻塞其他UPDATE,也不阻止SELECT FOR NO KEY UPDATE, SELECT FOR SHARE 或SELECT FOR KEY SHARE。

为了避免操作等待其它事务提交,使用NOWAIT选项。如果被选择的行不能立即被锁住, 那么语句将会立即汇报一个错误,而不是等待。请注意,NOWAIT只适用于行级别的锁。 如果需要申请表级别的锁同时又不等待,那么你可以使用LOCK的 NOWAIT选项。

在Odoo中的应用

在Odoo官方模块中,行级锁的应用一共出现了四处:

位于odoo/addons/base/ir/ir_sequence.py:
def _update_nogap(self, number_increment):
    number_next = self.number_next
    self._cr.execute("SELECT number_next FROM %s WHERE id=%s FOR UPDATE NOWAIT" % (self._table, self.id)) (1)
    self._cr.execute("UPDATE %s SET number_next=number_next+%s WHERE id=%s " % (self._table, number_increment, self.id))
    self.invalidate_cache(['number_next'], [self.id])
    return number_next
1 当更新某个序列号记录的下一个流水号时加行级锁
位于odoo/addons/base/ir/ir_cron.py:
for job in jobs:
    lock_cr = db.cursor()
    try:
        # Try to grab an exclusive lock on the job row from within the task transaction
        # Restrict to the same conditions as for the search since the job may have already
        # been run by an other thread when cron is running in multi thread
        lock_cr.execute("""SELECT *
                           FROM ir_cron
                           WHERE numbercall != 0
                              AND active
                              AND nextcall <= (now() at time zone 'UTC')
                              AND id=%s
                           FOR UPDATE NOWAIT""",   (1)
                       (job['id'],), log_exceptions=False)
1 当执行定时任务时,用行级锁来防止多线程情况下定时任务被重复执行
位于addons/stock/wizard/stock_scheduler_compute.py:
def _procure_calculation_orderpoint(self):
    with api.Environment.manage():
        # As this function is in a new thread, I need to open a new cursor, because the old one may be closed
        new_cr = self.pool.cursor()
        self = self.with_env(self.env(cr=new_cr))
        scheduler_cron = self.sudo().env.ref('stock.ir_cron_scheduler_action')
        # Avoid to run the scheduler multiple times in the same time
        try:
            with tools.mute_logger('odoo.sql_db'):
                self._cr.execute("SELECT id FROM ir_cron WHERE id = %s FOR UPDATE NOWAIT", (scheduler_cron.id,)) (1)
        except Exception:
            _logger.info('Attempt to run procurement scheduler aborted, as already running')
            self._cr.rollback()
            self._cr.close()
            return {}
1 与定时任务的行级锁一样,当运行mrp运算的定时任务时,行级锁保障不被重复运行
位于addons/stock/models/stock_quant.py:
try:
    with self._cr.savepoint():
        self._cr.execute("SELECT 1 FROM stock_quant WHERE id = %s FOR UPDATE NOWAIT", [quant.id], log_exceptions=False) (1)
        quant.write({
            'quantity': quant.quantity + quantity,
            'in_date': in_date,
        })
        # cleanup empty quants
        if quant.quantity == 0 and quant.reserved_quantity == 0:
            quant.unlink()
        break
except OperationalError as e:
    if e.pgcode == '55P03':  # could not obtain the lock
        continue
    else:
        raise
else:
self.create({
    'product_id': product_id.id,
    'location_id': location_id.id,
    'quantity': quantity,
    'lot_id': lot_id and lot_id.id,
    'package_id': package_id and package_id.id,
    'owner_id': owner_id and owner_id.id,
    'in_date': in_date,
})
1 当更新产品库存可用数量时加行级锁,以防止同时的更新操作。