1. 从零梳理:这个考试报名系统到底要解决什么问题
先聊个实际场景。我接过不少类似的项目,有帮某教育机构做的等级考试报名,也有替某公司做的内部认证考试系统,还有给某高校做的课程补考报名。这些需求看起来五花八门,但拆开来看核心就一句话:让考生在线完成报名信息填写、资格审核、费用缴纳、准考证生成这一整套流程,同时让管理员能高效处理大量报名数据。
很多人一听说“考试报名系统”,第一反应是“这不就是做个表单嘛,撑死再加个列表”。真上手做才知道,这里面的坑比想象中多得多。就单说“一个考生能否重复报名”这个问题,处理不好你就等着后台被垃圾数据淹没吧。更别说缴费状态如何同步、准考证号怎么生成、批次不同考试时间冲突怎么办,还有报名高峰期并发怎么顶住。
我这次做的系统,技术栈选了 Python + Django,数据库用的 MySQL,前端用的是 Bootstrap + 原生 JavaScript,部署在 Linux 服务器上,配 Nginx + Gunicorn。整个项目从需求分析到上线,前后花了三周左右。这篇文章我就把这个系统的完整设计和实现过程掰开讲清楚,包括数据模型怎么设计、核心报名流程怎么走、哪些地方容易出幺蛾子,以及我是怎么一步一步排掉这些坑的。
如果你是刚学 Django 不久,想找一个完整的实战项目练手,或者你手头正好有类似的报名管理需求,这篇文章的代码逻辑和设计思路可以直接拿去参考。我不会只贴一堆代码然后让你自己消化,每一步都会说清楚“为什么这样做”,因为我踩过的坑你大概率也会踩。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型:为什么偏偏是 Django,而不是 Flask 或别的方案
2.1 选 Django 的理由,放在这个项目里特别实在
很多人纠结 Flask 和 Django。我的观点是:如果你的项目涉及后台管理、用户认证、数据库迁移、表单处理这些“标准件”,Django 的效率碾压 Flask。考试报名系统恰恰就是这么个项目。
最核心的一点是 Django 自带 Admin 后台。我拿到需求的第一天,先花一个小时把数据模型建好,Django Admin 一开,管理员就已经能往里面录考试批次、专业信息了。这种“边开发边让业务方看到东西”的节奏,在项目沟通中价值巨大——业务方不会等技术全部做完才提修改意见,而是从第一天起就开始反馈字段加得对不对、状态流转符不符合实际。
再一个就是 Django ORM。报名系统涉及的数据关系很典型:一个考试批次对应多个报名记录,一个报名记录关联一个考生,一个考生可以报多个批次。ORM 用 ForeignKey、ManyToManyField 一挂,查询和过滤都极其顺手。多表关联查询的代码,用 Flaks 加 SQLAlchemy 也能写,但 Django 的 ORM 在复杂条件筛选、按时间聚合统计这些场景下,省掉的代码量是很可观的。
2.2 配套组件怎么挑,以及备份方案考虑的细节
配套的东西我按下面这个组合来选,每一样都有明确原因:
- 数据库:MySQL 8.0。项目方明确要求用 MySQL,如果没这个约束,SQLite 也能快速跑起来。但考虑到报名高峰期并发写,MySQL 的锁机制和连接池明显更靠谱。库用
utf8mb4字符集,别问,问就是防止生僻字。 - 前端框架:Bootstrap 5。我做这个项目没打算上 Vue React,因为页面形态主要就是表单、列表、详情、流程状态,Bootstrap 足够,而且服务端渲染配合表单校验,逻辑更直接。
- 缓存与队列:Redis。有两个用途,一是存登录 session 和验证码,二是用 Redis 分布式锁解决重复提交问题。后面章节我会详细展开。
- 部署:Nginx + Gunicorn + Supervisor。Gunicorn 启动 4 个 worker,Nginx 挂静态文件,Supervisor 看护进程。
这套组合最大的感受就是省心。每个组件在项目里的分工非常清楚,调试起来也方便,哪个环节出了问题,定位路径很明确。
3. 数据模型设计:报名系统最核心的四张表和它们的关系
3.1 从业务视角反推表结构
设计数据库之前,先把业务角色和状态流转捋清楚。这个系统的角色其实就三种:考生、审核员、超管。考生要做的事是注册、选批次、填信息、交材料、缴费、下载准考证;审核员要做的事是核对考生材料和资格;超管负责建批次、设置规则、看统计报表。
从这些动作反推出来,核心表至少有这几张:
- 考试批次表(ExamBatch):批次名称、报名开始时间、报名截止时间、考试时间、考试地点、费用、最大报名人数、是否可报名。
- 考生表(Candidate):这里我不建议直接用 Django 自带 User 表存业务字段,而是用
OneToOneField一对一关联一个 Profile。这样想换回 Django 原生认证也好扩展。 - 报名记录表(Application):这是当之无愧的核心表。关联考生、关联批次、当前状态、备注、材料上传路径、缴费流水号。
- 审核记录表(AuditLog):谁在什么时候审核了什么,结论是什么。这个表在权限紧张、需要追责的场景下是必须的。
状态流转是整个系统的命根子。报名记录的状态我设计了以下几个值:PENDING_SUBMIT(待提交)、PENDING_REVIEW(待审核)、REJECTED(已驳回)、PENDING_PAY(待缴费)、PAID(已缴费)、CANCELLED(已取消)。
3.2 数据模型的 Django 代码示例
直接上核心代码,Django 模型文件长这样:
python复制from django.db import models
from django.contrib.auth.models import User
class ExamBatch(models.Model):
name = models.CharField('批次名称', max_length=100, unique=True)
start_time = models.DateTimeField('报名开始时间')
end_time = models.DateTimeField('报名截止时间')
exam_time = models.DateTimeField('考试时间')
location = models.CharField('考试地点', max_length=200)
fee = models.DecimalField('报名费用', max_digits=8, decimal_places=2, default=0)
max_applicants = models.PositiveIntegerField('最大报名人数', default=0)
is_active = models.BooleanField('是否开放报名', default=False)
class Meta:
ordering = ['-exam_time']
indexes = [
models.Index(fields=['is_active', 'end_time']),
]
def __str__(self):
return self.name
class Candidate(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='candidate_profile')
id_card = models.CharField('证件号码', max_length=18, unique=True)
phone = models.CharField('手机号', max_length=11)
photo = models.ImageField('证件照', upload_to='candidate_photos/%Y%m%d/', blank=True)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f'{self.user.username}-{self.id_card}'
class Application(models.Model):
STATUS_CHOICES = [
('PENDING_SUBMIT', '待提交'),
('PENDING_REVIEW', '待审核'),
('REJECTED', '已驳回'),
('PENDING_PAY', '待缴费'),
('PAID', '已缴费'),
('CANCELLED', '已取消'),
]
candidate = models.ForeignKey(Candidate, on_delete=models.CASCADE, related_name='applications')
batch = models.ForeignKey(ExamBatch, on_delete=models.CASCADE, related_name='applications')
status = models.CharField('状态', max_length=20, choices=STATUS_CHOICES, default='PENDING_SUBMIT')
material_json = models.JSONField('报名材料', default=dict, blank=True)
remark = models.TextField('备注', blank=True)
created_at = models.DateTimeField('创建时间', auto_now_add=True)
updated_at = models.DateTimeField('更新时间', auto_now=True)
class Meta:
# 一个考生对一个批次只能有一条有效报名记录
constraints = [
models.UniqueConstraint(
fields=['candidate', 'batch'],
condition=models.Q(status__in=['PENDING_SUBMIT', 'PENDING_REVIEW', 'PENDING_PAY', 'PAID']),
name='unique_active_application'
)
]
indexes = [
models.Index(fields=['batch', 'status']),
]
def __str__(self):
return f'{self.candidate}-{self.batch}'
分开解释几个关键设计点:
为什么状态字段是字符串而不是布尔值。 报名流程不是“通过/不通过”二元状态,中间有“待提交→待审核→待缴费→已缴费”这么长的链路。字符串状态配合 Django 的 choices 参数,在 Admin 后台能直接变成下拉框,清晰直观。
为什么加 unique_active_application 这个约束。 这是防重复报名的数据库层面的兜底。有的开发只在代码里判断“当前批次是否已报名”,但并发请求下这个判断容易被绕过。数据库层的唯一约束才是真正的最后防线。用 condition 限制“只对有效状态生效”,这样已经取消的报名不占锁,考生还能重新报。
材料为什么用 JSONField。 每个考试的报名材料可能不太一样,有的是需要学历证书编号,有的需要工作证明文件路径。用 JSON 字段可以灵活适配,不用每次加需求都改表结构。但要提醒一句,别把所有东西都往 JSON 里塞,频繁查询的字段还是要独立成列,JSON 字段查询效率有限。
3.3 状态流转的保存方式:别再在视图函数里到处改状态了
状态流转的代码,我强烈建议用独立的方法封装。举个最简单的“提交报名”逻辑:
python复制class Application(models.Model):
# ...上面字段省略...
def submit(self):
if self.status != 'PENDING_SUBMIT':
raise ValueError('当前状态不允许提交')
required_keys = ['name', 'id_card', 'phone', 'major']
for key in required_keys:
if not self.material_json.get(key):
raise ValueError(f'缺少必填材料: {key}')
self.status = 'PENDING_REVIEW'
self.save(update_fields=['status', 'updated_at'])
def approve(self, auditor=None):
if self.status != 'PENDING_REVIEW':
raise ValueError('当前状态不允许审核通过')
self.status = 'PENDING_PAY'
self.save(update_fields=['status', 'updated_at'])
def reject(self, reason, auditor=None):
if self.status != 'PENDING_REVIEW':
raise ValueError('当前状态不允许驳回')
self.status = 'REJECTED'
self.remark = reason
self.save(update_fields=['status', 'remark', 'updated_at'])
这套做法的好处有两个。一是状态不合法的地方能尽早报错,比如已取消的报名被误调用了 approve(),直接抛异常而不是让脏数据进入系统。二是所有状态迁移逻辑集中管理,排查问题的时候只要看模型层,不用去视图、Celery 任务里到处翻。
如果你真的在 View 里散落着 application.status='PENDING_REVIEW'; application.save() 这种代码,后期状态一多,你会被自己坑死的。这是我第一次做类似系统时流下的血泪。
4. 报名核心流程实现:并发、幂等与资格校验的处理
4.1 整个报名服务的请求链路拆解
考生端发起一次报名,前端实际上会经过这些步骤:
- 用户登录,判断是否已填写基本资料(证件、照片等)
- 选择考试批次,系统校验批次是否开放、是否满员、是否在报名时间段内
- 填写报名材料,前端先做第一轮格式校验
- 点提交,Ajax 请求到后端
submit_application - 后端做第二轮校验 + 并发控制
- 入库成功,跳转到“待审核”页面
大家最容易忽略的是第 2 步里“是否满员”这个校验。光用 if batch.applicants_count >= batch.max_applicants 这种判断,遇到秒杀式报名请求就要出问题。两个用户同时查,同时看到名额还剩 1,同时提交,结果实际只有 1 个名额,数据库里却写进了两条记录。
4.2 select_for_update 行锁的具体用法
解决满员超卖问题最直接的办法是给批次的查询加行锁。在事务里对即将更新的行 select_for_update,后续事务就必须等它提交完成才能读。示例代码如下:
python复制from django.db import transaction
from django.core.exceptions import ValidationError
@transaction.atomic
def submit_application(user, batch_id, material):
# 查询批次并加行锁,防止并发超卖
batch = ExamBatch.objects.select_for_update().get(pk=batch_id)
if not batch.is_active:
raise ValidationError('该批次已停止报名')
if not (batch.start_time <= timezone.now() <= batch.end_time):
raise ValidationError('不在报名时间段内')
active_count = batch.applications.filter(
status__in=['PENDING_SUBMIT', 'PENDING_REVIEW', 'PENDING_PAY', 'PAID']
).count()
if active_count >= batch.max_applicants:
raise ValidationError('报名名额已满')
candidate = Candidate.objects.get(user=user)
app = Application(candidate=candidate, batch=batch, material_json=material)
app.submit()
return app
整个过程包在 transaction.atomic 里。因为同一时刻同一个批次的行锁只有一个人能拿到,所以后面的事顺序执行,就不会存在两个人都查到还剩一个名额的问题。
这里有个细节要说一下:Django 的 select_for_update() 必须在一个事务块里执行,并且它锁的是满足条件的行,不是整张表。第一行 ExamBatch.objects.select_for_update().get(pk=batch_id) 锁死的只是这一个批次对象,并发报考其他批次完全不受影响,性能上没问题。
再说个容易踩的坑:锁顺序必须一致。如果代码里有些地方先锁 Application 再锁 ExamBatch,有些地方反过来,并发一高就可能死锁。我的经验是,凡是涉及名额扣减的业务,一律先锁批次对象,这是整个流程的统一入口。
4.3 重复提交的幂等保护:Redis 分布式锁的落地
光靠数据库唯一约束还不够。前面那个 UniqueConstraint 兜底了“同一个人对同一批次只能有一条有效报名”,但用户快速点两次提交,第一次还没入库完成,第二次又来了,两次事务并行执行时唯一约束还没生效,就会有一条被数据库当作重复键拒绝,此时报给用户的错误提示很不友好。
为了提升体验,我在视图层再加一道 Redis 分布式锁。做法是这样的:
python复制import hashlib
from django.conf import settings
from django.core.cache import cache
def submit_view(request, batch_id):
cache_key = 'apply_lock:{}:{}'.format(request.user.id, batch_id)
lock_id = hashlib.sha256(str(request.user.id).encode()).hexdigest()
# acquire 锁 5 秒,有效期 5 秒
if not cache.add(cache_key, lock_id, timeout=5):
return JsonResponse({'code': 400, 'msg': '请勿重复提交,正在处理中'})
try:
app = submit_application(request.user, batch_id, request.POST)
return JsonResponse({'code': 200, 'msg': '提交成功', 'data': {'app_id': app.id}})
except ValidationError as e:
return JsonResponse({'code': 400, 'msg': e.message})
finally:
# 处理完了释放锁
cache.delete(cache_key)
为什么用 cache.add 而不是 cache.set?cache.add 的语义是“只有键不存在时才设置成功”,天然就是一锤子买卖,正好符合锁的互斥要求。这里缓存后端我配的是 Redis,生产环境千万别用 LocMemCache,多进程下完全不互斥。
还有一个需要想清楚的细节:锁的过期时间设为 5 秒,如果 5 秒内事务没跑完,锁自动释放了,用户又提交一次,还是会进来。所以锁的超时时间一定要根据业务耗时来定,报名转人工审核这类操作一般不会超过 2 秒,5 秒基本稳妥。
4.4 审核、缴费、准考证:串联起来看核心逻辑
审核环节相对简单。审核员打开待审核列表,看材料,然后调 approve() 或者 reject()。这里有一个体验优化:审核员驳回时必须写理由,没有理由不能驳回。这个逻辑写在 reject() 方法里了,如果 reason 为空会直接抛异常。
缴费环节我没接真实支付网关,用的是模拟支付回调。真实的项目里,这一步一般会对接第三方支付,回调地址验证签名之后再把 Application 状态从 PENDING_PAY 改成 PAID。接支付有个非常关键的坑:回调接口必须是幂等的。第三方支付可能因为网络超时重发回调,你的接口不能因为重复回调而产生两条流水或重复改状态。
准考证号生成我放在缴费成功之后。生成规则用了 批次编号 + 日期 + 顺序号,类似 202501A0001,其中 0001 部分是当天该批次内报名成功的顺序号,用到了数据库自增主键加格式化对齐的方式。需要特别注意的是准考证号一旦生成,一般不允许修改或复用,即使考生后来取消报名,这个号也要在系统里留痕。审计的时候你可能需要根据一个准考证号追溯整个生命周期,这个号就是业务上的唯一标识。
5. 后台管理的效率问题:Admin 自定义和批量操作
5.1 Django Admin 不是摆设,但一定要改造
很多人只用 Django Admin 做简单的表格增删改查,报名系统这种重流程管理后台,直接把默认 Admin 丢给业务方会被嫌弃的。原因很直接:默认列表页按创建时间倒序排,满屏都是记录 ID,业务方要按批次、按状态筛选的时候体验很差。
我改造了报名记录的后台列表页。用关键字段注册了搜索和列表过滤:
python复制@admin.register(Application)
class ApplicationAdmin(admin.ModelAdmin):
list_display = ('id', 'candidate', 'batch', 'status_colored', 'created_at', 'updated_at')
list_filter = ('batch', 'status', 'created_at')
search_fields = ('candidate__user__username', 'candidate__id_card', 'candidate__phone')
list_per_page = 50
date_hierarchy = 'created_at'
这里 status_colored 是个自定义字段显示方法,根据状态不同渲染不同颜色的 label:
python复制def status_colored(self, obj):
color_map = {
'PENDING_SUBMIT': 'gray',
'PENDING_REVIEW': 'orange',
'REJECTED': 'red',
'PENDING_PAY': 'blue',
'PAID': 'green',
'CANCELLED': 'black',
}
return format_html('<span style="color:{};">{}</span>', color_map.get(obj.status, 'gray'), obj.get_status_display())
status_colored.short_description = '报名状态'
这块我认为是最值得花时间的部分,不是没价值的时间,因为业务方最常用的界面就是这个后台。列表预览清晰,筛选响应快,业务方对你的系统满意度能直接上一个台阶。
5.2 “一键批量审核”千万别乱写,踩坑经验分享
我一开始做了一个“批量审核通过”的 Admin action,逻辑很直白:选中 50 条记录,循环调 approve(),完事。但我很快发现了问题——如果 50 条里有 1 条状态不是 PENDING_REVIEW,前面 49 条都审了,最后一条报错,整体状态就很混乱。
后来改成先做“预检”:选中记录之后,先全量检查状态是否都是 PENDING_REVIEW,有任何一条不合法就不执行任何操作,并返回错误提示,告诉用户哪几条不满足条件。代码大概是这样的:
python复制@admin.action(description='批量审核通过(仅限待审核状态)')
def bulk_approve(self, request, queryset):
invalid_items = queryset.exclude(status='PENDING_REVIEW')
if invalid_items.exists():
ids = ', '.join(str(i) for i in invalid_items.values_list('id', flat=True))
self.message_user(request, f'以下记录不在待审核状态,无法审核:{ids}', level='error')
return
for app in queryset:
app.approve()
self.message_user(request, f'已成功审核通过 {queryset.count()} 条报名记录')
另外加了一个“批量驳回”操作,和批量审核对称。这些操作在 Admin action 里的意义非常实际,高峰期几百条待审记录,逐条点开页面看完了再点按钮很累,批量操作能节省大量时间。但要注意的是——凡是批量操作,必须先检查所有选中记录的可操作性,这是底线。
5.3 用数据看板辅助超管决策
后台还加了一个简单的统计看板,用 aggregate 和 annotate 做聚合查询。比如按批次统计报名人数、按状态统计数量、近七天报名趋势。这些统计最方便的地方是:Django ORM 的查询集是惰性的,你可以在一个请求里跑多个统计而不需要重复查全表。
python复制def dashboard(request):
total_batches = ExamBatch.objects.count()
total_applications = Application.objects.count()
status_counts = Application.objects.values('status').annotate(total=models.Count('id'))
batch_stats = ExamBatch.objects.annotate(
app_count=models.Count('applications', filter=Q(applications__status__in=['PENDING_REVIEW', 'PENDING_PAY', 'PAID']))
).order_by('-exam_time')[:10]
# ...
最开始统计报名人数,我用的是 Python 层循环,效率非常低。后来全部换成 ORM 聚合,响应时间从几秒降到几十毫秒。一次性能调优的感悟就是:能用 SQL 层做的聚合,绝不要拉到 Python 层做。
6. 从开发到上线:部署方案和一些不太常见但非常关键的细节
6.1 Gunicorn + Nginx + Supervisor 的配置参考
部署这块踩的坑比写业务代码还多。先说 Gunicorn,我的配置是:
ini复制[program:exam_app]
directory=/home/deploy/exam_system
command=/home/deploy/venv/bin/gunicorn exam_system.wsgi:application -b 127.0.0.1:8000 -w 4 --max-requests=1000 --max-requests-jitter=200
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=deploy
关键参数说一下:
-w 4:4 个 worker 进程。不是说越大越好,还要看服务器 CPU 核数。我这里是 2 核机器,4 个 worker 是合理偏上的配置,太多反而增加上下文切换的开销。--max-requests=1000:每个 worker 处理满 1000 个请求后平滑重启。防止内存泄漏问题越积越严重,这是长期跑生产环境的经验。stopasgroup=true:这是我最想强调的。如果不加这个,Supervisor 停止进程时可能只杀主进程,worker 进程残留,端口被占,重启就会失败。这个坑我至少折腾了半小时才想明白。
Nginx 配置重点说静态文件和服务转发:
nginx复制server {
listen 80;
server_name exam.example.com;
client_max_body_size 10m;
location /static/ {
alias /home/deploy/exam_system/staticfiles/;
}
location /media/ {
alias /home/deploy/exam_system/media/;
}
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 60s;
}
}
6.2 报名高峰期的压测结果与瓶颈分析
系统的耐力到底好不好,不能靠感觉,得上压测。我用本地脚本模拟高频报名请求,主要用并发发起对 submit_application 的压力测试。
测试环境是 2 核 4GB 的服务器,4 个 Gunicorn worker。压测结果:100 个并发用户同时提交报名,成功率 100%,没有出现超卖,没有出现重复记录。接口平均响应时间大概 800ms,p99 在 1.5 秒左右。这个表现对大多数场景来说够用了。
不过压测过程中暴露了真实瓶颈:静态文件加载。报名高峰期的前端页面大量依赖 jQuery、Bootstrap 的 CSS JS 文件,如果这些靠 Django 默认的 runserver 处理,QPS 一高 CPU 直接飙满。后来把所有静态文件全部切给 Nginx,服务器负载瞬间降了一半。这一点哪怕你项目再小也值得一开始就做对。
另外压测时还出现了一个现象:当容器网络不稳定时,请求可能超时,如果用户在超时页面又点了一次提交,Redis 锁因为已过期没有拦住,便有重复数据产生。针对这个问题,我在“报名结果页”增加了一个刚刚生成的报名号展示,让用户看清自己的操作是否真的成功。这治标不治本,核心仍是尽量缩短事务执行时间,让锁的有效期更贴近真实业务时间。
6.3 上线前的检查清单,一项一项打勾
我每次上线前都会拿着这个清单走一遍,避免上线当晚被电话叫醒:
- [ ]
settings.py严格关闭DEBUG=False,否则访问报错就暴露堆栈,甚至还有调试页面的安全风险 - [ ]
ALLOWED_HOSTS填写真实域名或 IP,别留* - [ ] 静态文件已执行
collectstatic收集到指定目录 - [ ] 数据库备份已配置,每天一份,异地留痕
- [ ] 上传目录权限正确,普通用户不能覆盖已上传的证件照
- [ ] CSRF 中间件和 HTTPS 已开启(如果条件允许)
- [ ] 登录接口加了访问频率限制,防止恶意爆破
这些看起来是老生常谈,但每一次线上事故复盘,最后基本都能落到其中某一条没做。报名系统的安全性尤其重要,里面有大量考生身份信息,数据泄漏的后果不是闹着玩的。
7. 复盘:这个项目里让我印象最深的三件事
先说第一件:数据模型设计阶段多花两小时,后面少加三天的班。报名记录的状态本来设计得比较简单,只有“已提交”“已通过”“已拒绝”三个状态。后来业务方说要加缴费环节,又加“待缴费”“已缴费”,又多补了“已取消”。如果一开始把状态机做清楚、给状态迁移定义好方法,后面就不用到处改判断逻辑。现在这个系统如果再增加“转考”“退费”之类的状态,我只要往模型层加方法就行,视图层几乎不用动。
第二件:Admin 后台是给业务方看的第一个“成品”,花时间优化它绝对物超所值。别老想着开发一个独立的前端管理页面,大量内部系统的管理需求其实 Django Admin 都能覆盖,用好它的自定义能力,比从零写一套前端快一个数量级。业务方用着舒服,才愿意给你提真实需求,项目才能螺旋上升。
第三件:锁这东西,用之前先想清楚“锁的目标是什么”。select_for_update 锁行,Redis 锁键,脏数据天然就少很多。但锁不是越多越好,锁粒度太粗容易把并发全部串行化,这个系统里的锁目标非常明确:锁批次名额、锁重复提交。其他地方都不要乱加锁,否则压测数据一出来你自己都看不过去。
最后分享一个我很受用的理念:这类系统的成败,不在功能多炫酷,而在于“并发下不出错、重复操作有兜底、状态流转不乱”。把这三条守住,系统就稳了。这个考试报名系统跑了小半年,处理了上万条报名记录,除了偶尔有考生填错信息来申请修改之外,没有出过大乱子。希望这次分享能帮你在做类似系统的时候,少走一些我当年走过的弯路。
