You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Django商品书签系统并发问题及高性能解决方案咨询

书签系统并发问题解决方案

针对你遇到的DB锁、计数错误及并发读写问题,结合你的代码给出以下针对性优化方案:


一、先修复核心逻辑漏洞

你的现有代码存在两个关键问题,是导致计数错误的主要原因:

  1. 未限制用户重复收藏同一商品,重复点击会重复创建BookmarkItems并累加计数
  2. 删除书签时未校验用户是否真的收藏过该商品,无书签时仍会减少计数

1. 优化模型设计

删掉冗余的Bookmark表,给BookmarkItems加唯一约束避免重复收藏,同时修正user字段类型:

class BookmarkItems(models.Model):
    _id = models.AutoField(primary_key=True, editable=False)
    # 替换CharField为ForeignKey,关联更准确且查询高效
    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='bookmarks')
    image = models.CharField(max_length=255, null=True, blank=True)
    product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='bookmarks')
    created_at = models.DateTimeField(auto_now_add=True)

    # 唯一约束:同一用户不能重复收藏同一商品
    class Meta:
        unique_together = ('user', 'product')

    def image_preview(self):
        if self.image:
            return mark_safe(f'<img src="{self.image}" width="75" height="75" />')
        else:
            return '(No image)'

    def __str__(self):
        return str(self.product)

2. 修复添加书签逻辑

利用唯一约束避免重复操作,仅在成功创建书签时更新计数:

@api_view(['POST'])
@permission_classes([IsAuthenticated])
def addBookmark(request):
    user = request.user
    product_id = request.data.get('product_id')
    image = request.data.get('image')

    if not product_id:
        return Response({'error': '缺少商品ID'}, status=status.HTTP_400_BAD_REQUEST)

    try:
        with transaction.atomic():
            # 尝试创建书签,已存在则返回提示
            bookmark_item, created = BookmarkItems.objects.get_or_create(
                user=user,
                product_id=product_id,
                defaults={'image': image}
            )
            if created:
                # F表达式确保数据库层面原子更新,避免并发计数错误
                Product.objects.filter(_id=product_id).update(
                    bookmark_count=F('bookmark_count') + 1
                )
                return Response({'success': '书签已添加'})
            else:
                return Response({'info': '您已收藏过该商品'}, status=status.HTTP_200_OK)
    except Product.DoesNotExist:
        return Response({'error': '商品不存在'}, status=status.HTTP_404_NOT_FOUND)

3. 修复删除书签逻辑

先校验书签存在性,再执行删除和计数更新:

@api_view(['DELETE'])
@permission_classes([IsAuthenticated])
def deleteBookmark(request, pk):
    user = request.user
    try:
        with transaction.atomic():
            # 先查询书签,不存在则直接抛出异常
            bookmark_item = BookmarkItems.objects.get(user=user, product_id=pk)
            bookmark_item.delete()
            # 原子更新计数
            Product.objects.filter(_id=pk).update(
                bookmark_count=F('bookmark_count') - 1
            )
            return Response({'success': '书签已删除'})
    except (BookmarkItems.DoesNotExist, Product.DoesNotExist):
        return Response({'error': '书签或商品不存在'}, status=status.HTTP_404_NOT_FOUND)

二、解决DB锁与并发性能问题

1. 利用F表达式的原子性

你已经用到的F('bookmark_count') + 1是数据库层面的原子操作,会直接在数据库执行SET bookmark_count = bookmark_count + 1,不需要额外加排他锁,能避免大部分并发计数错误。

2. 缩短事务持有时间

确保事务内只执行必要操作:

  • 移除事务外的Product.objects.get()调用(原代码中该调用无实际作用,还可能引发脏读)
  • 避免在事务内执行非必要的查询或IO操作

3. 可选:行级锁优化(仅在强一致性场景使用)

如果需要确保书签记录与计数完全一致(极端并发场景),可以在更新前给商品行加行级锁,注意这会轻微增加开销,但远小于表锁:

# 在addBookmark的事务内添加
product = Product.objects.select_for_update().get(_id=product_id)
product.bookmark_count = F('bookmark_count') + 1
product.save()

4. 进阶优化:缓存+异步处理

如果计数的实时性要求不是极高,可以进一步降低数据库压力:

  • 用Redis缓存bookmark_count,收藏/取消时先调用incr/decr更新缓存
  • 用Celery等异步任务框架,异步同步缓存值到数据库
  • 用户查看商品时优先从缓存读取计数,减少数据库查询

三、额外性能优化

  • 给BookmarkItems的user和product字段加联合索引,加速查询与删除操作
  • 开启数据库慢查询日志,排查耗时操作并优化
  • 确保数据库使用InnoDB引擎(MySQL)或支持行级锁的引擎

内容的提问来源于stack exchange,提问作者Luke.Lee

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.02 05:15:35