广告系统应该需要实现的最基本的功能
广告投放系统 -> 既然是广告系统, 一定得有广告数据, 数据当然是由广告主或代理商投放, 那么, 也就需要有个投放广告的
平台, 这就是广告投放系统
广告检索系统 -> 媒体方对广告系统发起请求, 广告系统能够检索符合要求的广告数据, 这就是广告检索系统的核心功能
完整的广告系统需要包的些子系统
曝光监测系统 -> 监测广告数据的曝光记录
报表系统 -> 构建广告数据报表, 比如广告 A 在地域 B 中一共曝光了多少次, 主要是 OLAP 的过程
扣费系统 -> 广告的每一次曝光都是需要扣费的, 且这个系统里面负责了将广告数据置位的功能
项目架构和代码设计
通用模块(ad-common)设计的思想与实现的功能
通用的代码、 配置不应该散落在各个业务模块中, 不利于维护与更新。一个大的系统, 响应对象需要统一外层格式,各种业务设计与实现, 可能会抛出各种各样的异常, 异常信息的收集也应该做到统一
package com.imooc.ad.vo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; @Data @NoArgsConstructor @AllArgsConstructor public class CommonResponse<T> implements Serializable { private Integer code; private String message; private T data; public CommonResponse(Integer code, String message) { this.code = code; this.message = message; } }
package com.imooc.ad.advice; import com.imooc.ad.exception.AdException; import com.imooc.ad.vo.CommonResponse; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import javax.servlet.http.HttpServletRequest; @RestControllerAdvice public class GlobalExceptionAdvice { @ExceptionHandler(value = AdException.class) public CommonResponse<String> handlerAdException(HttpServletRequest req, AdException ex) { CommonResponse<String> response = new CommonResponse<>(-1, "business error"); response.setData(ex.getMessage()); return response; } }
public class AdException extends Exception { public AdException(String message) { super(message); } }
package com.imooc.ad.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface IgnoreResponseAdvice { }
package com.imooc.ad.advice; import com.imooc.ad.annotation.IgnoreResponseAdvice; import com.imooc.ad.vo.CommonResponse; import org.springframework.core.MethodParameter; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.lang.Nullable; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; /** * Created by Qinyi. */ @RestControllerAdvice public class CommonResponseDataAdvice implements ResponseBodyAdvice<Object> { @Override @SuppressWarnings("all") public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) { if (methodParameter.getDeclaringClass().isAnnotationPresent( IgnoreResponseAdvice.class )) { return false; } if (methodParameter.getMethod().isAnnotationPresent( IgnoreResponseAdvice.class )) { return false; } return true; } @Nullable @Override @SuppressWarnings("all") public Object beforeBodyWrite(@Nullable Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) { CommonResponse<Object> response = new CommonResponse<>(0, ""); if (null == o) { return response; } else if (o instanceof CommonResponse) { response = (CommonResponse<Object>) o; } else { response.setData(o); } return response; } }
Eureka 的架构和高可用
问题说明: 单节点的 Eureka Server 虽然能够实现基础功能, 但是存在单点故障的问题, 不能实现高可用。 因为 Eureka Server 中存储了整个系统中所有的微服务的元数据信息, 单节点一旦挂了, 所有的服务信息都会丢失, 造成整个系统的瘫痪。
解决办法: 搭建 Eureka Server 集群, 让各个 Server 节点之间互相注册, 从而实现微服务元数据的复制/备份, 即使单个节点失效, 其他的 Server 节点仍可以继续提供服务
Zuul 的介绍
在介绍 Zuul 可以提供的功能之前, 请大家先考虑一个问题:微服务系统中往往包含很多个功能不同的子系统或微服务, 那么, 外部应用怎样去访问各种各样的微服务呢? 这也是 Zuul 所要解决的一个主要问题。在微服务架构中, 后端服务往往不直接开放给调用端, 而是通过一个服务网关根据请求的 url, 路由到相应的服务, 即实现请求转发, 效果如下图所示
Zuul 提供了服务网关的功能, 可以实现负载均衡、 反向代理、 动态路由、 请求转发等功能。 Zuul 大部分功能都是通过过滤器实现的, Zuul 中定义了四种标准的过滤器类型, 同时, 还支持自定义过滤器( 项目中实现了两个自定义过滤器, 用来记录访问延迟) 。 这些过滤器的类型也对应于请求的典型生命周期,如下图所示。
pre: 在请求被路由之前调用
route: 在路由请求时被调用
post: 在 route 和 error 过滤器之后被调用
error: 处理请求时发生错误时被调用
表结构的设计
数据表的设计与广告数据的核心要素是相对的, 目的就是去表达各个要素。 但是, 数据表中的数据字段与类型定义是可以“自由发挥”的, 这需要与当前的具体业务进行匹配
推广计划 -> 一类品牌或产品广告投放的规划, 自身并不定义太多关于广告自身的信息, 它会将信息打包下放到推广单元层级
推广单元 -> 一个确定的广告投放策略, 描述了投放广告的规则信息
推广单元维度限制 -> 广告投放会有一些限制条件, 例如只投放到北京、 上海地区, 对一些关键字进行投放等等 ( 这可能是
比较难理解的概念, 需要大家好好的思考)
广告创意 -> 展示给用户看到的数据, 可以是图片、 文本或者一段视频
广告投放系统的核心功能
投放系统是比较简单的模块, 其核心实现的功能就是对广告数据( 各个表) 进行增删改查, 即能够让用户( 广告主/代理商) 对数据进行查看、 上传、 修改与删除
网关模块(ad-gateway)的设计
package com.imooc.ad.filter; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; import com.netflix.zuul.exception.ZuulException; import lombok.extern.slf4j.Slf4j; import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; @Slf4j @Component public class AccessLogFilter extends ZuulFilter { @Override public String filterType() { return FilterConstants.POST_TYPE; } @Override public int filterOrder() { return FilterConstants.SEND_RESPONSE_FILTER_ORDER - 1; } @Override public boolean shouldFilter() { return true; } @Override public Object run() throws ZuulException { RequestContext context = RequestContext.getCurrentContext(); HttpServletRequest request = context.getRequest(); Long startTime = (Long) context.get("startTime"); String uri = request.getRequestURI(); long duration = System.currentTimeMillis() - startTime; log.info("uri: " + uri + ", duration: " + duration / 100 + "ms"); return null; } }
package com.imooc.ad.client; import com.imooc.ad.client.vo.AdPlan; import com.imooc.ad.client.vo.AdPlanGetRequest; import com.imooc.ad.vo.CommonResponse; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import java.util.List; @FeignClient(value = "eureka-client-ad-sponsor", fallback = SponsorClientHystrix.class) public interface SponsorClient { @RequestMapping(value = "/ad-sponsor/get/adPlan", method = RequestMethod.POST) CommonResponse<List<AdPlan>> getAdPlans( @RequestBody AdPlanGetRequest request); }
package com.imooc.ad; import org.springframework.boot.SpringApplication; import org.springframework.cloud.client.SpringCloudApplication; import org.springframework.cloud.netflix.zuul.EnableZuulProxy; @EnableZuulProxy @SpringCloudApplication public class ZuulGatewayApplication { public static void main(String[] args) { SpringApplication.run(ZuulGatewayApplication.class, args); } }
server: port: 9000 spring: application: name: ad-gateway eureka: client: service-url: defaultZone: http://server1:8000/eureka/ zuul: prefix: /imooc routes: sponsor: path: /ad-sponsor/** serviceId: eureka-client-ad-sponsor strip-prefix: false search: path: /ad-search/** serviceId: eureka-client-ad-search strip-prefix: false
数据库表的创建
-- imooc-ad 数据库 drop DATABASE imooc_ad_data; CREATE DATABASE imooc_ad_data character set utf8; use imooc_ad_data; -- 用户表 CREATE TABLE `ad_user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键', `username` varchar(128) NOT NULL DEFAULT '' COMMENT '用户名', `token` varchar(256) NOT NULL DEFAULT '' COMMENT '给用户生成的 token', `user_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '用户状态', `create_time` datetime NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT '创建时间', `update_time` datetime NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `username` (`username`) ) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8 COMMENT='用户信息表'; -- 推广计划表 CREATE TABLE `ad_plan` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键', `user_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '标记当前记录所属用户', `plan_name` varchar(48) NOT NULL COMMENT '推广计划名称', `plan_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '推广计划状态', `start_date` datetime NOT NULL COMMENT '推广计划开始时间;', `end_date` datetime NOT NULL COMMENT '推广计划结束时间;', `create_time` datetime NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT '创建时间', `update_time` datetime NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT '更新时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8 COMMENT='推广计划表'; -- 推广单元表 CREATE TABLE `ad_unit` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键', `plan_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '关联推广计划 id', `unit_name` varchar(48) NOT NULL COMMENT '推广单元名称', `unit_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '推广单元状态', `position_type` tinyint(4) NOT NULL DEFAULT '0' COMMENT '广告位类型(开屏, 贴片, 中贴, 暂停帖, 后贴)', `budget` bigint(20) NOT NULL COMMENT '预算', `create_time` datetime NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT '创建时间', `update_time` datetime NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT '更新时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8 COMMENT='推广单元表'; -- 创意表 CREATE TABLE `ad_creative` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键', `name` varchar(48) NOT NULL COMMENT '创意名称', `type` tinyint(4) NOT NULL DEFAULT '0' COMMENT '物料类型(图片, 视频)', `material_type` tinyint(4) NOT NULL DEFAULT '0' COMMENT '物料子类型(图片: bmp, jpg 等等)', `height` int(10) NOT NULL DEFAULT '0' COMMENT '高度', `width` int(10) NOT NULL DEFAULT '0' COMMENT '宽度', `size` bigint(20) NOT NULL DEFAULT '0' COMMENT '物料大小, 单位是 KB', `duration` int(10) NOT NULL DEFAULT '0' COMMENT '持续时长, 只有视频才不为 0', `audit_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '审核状态', `user_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '标记当前记录所属用户', `url` varchar(256) NOT NULL COMMENT '物料地址', `create_time` datetime NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT '创建时间', `update_time` datetime NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT '更新时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8 COMMENT='创意表'; -- 创意与推广单元的关联表 CREATE TABLE `creative_unit` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `creative_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '创意 id', `unit_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '推广单元 id', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8 COMMENT='创意和推广单元关联表'; -- 推广单元关键词 Feature CREATE TABLE `ad_unit_keyword` ( `id` int(11) NOT NULL AUTO_INCREMENT, `unit_id` int(11) NOT NULL COMMENT '推广单元 id', `keyword` varchar(30) NOT NULL COMMENT '关键词', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8 COMMENT='推广单元关键词 Feature'; -- 推广单元兴趣 Feature CREATE TABLE `ad_unit_it` ( `id` int(11) NOT NULL AUTO_INCREMENT, `unit_id` int(11) NOT NULL COMMENT '推广单元 id', `it_tag` varchar(30) NOT NULL COMMENT '兴趣标签', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8 COMMENT='推广单元兴趣 Feature'; -- 推广单元地域 Feature CREATE TABLE `ad_unit_district` ( `id` int(11) NOT NULL AUTO_INCREMENT, `unit_id` int(11) NOT NULL COMMENT '推广单元 id', `province` varchar(30) NOT NULL COMMENT '省', `city` varchar(30) NOT NULL COMMENT '市', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8 COMMENT='推广单元地域 Feature';