小滴云在线教育平台是一个面向 C 端用户的综合性学习系统,支持课程浏览、视频点播、直播互动、在线测验、订单支付、用户积分等核心业务。项目以“商业级”为标准,要求 高可用(99.99% SLA)、高并发(峰值 QPS ≥ 5000)、数据一致性(支付/订单事务)和 快速迭代(每日发布)。
整体采用 前后端分离 + 微服务 + 云原生 架构,部署于腾讯云容器服务(TKE)与 Serverless 环境。架构图如下:
┌─────────────────────────────────────────────────────────────┐
│ CDN + 腾讯云 CLB │
└─────────────────────────────────────────────────────────────┘
│
┌────────────────────┼────────────────────┐
│ │ │
┌────▼────┐ ┌─────▼─────┐ ┌─────▼─────┐
│ Vue3 SPA│ │ H5/小程序 │ │ 管理后台 │
│ (SSR) │ │ (Uni-app) │ │ React │
└────┬────┘ └─────┬─────┘ └─────┬─────┘
└────────────────────┼────────────────────┘
│
┌─────────▼─────────┐
│ API Gateway │
│ (Spring Cloud Gateway)│
└─────────┬─────────┘
│
┌────────────────────┼────────────────────┐
│ │ │
┌────▼────┐ ┌─────▼─────┐ ┌─────▼─────┐
│用户/认证 │ │ 课程/内容 │ │ 订单/支付 │
│微服务 │ │ 微服务 │ │ 微服务 │
└────┬────┘ └─────┬─────┘ └─────┬─────┘
│ │ │
└────────────────────┼────────────────────┘
│
┌─────────▼─────────┐
│ 消息中间件 (RocketMQ)│
│ 分布式事务 (TCC) │
└─────────┬─────────┘
│
┌─────────▼─────────┐
│ 数据层 │
│ MySQL (分库分表) │
│ Redis (缓存/会话) │
│ MongoDB (课程详情)│
│ COS (视频/图片) │
└───────────────────┘层级 | 技术栈 | 腾讯云产品 |
|---|---|---|
前端 | Vue3 + TypeScript + Vite + Pinia + Nuxt3 (SSR) | CDN、CLS(日志服务) |
后端 | Spring Boot 3.2 + Spring Cloud 2023 + Alibaba Nacos | TKE、CVM |
网关 | Spring Cloud Gateway + 限流(Sentinel) | CLB、NAT |
数据库 | MySQL 8.0(ShardingSphere-JDBC分库分表) + Redis 7.0 | TencentDB for MySQL、Redis |
存储 | MinIO 自建 + 腾讯云 COS(冷热分离) | COS、CI(数据万象) |
消息 | Apache RocketMQ 5.0 | TDMQ for RocketMQ |
监控 | Prometheus + Grafana + SkyWalking | 云监控、APM |
容器 | Docker + Kubernetes(Helm) | TKE(弹性容器) |
CI/CD | GitLab CI + ArgoCD | CODING DevOps |
为了提升 SEO 和首屏加载速度,我们选用 Nuxt3 实现服务端渲染。核心配置如下:
// nuxt.config.ts
export default defineNuxtConfig({
ssr: true,
nitro: {
preset: 'node-server',
compressPublicAssets: true,
},
vite: {
build: {
rollupOptions: {
output: {
manualChunks: {
'vendor': ['vue', 'vue-router', 'pinia'],
'ui': ['element-plus', '@vueuse/core']
}
}
}
}
},
// 腾讯云 COS 静态资源加速
app: {
cdnURL: process.env.CDN_BASE_URL || 'https://static.xiaodi-edu.com'
}
})使用 Pinia 管理用户状态,结合 pinia-plugin-persistedstate 将 token 存储于 localStorage,并设置自动刷新机制:
// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { jwtDecode } from 'jwt-decode'
export const useUserStore = defineStore('user', () => {
const token = ref<string | null>(null)
const userInfo = ref<UserInfo | null>(null)
const isTokenValid = computed(() => {
if (!token.value) return false
try {
const { exp } = jwtDecode(token.value)
return Date.now() < exp * 1000 - 60000 // 提前1分钟刷新
} catch {
return false
}
})
// 自动刷新 token(调用 refresh 接口)
async function refreshToken() {
const res = await $fetch('/api/auth/refresh', {
method: 'POST',
credentials: 'include'
})
token.value = res.accessToken
userInfo.value = res.user
}
// 每隔 10 分钟检查并刷新
setInterval(() => {
if (!isTokenValid.value) refreshToken()
}, 10 * 60 * 1000)
return { token, userInfo, isTokenValid, refreshToken }
})使用腾讯云点播(VOD)或 COS 预签名 URL,结合 hls.js 实现自适应码率播放。前端采用 @videojs/http-streaming 并启用 preload="metadata" 减少流量消耗:
<template>
<video ref="videoRef" class="video-js vjs-default-skin" />
</template>
<script setup>
import videojs from 'video.js'
import 'video.js/dist/video-js.css'
import { onMounted, ref, watch } from 'vue'
const videoRef = ref(null)
const props = defineProps(['videoId'])
let player = null
onMounted(() => {
player = videojs(videoRef.value, {
controls: true,
autoplay: false,
preload: 'metadata',
techOrder: ['html5'],
sources: [{
src: `/api/video/stream/${props.videoId}`,
type: 'application/x-mpegURL'
}],
html5: {
vhs: {
enableLowInitialPlaylist: true,
smoothQualityChange: true,
overrideNative: true
}
}
})
// 监听播放进度并上报
player.on('timeupdate', () => {
const progress = player.currentTime() / player.duration()
if (progress % 0.1 < 0.01) { // 每10%上报一次
reportProgress(props.videoId, player.currentTime())
}
})
})
</script>基于 Spring Security 6 实现 OAuth2 资源服务器,使用 RS256 非对称加密签名 JWT,私钥存于腾讯云 KMS:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**", "/actuator/health").permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.decoder(jwtDecoder()))
)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}
@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withPublicKey(loadPublicKey()).build();
}
// 从腾讯云 KMS 获取公钥,实现动态轮转
private RSAPublicKey loadPublicKey() {
String publicKeyPem = kmsClient.getPublicKey("jwt-sign-key");
return KeyUtils.pemToPublicKey(publicKeyPem);
}
}课程购买涉及 订单创建 → 扣减库存 → 支付回调 → 发放权益,我们采用 TCC 模式 + RocketMQ 事务消息 保证最终一致性。核心代码:
@Component
public class OrderTccService {
@Autowired
private OrderRepository orderRepo;
@Autowired
private InventoryClient inventoryClient;
@Autowired
private RocketMQTemplate rocketMqTemplate;
// Try 阶段:预扣库存,创建待支付订单
@Transactional
public Order tryCreateOrder(OrderRequest request) {
// 1. 调用库存服务 TCC try
inventoryClient.tryDeductStock(request.getCourseId(), request.getQuantity());
// 2. 创建订单状态为 "INIT"
Order order = new Order();
order.setStatus(OrderStatus.INIT);
order.setAmount(request.getAmount());
orderRepo.save(order);
// 3. 发送半事务消息
TransactionSendResult result = rocketMqTemplate.sendTransactionMessage(
"order-pay-topic",
buildTransactionMessage(order),
order.getId()
);
return order;
}
// Confirm 阶段(支付成功后由支付回调触发)
public void confirmOrder(Long orderId) {
Order order = orderRepo.findById(orderId).orElseThrow();
order.setStatus(OrderStatus.PAID);
// 发放课程权益
userCourseService.enroll(order.getUserId(), order.getCourseId());
inventoryClient.confirmDeduct(order.getCourseId());
}
// Cancel 阶段(超时未支付或支付失败)
public void cancelOrder(Long orderId) {
Order order = orderRepo.findById(orderId).orElseThrow();
order.setStatus(OrderStatus.CANCELLED);
inventoryClient.cancelDeduct(order.getCourseId());
}
}配合 RocketMQ 事务监听器:
@RocketMQTransactionListener
public class OrderTransactionListener implements RocketMQLocalTransactionListener {
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
Long orderId = (Long) arg;
// 检查订单是否已支付(幂等)
Order order = orderRepo.findById(orderId).orElse(null);
if (order != null && order.getStatus() == OrderStatus.PAID) {
return RocketMQLocalTransactionState.COMMIT;
}
return RocketMQLocalTransactionState.UNKNOWN;
}
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
// 回查,确保事务最终一致
Long orderId = extractOrderId(msg);
Order order = orderRepo.findById(orderId).orElse(null);
if (order == null) return RocketMQLocalTransactionState.ROLLBACK;
return order.getStatus() == OrderStatus.PAID ?
RocketMQLocalTransactionState.COMMIT :
RocketMQLocalTransactionState.UNKNOWN;
}
}课程详情接口 QPS 高达 8000,采用 多级缓存:本地 Caffeine(L1) + Redis(L2) + 数据库(L3)。使用 @Cacheable 结合自定义双检锁:
@Service
public class CourseDetailService {
@Autowired
private CourseRepository courseRepo;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 本地缓存
private final Cache<String, CourseDetail> localCache = Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.recordStats()
.build();
public CourseDetail getDetail(Long courseId) {
String key = "course:detail:" + courseId;
// L1 本地缓存
CourseDetail detail = localCache.getIfPresent(key);
if (detail != null) return detail;
// L2 Redis 缓存,使用互斥锁防止缓存击穿
Boolean locked = redisTemplate.opsForValue().setIfAbsent("lock:" + key, "1", 3, TimeUnit.SECONDS);
try {
if (Boolean.TRUE.equals(locked)) {
detail = redisTemplate.opsForValue().get(key);
if (detail == null) {
detail = courseRepo.findDetailById(courseId); // 查DB
redisTemplate.opsForValue().set(key, detail, 30, TimeUnit.MINUTES);
}
localCache.put(key, detail);
} else {
// 等待100ms重试
Thread.sleep(100);
return getDetail(courseId);
}
} finally {
redisTemplate.delete("lock:" + key);
}
return detail;
}
}每个微服务使用 Jib 构建镜像并推送至腾讯云 TCR(容器镜像仓库)。Helm Chart 管理所有服务配置:
# values-prod.yaml
replicaCount: 3
image:
repository: ccr.ccs.tencentyun.com/xiaodi/order-service
tag: latest
pullPolicy: Always
service:
type: ClusterIP
port: 8080
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "2Gi"
cpu: "2000m"
env:
- name: DB_HOST
valueFrom:
secretKeyRef:
name: db-secret
key: host
- name: REDIS_HOST
value: "redis-cluster.redis.svc.cluster.local"
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 20
targetCPUUtilizationPercentage: 70
targetMemoryUtilizationPercentage: 80
# 使用腾讯云 CLB 暴露网关
ingress:
enabled: true
annotations:
kubernetes.io/ingress.class: "qcloud"
qcloud.com/tls-cert-id: "cert-xxxxxx"
hosts:
- host: api.xiaodi-edu.com
paths:
- path: /
pathType: Prefix结合腾讯云 TKE 的 Horizontal Pod Autoscaler 和 Cluster Autoscaler,根据 CPU/内存及自定义指标(如 RocketMQ 堆积数)动态扩容:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: order-service-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: order-service
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Pods
pods:
metric:
name: rabbitmq_queue_messages
target:
type: AverageValue
averageValue: 1000使用腾讯云 服务网格(TCM) 或 Nacos 实现金丝雀发布。通过请求头 x-version 路由至灰度版本:
@Configuration
public class GrayRouteConfig {
@Bean
public GlobalFilter grayFilter() {
return (exchange, chain) -> {
String version = exchange.getRequest().getHeaders().getFirst("x-version");
if (StringUtils.hasText(version)) {
exchange.getAttributes().put("version", version);
}
return chain.filter(exchange);
};
}
}并在 Nacos 中配置路由规则,将 version=gray 的流量转发至灰度服务。
接入 SkyWalking Java Agent,并上报至腾讯云 APM。核心配置:
# agent.config
agent.service_name=order-service
collector.backend_service=apm.tencentyun.com:11800
plugin.mysql.trace_sql_parameters=true自定义 Span 记录业务关键路径:
@Trace
public void processPayment(PaymentRequest request) {
ActiveSpan.tag("payment.amount", request.getAmount().toString());
ActiveSpan.info("Payment processing started");
// 业务逻辑
}使用 Filebeat 采集容器日志,发送至腾讯云 CLS(日志服务),并配置告警规则(如错误率 > 5% 触发钉钉通知)。
# filebeat-config.yaml
filebeat.inputs:
- type: container
paths:
- /var/log/containers/*.log
processors:
- add_kubernetes_metadata:
host: ${NODE_NAME}
matchers:
- logs_path:
logs_path: "/var/log/containers/"
output.logstash:
hosts: ["cls-logstash.internal:5044"]使用 JMeter 对核心接口进行压测(腾讯云内网,8C16G 节点 * 5):
接口 | 平均响应时间 | 99% 线 | 最大 QPS |
|---|---|---|---|
课程详情 | 12ms | 45ms | 12,000 |
下单(TCC) | 86ms | 210ms | 3,200 |
视频流首帧 | 240ms | 580ms | 8,500 |
登录认证 | 28ms | 72ms | 15,000 |
通过 缓存命中率 达 98.7%,数据库连接池 合理配置(HikariCP max=50),以及 Redis 集群(6 节点,3主3从)支撑高并发。
小滴云在线教育平台通过 微服务化、云原生容器编排 和 全链路可观测,成功支撑了百万级用户和日均千万次请求。代码层面严格遵循 DDD 分层,结合 TCC 和消息队列保障事务最终一致性,前端采用 SSR 和流式播放提升用户体验。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。