Mar*_* M. 18 django orm locking
我的问题如下:
我有一个汽车经销商A和一个名为的db表sold_cars
.当汽车正在出售时,我在此表中创建了条目.
Table有一个名为的整数列order_no
.它应该是经销商销售的汽车中独一无二的.
因此,如果经销商A出售汽车a, b and c
,那么这一栏应该是1, 2, 3
.我必须使用这个列,而不是主键,因为我不想在我的数字中有任何漏洞 - 经销商A和B(可能会在以后添加)应该有订单号1,2,3,而不是A :1,3,5和B:2,4,6.所以...我为给定的经销商选择最后一个最大的order_no,将其增加1并保存.
问题是两个人在同一毫秒内从经销商A购买了汽车,两个订单都获得了相同的订单号.有什么建议?我正在考虑在事务块中关闭此进程,并锁定此表直到事务完成,但无法找到有关如何执行此操作的任何信息.
jde*_*oix 20
我知道这个问题有点老了,但我遇到了同样的问题,想分享我的经验.
我对st0nes的答案不太满意,因为(至少对于postgres)一个LOCK TABLE
语句只能在一个事务中发出.虽然在Django中通常几乎所有事情都发生在一个事务中,但这LockingManager
并不能确保你实际上是在事务中,至少在我的理解中.此外,我不想完全改变模型Manager
只是为了能够将它锁定在一个位置,因此我更多地寻找有点类似的东西with transaction.atomic():
,但也锁定给定的模型.
所以我想出了这个:
from django.conf import settings
from django.db import DEFAULT_DB_ALIAS
from django.db.transaction import Atomic, get_connection
class LockedAtomicTransaction(Atomic):
"""
Does a atomic transaction, but also locks the entire table for any transactions, for the duration of this
transaction. Although this is the only way to avoid concurrency issues in certain situations, it should be used with
caution, since it has impacts on performance, for obvious reasons...
"""
def __init__(self, model, using=None, savepoint=None):
if using is None:
using = DEFAULT_DB_ALIAS
super().__init__(using, savepoint)
self.model = model
def __enter__(self):
super(LockedAtomicTransaction, self).__enter__()
# Make sure not to lock, when sqlite is used, or you'll run into problems while running tests!!!
if settings.DATABASES[self.using]['ENGINE'] != 'django.db.backends.sqlite3':
cursor = None
try:
cursor = get_connection(self.using).cursor()
cursor.execute(
'LOCK TABLE {db_table_name}'.format(db_table_name=self.model._meta.db_table)
)
finally:
if cursor and not cursor.closed:
cursor.close()
Run Code Online (Sandbox Code Playgroud)
所以,如果我现在要锁定模型ModelToLock
,可以像这样使用:
with LockedAtomicTransaction(ModelToLock):
# do whatever you want to do
ModelToLock.objects.create()
Run Code Online (Sandbox Code Playgroud)
编辑:请注意,我只使用postgres进行了测试.但据我了解,它也应该像mysql一样工作.
st0*_*0ne 10
假设您正在使用MySQL,我认为此代码段符合您的需求.如果没有,您可能需要稍微调整一下语法,但这个想法仍然有用.
来源:锁定表
class LockingManager(models.Manager):
""" Add lock/unlock functionality to manager.
Example::
class Job(models.Model):
manager = LockingManager()
counter = models.IntegerField(null=True, default=0)
@staticmethod
def do_atomic_update(job_id)
''' Updates job integer, keeping it below 5 '''
try:
# Ensure only one HTTP request can do this update at once.
Job.objects.lock()
job = Job.object.get(id=job_id)
# If we don't lock the tables two simultanous
# requests might both increase the counter
# going over 5
if job.counter < 5:
job.counter += 1
job.save()
finally:
Job.objects.unlock()
"""
def lock(self):
""" Lock table.
Locks the object model table so that atomic update is possible.
Simulatenous database access request pend until the lock is unlock()'ed.
Note: If you need to lock multiple tables, you need to do lock them
all in one SQL clause and this function is not enough. To avoid
dead lock, all tables must be locked in the same order.
See http://dev.mysql.com/doc/refman/5.0/en/lock-tables.html
"""
cursor = connection.cursor()
table = self.model._meta.db_table
logger.debug("Locking table %s" % table)
cursor.execute("LOCK TABLES %s WRITE" % table)
row = cursor.fetchone()
return row
def unlock(self):
""" Unlock the table. """
cursor = connection.cursor()
table = self.model._meta.db_table
cursor.execute("UNLOCK TABLES")
row = cursor.fetchone()
return row
Run Code Online (Sandbox Code Playgroud)
from contextlib import contextmanager
from django.db import transaction
from django.db.transaction import get_connection
@contextmanager
def lock_table(model):
with transaction.atomic():
cursor = get_connection().cursor()
cursor.execute(f'LOCK TABLE {model._meta.db_table}')
try:
yield
finally:
cursor.close()
Run Code Online (Sandbox Code Playgroud)
这与@jdepoix 解决方案非常相似,但更密集。
你可以这样使用它:
with lock_table(MyModel):
MyModel.do_something()
Run Code Online (Sandbox Code Playgroud)
请注意,这仅适用于 PostgreSQL 并使用python 3.6 的 f-strings aka 文字字符串插值。
我建议使用F() 表达式而不是锁定整个表。如果您的应用程序被大量使用,锁定表将对性能产生重大影响。
您描述的确切场景在此处的Django 文档中有所提及。根据您的情况,以下是您可以使用的代码:
from django.db.models import F
# Populate sold_cars as you normally do..
# Before saving, use the "F" expression
sold_cars.order_num =F('order_num') + 1
sold_cars.save()
# You must do this before referring to order_num:
sold_cars.refresh_from_db()
# Now you have the database-assigned order number in sold_cars.order_num
Run Code Online (Sandbox Code Playgroud)
请注意,如果您在更新操作期间设置 order_num,请改用以下内容:
sold_cars.update(order_num=F('order_num')+1)
sold_cars.refresh_from_db()
Run Code Online (Sandbox Code Playgroud)
由于数据库负责更新字段,因此不会有任何竞争条件或重复的 order_num 值。另外,这种方法比使用锁定表的方法快得多。
归档时间: |
|
查看次数: |
10550 次 |
最近记录: |