生成无间隙的整数值序列。
项目描述
默认情况下,Django为每个模型提供一个自动增长的整数主键。这些主键看起来像是在生成一个连续的整数序列。
然而,这种行为并不能保证。
如果事务插入一行然后回滚,由于性能原因,序列计数器不会回滚,从而在主键中创建一个间隙。
这种情况可能会在Django原生支持的所有数据库中发生
MariaDB / MySQL
它们也可能发生在大多数通过第三方后端支持的数据库中。
这可能会对某些用例(如会计)造成合规性问题。
这种风险并不为人所知。由于大多数事务都成功,值看起来是连续的。只有通过审计才能揭示这些间隙。
django-sequences通过一个设计用来如下使用的get_next_value函数解决了这个问题
from django.db import transaction
from sequences import get_next_value
from invoices.models import Invoice
with transaction.atomic():
Invoice.objects.create(number=get_next_value("invoice_numbers"))
或者,如果您更愿意使用面向对象的API
from django.db import transaction
from sequences import Sequence
from invoices.models import Invoice
invoice_numbers = Sequence("invoice_numbers")
with transaction.atomic():
Invoice.objects.create(number=next(invoice_numbers))
get_next_value 依赖于数据库的事务完整性来确保每个值恰好返回一次。因此,django-sequences的保证仅在您在同一个事务中调用 get_next_value 并将返回值保存到数据库时才适用!
目录
入门
django-sequences已在Django 3.2(LTS)、4.0、4.1、4.2(LTS)和5.0上进行了测试。
它还与Django内置的所有数据库后端进行了测试:MySQL/MariaDB、Oracle、PostgreSQL和SQLite。
它与Django本身一样,采用BSD许可协议。
安装django-sequences
$ pip install django-sequences
将其添加到项目设置的配置文件中的应用程序列表中
INSTALLED_APPS = [
...,
"sequences.apps.SequencesConfig",
...
]
运行迁移
$ django-admin migrate
API
get_next_value
>>> from sequences import get_next_value
此函数生成一个无间隔的整数值序列
>>> get_next_value()
1
>>> get_next_value()
2
>>> get_next_value()
3
它支持多个独立的序列
>>> get_next_value("cases")
1
>>> get_next_value("cases")
2
>>> get_next_value("invoices")
1
>>> get_next_value("invoices")
2
第一个值默认为1。它可以自定义
>>> get_next_value("customers", initial_value=1000) # pro growth hacking
initial_value 参数仅在第一次调用给定序列的 get_next_value 时才有意义——假设相应的数据库事务得到提交;如上所述,如果事务回滚,则生成的值不会被消耗。也可以在数据迁移中初始化序列,而在实际代码中不使用 initial_value。
序列可以循环
>>> get_next_value("seconds", initial_value=0, reset_value=60)
当序列达到 reset_value 时,它将从 initial_value 重新开始。换句话说,它生成 reset_value - 2、reset_value - 1、initial_value、initial_value + 1 等。在这种情况下,每次调用 get_next_value 时都必须提供 initial_value(如果不是默认值)和 reset_value。
调用给定序列的 get_next_value 的数据库事务是串行化的。 因此,当您在数据库事务中调用 get_next_value 时,其他尝试从同一序列获取值的调用者将阻塞,直到事务完成(无论是提交还是回滚)。您应该尽量缩短此类事务,以最小化对性能的影响。
这就是为什么数据库默认采用可能产生空隙的更快行为的原因。
传递 nowait=True 将使 get_next_value 在此场景中引发异常而不是阻塞。这很少有用。此外,它不适用于第一次调用。(这是一个错误,但它无害且难以修复。)
对不同序列的 get_next_value 调用之间不交互。
最后,通过传递 using="..." 可以选择存储当前序列值的数据库。当此参数未提供时,它默认为 sequences 应用程序模型写入的默认数据库。有关详细信息,请参阅多个数据库。
总结一下,get_next_value 的完整签名是
get_next_value(
sequence_name="default",
initial_value=1,
reset_value=None,
*,
nowait=False,
using=None,
)
get_next_values
>>> from sequences import get_next_values
此函数批量生成值
>>> get_next_values(10)
range(1, 11)
>>> get_next_values(10)
range(11, 21)
提醒一下,您必须在同一个事务中保存所有这些值,才能从django-sequences的保证中受益。
get_next_values 支持与 get_next_value 相同的参数,除了 reset_value。
get_next_values 的完整签名是
get_next_values(
batch_size,
sequence_name="default",
initial_value=1,
*,
nowait=False,
using=None,
)
get_last_value
>>> from sequences import get_last_value
此函数返回序列生成的最后一个值
>>> get_last_value()
None
>>> get_next_value()
1
>>> get_last_value()
1
>>> get_next_value()
2
>>> get_last_value()
2
如果序列尚未生成值,则 get_last_value 返回 None。
它支持独立的序列,如 get_next_value。
>>> get_next_value("cases")
1
>>> get_last_value("cases")
1
>>> get_next_value("invoices")
1
>>> get_last_value("invoices")
1
它接受 using="..." 以选择存储当前序列值的数据库,默认为 sequences 应用程序的读取模型的默认数据库。
get_last_value 的完整签名是
get_last_value(
sequence_name="default",
*,
using=None,
)
get_last_value 是一种方便快捷的方式来了解序列生成了多少值,但它不提供任何保证。并发调用 get_next_value 可能会产生 get_last_value 的意外结果。
删除
>>> from sequences import delete
此函数删除一个序列。如果序列存在并被删除,则返回 True,否则返回 False。
>>> company_id = "b1f6cdef-367f-49e4-9cf5-bb0d34707af8"
>>> get_next_value(f"invoices—{company_id}")
1
>>> delete(f"invoices—{company_id}")
True
>>> delete(f"invoices—{company_id}")
False
它接受 using="..." 以选择数据库,类似于 get_next_value。
delete 的完整签名是
delete(
sequence_name="default",
*,
using=None,
)
delete 在创建许多序列并希望丢弃一些时很有用。
序列
>>> from sequences import Sequence
(不要与 sequences.models.Sequence 混淆,这是一个私有 API)
此类存储序列的参数并提供 get_next_value、get_next_values、get_last_value 和 delete 方法
>>> claim_ids = Sequence("claims")
>>> claim_ids.get_next_value()
1
>>> claim_ids.get_next_value()
2
>>> claim_ids.get_last_value()
2
>>> claim_ids.delete()
True
这降低了在多个地方使用相同序列时的错误风险。
Sequence 的实例也是无限迭代器
>>> next(claim_ids)
3
>>> next(claim_ids)
4
完整的 API 是
Sequence(
sequence_name="default",
initial_value=1,
reset_value=None,
*,
using=None,
)
Sequence.get_next_value(
self,
*,
nowait=False,
)
Sequence.get_next_values(
self,
batch_size,
*,
nowait=False,
)
Sequence.get_last_value(
self,
)
Sequence.delete(
self,
)
所有参数的含义与 get_next_value、get_last_values、get_last_value 和 delete 函数中的含义相同。
示例
按日期序列
如果您想按日、月或年创建独立的序列,请在序列名称中使用适当的日期片段。例如
from django.utils import timezone
from sequences import get_next_value
# Per-day sequence
get_next_value(f"books-{timezone.now().date().isoformat()}")
# Per-year sequence
get_next_value(f"prototocol-{timezone.now().year}")
上述调用将产生如 books-2023-03-15 或 protocol-2024 之类的单独序列。
数据库支持
django-sequences 在 PostgreSQL、MariaDB/MySQL、Oracle 和 SQLite 上进行了测试。
MySQL 仅从版本 8.0.1 开始支持 nowait 参数。MariaDB 仅从版本 10.3 开始支持 nowait。
多个数据库
由于 django-sequences 依赖于数据库来保证事务完整性,给定序列的当前值必须存储在包含生成值的模型的同一数据库中。
在使用多个数据库的项目中,您必须编写合适的数据库路由器,以便在所有存储包含序列号的模型的数据库上创建 sequences 应用的表。
每个数据库都有自己的命名空间:在两个数据库中存储的具有相同名称的序列将在每个数据库中都有自己的计数器。
隔离级别
由于 django-sequences 依赖于数据库的事务完整性,使用非默认的事务隔离级别需要特别注意。
读取未提交: django-sequences 无法在此隔离级别下工作。
确实,并发事务可以创建间隙,如下所示
事务 A 读取 N 并写入 N + 1;
事务 B 读取 N + 1(脏读)并写入 N + 2;
事务 A 回滚;
事务 B 提交;
N + 1 是一个间隙。
读取未提交的隔离级别不提供足够的保证。它将永远不会被支持。
读取已提交: django-sequences 在此隔离级别下工作得最好,就像 Django 本身一样。
可重复读: django-sequences 也在此隔离级别下工作,前提是你的代码处理了序列化失败并重试事务。
本要求并不仅限于 django-sequences。在可重复读隔离级别运行时通常需要它。
以下是在 PostgreSQL 上只有两个并发事务中的一个可以完成的情况
事务 A 读取 N 并写入 N + 1;
事务 B 尝试读取;它必须等待事务 A 完成;
事务 A 被提交;
事务 B 被中止。
在 PostgreSQL 上,序列化失败报告为: OperationalError: could not serialize access due to concurrent update。
在 MySQL 上,它们会导致: OperationalError: (1213, 'Deadlock found when trying to get lock; try restarting transaction')。
同时初始化相同序列的事务也容易受到影响,尽管在实践中这很少成为问题。
在 PostgreSQL 上,这表现为 IntegrityError: duplicate key value violates unique constraint "sequences_sequence_pkey"。
可序列化:该情况与可重复读级别相同。
SQLite 总是运行在可序列化隔离级别。序列化失败会导致: OperationalError: database is locked。
贡献
准备开发环境
安装 Poetry。
运行 poetry install。
运行 poetry shell 以加载开发环境。
准备测试数据库
安装 PostgreSQL、MariaDB 和 Oracle。
创建一个名为 sequences 的数据库,由名为 sequences 的用户所有,密码为 sequences,具有创建 test_sequences 测试数据库的权限。您可以使用环境变量覆盖这些值;有关详细信息,请参阅 tests/*_settings.py。
进行更改
对代码、测试或文档进行更改。
运行 make style 并修复错误。
运行 make test 以在所有数据库上运行所有套件。
直到满意为止。
检查质量并提交更改
安装 tox。
运行 tox 以在所有 Python 和 Django 版本以及所有数据库上进行测试。
提交拉取请求。
发布
在 pyproject.toml 中递增版本号 X.Y。
提交、标记和推送更改
$ git commit -m "Bump version number".
$ git tag X.Y
$ git push
$ git push --tags
构建和发布新版本
$ poetry build
$ poetry publish
变更日志
3.0
添加生成批处理值的 get_next_values 函数。
2.9
添加 delete 函数。
2.8
没有重大变化。
2.7
序列值可以高达 2 ** 63 - 1,而不是之前的前 2 ** 31 - 1。确切的限制取决于数据库后端。
迁移 0002_alter_sequence_last.py 将存储序列值的字段从 PositiveIntegerField 更改为 PositiveBigIntegerField。运行它需要表的独占锁,这会阻止其他操作,包括读取。
如果您有大量不同的序列,例如,如果您为每个用户创建一个序列并且您有数百万用户,请在运行迁移之前审查它对应用程序的影响,或者使用 migrate --fake 跳过它。
2.6
改进文档。
2.5
修复日语和土耳其语翻译。
恢复与 Python 3.5 的兼容性。
支持使用自定义 AppConfig 重命名 sequences 应用。
2.4
添加 get_last_value 函数。
添加 Sequence 类。
2.3
优化 MySQL 性能。
在 MySQL、SQLite 和 Oracle 上进行测试。
2.2
优化 PostgreSQL ≥ 9.5 的性能。
2.1
提供带有 reset_value 的循环序列。
2.0
添加对多个数据库的支持。
添加翻译。
nowait 成为关键字参数。
取消对 Python 2 的支持。
1.0
初始稳定版本。
项目详情
下载文件
下载适合您平台的文件。如果您不确定选择哪个,请了解有关 安装包 的更多信息。