利用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中操作列表之list.extend()方法的使用
May 20 Python
dataframe设置两个条件取值的实例
Apr 12 Python
pandas DataFrame实现几列数据合并成为新的一列方法
Jun 08 Python
python对绑定事件的鼠标、按键的判断实例
Jul 17 Python
python多线程与多进程及其区别详解
Aug 08 Python
Django rstful登陆认证并检查session是否过期代码实例
Aug 13 Python
python使用if语句实现一个猜拳游戏详解
Aug 27 Python
python使用celery实现异步任务执行的例子
Aug 28 Python
浅谈tensorflow 中tf.concat()的使用
Feb 07 Python
Python django框架 web端视频加密的实例详解
Nov 20 Python
python爬虫实现爬取同一个网站的多页数据的实例讲解
Jan 18 Python
Python预测分词的实现
Jun 18 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
颠覆常识!无色透明的咖啡诞生了(中日双语)
2021/03/03 咖啡文化
PHP网页游戏学习之Xnova(ogame)源码解读(一)
2014/06/23 PHP
php获取远程文件内容的函数
2015/11/02 PHP
php数值转换时间及时间转换数值用法示例
2017/05/18 PHP
关于laravel 日志写入失败问题汇总
2019/10/17 PHP
JavaScript 判断浏览器类型及版本
2009/02/21 Javascript
jQuery实现鼠标滚轮动态改变样式或效果
2015/01/05 Javascript
Javascript显示和隐藏ul列表的方法
2015/07/15 Javascript
jQuery左侧大图右侧小图焦点图幻灯切换代码分享
2015/08/19 Javascript
JavaScript基于replace+正则实现ES6的字符串模版功能
2017/04/25 Javascript
Angular 4依赖注入学习教程之组件服务注入(二)
2017/06/04 Javascript
理解 javascript 中的函数表达式与函数声明
2017/07/07 Javascript
Vue2.0如何发布项目实战
2017/07/27 Javascript
bootstrap中selectpicker下拉框使用方法实例
2018/03/22 Javascript
Element输入框带历史查询记录的实现示例
2019/01/15 Javascript
怎么使用javascript深度拷贝一个数组
2019/06/06 Javascript
用python + hadoop streaming 分布式编程(一) -- 原理介绍,样例程序与本地调试
2014/07/14 Python
用Python遍历C盘dll文件的方法
2015/05/06 Python
Python3字符串学习教程
2015/08/20 Python
Python编程之string相关操作实例详解
2017/07/22 Python
Python2和Python3.6环境解决共存问题
2018/11/09 Python
Python3爬虫中Splash的知识总结
2020/07/10 Python
详解Canvas事件绑定
2018/06/27 HTML / CSS
详解通过HTML5 Canvas实现图片的平移及旋转变化的方法
2016/03/22 HTML / CSS
机电专业毕业生推荐信
2013/11/10 职场文书
车间统计员岗位职责
2014/01/05 职场文书
高二英语教学反思
2014/01/19 职场文书
母亲节感恩活动记录
2014/03/16 职场文书
高中同学会活动方案
2014/08/14 职场文书
励志演讲稿500字
2014/08/21 职场文书
就业推荐表导师评语
2014/12/31 职场文书
暑期实践个人总结
2015/03/06 职场文书
致地震灾区的慰问信
2015/03/23 职场文书
签证扫盲贴,41个常见签证知识,需要的拿走
2019/08/09 职场文书
导游词之镜泊湖
2019/12/09 职场文书
了解Kubernetes中的Service和Endpoint
2022/04/01 Servers