ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

广告投放系统微服务改造:SpringCloudAlibaba组件落地与MySQL实践

广告投放系统微服务改造:SpringCloudAlibaba组件落地与MySQL实践 简介面向微服务开发学习者与广告投放业务初学者这是一份基于SpringCloudAlibaba和MySQL实现的广告投放系统源码工程涵盖网关、广告检索、广告投放、公共模块等核心模块划分可帮助理解微服务项目拆分、配置管理及数据库初始化方式。压缩包共97个文件主要以73个Java源码文件承载业务逻辑配合XML配置、YML环境配置、建表SQL脚本、Maven启动脚本与readme说明整体仅88KB目录紧凑、便于快速审阅。资源已有98人学习下载适合正在实践Spring Cloud Alibaba组件、或需要参考广告系统数据建模的开发者参考。通过阅读源码能够梳理各服务模块间的调用关系、广告检索与投放处理流程、SQL初始化设计还可借助Maven Wrapper与初始化SQL降低环境准备成本。整体代码结构简洁适合作为课程设计或毕业设计的改造基础减少从零搭建同类工程的时间。1. 广告投放系统的微服务改造这套源码解决的三个真问题广告投放系统有个很反直觉的特点大部分代码不是在投广告而是在“算账”。用户每点一次广告要在几百毫秒内完成扣费、记录曝光、判断频控、回传转化晚上还要跑对账把当天所有流水重新算一遍。这套基于SpringCloudAlibaba的MySQL广告投放系统源码把整个链路拆成了微服务Gateway统一收请求Nacos管注册和配置Sentinel挡流量Seata保扣费和流水的事务一致MySQL存广告计划、单元、创意和扣费流水。适合想用一套真实业务把SpringCloudAlibaba五大组件串起来的Java后端也适合课程设计或简历项目需要落地细节的人。源码这东西最怕的是“能启动但看不懂”。这套项目的好处是业务边界清楚广告主投钱、平台扣费、流量主收钱三个角色对应三组服务MySQL表结构也按这个逻辑拆。下面按我实际跑通的顺序从架构、库表、部署、踩坑到改造逐层拆给你看。2. 拆开源码包看架构SpringCloudAlibaba五大组件在广告系统里怎么落位拿到源码包先别急着启动。我先看pom.xml和启动类把服务边界划清楚后面改配置、查日志才知道去哪找。2.1 先看pom.xml依赖到底引了哪些源码里各模块的pom结构基本一致核心依赖集中在以下几点dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-nacos-discovery/artifactId version2021.1/version /dependency dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-nacos-config/artifactId version2021.1/version /dependency dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-sentinel/artifactId version2021.1/version /dependency dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-seata/artifactId version2021.1/version /dependency dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-gateway/artifactId /dependency dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-openfeign/artifactId /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency版本号这里要注意SpringCloudAlibaba的版本命名和SpringCloud版本有对应关系2021.1对应Spring Boot 2.6.x。你在pom里看到的版本如果和本地JDK对不上启动时会直接抛BeanCreationException后面避坑章节会细说。依赖只解决“能用”真正决定系统怎么跑的是配置和注解。这套源码把服务拆成了网关、广告检索、扣费、对账、管理后台五个模块每个模块独立一个Spring Boot应用通过Nacos互相发现。2.2 Nacos承担什么注册中心与配置中心合一的实际效果广告投放系统里服务数量不多但调用关系复杂网关要路由到检索服务检索服务要调扣费服务扣费服务要异步通知对账服务。没有注册中心时这些地址写死在配置文件里每加一个实例就得改一遍配置。Nacos在这里同时做了两件事服务注册与发现、配置管理。服务注册解决“找得到”配置管理解决“改得动”。我一般会把数据源、Redis、Seata这类每个服务都要用的配置放在Nacos的共享配置里服务自己的端口、服务名放在本地bootstrap.yml。spring: cloud: nacos: discovery: server-addr: 127.0.0.1:8848 namespace: ad-platform config: server-addr: 127.0.0.1:8848 namespace: ad-platform file-extension: yaml shared-configs: ->spring: cloud: sentinel: transport: dashboard: 127.0.0.1:8858 datasource: - nacos: server-addr: ${spring.cloud.nacos.discovery.server-addr} >GlobalTransactional(name ad-deduct, rollbackFor Exception.class) public void deduct(DeductRequest request) { accountClient.deductBalance(request.getAccountId(), request.getAmount()); deductMapper.insert(DeductRecord.from(request)); planClient.updateDailyCost(request.getPlanId(), request.getAmount()); }三个操作跨了三个服务但加了GlobalTransactional后任何一个失败都会触发全局回滚。注意Seata的TC端事务协调器要先启动否则服务启动时报Cannot connect to Seata TC。2.5 Gateway加OpenFeign请求链路和调用链怎么走实际跑这套源码时我把请求链路完整打通过前端请求先到GatewayGateway根据路径前缀路由到具体服务。服务之间用OpenFeign同步调用异步对账用Spring的事件机制解耦。Gateway路由配置片段spring: cloud: gateway: routes: - id: ad-search uri: lb://ad-search-service predicates: - Path/api/search/** filters: - StripPrefix1 - id: ad-deduct uri: lb://ad-deduct-service predicates: - Path/api/deduct/** filters: - StripPrefix1Gateway用lb://开头走Nacos负载均衡。OpenFeign调用时注意加超时配置广告检索要控制200ms内返回超时设大了会把下游拖垮。feign: client: config: default: connectTimeout: 1000 readTimeout: 3000整体架构理顺后下一步就是看MySQL的表怎么设计。广告投放系统的表不比电商简单尤其是流水表字段错了对账时根本查不出来。3. 库表设计广告投放的MySQL建模与导入脚本源码包里一般带sql目录里面是建库建表脚本。我拿到脚本第一件事不是执行而是先看表结构理解业务模型后再导入。3.1 四级模型广告主、计划、单元、创意广告投放系统最常见的组织方式是四级广告主account→ 投放计划plan→ 广告单元unit→ 创意creative。计划决定预算和投放周期单元决定定向和出价方式创意是用户真正看到的素材。表关系是这样设计的CREATE TABLE ad_account ( id BIGINT PRIMARY KEY AUTO_INCREMENT, account_name VARCHAR(128) NOT NULL, balance DECIMAL(12,3) NOT NULL DEFAULT 0.000 COMMENT 账户余额单位元, status TINYINT NOT NULL DEFAULT 1 COMMENT 1启用 0暂停, created_time DATETIME NOT NULL, updated_time DATETIME NOT NULL ); CREATE TABLE ad_plan ( id BIGINT PRIMARY KEY AUTO_INCREMENT, account_id BIGINT NOT NULL, plan_name VARCHAR(128) NOT NULL, budget DECIMAL(12,3) NOT NULL DEFAULT 0.000, cost DECIMAL(12,3) NOT NULL DEFAULT 0.000, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, status TINYINT NOT NULL DEFAULT 1, INDEX idx_account_id (account_id) ); CREATE TABLE ad_unit ( id BIGINT PRIMARY KEY AUTO_INCREMENT, plan_id BIGINT NOT NULL, unit_name VARCHAR(128) NOT NULL, target_info JSON COMMENT 定向信息地域、性别、年龄, bid_price DECIMAL(10,3) NOT NULL COMMENT 出价单位元, status TINYINT NOT NULL DEFAULT 1, INDEX idx_plan_id (plan_id) ); CREATE TABLE ad_creative ( id BIGINT PRIMARY KEY AUTO_INCREMENT, unit_id BIGINT NOT NULL, creative_name VARCHAR(128) NOT NULL, material_url VARCHAR(512), width INT, height INT, status TINYINT NOT NULL DEFAULT 1, INDEX idx_unit_id (unit_id) );JSON字段是MySQL 5.7以后才能用的我跑这套源码时本地用的MySQL 8.0没遇到兼容问题。如果你用5.6会遇到语法报错建议直接升级到8.0。“mysql设置默认值为0”这种需求在余额、成本字段上很常见上面的DEFAULT 0.000就是避免业务判断空值。3.2 扣费流水表这表才是广告系统的核心账户余额是状态表扣费流水是流水表两者必须分开。流水表只做插入不做更新对账时全量查流水重新汇总。源码里扣费流水表会包含这些字段CREATE TABLE deduct_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, request_id VARCHAR(64) NOT NULL COMMENT 幂等键每次扣费请求唯一, account_id BIGINT NOT NULL, plan_id BIGINT NOT NULL, unit_id BIGINT NOT NULL, deduct_amount DECIMAL(10,3) NOT NULL, balance_before DECIMAL(12,3) NOT NULL, balance_after DECIMAL(12,3) NOT NULL, position_id BIGINT COMMENT 广告位ID, device_ip VARCHAR(32), status TINYINT NOT NULL DEFAULT 0 COMMENT 0待对账 1已对账 2异常, created_time DATETIME NOT NULL, UNIQUE KEY uk_request_id (request_id) );request_id是幂等键扣费接口被重试时同一个request_id只能扣一次。balance_before和balance_after是快照对账时靠这两个字段判断扣费金额是否异常。status字段标记对账状态默认0跑对账脚本后变成1异常流水变成2方便人工处理。这套设计里最容易被忽略的是created_time索引。按天对账时如果没有索引流水量过百万后查询直接慢查询。源码里如果没加索引我建议补上ALTER TABLE deduct_record ADD INDEX idx_created_time (created_time);3.3 用Navicat导入SQL脚本的操作步骤拿到sql脚本后我习惯先用Navicat连上MySQL建好目标数据库再执行脚本。步骤如下用root账号连上MySQL新建数据库ad_platform字符集选utf8mb4排序规则选utf8mb4_general_ci。右键ad_platform选择“运行SQL文件”选中源码包里的schema.sql。执行完后刷新表列表确认ad_account、ad_plan、ad_unit、ad_creative、deduct_record五张核心表都在。接着执行data.sql导入默认广告主和测试计划数据。导入后验证一下数据量SELECT COUNT(*) FROM ad_account; SELECT COUNT(*) FROM ad_plan;如果第二句返回0说明data.sql里没有测试计划后续跑接口时没有数据可投。这时候自己补两条测试数据就行广告计划的状态记得设成1不然检索接口查不到。MySQL安装配置教程里最常见的坑是root密码和连接权限。源码里的数据源配置默认root/root如果你本机root密码不是这个直接改配置最简单不用重新建用户授权。库表就绪后开始搭运行环境。这套系统的环境坑比代码坑多版本不匹配的直接后果就是服务起不来或者起来后注册不上Nacos。4. 本地跑通整套系统MySQL安装、配置修改与服务启动从零到所有服务注册进Nacos这一步卡住的人最多。按下面的顺序来能省掉大半启动报错。4.1 版本搭配JDK、MySQL、Nacos怎么选我先说我这套跑通过的版本组合组件版本说明JDK1.8Spring Boot 2.6.x必须配JDK8MySQL8.0.x5.7也能跑但JSON函数差异要注意Nacos2.2.32.x版本兼容性最好Seata1.6.1对应SpringCloudAlibaba 2021.1Maven3.6.3编译源码必需Linux装MySQL时建议用官方yum源而不是包管理器自带版本因为自带版本可能过旧。下载mysql-community-server后用systemctl启动mysqld初始密码在/var/log/mysqld.log里。# 获取MySQL 8.0的临时密码 grep temporary password /var/log/mysqld.log # 登录并修改密码 mysql -uroot -p ALTER USER rootlocalhost IDENTIFIED BY YourPassword123!;MySQL 8.0默认密码策略对强度有要求纯数字密码会报错。本地开发可以直接降低策略SET GLOBAL validate_password.policy LOW;Nacos启动前要确认它的存储模式。默认是嵌入式derby数据库我建议改成MySQL存储这样配置和注册数据持久化Nacos重启不丢。Nacos的conf/application.properties里改spring.datasource.platformmysql db.num1 db.url.0jdbc:mysql://127.0.0.1:3306/nacos_config?characterEncodingutf8 db.user.0root db.password.0你的密码改完后执行nacos-mysql.sql初始化脚本再启动Nacos。访问控制台127.0.0.1:8848/nacos默认账号密码都是nacos能登录说明配置中心可用了。4.2 修改配置文件数据源、Nacos地址、Seata源码里每个服务都有bootstrap.yml和application.yml。我习惯把所有环境相关的配置统一改成自己的本地值避免哪漏了改不到。spring: datasource: url: jdbc:mysql://127.0.0.1:3306/ad_platform?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000useSSLfalse自己加上MySQL 8.0默认开SSL本地连接没配证书会报SSL连接错误。serverTimezone必须设置否则报“The server time zone value is unrecognized”这是个高频报错。Seata配置里主要改注册类型和命名空间seata: enabled: true application-id: ad-deduct-service tx-service-group: ad-tx-group registry: type: nacos nacos: server-addr: 127.0.0.1:8848 namespace: public config: type: nacostx-service-group要和Seata服务端配置的映射一致不然客户端报“could not find service group”。4.3 启动顺序与接口验证微服务启动顺序有讲究。我一般按这个顺序先启动MySQL和NacosNacos控制台能看到服务列表为空。启动Seata Server确认tc端口8091监听正常。按依赖顺序启动业务服务账号服务、计划服务、扣费服务、检索服务、网关。启动每个服务后在日志里确认一行# 看到这行才算注册成功 nacos registry, nacos service xxx register finished用管理后台服务测试整条链路# 1. 通过网关创建一个广告计划 curl -X POST http://127.0.0.1:8080/api/plan \ -H Content-Type: application/json \ -d {accountId:1,planName:测试计划,budget:1000,startTime:2024-01-01 00:00:00,endTime:2024-12-31 23:59:59} # 2. 查询计划列表 curl http://127.0.0.1:8080/api/plan/list?accountId1网关端口取决于源码配置有的项目是8888有的是8080以application.yml里server.port为准。如果返回JSON里包含刚创建的记录说明网关路由、Nacos注册、MySQL读写都通了。都跑通后别急着高兴这套系统有几个坑是必踩的我把遇到过的和排查方法整理在下一章。5. 避坑指南这套广告投放系统源码跑崩过的5个地方源码能跑通是运气跑崩才是常态。下面这5个问题都是实操中高频出现的按“现象→原因→解决”写你在部署时遇到能少走弯路。5.1 MySQL 8.0连接报错SSL与时区一起报现象启动服务时数据源初始化失败日志里同时出现“SSL connection error”和“The server time zone value is unrecognized”。原因MySQL 8.0默认启用SSL且时区不是标准时区JDBC连接串里没显式关闭SSL和指定时区。解决JDBC URL里加上useSSLfalse和serverTimezoneAsia/Shanghai两个参数缺一不可。我第一次只加了时区没关SSL还是报错。5.2 Nacos 2.x端口冲突客户端连不上8848现象服务启动时日志报connect timed out但Nacos控制台明明能打开。原因Nacos 2.x客户端不仅用8848还要用9848这个gRPC端口。防火墙只放行了8848gRPC端口被挡。解决放行9848端口或者本地开发直接关防火墙。Nacos 2.x的客户端发现机制和1.x不一样老帖子里的排查方法不适用。5.3 Seata AT模式回滚失效事务没生效现象扣费接口故意让第三个服务抛异常但账户余额没回滚。原因Seata客户端和TC端之间通信正常但数据源没有被代理。常见原因是业务服务里用了多个数据源而Seata只代理了主数据源。解决检查扣费服务的数据源配置确认spring.datasource里没有额外定义其他数据源。另外GlobalTransactional要加在入口方法上而不是每个Feign调用上。Feign调用链上任何一环抛了异常都要让异常传到入口方法才能触发回滚。5.4 Sentinel规则不生效流量控制形同虚设现象配置了QPS阈值用压测工具打流量接口该限流时没限流。原因Sentinel规则配置在了Nacos配置中心但应用里没引入sentinel-datasource-nacos依赖规则没加载到内存。解决pom.xml里确认有这个依赖。另一个常见坑是规则数据ID命名要和源码里一致我在2.3节写的data-id是sentinel-ad-deduct.json你拿到源码后要以实际文件名为准不一致时规则静默加载失败。5.5 定时任务重复执行对账任务两边都跑现象晚上对账后数据被算了两遍扣费金额翻倍。原因广告投放系统的对账任务可能在管理后台服务和扣费服务里各配置了一份或者部署了多个实例但没加分布式锁。解决看源码里Scheduled注解加在哪个类上确认只有一个服务启用了它。部署多实例时用Redis分布式锁包住任务体获取不到锁的实例直接跳过。锁的key用对账日期过期时间设为任务超时上限的两倍避免任务还在跑锁就过期。Scheduled(cron 0 0 2 * * ?) public void dailyReconcile() { String lockKey reconcile:lock: LocalDate.now(); boolean locked redisTemplate.opsForValue().setIfAbsent(lockKey, 1, Duration.ofHours(2)); if (!locked) { return; } // 执行对账逻辑 }这些坑绕过去之后整套系统能稳定跑。但如果只是把源码跑起来价值不大。源码的真正价值在于你能看出哪里设计得不够然后动手改。6. 从跑通到上线这套源码值得动手改的三个地方源码跑通只是起点真正让这套系统能扛住线上流量还需要三处改造。这三个方向也是面试官最爱问的。6.1 扣费流水分表按月份拆还是按账户拆流水表是增长最快的表百万级数据后查询性能明显下降。常见做法是按月份分表deduct_record_202401、deduct_record_202402这样。按月分表逻辑简单对账任务只需要查对应月份的表。如果单月流水量也巨大就按账户ID取模分表比如分成16张表路由规则是accountId % 16。改造成本高一些查询时必须带accountId否则要遍历所有表。我建议先按月分等单月千万级再考虑取模。6.2 广告检索从MySQL挪到Redis检索服务每次请求都要查计划、单元、创意三张表流量上来后MySQL扛不住。常见做法是启动时把有效广告计划加载到Redis用ZSet存计划ID和出价用Hash存计划详情。检索时先从Redis过滤出符合定向的计划再回源MySQL补全创意信息。Redis内存只有几十MB时注意淘汰策略别把计划详情和流水数据混在同一个Redis实例。线上一般单独拆一个广告检索Redismaxmemory-policy配allkeys-lru。6.3 对账脚本加上幂等和告警对账脚本最怕跑了一半挂掉重启后从哪继续。常见做法是对账任务记录每次处理的request_id断点续跑时跳过已处理的流水。我自己的习惯是对账结果先写入reconcile_result表全部算完再批量更新deduct_record的status这样即使脚本中途崩了也不会产生“对账一半”的脏状态。这套源码配合MySQL主从复制做读写分离也是常见的上线改造账号余额的读走从库扣费写走主库能显著降低主库压力。主从配置好之后注意从库的复制延迟对账场景能接受秒级延迟但扣费结果实时查询不能走从库。把这套源码从跑通到改造过一遍我的体会是项目里最难的不是SpringCloudAlibaba的组件配置而是理解业务边界和数据流。我习惯拿到源码先画一遍表关系再对着代码确认每条流水的来龙去脉最后才动手改配置。希望这套系统的拆解思路能帮到你遇到具体报错时按章节里的排查路径走基本都能解决。本文还有配套的精品资源点击获取
返回列表