Django商品书签系统并发问题及高性能解决方案咨询
书签系统并发问题解决方案
针对你遇到的DB锁、计数错误及并发读写问题,结合你的代码给出以下针对性优化方案:
一、先修复核心逻辑漏洞
你的现有代码存在两个关键问题,是导致计数错误的主要原因:
- 未限制用户重复收藏同一商品,重复点击会重复创建
BookmarkItems并累加计数 - 删除书签时未校验用户是否真的收藏过该商品,无书签时仍会减少计数
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
相关产品推荐
相关产品推荐

