看到这个标题,我第一反应是:又要做“大数据+推荐”的毕业设计或练手项目了。做这类项目最大的痛点不是代码写不出来,而是数据从哪来、分析完怎么用、以及怎么在答辩或演示时让人眼前一亮。Django这东西在Web开发里确实顺手,但真要和“大数据”结合,不少人的做法是挂个名字,实质还是SQL里跑个count完事。这篇我就把自己做“短视频用户兴趣分析”这套东西的完整链路拆开来讲,从数据采集、兴趣建模、Django服务端实现,到WebSocket实时推送和可视化大屏,全部基于我实际跑通过的方案,里面包含了不少查不到但确实管用的细节。
1. 为什么短视频平台一定要做兴趣分析:业务痛点与技术选型
1.1 从“瞎猜推荐”到“投其所好”:兴趣分析在短视频场景下的价值
短视频这个场景有个非常鲜明的特点:用户决策成本极低。看到不感兴趣的内容,手指一滑就是0.5秒的事。这种“轻决策”带来的后果是,如果平台不能快速捕捉用户的兴趣偏好,留存率会非常难看。
我见过太多新手做推荐类项目,上来就写协同过滤或深度学习模型,但忽略了一个根本问题:模型输入的特征从哪来? 没有用户兴趣画像,模型就是空中楼阁。所谓兴趣分析,本质上是从用户的行为序列中提取结构化标签和偏好分数,比如“这个用户过去一周对宠物类视频的完播率高达80%”,这就是一个可以直接支撑推荐系统、广告投放、内容运营的中间产物。
在我这个项目里,兴趣分析被拆成两层:
- 第一层是实时层,用户在App或Web端产生了浏览、点赞、评论、分享等行为后,Django服务端接收到行为日志,马上更新一份轻量级的用户实时兴趣状态,用于当天或当场的推荐排序。
- 第二层是离线层,通过大数据组件(Spark或Hive)对一天积累的海量行为日志做批量计算,生成完整的用户画像,回填到业务数据库,供次日冷启动和策略优化使用。
两层结合的好处是既照顾了时效性,又保证了大数据的“大”字不是摆设。
1.2 Django+大数据的组合为什么比单机爬虫方案更可靠
很多同学做这类项目喜欢用爬虫抓数据,然后塞进一个脚本里算完事。这个思路应付几千条数据没问题,但离“大数据”这三个字差得很远。真正的用户行为数据有几个特点:海量(一天几十万条起)、高并发(瞬间涌入)、多维度(时间、设备、内容、行为类型)。单机爬虫方案根本接不住。
我选择Django作为业务中台,理由很务实:
- 生态成熟:Django的ORM、认证体系、Admin后台,几乎覆盖了业务服务端需要的所有能力,加上Django Channels可以无缝支持WebSocket,这让后端在推送实时分析结果这件事上有了天然优势。
- 和数据分析组件的边界清晰:Django负责“接数据、存数据、提供API”,Spark/Hive负责“算数据”,两者通过数据库和消息队列解耦,谁挂了都不至于拖垮对方。
技术栈选型如下:
| 层级 | 选型 | 承担职责 |
|---|---|---|
| 接入层 | Django + Django REST Framework | 接收行为埋点、提供查询API、用户认证 |
| 实时层 | Django Channels + Redis | 分析结果实时推送到前端大屏 |
| 存储层 | MySQL(业务数据)/ Hive(离线数仓)/ Redis(缓存与实时计数) | 分层存储,热数据与冷数据分离 |
| 计算层 | Spark SQL / Hive SQL | 离线批量计算用户兴趣画像 |
| 可视化层 | Vue + ECharts(或直接Django模板+ECharts) | 大屏展示、管理后台图表 |
这套组合下来,既有Web项目的完整度,又有大数据分析的说服力,在毕业设计或项目实战里都站得住脚。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据从哪来:行为埋点、数据分层存储与清洗链路
2.1 埋点事件表:一条完整行为日志从产生到入库的路径
兴趣分析的第一步永远是数据。没有埋点,后面全是无源之水。我在项目里定义了一张核心行为事件表,主要记录用户在短视频上的每一次关键动作。下面是我实际使用的表结构(简化过):
python复制# apps/behavior/models.py
from django.db import models
class BehaviorEvent(models.Model):
"""
用户行为事件表 - 埋点上报的落库模型
"""
BEHAVIOR_TYPES = [
('view', '浏览'),
('like', '点赞'),
('comment', '评论'),
('share', '分享'),
('follow', '关注作者'),
('not_interested', '不感兴趣'),
]
user_id = models.CharField(max_length=64, db_index=True, verbose_name='用户ID')
video_id = models.CharField(max_length=64, db_index=True, verbose_name='视频ID')
behavior_type = models.CharField(max_length=32, choices=BEHAVIOR_TYPES, verbose_name='行为类型')
duration_ratio = models.FloatField(default=0.0, verbose_name='观看时长占比(0-1)')
video_category = models.CharField(max_length=64, db_index=True, verbose_name='视频分类')
device_platform = models.CharField(max_length=16, verbose_name='设备平台')
created_at = models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='行为发生时间')
class Meta:
db_table = 'behavior_event'
ordering = ['-created_at']
indexes = [
models.Index(fields=['user_id', 'created_at'], name='idx_user_time'),
]
这里我特别解释下两个容易被忽略的字段:
- duration_ratio(观看时长占比):这是判断兴趣强度最核心的指标。一个用户把30秒的视频看完,和看了2秒就划走,意义完全不同。取值是0到1之间的浮点数,由前端在埋点时计算好上传。
- video_category(视频分类):这条非常重要。兴趣分析本质上是在分析“用户对哪些分类的内容感兴趣”,而不是对“哪一条具体视频”感兴趣。没有分类标签,后面做聚合分析时会寸步难行。
埋点上报的API我用了DRF(Django REST Framework),因为需要做身份认证和限流,这些DRF都有现成方案:
python复制# apps/behavior/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from rest_framework.throttling import UserRateThrottle
from .models import BehaviorEvent
from .serializers import BehaviorEventSerializer
class BehaviorReportView(APIView):
permission_classes = [IsAuthenticated]
throttle_classes = [UserRateThrottle] # 防止刷接口
def post(self, request):
serializer = BehaviorEventSerializer(data=request.data)
if serializer.is_valid():
serializer.save(user_id=request.user.id)
# 异步触发实时兴趣更新,放Celery或Channels后台任务
return Response({'status': 'ok'})
return Response(serializer.errors, status=400)
有一个实战中的坑提醒大家:用户快速滑动短视频时,埋点请求会在短时间内密集到达。如果每次上报都同步写数据库,Django的数据库连接池很快就会被打满。我的做法是把行为数据先写入Redis的Stream(或者简单的List),由后台Celery任务批量落库到MySQL,同时大数据侧的Flink/Spark Streaming也可以直接消费这份数据,实现一份数据多处使用。
2.2 MySQL与大数据组件各司其职:冷热数据分层的设计思路
我在项目里做了一次很关键的架构取舍:业务查询走MySQL,离线分析走Hive,实时计算走Redis + Spark Streaming。
为什么要这么折腾,而不是一个MySQL通吃?
以一百万条行为数据为例,如果用MySQL直接跑一个复杂的画像聚合SQL,比如要计算“每个用户过去30天每个视频分类的完播次数、平均观看时长、点赞率”,这种SQL在单表上很可能需要几秒甚至几十秒,而且会拖垮正在处理的业务请求。这是典型的OLTP(在线事务处理)场景做了OLAP(在线分析处理)的活。
所以我的分层思路是这样的:
- 热数据层(Redis):存储当天的实时行为计数,比如“用户今天看了多少条宠物类视频”“最近一小时的点赞数”,这类数据要极快速地读写,用于实时推荐和展示。
- 业务数据层(MySQL):存储行为明细、用户基础信息、视频基础信息、生成好的用户画像表。Django的API查询都打在这一层。
- 离线数仓层(Hive):每天凌晨通过Spark SQL从MySQL或日志中心抽取当天的全量行为数据,做复杂的清洗、加工、计算,生成新一天的画像结果,再回写到MySQL的画像表中。
听起来复杂,但实际建模后就是一条清晰的管道:
code复制前端埋点 -> Kafka / Redis -> Spark Streaming(实时计算) -> Redis
-> Hive(离线数仓) -> Spark SQL(画像计算) -> MySQL -> Django API
2.3 数据清洗时的三个关键过滤规则
清洗这一步被很多人草率跳过,但这恰恰是兴趣分析结果准不准的分水岭。我在项目中总结了三个必须执行的过滤规则:
-
过滤机器人行为和异常高频行为。一个用户1小时内上报了500条浏览记录,这大概率是脚本刷量。清洗时加一个阈值判断,超过阈值的数据直接不进计算流程。
-
过滤超短播放记录。观看时长低于0.5秒的行为基本等同于误触,这类记录的权重如果是正向的,会给用户打上错误标签。我在写入时直接丢弃无效记录:
python复制# 清洗规则示例
def clean_behavior_event(row):
if row['duration_ratio'] < 0.05:
return None # 低于5%的观看时长,视为无效
if row['device_platform'] not in ('ios', 'android', 'web'):
return None
return row
- 处理分类标签缺失。业务方偶尔会漏传视频分类,导致行为数据没有归属。这类记录我建议标记为“unknown”分类,而不是直接丢弃,否则会低估用户在其他有效分类上的兴趣浓度。
3. 兴趣模型的构建:从行为数据到用户画像分数的计算逻辑
3.1 用户-视频-行为的权重体系:为什么认真看完比点赞更有价值
画像计算不是简单地把行为次数相加,核心逻辑是不同行为代表不同的兴趣强度。用户手上动作的成本越低,其兴趣信号越弱。举几个例子:
- 产生一次“观看”行为的成本是0.5秒(滑到就看),所以它的兴趣权重低;
- 点一个“赞”的成本是1秒,说明用户至少觉得有意思;
- 写一条“评论”需要十几秒甚至几分钟,这代表强烈的表达欲;
- “分享”是把个人品味曝光给社交关系链的行为,权重最高。
我实际使用的权重表如下:
| 行为类型 | 行为成本 | 兴趣权重 |
|---|---|---|
| view(浏览) | 极低 | 1 |
| like(点赞) | 低 | 3 |
| comment(评论) | 中 | 5 |
| share(分享) | 高 | 8 |
| follow(关注作者) | 高 | 8 |
| not_interested(不感兴趣) | 中 | -5(负向) |
当然,如果只看行为类型不看视频分类,等于白算。所以在后续的聚合SQL里,所有权重分数都要按 video_category 维度累加,才能形成“用户对XX分类感兴趣”的结论。
3.2 时间衰减与遗忘曲线:让兴趣分数“活”起来
还有一个参数必须处理,就是时间衰减。用户本周喜欢宠物视频,不代表三个月后还喜欢。如果画像表里累积了半年的历史权重不处理,用户最近一周的兴趣变化会被历史数据淹没。
我采用的时间衰减公式非常简单有效,在每天的离线计算中执行:
code复制decay_factor = exp(-lambda * age_days)
其中 lambda 是衰减系数,我取 0.05,对应大约20天内的行为权重能保留到初始值的60%左右,3个月前的行为权重会降到很低。这样做的好处是:画像能跟随用户最近的兴趣迁移,不至于僵化。
下面是一段画像计算的Spark SQL核心逻辑(简化):
sql复制-- 每天计算一次,用户在各分类上的兴趣权重
SELECT
user_id,
video_category,
SUM(CASE behavior_type
WHEN 'view' THEN 1 * EXP(-0.05 * datediff(CURDATE(), created_at))
WHEN 'like' THEN 3 * EXP(-0.05 * datediff(CURDATE(), created_at))
WHEN 'comment' THEN 5 * EXP(-0.05 * datediff(CURDATE(), created_at))
WHEN 'share' THEN 8 * EXP(-0.05 * datediff(CURDATE(), created_at))
WHEN 'follow' THEN 8 * EXP(-0.05 * datediff(CURDATE(), created_at))
WHEN 'not_interested' THEN -5 * EXP(-0.05 * datediff(CURDATE(), created_at))
END) AS interest_score
FROM behavior_log
WHERE created_at >= date_sub(CURDATE(), 90) -- 只取近90天行为
GROUP BY user_id, video_category
这段SQL跑完,得到的 interest_score 就是一张完整的“用户-分类兴趣权重表”。我建议把结果写入MySQL的 user_interest_profile 表,Django侧直接读取这张表做API输出。
3.3 冷启动与兴趣探索:新用户和新视频的个性化策略
新用户没有足够的行为数据,画像算出来是空的,怎么做推荐?这属于冷启动问题。我在项目里做了三层兜底策略:
- 热门兜底:对所有冷启动用户,推荐全局热门分类Top,N。即使用户没有画像,先推送大众内容,在推荐结果中夹带一些“试探性”冷门内容。
- 实时偏好试探:用户第一次会话中产生的前5条完整观看行为(观看时长占比超过60%),立即作为临时兴趣信号,优先更新到对应的实时推荐列表。这个逻辑我用Redis计数器实现,不落MySQL,速度非常快。
- 相似用户迁移:在离线计算时,我用协同过滤思路简单找一下“相似用户”——比如基于视频分类的向量余弦相似度,把老用户的兴趣画像迁移给相似度高的新用户。这段逻辑在Spark里跑,代码量不大但效果好。
4. Django服务端核心实现:ORM模型、Token认证与查询优化
4.1 画像模型与多对多关系设计:避免后期改表的三个经验
Django的数据模型是整个服务端的地基。我在这篇里直接给出我当时设计的画像相关模型,核心思路是一条用户记录对应多个分类兴趣记录,而不是把所有分类塞进一个字段。
python复制# apps/profile/models.py
from django.db import models
class UserInterestProfile(models.Model):
"""
用户兴趣画像主表
"""
user = models.OneToOneField('auth.User', on_delete=models.CASCADE, related_name='interest_profile')
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'user_interest_profile'
class UserInterestItem(models.Model):
"""
用户在某分类上的兴趣明细
"""
profile = models.ForeignKey(UserInterestProfile, on_delete=models.CASCADE, related_name='items')
category = models.CharField(max_length=64, db_index=True, verbose_name='视频分类')
interest_score = models.FloatField(default=0.0, verbose_name='兴趣得分')
watch_count = models.IntegerField(default=0, verbose_name='观看次数')
like_count = models.IntegerField(default=0, verbose_name='点赞次数')
rank = models.IntegerField(default=0, verbose_name='分类排名')
class Meta:
db_table = 'user_interest_item'
ordering = ['-interest_score']
constraints = [
models.UniqueConstraint(fields=['profile', 'category'], name='uniq_profile_category')
]
在设计阶段,我踩过几个坑,给你们画一下重点:
第一,不要用JSON字段存放某个用户的所有兴趣分类。虽然Django 3.1之后支持 JSONField,看起来查询也方便,但一旦你需要按分类聚合、按分数排序、或者做SQL联表分析,JSON字段会非常痛苦。正经的做法就是一对多拆表。
第二,合理使用 on_delete 参数。用户删除时,画像表要同步清掉,所以 UserInterestProfile 对 User 用了 CASCADE。
第三,每个模型都要有 db_table 显式声明。大数据侧Spark任务读取数据时,可不想猜Django默认生成的“app_model”这种名称。
4.2 Token认证与Cookie联动:移动端与Web端共存的认证方案
短视频用户分析系统通常有多个客户端:Web端看大屏和管理后台,移动端上报行为数据、刷新推荐流。我采用了 djangorestframework-simplejwt 的Token认证方案。
JWT的优势不多讲了,重点说一个我遇到的实战问题:移动端总要手动带Token请求头,Web端可以通过Cookie自动携带,怎么统一?
我的方案是双通道认证:
- 移动端在登录接口获取
access_token,后续请求放在Authorization: Bearer <token>头中。 - Web端登录成功后,用
set_cookie把access_token种在浏览器里,同时配置DRF让它同时支持从Cookie读取Token。
核心配置代码如下:
python复制# settings.py
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'apps.common.auth.CsrfExemptJWTAuthentication',
],
}
# apps/common/auth.py
from rest_framework.authentication import BaseAuthentication
from rest_framework_simplejwt.authentication import JWTAuthentication
from rest_framework_simplejwt.exceptions import InvalidToken
class CsrfExemptJWTAuthentication(JWTAuthentication):
"""
Web端通过Cookie传输Token,移动端通过Authorization头传输Token
"""
def authenticate(self, request):
raw_token = request.META.get('HTTP_AUTHORIZATION', '').removeprefix('Bearer ')
if not raw_token:
raw_token = request.COOKIES.get('access_token')
if not raw_token:
return None
validated_token = self.get_validated_token(raw_token)
return self.get_user(validated_token), validated_token
再提醒一个细节:Token过期后,移动端要靠Refresh Token刷新,Web端则需要在Cookie过期前主动调用刷新接口,否则用户正看在兴头上,接口突然全部401,体验很糟。
4.3 ORM查询优化的实操笔记:select_related、Prefetch与批量删除的坑
Django ORM在数据量小的时候随便写,数据量一大就原形毕露。我基于这个项目的经验,把高频踩坑点整理成一份笔记:
第一,视图集里一定要防N+1查询。 比如返回用户画像列表时,如果没加 select_related,每次取 profile.user.username 都会发一条SQL。我当时就查过日志,一个列表接口背后跟了上百条重复查询。
正确姿势:
python复制from django.db.models import Prefetch
queryset = UserInterestProfile.objects.select_related('user').prefetch_related(
Prefetch('items', queryset=UserInterestItem.objects.order_by('-interest_score')[:10])
)
第二,聚合计算尽量下推到数据库层。 比如要统计所有用户的TOP3兴趣分类,如果先把所有明细load到Python内存再排序,内存会被打爆。用ORM的 values().annotate():
python复制from django.db.models import Count, F, Sum
category_stats = (
BehaviorEvent.objects
.filter(user_id=some_user_id, created_at__gte=time_threshold)
.values('video_category')
.annotate(total_score=Sum('duration_ratio'))
.order_by('-total_score')[:3]
)
第三,批量删除一定要批量执行。 Django ORM有个经典陷阱:QuerySet.delete() 默认会把每一条对象实例化到内存再调 collect 信号,一旦数据量大,又是一个灾难。正确做法是用 queryset.delete() 的批量逻辑,并且不要在这个表上挂多余的 post_delete 信号(或者至少保证信号里的逻辑是轻量的)。
5. 实时数据推送:用Django Channels把分析结果送到前端
5.1 从HTTP轮询到WebSocket:为什么实时推送必须换方案
项目初期,我没上WebSocket,用的是前端 setInterval 每5秒轮询一次后端接口拿最新分析数据。数据量小、用户量小的时候,轮询写起来简单,完全够用。
但等模拟用户数和行为量上来后,问题就暴露了:
- 前端大屏同时发几十个HTTP请求,服务端连接被无意义占用;
- 数据变化的粒度是秒级,轮询存在天然延迟;
- 每次轮询都把整张画像表查一遍,MySQL压力山大。
WebSocket方案彻底解决了这三个问题:连接建立一次,后端有数据更新时主动推给前端,无数据时不产生网络开销。这个体验上的差异,演示大屏时特别明显——数据是“涌”出来的,而不是“刷”出来的。
5.2 Channels与Redis Channel Layer的接入步骤
Django Channels是把WebSocket能力整合进Django的标准方案。我的实现链路是:
- Django Channels作为ASGI服务运行,处理WebSocket连接和消息发送;
- Redis作为Channel Layer(消息通道层),负责多个Django进程间的通信;
- 后台Celery任务或Spark实时计算的结果写入Redis Channel,再通过Channel Layer推送给所有订阅该频道的WebSocket连接。
先看配置:
python复制# settings.py
INSTALLED_APPS = [
...
'daphne',
'channels',
...
]
ASGI_APPLICATION = 'config.asgi.application'
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
"hosts": [('127.0.0.1', 6379)],
},
},
}
再写消费者:
python复制# apps/realtime/consumers.py
import json
from channels.generic.websocket import AsyncJsonWebsocketConsumer
class InterestAnalysisConsumer(AsyncJsonWebsocketConsumer):
"""兴趣分析大屏实时推送"""
async def connect(self):
self.group_name = 'interest_analysis_room'
await self.channel_layer.group_add(self.group_name, self.channel_name)
await self.accept()
async def disconnect(self, close_code):
await self.channel_layer.group_discard(self.group_name, self.channel_name)
async def analysis_update(self, event):
# 由服务端通过 group_send 触发
await self.send_json({
'type': 'analysis.update',
'payload': event['payload'],
})
后台分析完数据后,推送消息的代码如下:
python复制# 在Celery任务或View中
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
channel_layer = get_channel_layer()
result_payload = {
'timestamp': timezone.now().isoformat(),
'hot_categories': hot_categories_rank,
'user_portrait_update': latest_portrait,
}
async_to_sync(channel_layer.group_send)(
'interest_analysis_room',
{
'type': 'analysis.update',
'payload': result_payload,
}
)
5.3 后端分析完成后的消息推送与前端重连处理
实时推送还有一个很容易被忽略的问题:WebSocket连接是脆弱的,前端断网、切换页面、服务端重启,连接都会断开,必须做自动重连和消息补偿。
我在前端用了一个简单的重连机制:遇到 onclose 事件后,每3秒重新连接一次;同时前端保存一份最后一次收到的画像数据时间戳,重连后主动向后端请求一次增量更新,避免连接断开期间的数据黑洞。
后端也需要配套处理:消费者收到 get_latest 事件时,把最近一次的分析结果从Redis缓存中拉出来补发一次。这一步看似简单,但在演示现场特别救命——一旦WebSocket断过一次,如果重连后没有补偿逻辑,大屏就会停留在一张旧图上,观感极差。
6. 可视化与管理后台:把分析结果变成可用的业务工具
6.1 用Django Unfold重构后台:自带的数据可视化管理方案
很多项目做到API结束就完事了,但答辩或实际交付时,一个漂亮的管理后台会是极大的加分项。我用 django-unfold 快速搭建了管理后台的界面层。
Unfold是一个基于TailwindCSS的Django Admin主题,特点是现代感强、配置简单,不需要单独写前端页面。我把兴趣画像的 UserInterestProfile 和 BehaviorEvent 都挂到了Unfold后台,运营人员可以直接点进某个用户看到他最近30天的行为明细和兴趣排名。
关键配置:
python复制# settings.py
INSTALLED_APPS = [
'unfold', # 必须放在 django.contrib.admin 之前
'django.contrib.admin',
...
]
# admin.py
from django.contrib import admin
from unfold.admin import ModelAdmin
from apps.profile.models import UserInterestProfile, UserInterestItem
@admin.register(UserInterestProfile)
class UserInterestProfileAdmin(ModelAdmin):
list_display = ['user', 'top_category', 'total_score', 'updated_at']
list_filter = ['updated_at', 'items__category']
search_fields = ['user__username']
def top_category(self, obj):
top_item = obj.items.first()
return f"{top_item.category} ({top_item.interest_score:.1f})" if top_item else '-'
top_category.short_description = 'TOP兴趣分类'
Unfold的表格样式、侧边栏布局都是开箱即用,配合ECharts大屏展示,整体视觉基本能到商用系统的质感。值得注意的是Unfold要求Django版本3.2以上,直接用最新LTS版本就好。
6.2 ECharts大屏与接口聚合:最耗性能的不是图表而是SQL
大屏我采用的方案是 Vue3 + ECharts + WebSocket。ECharts的散点图、柱状图、关系图都非常适合展示用户兴趣分布。真正费劲的不是画图,而是数据接口的聚合逻辑。
我设计了一个 /api/dashboard/interest-overview/ 接口,返回以下聚合数据:
- 全站各视频分类的兴趣权重TOP10
- 各年龄段的偏好分类分布
- 近7天兴趣浓度变化趋势
- 用户实时活跃热力图(按小时)
这个接口本身很简单,难在SQL要写得高效。不要用ORM一层一层套子查询,直接在Django里写原生SQL或视图:
python复制from django.db import connection
def interest_overview(request):
with connection.cursor() as cursor:
cursor.execute("""
SELECT video_category, SUM(interest_score) AS total_score
FROM user_interest_item
GROUP BY video_category
ORDER BY total_score DESC
LIMIT 10
""")
rows = dictfetchall(cursor)
# 再补实时数据,从Redis读取
realtime = redis_client.hgetall('realtime_category_score')
...
这里强调一个容易被忽视的问题:大屏接口如果每次都实时聚合全表,性能会比较差。 我的优化方案是把聚合结果缓存到Redis,设置TTL为30秒,前端大屏本来就有轮询兜底,30秒的延迟完全在可接受范围内。缓存击穿的问题也要考虑,在Django里我用了简单的双重检查锁:
python复制import redis
cache_key = 'interest_overview_cache'
data = redis_client.get(cache_key)
if data is None:
lock_key = cache_key + '_lock'
if redis_client.set(lock_key, '1', nx=True, ex=5):
try:
data = query_interest_data()
redis_client.setex(cache_key, 30, data)
finally:
redis_client.delete(lock_key)
else:
# 拿锁失败,返回上一次缓存数据或等待50ms重试
time.sleep(0.05)
data = redis_client.get(cache_key)
6.3 我自己踩过的三个坑:字段类型、聚合查询、缓存击穿
最后分享三个我在这个项目里真实踩过的坑,每一个都让我花了不少时间定位,希望能帮你提前绕开。
坑一:Django模型的 AutoField 与大数据侧任务对接时类型不一致。 我一开始 user_id 用的 CharField(32),存的是UUID字符串,但Spark作业里写的是 BIGINT,两边一join就报错。最后统一约定:行为日志里的 user_id 和业务库里的 auth_user.id 都用字符串主键,并对齐了Hive表里的字段类型。别的同学做这类项目时,建议先画一张“字段类型对照表”,把Django、MySQL、Hive里的同名同义字段类型一次性对齐,能省下大把联调时间。
坑二:聚合查询的时区偏差。 Django默认 USE_TZ=True,存入数据库的时间是UTC时区。但我查询 created_at__gte=时间 时,如果传入的是本地时间,直接对比会少了8小时。我后来统一在查询入口把本地时间转成UTC:
python复制from django.utils import timezone
from datetime import datetime, timedelta
start_time = timezone.now() - timedelta(days=30)
# 查询时确保 start_time 是 UTC aware datetime
坑三:缓存雪崩。 我一开始给大屏接口和各榜单接口都设置了相同的60秒TTL,结果每隔60秒整点,所有缓存同时失效,大量请求瞬间打到MySQL上,直接把数据库连接池打满了。后来我把各接口的TTL错开,比如榜单缓存45秒、大屏缓存30秒、用户画像缓存5分钟,同时给MySQL的 max_connections 留足缓冲。这个思路在毕业设计答辩时提出来,也是很好的加分点。
整套项目跑下来,我最大的体会是:一个合格的“短视频用户兴趣分析”系统,技术点不在于某个单一框架有多强,而在于Django + 大数据组件 + 可视化 + 实时推送这整条链路能不能串起来,并且每个环节都有合理的设计理由。这套方案我实测在10万级用户、日均百万行为数据下运行稳定,如果你只是练手,把用户量降到万级、用单机Spark跑离线计算也完全够用。希望这篇能帮你少走弯路,把更多精力放在真正能体现思考深度的地方。
