Django博客如何避免同一用户多次GET请求重复增加浏览量
博客单用户单次访问计数实现方案
核心逻辑是单独存储每个用户/匿名访客的文章访问记录,只有首次访问时才累加浏览量,同时通过数据库约束和原子操作避免高并发下的计数错误。
1. 新增访问记录模型
原有Blog模型保持不变,新增ReadRecord表存储访问痕迹:登录用户关联用户ID,匿名用户关联会话ID,同时加数据库层面的联合唯一约束,从根源避免重复记录。
from django.db import models from django.contrib.auth.models import User # 原有Blog模型无需修改 class Blog(models.Model): author=models.ForeignKey(User,on_delete=models.CASCADE,related_name='post_author') blog_title=models.CharField(max_length=264,verbose_name='Put a Title') category=models.ForeignKey(Category,on_delete=models.CASCADE,related_name='category',default=None) slug= models.SlugField(max_length=264,unique=True,null=True,allow_unicode=True) blog_content=models.TextField(verbose_name='what is on your mind?') blog_image=models.ImageField(upload_to='blog_images',verbose_name='Image') publish_date=models.DateTimeField(auto_now_add=True) update_date=models.DateTimeField(auto_now=True) view_count=models.IntegerField(null=True,default=0,blank=True) class Meta: ordering = ('-publish_date',) # 新增访问记录模型 class ReadRecord(models.Model): blog = models.ForeignKey(Blog, on_delete=models.CASCADE, related_name='read_records') user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True) # 匿名用户用Django自带的session_key做身份标识 session_key = models.CharField(max_length=40, null=True, blank=True) first_read_time = models.DateTimeField(auto_now_add=True) class Meta: constraints = [ # 登录用户对单篇文章只能有一条访问记录 models.UniqueConstraint(fields=['blog', 'user'], condition=models.Q(user__isnull=False), name='unique_user_blog_read'), # 同一会话的匿名用户对单篇文章只能有一条访问记录 models.UniqueConstraint(fields=['blog', 'session_key'], condition=models.Q(session_key__isnull=False), name='unique_session_blog_read') ]
改完模型后执行两条命令完成数据表创建:
python manage.py makemigrationspython manage.py migrate
2. 修改视图计数逻辑
替换原来每次GET请求直接加1的逻辑,先判断当前访客是否已有该文章的访问记录,没有记录时才累加浏览量,同时用原子操作和F表达式避免并发计数错误:
from django.db import transaction from django.db.models import F from .models import Blog, ReadRecord def blog_details(request, slug): blog = Blog.objects.get(slug=slug) if request.method == 'GET': # 区分登录用户和匿名用户的身份标识 current_user = request.user if request.user.is_authenticated else None session_key = request.session.session_key # 匿名用户没有有效session时主动创建,保证身份可识别 if not current_user and not session_key: request.session.create() session_key = request.session.session_key # 开启事务保证查询、创建记录、更新计数的原子性 with transaction.atomic(): # 区分身份查询访问记录,没有就创建新记录 if current_user: _, is_first_visit = ReadRecord.objects.get_or_create( blog=blog, user=current_user ) else: _, is_first_visit = ReadRecord.objects.get_or_create( blog=blog, session_key=session_key ) # 只有首次访问才累加浏览量 if is_first_visit: # 用F表达式直接在数据库层面做数值更新,避免多请求同时读写导致的计数偏差 blog.view_count = F('view_count') + 1 blog.save(update_fields=['view_count']) # 刷新对象拿到最新计数,方便后续模板渲染使用 blog.refresh_from_db() # 下方保留原有的其余视图逻辑,比如模板渲染、响应返回等 # *省略原有无关代码*
实现说明
- 匿名用户识别依赖Django默认session机制,用户清除浏览器Cookie后会被判定为新访客,这是无登录态下的通用实现方案,兼顾实现成本和统计准确率。
- 不建议直接把已读文章ID存在session里:如果用户浏览的文章数量多,会撑大session存储体积,且Cookie清理后所有记录都会丢失,单独建表的方案长期运行更稳定。
- 数据库层的唯一约束是最后一道防线,就算视图逻辑出现异常,也不会插入重复的访问记录,不会出现重复计数问题。
内容的提问来源于stack exchange,提问作者Abu RayhaN
相关产品推荐
相关产品推荐

