利用Celery实现Django博客PV统计功能详解


Posted in Python onMay 08, 2017

前言

前几天给网站的文章增加了pv统计,之前只有uv统计。之前没加pv统计是觉得每个用户每访问一次文章,我都需要做一次数据库写操作实在是有损性能,毕竟从用户在the5fire博客的的一次访问来看,只需要从数据库里拿到对应的文章(通常情况下是从缓存中拿),然后返回给浏览器。写操作无意义。之前的uv,也是针对每个用户24小时内只会有一次写操作。

不过话说回来,就对于the5fire博客这么个小站点来说,就算每次访问我写十几次数据库都没啥影响,毕竟量小的可怜。但是咱们码农不是得有颗抗亿级流量的心嘛。

对于不理解的同学,可以出门调研下,看看别人家的网站。对,就是那些访问量上亿,十亿,百亿的网站,看看他们是怎么处理用户写入的,比如留言。

PV的意义

说完原因,再说业务。所有的网站都会有pv,uv这样的统计。甚至是停留时长,各类型页面转换率等等各方各面的统计。我在搜狐的工作,大白话来说就是做网站。关注的业务指标就是流量相关的东西。同时作为站长这么多年,也会参考百度统计里的一些指标来做些调整。

不过这次只说pv,一篇文章的pv。

抛开非正常访问,互联网上的一篇文章,访问他的人越多,那么意味着这篇文章的价值越高。毕竟有价值的东西大家才会点开看嘛。这个访问量就是uv(User View/Visit)。那么pv是什么呢,一篇文章写得很不错,尤其是技术文章,可能会多次访问,比如说我就喜欢把不错的文章收藏起来,有空时回顾一下。每次回顾(刷新页面)都算是一个pv。能做到人读者多次阅读的文章,价值会更高。所以一篇文章的pv/uv比也是衡量文章价值的一个指标。尤其是在标题党遍布的年代。(好吧,这里再歪一句,标题党不是自媒体时代的产物,博客时代就有,只是自媒体时代显得更加集中显现了而已)

单纯的说价值没啥感觉,古人不是说了吗,价值能换几斗米。(我胡诌的)

拿现在的所有新闻网站/媒体平台来说,pv是可以和¥划等号的。流量越大,意味着能够有更多的收入,无论是来自广告的收入,还是把流量释放到其他渠道。有时候我也考虑,一切的目标真的是更好的理解用户,给用户推送他想看的东西吗?或许是吧,但是始终绕不开的一个问题是,构建一个商业模式,让广告主和投资人为用户的停留时长买单。让用户更多的停留在平台上,消费更多的时间。(纯属个人观点,明辨之,慎思之)

再拿另外一个更直接的例子,现在自媒体盛行,多少人想要100000+,一个好的公众号,可以根据以往文章的浏览量(或者粉丝量)来定价广告/软文等各种类型合作的价格。其实你到微播易或者易赞看看就知道了。
这么看来pv是不是变得有吸引力了。

统计的方式

对于网站来说,the5fire了解到的pv,uv的统计方式有这么几种

  • 像the5fire早期的做法:用户每访问一篇文章,文章pv+1,uv+1。傻大粗的做法。
  • the5fire博客现在的做法,写一个分布式的任务服务,然后在业务代码中调用。
  • 页面埋点,标签,或者引用js来发送数据到统计服务器上。
  • 收集nginx access-log(如果是用nginx的话),当然,格式需要自定义,起码得加上user_id,然后做离线统计、汇总。

前两种都是耦合比较重的实现方式,需要在具体页面里插代码。后两种也类似,本质上都是收集nginx日志,但是收集的阶段不同,第三种是页面完全打开之后,nginx才会收到日志。而第四种是只要访问页面,并且upstream返回状态码为200就算成功,那怕最终用户并未看到页面。

总之,各有利弊,可以相互参考。

博客实现的方式

上面也说了,主要也是为了用下celery这个分布式任务队列。在Django中使用是比较简单的事情。

在Django中使用Celery,需要Celery运行时能够使用这个Django项目的各个模块,因此首先要指明settings模块。我用的Django版本为1.11。在wsgi.py同级目录下增加celery.py,代码如下:

# coding:utf-8
from __future__ import absolute_import, unicode_literals
import os
from celery import Celery
PROFILE = os.environ.get('DJANGO_SELFBLOG_PROFILE', 'develop') # 我是把settings.py拆成了:develop.py,product.py
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_selfblog.settings.%s" % PROFILE)
app = Celery('selfblog', broker="redis://127.0.0.1:6666/2")
app.config_from_object('django.conf:settings', namespace='CELERY')
# Load task modules from all registered Django app configs.
app.autodiscover_tasks()

这里使用了官方并不建议的redis作为broker,而不是Rabbitmq,主要是缓存用的是Redis,为了不引入更多需要维护的系统。

定义好启动文件之后,就需要定义具体的tasks,在app/tasks.py中写具体的任务:

# coding:utf-8
from __future__ import unicode_literals
from django.db.models import F
from .models import Post
from django_selfblog.celery import app
@app.task
def increase_pv(post_id):
 return Post.objects.filter(id=post_id).update(pv=F('pv')+1)
@app.task
def increase_uv(post_id):
 return Post.objects.filter(id=post_id).update(uv=F('uv')+1)

在访问文章页面的views.py对应位置增加调用:

from .tasks import increase_pv, increase_uv
# ....省略上下文
increase_pv.delay(self.post.id)
increase_uv.delay(self.post.id)

这样,每次用户访问时计算pv和uv的逻辑就放到分布式的任务管理器中去执行了,不会影响本次访问。

如果你想要查看任务的执行状态,比如通过:

r = increase_pv.delay(self.post.id)
print r.ready()

想要这样查看任务是否完成,那就需要引入django-celery-results,使用步骤如下:

  • pip install django-celery-results
  • 把django_celery_results放到INSTALLED_APPS中
  • 配置CELERY_RESULT_BACKEND = 'django-db'或者'django-cache'
  • 如果配置的是django-db,意味着结果需要存储到数据库中,那就要执行python manage.py migrate django_celery_results来建表

这些配置完成之后,剩下的就是部署了,the5fire博客每次更新完代码重新部署时都是通过fabric来做的 fab re_deploy:master 代码就会部署到服务器上。增加celery之后,只需要增加supervisord的配置,现在毕竟celery的代码也是在博客代码里。

supervisord增加配置:

[program:celery]
command=celery -A selfblog worker -P gevent --loglevel=INFO --concurrency=5
directory=/home/the5fire/selfblog/
process_name=%(program_name)s_%(process_num)d
umask=022
startsecs=0
stopwaitsecs=0
redirect_stderr=true
stdout_logfile=/tmp/log/celery_%(process_num)02d.log
numprocs=1
numprocs_start=1
environment=DJANGO_SELFBLOG_PROFILE=product

这样每次重新部署,celery进程也会重新启动。

Django Tips

在Django项目中,性能损耗最多的就是ORM,不熟悉的话很容易被坑。

就拿增加pv来说,用户每次访问一篇文章,pv字段+1,用代码来说就是:

# 绝对不要写这么蠢的代码
post = Post.objects.get(pk=post_id)
post.pv = post.pv + 1
post.save()

这是最简单的做法,但是大部分情况,用户访问一篇文章,这篇文章通常会在缓存中,毕竟不需要每次都去数据库中获取。这样的话应该怎么处理呢,直观的做法还是先获取到post,然后+1,save,如上一样。但这样会存在竞争的问题。

比方说,同时100个人访问一篇文章,我是启动了多个线程/进程来处理请求,有可能出现所有进程在同一时刻执行了 post = Post.objects.get(pk=post_id) 假设现在数据库中这篇文章的pv是100,那么此时post.pv就是100。那所有用户执行完post.save()之后,结果均为101,也就是一百次并发访问,可能出现pv只加1的情况。

要解决这个问题,两个办法。

一、加锁,这个据我的了解Django没有提供,需要自己来实现。但是没人会这么做吧。 二、用mysql来执行自增,也就是我上面用到的。

对于方法二,在Django中怎么实现呢。其实翻译为sql就是

UPDATE `blog_post` SET `pv` = (`blog_post`.`pv` + 1) WHERE `blog_post`.`id` = <post_id>;

Django代码就是: Post.objects.filter(id=post_id).update(pv=F('pv')+1) ,关于F表达式可以参考官方文档:https://docs.djangoproject.com/en/1.11/ref/models/expressions/#django.db.models.F

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对三水点靠木的支持。

Python 相关文章推荐
python根据经纬度计算距离示例
Feb 16 Python
初步解析Python下的多进程编程
Apr 28 Python
python+pygame简单画板实现代码实例
Dec 13 Python
python pands实现execl转csv 并修改csv指定列的方法
Dec 12 Python
浅谈python编译pyc工程--导包问题解决
Mar 20 Python
Python当中的array数组对象实例详解
Jun 12 Python
django model通过字典更新数据实例
Apr 01 Python
解决django无法访问本地static文件(js,css,img)网页里js,cs都加载不了
Apr 07 Python
python logging模块的使用详解
Oct 23 Python
python 基于UDP协议套接字通信的实现
Jan 22 Python
python如何用matplotlib创建三维图表
Jan 26 Python
python 镜像环境搭建总结
Sep 23 Python
浅谈Python生成器generator之next和send的运行流程(详解)
May 08 #Python
python生成式的send()方法(详解)
May 08 #Python
python实时分析日志的一个小脚本分享
May 07 #Python
python分割列表(list)的方法示例
May 07 #Python
Python 常用的安装Module方式汇总
May 06 #Python
python中OrderedDict的使用方法详解
May 05 #Python
Python编程生成随机用户名及密码的方法示例
May 05 #Python
You might like
第一节--面向对象编程
2006/11/16 PHP
CodeIgniter php mvc框架 中国网站
2008/05/26 PHP
PHP大转盘中奖概率算法实例
2014/10/21 PHP
PHP解决中文乱码
2017/04/28 PHP
从ThinkPHP3.2.3过渡到ThinkPHP5.0学习笔记图文详解
2019/04/03 PHP
PHP+Apache实现二级域名之间共享cookie的方法
2019/07/24 PHP
JavaScript 函数式编程的原理
2009/10/16 Javascript
ExtJs扩展之GroupPropertyGrid代码
2010/03/05 Javascript
Jquery Ajax的Get方式时需要注意URL地方
2011/04/07 Javascript
解析JavaScript中delete操作符不能删除的对象
2013/12/03 Javascript
js鼠标点击图片实现随机变换图片的方法
2015/02/16 Javascript
nodejs搭建本地服务器并访问文件的方法
2017/03/03 NodeJs
详解react-redux插件入门
2018/04/19 Javascript
vue根据值给予不同class的实例
2018/09/29 Javascript
jQuery实现简单的Ajax调用功能示例
2019/02/15 jQuery
toString.call()通用的判断数据类型方法示例
2020/08/28 Javascript
[42:00]完美世界DOTA2联赛PWL S3 Phoenix vs INK ICE 第一场 12.13
2020/12/17 DOTA
python打开url并按指定块读取网页内容的方法
2015/04/29 Python
Python sys.argv用法实例
2015/05/28 Python
Python多进程同步简单实现代码
2016/04/27 Python
Python的包管理器pip更换软件源的方法详解
2016/06/20 Python
python中abs&amp;map&amp;reduce简介
2018/02/20 Python
深入浅析python with语句简介
2018/04/11 Python
Python搭建Spark分布式集群环境
2019/07/05 Python
Win10下Python3.7.3安装教程图解
2019/07/08 Python
django框架模型层功能、组成与用法分析
2019/07/30 Python
python+opencv实现移动侦测(帧差法)
2020/03/20 Python
浅谈keras中的batch_dot,dot方法和TensorFlow的matmul
2020/06/18 Python
Django+RestFramework API接口及接口文档并返回json数据操作
2020/07/12 Python
实例教程 一款纯css3实现的数字统计游戏
2014/11/10 HTML / CSS
仓库保管员岗位职责
2013/12/20 职场文书
宠物店的创业计划书范文
2014/01/11 职场文书
大学生实习感言
2014/01/16 职场文书
小学教师师德整改措施
2014/09/29 职场文书
教师批评与自我批评
2014/10/15 职场文书
2016党员读书思廉心得体会
2016/01/23 职场文书