基于SpringCloud的广告系统设计与实现(三)

发布于:2025-02-10 ⋅ 阅读:(25) ⋅ 点赞:(0)

一、广告系统业务需求

广告投放数据的核心要素

  • 用户账户 -> 最高层级,用于定义广告主或代理商,只有有了用户才会有接下来的数据投放

  • 推广计划 -> 一类品牌或产品广告投放的规划,自身并不定义太多关于广告自身的信息,它会将信息打包下放到推广单元层级

  • 推广单元 -> 一个确定的广告投放策略,描述了投放广告的规则信息

  • 推广单元维度限制 -> 广告投放会有一些限制条件,例如只投放到北京、上海地区,对一些关键字进行投放等等 (这可能是比较难理解的概念,需要大家好好的思考)

  • 广告创意 -> 展示给用户看到的数据,可以是图片、文本或者一段视频

二、业务系统开发注意

1.@SpringBootApplication 与@SpringCloudApplication

@SpringBootApplication 、@SpringCloudApplication,这两个注解不是一个功能,他们都是组合注解,但是,你可以看到它们名字的区别。一个是 Boot,一个是 Cloud。

    注解 @SpringCloudApplication 包括:@SpringBootApplication、@EnableDiscoveryClient、@EnableCircuitBreaker,分别是 SpringBoot 注解、注册服务中心 Eureka 注解、断路器注解。对于SpringCloud 来说,这是每一微服务必须应有的三个注解,所以才推出了 @SpringCloudApplication 这一注解集合。    

    所以,当你的工程只是一个 SpringBoot 工程,那么,只需要加上 @SpringBootApplication 注解就可以了;如果你的工程需要服务发现,那么,无疑,需要添加 @SpringCloudApplication 注解。


2.主类上的注解:

    @EnableFeignClients:让当前的工程开启 Feign 的功能;

    @EnableCircuitBreaker:让当前的工程允许熔断;

    @EnableEurekaClient:让当前的工程作为 Eureka Client(相对于 Eureka Server 而言)


3.MyBatis 与 Spring Data Jpa 对比:

    1. MyBatis 的优势在于 SQL 的自由度上,SQL优化和返回对象的大小都是可控的;

    2. Spring Data Jpa 在开发效率上会更高,不需要编写大量的 SQL

    其实,在实际的企业级开发中,MyBatis 和 Spring Data Jpa 的选择上并没有太多考虑,可能是项目自身的约束,也可能是个人的开发习惯。对于当前的项目来说,完全可以将 Jpa 的方式自行替换成 MyBatis。

 JPA 我们在使用的时候编写的是接口,在实现上,JPA 会通过动态代理给我们生成对应的查询类,所以,比你直接使用 SQL 型的查询肯定要效率低一些。不过,这大大方便了我们的编码过程,性能方面,除非你有很高并发的需求,否则,忽略不计。

4.业务系统在网关中的配置

业务系统application.yml

server:
  port: 7000
  servlet:
    context-path: /ad-sponsor

网关配置application.yml:

zuul:
  prefix: /imooc
  routes:
    sponsor:
      path: /ad-sponsor/**
      serviceId: eureka-client-ad-sponsor
      strip-prefix: false

这段代码是基于Spring Cloud Zuul的配置,用于定义Zuul网关的路由规则。以下是对这段配置的详细解析:

配置解析

1. zuul.prefix: /imooc
  • 作用:为所有通过Zuul网关的请求添加一个统一的前缀/imooc

  • 示例:如果客户端请求http://<gateway-host>/imooc/ad-sponsor/...,则该请求会被Zuul网关处理。

2. routes.sponsor
  • 定义了一个路由规则,名称为sponsor

3. path: /ad-sponsor/**
  • 作用:定义了该路由规则匹配的路径模式。

  • 含义:所有以/ad-sponsor/开头的请求路径都会被匹配到这个路由规则。

  • 示例:请求路径/imooc/ad-sponsor/api/v1/some-endpoint会被匹配到这个路由。

4. serviceId: eureka-client-ad-sponsor
  • 作用:指定当请求匹配到/ad-sponsor/**时,Zuul网关会将请求转发到Eureka服务注册中心中名为eureka-client-ad-sponsor的服务。

  • 含义eureka-client-ad-sponsor是服务提供者在Eureka服务注册中心注册时的服务ID。

  • 示例:如果eureka-client-ad-sponsor服务的实际地址是http://localhost:8081,则请求http://<gateway-host>/imooc/ad-sponsor/api/v1/some-endpoint会被转发到http://localhost:8081/api/v1/some-endpoint

5. strip-prefix: false
  • 作用:控制是否在转发请求时去掉匹配的前缀。

  • 含义false表示不会去掉匹配的前缀/ad-sponsor/

  • 示例:请求http://<gateway-host>/imooc/ad-sponsor/api/v1/some-endpoint会被完整转发到http://localhost:8081/ad-sponsor/api/v1/some-endpoint

总结

这段配置的作用是:

  1. 定义了一个路由规则sponsor,匹配所有以/imooc/ad-sponsor/开头的请求。

  2. 将匹配到的请求转发到Eureka服务注册中心中名为eureka-client-ad-sponsor的服务。

  3. 在转发请求时,保留原始请求的路径(不去掉/ad-sponsor/前缀)。

注意事项

  1. 服务注册:确保eureka-client-ad-sponsor服务已经正确注册到Eureka服务注册中心。

  2. 路径匹配:客户端请求的路径必须以/imooc/ad-sponsor/开头,否则不会匹配到该路由规则。

  3. 转发路径:由于strip-prefix: false,服务提供者需要能够处理完整的路径(包括/ad-sponsor/前缀)。

5.数据库表主键 

目前的表设计主键大多都是 int 或者 bigint,极少数才会使用 string 类型的。整数类型占用空间少,且索引速度快。而且对于绝大多数场景来说,int 类型有 20 亿的上限,基本上都是够用的。

三、MySQL

1.慢查询

如何处理慢查询

一般数据表查询,超过一分钟的都是不可以接受的了。可以考虑从两方面去解决这个问题:

    1. 根据查询条件建立对应的索引

    2. 将数据构造成索引放在 redis 这样的缓存里面

常见的处理方法:

1.添加索引,需要经常查询的字段都要添加,主键本身就是索引,按照主键查询记录效率是最高的;如果有多个字段都需要有索引,且是同时查询(即 where 条件有多个),一定要使用联合索引。
2. 避免使用 *,指定具体字段。如果表中包含的列特别多(例如超过20个),或者其中的几列存储的数据过大(例如 mediumtext 类型)。那么,只查询需要的列,不要使用 select *。
3. 用exists替代in,当然也可以用not exists替代not in,exists 会比 in 的查询效率高,数据量大的时候会有明显的速度提升。
4. 避免使用like,能不用模糊查询就不用,使用 like 时,很多情况下不能使用到索引。所以,效率会降低很多,不要把耗时的操作放在数据库里做,即能不用模糊查询就不用。

2.索引

创建索引的目的就是为了加快查询的速度,如果没有索引,MySQL 在查询时,只能从第一条记录开始然后读完整个表找到匹配的行。MySQL 支持多种存储引擎,不同的引擎对索引的支持也不相同。我这里只会介绍 B树 索引,对应 InnoDB 存储引擎。

索引类型及操作

索引类型
  • 普通索引

这是最基本的索引类型,支持单列和多列。可以通过以下的几种方式创建:

CREATE INDEX 索引名 ON 表名(列名1,列名2,...);                  -- 创建索引
ALTER TABLE 表名 ADD INDEX 索引名 (列名1,列名2,...);            -- 修改表
CREATE TABLE 表名 ( [...], INDEX 索引名 (列名1,列名 2,...) );  -- 创建表时指定索引
  • 唯一索引

表示唯一的,不允许重复的索引,支持单列和多列。 注意,如果是多列共同构成唯一索引,代表的是多列的数据组合是唯一的。可以通过以下的几种方式创建:

CREATE UNIQUE INDEX 索引名 ON 表名(列名1,列名2,...);           -- 创建索引
ALTER TABLE 表名 ADD UNIQUE 索引名 (列名1,列名2,...);           -- 修改表
CREATE TABLE 表名( [...], UNIQUE 索引名 (列名1,列名2,...) );   -- 创建表时指定索引
  • 主键索引

主键是特殊的唯一索引,同样支持单列和多列,但是必须被指定为 PRIMARY KEY。注意,每个表中只能有一个主键。可以通过以下的几种方式创建:

CREATE TABLE 表名( [...], PRIMARY KEY (列名1,列名2,...) );    -- 创建表的时候指定
ALTER TABLE 表名 ADD PRIMARY KEY (列名1,列名2,...);            -- 修改表
索引操作
  • 删除索引
-- 删除 talbe_name 中的索引 index_name
DROP INDEX index_name ON talbe_name
ALTER TABLE table_name DROP INDEX index_name
-- 删除主键索引,因为一个表只可能有一个PRIMARY KEY索引,因此不需要指定索引名
ALTER TABLE table_name DROP PRIMARY KEY
  • 查看索引
show index from table_name;
show keys from table_name;

-- 核心字段的解释
-- Table:表的名称
-- Non_unique:如果索引不能包括重复词,则为0。如果可以,则为1
-- Key_name:索引的名称
-- Seq_in_index:索引中的列序列号,从1开始
-- Column_name:列名称
-- Collation:列以什么方式存储在索引中。在MySQL中,‘A’(升序)或 NULL(无分类)。
-- Cardinality:索引中唯一值的数目的估计值
-- Sub_part:如果列只是被部分地编入索引,则为被编入索引的字符的数目。如果整列被编入索引,则为NULL
-- Packed:指示关键字如何被压缩。如果没有被压缩,则为NULL
-- Null:如果列含有NULL,则含有YES。如果没有,则该列含有NO
-- Index_type:索引类型(BTREE, FULLTEXT, HASH, RTREE)。

索引实现的原理

索引的最核心思想是通过不断的缩小数据的范围来筛选出最终想要的结果,同时把随机事件变成顺序事件(二分查找的核心思想)

InnoDB 存储引擎使用 B+ 树来构造索引,之所以使用 B+ 树构造索引,是因为数据和索引都保存在磁盘中,为了提高性能,每次会把部分数据读入内存来计算。所以,每次查找数据时把磁盘 IO 次数控制在一个很小的数量级是最优的,最好是常数数量级。那么我们就想到如果一个高度可控的多路搜索树是否能满足需求呢?就这样,B+树应运而生。

B 树和 B+ 树的特性总结
B 树

B树是一种多路平衡查找树,B是平衡的意思,即Balance,m阶(m >= 2)的B树有以下特性

  • 树中的每个节点最多有m个子节点

  • 除了根节点和叶子节点之外,其他每个节点至少有 m/2 个子节点

  • 所有的叶子节点都在同一层

  • 节点中关键字的顺序按照升序排列(单节点内部按照升序排列。结点之间,左节点<根节点<右节点

 每个节点可以存放多个数据,每个节点的数据实体处理存放数据外有左右指针指向自己的子节点,也就是说当前节点存放n个数据的话,它最多能有n-1个指针指向自己的子节点。

结构图如下所示

B+ 树

B+树是B树的一种变体,同样是多路平衡查找树,它与B树主要的不同是

  • 非叶子节点不存储数据,只存储索引

  • 叶子节点包含了全部的关键字信息,且叶子节点按照关键字顺序相互连接

结构图如下所示

 

问题与思考(还没完全懂,很多大厂面试都会涉及这个)
  • 你能根据表的索引与数据画出 B+ 树的存储结构吗 ?

  • 根据你画出的存储结构,描述下查找某个数据的匹配过程 ?

索引使用的原则

关于索引的使用原则,美团点评技术团队的文章 《MySQL索引原理及慢查询优化》里总结的很好,如下:

1. 最左前缀匹配原则,非常重要的原则,mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整;

2. =和in可以乱序,比如a = 1 and b = 2 and c = 3 建立(a,b,c)索引可以任意顺序,mysql的查询优化器会帮你优化成索引可以识别的形式;

3. 尽量选择区分度高的列作为索引,区分度的公式是count(distinct col)/count(*),表示字段不重复的比例,比例越大我们扫描的记录数越少,唯一键的区分度是1,而一些状态、性别字段可能在大数据面前区分度就是0,那可能有人会问,这个比例有什么经验值吗?使用场景不同,这个值也很难确定,一般需要join的字段我们都要求是0.1以上,即平均1条扫描10条记录;

4. 索引列不能参与计算,保持列“干净”,比如from_unixtime(create_time) = ’2014-05-29’就不能使用到索引,原因很简单,b+树中存的都是数据表中的字段值,但进行检索时,需要把所有元素都应用函数才能比较,显然成本太大。所以语句应该写成create_time = unix_timestamp(’2014-05-29’);

5. 尽量的扩展索引,不要新建索引。比如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可。

like与in索引情况

使用到索引的前提条件是 where 条件的这一列本身是有索引的。对于 like 和 in 的使用上:

    like:MySQL 在使用 like 查询的时候只有不以 % 开头的时候,才会使用到索引

    in:当你 in 中的数据个数 “过多”(比如上万个数据)时,MySQL 将不会使用到索引。可以参考知乎上的一个帖子:https://www.zhihu.com/question/26010353

    另外,在考虑自己的 SQL 语句是否会用到索引时,可以使用 explain 关键字校验下。

 limit offset如何优化

比如:select * from table limit 1700000,10

 limit offset 的实现方法是按顺序查找到对应的位置,并把 limit 之前的数据全部丢弃掉。最终,扫描的数据量是不会变化的。参考这篇:MYSQL分页limit速度太慢优化方法 | OurMySQL

limit10000,20的意思扫描满足条件的10020行,扔掉前面的10000行,返回最后的20行,问题就在这里。

如果我们之前记录了最大ID呢?

举个例子

   日常分页SQL语句

   select id,name,content from users order by id asc limit 100000,20

   扫描100020行

   如果记录了上次的最大ID

select id,name,content from users where id>100073 order by id asc limit 20

   扫描20行。

   总数据有500万左右

Group by

  • Group By 关键字由于涉及到数据的排序,对于数据量特别大的情况,还需要进行外排序,排序的过程会降低 SQL 语句的执行效率。所以,尽量对小数据量进行 Group By 操作;

    group by的常用优化方案

        效率低的根本原因就是因为数据量大导致的,所以,想要做优化,核心思想就是降低数据量。可以在 group by 之前把需要过滤的数据通过 where 条件给出;给 group by 的相关字段加上索引等等。

  • 索引能够加速数据的查询过程,所以,对于经常有 where 条件的字段,可以考虑建立索引
  • 没有实际查询需求的话 主键查询够用了,建立索引本身就占用资源的,能不建就不建,数据量大的话临时加索引还可能锁表影响向线上业务
  • 尽量是数据类型占用空间小的字段建立索引,建索引本身就是占用资源的,当还要结合实际查询需求综合考虑,经常需要查询的字段建索引。尽量考虑字段类型使用的空间,对于像 text 这样的字段,就不应该建立索引。

数据库常用的索引规范

    1、表的主键、外键必须有索引;

    2、数据量超过300的表应该有索引;

    3、经常与其他表进行连接的表,在连接字段上应该建立索引;

    4、经常出现在Where子句中的字段,特别是大表的字段,应该建立索引;

    5、索引应该建在选择性高的字段上;

    6、索引应该建在小字段上,对于大的文本字段甚至超长字段,不要建索引;

    7、复合索引的建立需要进行仔细分析;尽量考虑用单字段索引代替;

    8、频繁进行数据操作的表,不要建立太多的索引;

    9、删除无用的索引,避免对执行计划造成负面影响。

 3.MySQL 事务

事务隔离级别

什么是事务隔离级别

SQL标准定义了四种隔离级别,包括了一些具体规则,用来限定事务内外的哪些改变是可见的,哪些是不可见的。低级别的隔离级一般支持更高的并发处理,并拥有更低的系统开销。

四种隔离级别的说明
隔离级别 特性 锁机制
Read Uncommitted(未提交读)

在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为脏读(Dirty Read)。

案例:

老板要给程序员发工资,程序员的工资是3.6万/月。但是发工资时老板不小心按错了数字,按成3.9万/月,该钱已经打到程序员的户口,但是事务还没有提交,就在这时,程序员去查看自己这个月的工资,发现比往常多了3千元,以为涨工资了非常高兴。但是老板及时发现了不对,马上回滚差点就提交了的事务,将数字改成3.6万再提交。

分析:实际程序员这个月的工资还是3.6万,但是程序员看到的是3.9万。他看到的是老板还没提交事务时的数据。这就是脏读

无行锁,无共享锁S锁或排他锁X锁
Read Committed(提交读)

这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这种隔离级别也支持所谓的不可重复读(Nonrepeatable Read),因为同一事务的其他实例在该实例处理其间可能会有新的commit,所以同一select可能返回不同结果。

事例:

程序员拿着信用卡去享受生活(卡里当然是只有3.6万),当他买单时(程序员事务开启),收费系统事先检测到他的卡里有3.6万,就在这个时候!!程序员的妻子要把钱全部转出充当家用,并提交。当收费系统准备扣款时,再检测卡里的金额,发现已经没钱了(第二次检测金额当然要等待妻子转出金额事务提交完)。程序员就会很郁闷,明明卡里是有钱的…

分析:这就是读提交,若有事务对数据进行更新(UPDATE)操作时,读操作事务要等待这个更新操作事务提交后才能读取数据,可以解决脏读问题。但在这个事例中,出现了一个事务范围内两个相同的查询却返回了不同数据,这就是不可重复读。

行锁(S锁),锁持续时间短,不使用间隙锁
Repeatable Read(可重复读)

这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另一个棘手的问题:幻读 (Phantom Read)。简单的说,幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行。

事例:

程序员拿着信用卡去享受生活(卡里当然是只有3.6万),当他埋单时(事务开启,不允许其他事务的UPDATE修改操作),收费系统事先检测到他的卡里有3.6万。这个时候他的妻子不能转出金额了。接下来收费系统就可以扣款了。

分析:重复读可以解决不可重复读问题。写到这里,应该明白的一点就是,不可重复读对应的是修改,即UPDATE操作。但是可能还会有幻读问题。因为幻读问题对应的是插入INSERT操作,而不是UPDATE操作。

什么时候会出现幻读?

事例:程序员某一天去消费,花了2千元,然后他的妻子去查看他今天的消费记录(全表扫描FTS,妻子事务开启),看到确实是花了2千元,就在这个时候,程序员花了1万买了一部电脑,即新增INSERT了一条消费记录,并提交。当妻子打印程序员的消费记录清单时(妻子事务提交),发现花了1.2万元,似乎出现了幻觉,这就是幻读。

那怎么解决幻读问题?Serializable

Serializable 序列化

Serializable 是最高的事务隔离级别,在该级别下,事务串行化顺序执行,可以避免脏读、不可重复读与幻读。但是这种事务隔离级别效率低下,比较耗数据库性能,一般不使用。

行锁(S锁或X锁)、间隙锁(Gap Lock)、Next-Key Lock
Serializable(串行读) 这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。 行锁(S锁和X锁)、范围锁(Range Lock),可能使用表锁,锁持续到事务结束

四个级别逐渐增强,每个级别解决一个问题。事务级别越高,性能越差,大多数场景 read committed 可以满足需求


  • S锁(共享锁):允许多个事务并发读取数据,但不允许写操作。

  • X锁(排他锁):独占数据,允许写操作,但不允许其他事务读取或写入。


隔离级别与一致性

四种隔离级别采取不同的锁类型来实现,若读取的是同一个数据的话,就容易发生问题 :

  • 脏读(Drity Read): 某个事务已更新一份数据,另一个事务在此时读取了同一份数据,由于某些原因,前一个RollBack了操作,则后一个事务所读取的数据就会是不正确的。

  • 不可重复读(Non-repeatable read): 在一个事务的两次查询之中数据不一致,这可能是两次查询过程中间插入了一个事务更新的原有的数据。

  • 幻读(Phantom Read): 在一个事务的两次查询中数据笔数不一致,例如有一个事务查询了几列(Row)数据,而另一个事务却在此时插入了新的几列数据,先前的事务在接下来的查询中,就会发现有几列数据是它先前所没有的。

对应于 MySQL 的四种隔离级别,有可能会产生的问题如下 :

隔离级别 脏读 不可重复读 幻读
Read Uncommitted yes yes yes
Read Committed no yes yes
Repeatable Read no no yes
Serializable no no no
隔离级别的设置

注意:不同的 MySQL 版本,事务隔离级别对应的变量名也是不同的。

-- 查看事务隔离级别 (默认的是 REPEATABLE READ)
mysql> select @@global.transaction_isolation;
+--------------------------------+
| @@global.transaction_isolation |
+--------------------------------+
| REPEATABLE-READ                |
+--------------------------------+

mysql> show variables like '%iso%';
+-----------------------+-----------------+
| Variable_name         | Value           |
+-----------------------+-----------------+
| transaction_isolation | REPEATABLE-READ |
+-----------------------+-----------------+

-- 设置事务隔离级别
SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;

-- 更常见的设置事务隔离级别的方法是修改 my.cnf
transaction_isolation = REPEATABLE-READ

QUESTION:企业级开发中通常会把事务隔离级别设置为 Read Committed,你能说出这样做的理由吗 ?(可以对比其他隔离级别的劣势去解释)

默认的:

MySQL 的默认隔离级别是“可重复读”Repeatable Read,但是,我们在工作中几乎都是在使用“读已提交Read Committed这个隔离级别,那么,其中的原因有以下三点:

    (1)在RR隔离级别下,存在间隙锁,导致出现死锁的几率比RC大的多

    (2)在RR隔离级别下,条件列未命中索引会锁表,而在RC隔离级别下,只锁行

    (3)在RC隔离级别下,半一致性读 (semi-consistent) 特性增加了 update 操作的并发性

是考虑到安全和效率上的平衡。另外的三类隔离级别都是有比较大的缺陷,综合来看,提交读是最合理,也是最合适的。

      性能与一致性权衡:选择隔离级别时需要权衡数据一致性和并发性能。如果对数据一致性要求极高,可以选择Serializable;如果对性能要求更高,可以选择Read CommittedRead Uncommitted


网站公告

今日签到

点亮在社区的每一天
去签到