Django GET搜索API重复存储记录问题:如何实现单次存储?
碰到过一模一样的问题!Firefox有时候会因为预加载或者并行请求的机制,对GET接口多发一次请求,导致重复创建搜索记录。既然你要保留GET方法,又不想依赖CSRF、Cookie这些客户端信息,咱们可以从服务端实现幂等性来解决这个问题,给你几个实用的方案:
方案一:短时间窗口去重(最简便,无依赖)
重复请求一般都是瞬间发送的,所以我们可以设置一个短时间窗口,同一个搜索关键词在这个窗口内只存储一次记录。修改你的视图代码如下:
from django.utils import timezone from datetime import timedelta def search(request, search): products = Product.objects.filter(name__contains=search) results_number = products.count() # 建议用count()替代len(),更高效且避免加载所有数据到内存 # 检查最近5秒内是否已有相同关键词的搜索记录 has_recent_search = SearchHistory.objects.filter( searched_text=search, created__gte=timezone.now() - timedelta(seconds=5) ).exists() if not has_recent_search: SearchHistory.objects.create(searched_text=search, results_number=results_number) return HttpResponse('irrelevant ... :-)')
说明:这个方案不需要任何客户端配合,完全在服务端处理。唯一的小权衡是:如果用户确实在5秒内手动重复搜索同一个词,只会记录一次,但对于绝大多数搜索场景来说,这个限制是合理的,你可以根据需求调整时间窗口(比如3秒、10秒)。
方案二:基于请求唯一ID的严格幂等(需前端配合)
如果需要更严谨的去重,且你能控制前端代码,可以让前端每次请求时生成一个唯一UUID放在请求头(比如X-Request-ID),服务端通过这个ID来确保同一请求只处理一次。
首先给SearchHistory模型新增一个字段:
class SearchHistory(models.Model): searched_text = models.CharField(max_length=255) results_number = models.IntegerField(default=0) request_id = models.CharField(max_length=255, null=True, blank=True) # 新增请求ID字段 created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) def __str__(self): return f'{self.searched_text}' class Meta: verbose_name = "search term" verbose_name_plural = "history search terms" # 加唯一约束,确保同一个request_id只存一次 constraints = [ models.UniqueConstraint(fields=['request_id'], name='unique_search_request_id') ]
然后修改视图:
from django.utils import timezone from datetime import timedelta def search(request, search): request_id = request.headers.get('X-Request-ID') products = Product.objects.filter(name__contains=search) results_number = products.count() if request_id: # 检查该request_id是否已处理过 if not SearchHistory.objects.filter(request_id=request_id).exists(): SearchHistory.objects.create( searched_text=search, results_number=results_number, request_id=request_id ) else: # 没有request_id时,fallback到时间窗口去重 has_recent_search = SearchHistory.objects.filter( searched_text=search, created__gte=timezone.now() - timedelta(seconds=5) ).exists() if not has_recent_search: SearchHistory.objects.create(searched_text=search, results_number=results_number) return HttpResponse('irrelevant ... :-)')
说明:这个方案能100%避免重复记录,即使用户手动快速重复搜索也能正确处理。如果是第三方调用你的API且不带X-Request-ID,也有时间窗口的兜底方案,兼容性更好。
方案三:结合缓存实现(兼顾性能与去重)
如果你的搜索结果可以缓存,还可以通过缓存来避免重复处理请求,同时间接实现去重:
from django.core.cache import cache def search(request, search): cache_key = f"search_result_{search}" cached_data = cache.get(cache_key) if cached_data: # 从缓存取结果,不创建新记录 results_number = cached_data['count'] else: products = Product.objects.filter(name__contains=search) results_number = products.count() # 缓存搜索结果,有效期设为和时间窗口一致 cache.set(cache_key, {'count': results_number}, timeout=5) # 只在首次缓存时创建搜索记录 SearchHistory.objects.create(searched_text=search, results_number=results_number) return HttpResponse('irrelevant ... :-)')
说明:这个方案不仅解决了重复记录问题,还优化了搜索性能,避免多次执行相同的数据库查询。如果缓存失效后遇到重复请求,还是可能会创建多条记录,但这种情况概率极低。
总结
- 如果你不想依赖任何客户端逻辑,直接用方案一最省心;
- 如果需要严格的幂等性且能控制前端,选方案二;
- 如果想同时优化搜索性能,方案三是不错的选择。
内容的提问来源于stack exchange,提问作者IlConte
相关产品推荐
相关产品推荐

