0

0

优化 Django 投票系统:避免支付后票数重复增加及竞态条件

花韻仙語

花韻仙語

发布时间:2025-08-23 14:26:01

|

773人浏览过

|

来源于php中文网

原创

优化 Django 投票系统:避免支付后票数重复增加及竞态条件

本文旨在解决 Django 应用中支付完成后投票计数出现双重增加的异常问题。通过深入分析竞态条件(Race Condition)的成因,并引入 Django ORM 的 F() 表达式,教程将展示如何安全、准确地更新模型字段,从而避免数据不一致。文章提供了详细的代码示例和最佳实践,确保投票系统的数据完整性。

理解问题:投票计数异常增加

在开发涉及支付和投票功能的 django 应用时,开发者可能会遇到一个常见且棘手的问题:用户完成支付后,对应的投票计数却增加了两倍于预期值。例如,如果用户购买了10票,系统最终记录的票数却是20票。这种异常通常发生在支付回调或重定向处理不当,以及数据库更新操作缺乏原子性保障的情况下。

具体到提供的代码,verify_payment 视图函数负责处理支付验证和票数更新。当支付网关(如 Paystack)成功处理交易后,它可能会向应用发送一个回调请求,或者用户被重定向回应用的 verify_payment 页面。如果这些事件被触发多次,而 totalVote 的更新逻辑没有严格的幂等性检查,就可能导致票数被重复增加。

核心问题:竞态条件与幂等性缺失

  1. 幂等性缺失导致的双重计数: verify_payment 函数中的 payment.verify_payment() 方法会在验证成功后将 payment.verified 字段设置为 True 并保存。然而,在 verify_payment 视图中,profile_obj.totalVote += payment.vote 这行代码是在 payment.verify_payment() 返回 True 之后执行的,但并没有在执行票数增加前再次检查 payment.verified 状态。这意味着,如果 verify_payment 视图被多次调用(例如,由于网络延迟用户多次刷新页面,或支付网关多次发送回调),即使 payment.verify_payment() 方法由于 if not self.verified: 检查只执行一次外部 API 调用,但视图中 verified 变量为 True 的逻辑路径依然可能被多次执行,从而导致 totalVote 字段被重复更新。

  2. 竞态条件 (Race Condition): 即使解决了双重触发的问题,直接使用 profile_obj.totalVote += payment.vote 这样的 Python 层面操作仍然存在竞态条件风险。这种操作通常分为三个步骤:

    • 从数据库读取 totalVote 的当前值。
    • 在 Python 内存中计算新值(当前值 + payment.vote)。
    • 将新值写回数据库。 如果在多个并发请求同时执行这些步骤时,它们可能都读取到相同的旧值,然后基于旧值进行计算并写入,导致其中一个或多个更新被覆盖,最终结果不符合预期。虽然这通常表现为更新丢失而不是双倍增加,但它仍是数据库并发更新中需要避免的问题。

解决方案:结合幂等性检查与 Django F() 表达式

为了彻底解决上述问题,我们需要采取以下策略:

  1. 在更新票数前进行严格的幂等性检查:确保只有当支付尚未被标记为“已验证”时,才执行票数增加逻辑。
  2. 利用 Django F() 表达式进行原子性更新:避免在 Python 层面进行读-改-写操作,直接在数据库层面完成字段更新,从而有效防止竞态条件。

1. F() 表达式详解

Django 的 F() 表达式允许我们在不实际将数据库值取出到 Python 内存的情况下,直接在数据库层面引用模型字段的值进行操作。这使得数据库能够以原子方式执行更新,从而避免竞态条件。

例如,将 profile_obj.totalVote += payment.vote 改为:

from django.db.models import F
# ...
profile_obj.totalVote = F("totalVote") + payment.vote
profile_obj.save(update_fields=['totalVote'])

这告诉数据库直接将 totalVote 字段的当前值加上 payment.vote 的值,并将结果写回 totalVote 字段。整个操作在数据库内部完成,是原子性的。

Sheet+
Sheet+

Excel和GoogleSheets表格AI处理工具

下载

2. 重构 verify_payment 视图

结合幂等性检查和 F() 表达式,我们可以对 verify_payment 视图进行如下重构:

from django.db import transaction
from django.db.models import F
from django.shortcuts import render, get_object_or_404
from django.conf import settings
# 假设 Paystack, Payment, Contestant 等模型和类已正确导入

@transaction.atomic
def verify_payment(request, ref):
    print(f"verify_payment called for reference: {ref}")
    try:
        payment = Payment.objects.get(ref=ref)
    except Payment.DoesNotExist:
        print("Invalid payment reference:", ref)
        return render(
            request,
            "competitions/error.html",
            {"error_message": "Invalid payment reference."},
        )

    # 关键的幂等性检查:如果支付已经验证,则直接返回成功,不再重复增加票数
    if payment.verified:
        print(f"Payment {ref} already verified and votes processed. Skipping duplicate update.")
        # 可以记录一个警告日志,表示收到重复的回调/请求
        return render(request, "competitions/success.html")

    # 调用 Payment 模型的 verify_payment 方法进行外部支付网关验证
    # 此方法成功时会设置 payment.verified = True 并保存
    verified_with_paystack = payment.verify_payment()

    if verified_with_paystack:
        contestant = payment.contestant
        profile_obj = contestant.user.profile

        # 使用 F() 表达式进行原子性更新,避免竞态条件
        # 注意:这里我们只更新 totalVote 字段,以避免不必要的副作用
        profile_obj.totalVote = F("totalVote") + payment.vote
        profile_obj.save(update_fields=['totalVote'])

        # 刷新 profile_obj 以获取数据库中最新的 totalVote 值,用于日志输出
        profile_obj.refresh_from_db()
        print(f"Payment verified for {contestant.user.first_name}. New total votes: {profile_obj.totalVote}.")

        return render(request, "competitions/success.html")
    else:
        print(f"Payment verification failed for reference: {ref}")
        return render(
            request,
            "competitions/error.html",
            {"error_message": "Payment verification failed."},
        )

3. Payment 模型中的 verify_payment 方法

Payment 模型中的 verify_payment 方法设计良好,它内部已经包含了对 self.verified 的检查,确保不会重复调用外部支付网关 API。

class Payment(models.Model):
    # ... 其他字段

    def save(self, *args, **kwargs):
        while not self.ref:
            ref = secrets.token_urlsafe(50)
            object_with_similar_ref = Payment.objects.filter(ref=ref)
            if not object_with_similar_ref:
                self.ref = ref
        super().save(*args, **kwargs)

    def amount_value(self):
        return int(self.amount) * 100

    def verify_payment(self):
        # 确保只在未验证时才尝试与支付网关交互
        if not self.verified:
            paystack = Paystack() # 假设 Paystack 类已正确定义和导入
            status, result = paystack.verify_payment(self.ref, self.amount)
            if status:
                # 再次确认金额匹配,防止篡改
                if result["amount"] / 100 == self.amount:
                    self.verified = True
                    self.save() # 保存验证状态
        return self.verified

注意事项与最佳实践

  • 事务原子性:@transaction.atomic 装饰器确保 verify_payment 视图中的所有数据库操作(包括 Payment 对象的保存和 Profile 对象的更新)要么全部成功,要么全部回滚。这对于支付处理至关重要。
  • 日志记录:在关键操作前后(如支付验证开始、验证结果、票数更新前后)进行详细的日志记录。这对于调试问题、审计交易和监控系统健康状况至关重要。在示例代码中,我们增加了更多的 print 语句,但在生产环境中应使用 Django 的日志系统。
  • 错误处理:确保对所有可能的错误情况(如 Payment.DoesNotExist、支付网关验证失败)都有健壮的处理机制,并向用户提供有意义的反馈。
  • 并发测试:在部署前,务必对支付和投票更新功能进行并发测试,模拟多个用户同时进行操作的场景,以验证解决方案的鲁棒性。
  • Webhook 安全:如果支付网关使用 Webhook 进行回调,确保对 Webhook 请求进行签名验证,以防止伪造请求。

通过上述改进,您的 Django 投票系统将能够更安全、更准确地处理支付和投票计数,有效避免双重增加和竞态条件导致的数据不一致问题。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
python中print函数的用法
python中print函数的用法

python中print函数的语法是“print(value1, value2, ..., sep=' ', end=' ', file=sys.stdout, flush=False)”。本专题为大家提供print相关的文章、下载、课程内容,供大家免费下载体验。

187

2023.09.27

if什么意思
if什么意思

if的意思是“如果”的条件。它是一个用于引导条件语句的关键词,用于根据特定条件的真假情况来执行不同的代码块。本专题提供if什么意思的相关文章,供大家免费阅读。

786

2023.08.22

数据库三范式
数据库三范式

数据库三范式是一种设计规范,用于规范化关系型数据库中的数据结构,它通过消除冗余数据、提高数据库性能和数据一致性,提供了一种有效的数据库设计方法。本专题提供数据库三范式相关的文章、下载和课程。

360

2023.06.29

如何删除数据库
如何删除数据库

删除数据库是指在MySQL中完全移除一个数据库及其所包含的所有数据和结构,作用包括:1、释放存储空间;2、确保数据的安全性;3、提高数据库的整体性能,加速查询和操作的执行速度。尽管删除数据库具有一些好处,但在执行任何删除操作之前,务必谨慎操作,并备份重要的数据。删除数据库将永久性地删除所有相关数据和结构,无法回滚。

2083

2023.08.14

vb怎么连接数据库
vb怎么连接数据库

在VB中,连接数据库通常使用ADO(ActiveX 数据对象)或 DAO(Data Access Objects)这两个技术来实现:1、引入ADO库;2、创建ADO连接对象;3、配置连接字符串;4、打开连接;5、执行SQL语句;6、处理查询结果;7、关闭连接即可。

349

2023.08.31

MySQL恢复数据库
MySQL恢复数据库

MySQL恢复数据库的方法有使用物理备份恢复、使用逻辑备份恢复、使用二进制日志恢复和使用数据库复制进行恢复等。本专题为大家提供MySQL数据库相关的文章、下载、课程内容,供大家免费下载体验。

256

2023.09.05

vb中怎么连接access数据库
vb中怎么连接access数据库

vb中连接access数据库的步骤包括引用必要的命名空间、创建连接字符串、创建连接对象、打开连接、执行SQL语句和关闭连接。本专题为大家提供连接access数据库相关的文章、下载、课程内容,供大家免费下载体验。

326

2023.10.09

数据库对象名无效怎么解决
数据库对象名无效怎么解决

数据库对象名无效解决办法:1、检查使用的对象名是否正确,确保没有拼写错误;2、检查数据库中是否已存在具有相同名称的对象,如果是,请更改对象名为一个不同的名称,然后重新创建;3、确保在连接数据库时使用了正确的用户名、密码和数据库名称;4、尝试重启数据库服务,然后再次尝试创建或使用对象;5、尝试更新驱动程序,然后再次尝试创建或使用对象。

413

2023.10.16

go语言 注释编码
go语言 注释编码

本专题整合了go语言注释、注释规范等等内容,阅读专题下面的文章了解更多详细内容。

30

2026.01.31

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
最新Python教程 从入门到精通
最新Python教程 从入门到精通

共4课时 | 22.4万人学习

Django 教程
Django 教程

共28课时 | 3.8万人学习

SciPy 教程
SciPy 教程

共10课时 | 1.4万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号