You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Django REST API医生预约系统并发请求处理及预约顺序判定技术咨询

关于Django REST API并发预约的问题解答

嘿,作为Django新手碰到这个并发问题太正常了——你的担心完全正确,确实存在多个用户同时发起相同预约请求的可能,这种情况在热门医生的预约场景下很容易触发竞态条件(Race Condition),会导致两个核心问题:

  1. 超过医生的单日预约上限(比如两个请求同时读到date_counter为4,而医生上限是5,结果都创建了第5个预约,最终变成6个)
  2. 两个用户得到相同的就诊编号,破坏了就诊顺序的唯一性

为什么你的现有代码会有问题?

你当前的代码是先读取数据库里的预约数量,再判断是否可以创建新预约,但这个“读-判断-写”的过程不是原子的:当两个请求同时执行到len(Appointment.objects.filter(doctor, date))时,它们会读到相同的计数,然后都通过判断并创建预约,最终导致数据不一致。

解决方案:用数据库原子操作+行锁保证一致性

要解决这个问题,核心是把预约创建的整个流程变成原子操作,让数据库帮你锁定资源,避免并发冲突。下面是具体的改进方案:

1. 给Appointment模型加唯一约束(强制用户不能重复预约)

先在你的Appointment模型里添加唯一约束,从数据库层面拦截同一个用户给同一医生同一天的重复预约:

class Appointment(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    doctor = models.ForeignKey(Doctor, on_delete=models.CASCADE)
    date = models.DateField()
    patient_number = models.IntegerField()  # 就诊顺序号

    class Meta:
        # 确保同一用户-医生-日期组合唯一
        unique_together = ('user', 'doctor', 'date')

2. 修改视图函数,用原子事务+行锁处理并发

把预约创建的逻辑放在transaction.atomic()块里,并用select_for_update()锁定该医生当天的所有预约记录,确保同一时间只有一个请求能修改这个数据集:

from django.db import transaction, IntegrityError
from django.db.models import Max
from django.utils import timezone

def post(self, request, did):
    doctor = get_object_or_404(Doctor, pk=did)
    # 先处理日期格式,避免解析错误
    try:
        appointment_dt = timezone.datetime.strptime(request.GET['date'], '%Y-%m-%d').date()
    except (KeyError, ValueError):
        return Response(
            {'date': 'Invalid date format, please use YYYY-MM-DD'},
            status=status.HTTP_400_BAD_REQUEST
        )

    # 先判断医生当天是否出诊
    if not doctor.is_available(appointment_dt):
        return Response(
            {'date':'the doctor is not available at this date'},
            status=status.HTTP_409_CONFLICT
        )

    try:
        with transaction.atomic():
            # 锁定该医生当天的所有预约,防止其他并发请求修改
            daily_appointments = Appointment.objects.filter(
                doctor=doctor, date=appointment_dt
            ).select_for_update()
            
            # 原子性获取当前预约数量
            current_count = daily_appointments.count()
            if current_count >= doctor.patient_limit:
                return Response(
                    {'patient_number':'the date is full'},
                    status=status.HTTP_406_NOT_ACCEPTABLE
                )
            
            # 原子性计算下一个就诊编号:取当前最大编号+1,没有则从1开始
            max_number = daily_appointments.aggregate(max_num=Max('patient_number'))['max_num'] or 0
            patient_number = max_number + 1

            # 创建并保存预约
            appointment = Appointment(
                user=request.user,
                doctor=doctor,
                date=appointment_dt,
                patient_number=patient_number,
                # 其他字段从请求中获取,注意过滤掉不需要的参数
                **{k: v for k, v in request.GET.items() if k not in ['date']}
            )
            appointment.save()

            # 返回成功响应
            return Response(
                {
                    'id': appointment.id,
                    'patient_number': patient_number,
                    'date': appointment_dt.strftime('%Y-%m-%d'),
                    'doctor_id': did
                },
                status=status.HTTP_201_CREATED
            )
    except IntegrityError:
        # 捕获唯一约束冲突(用户重复预约)或数据库锁冲突
        return Response(
            {'error': 'Concurrent request detected, please try again later'},
            status=status.HTTP_409_CONFLICT
        )

额外优化建议

  • 前端防重复提交:在用户点击预约按钮后,立即禁用按钮并显示加载状态,避免用户重复点击发起多个请求。
  • 高并发场景可选Redis:如果你的API流量很大,可以用Redis的INCR命令原子生成就诊编号(按doctor_id:date作为key),性能比数据库锁更好,但需要额外配置Redis。
  • 日志监控:添加日志记录并发冲突的情况,方便后续排查问题。

内容的提问来源于stack exchange,提问作者th3plus

火山引擎 最新活动