1、微服务架构下为什么会产生数据不一致的问题?
在微服务架构下,多个服务之间通常会定义明确上下游关系,下游系统可以依赖上游系统,下游系统可以通过API查询或修改上游系统的数据;反过来则不然,上游系统不应该知道下游系统的存在,也就是说上游系统不能依赖下游系统,上游系统的变化只能通过异步事件的方式发出,下游系统监听事件并基于事件做对应的数据状态变化。
在基于上面原则的微服务架构下(见上面图示,本文不考虑服务间循环依赖的场景),在上下游服务间的数据通信(图示中的每个箭头表示一种数据通信)一旦发生问题,都会产生数据不一致的场景,下面我们逐一说明:
场景一:下游服务数据状态变化时同步调用上游服务接口失败
举个例子,订单服务是下游服务,库存服务是上游服务,在订单确认时要锁定库存,实现上订单服务在状态变化同时通过同步API修改库存的状态,为了保证数据一致性,在调用库存服务API异常后订单服务会回滚当前的数据状态变更。
在这个场景下,同一个业务流程,需要同时修改两个服务的数据,在以下两种情况下会发生数据不一致的问题:
-
库存服务API调用成功,库存状态变更,但订单状态变更提交到数据库时失败,结果是库存被锁定,但订单没有确认。
-
库存服务API调用失败,但实际上库存服务的数据变更已成功,失败原因是响应消息返回订单服务过程中网络异常,订单服务回滚数据变更,结果同样是库存被锁定但没有订单确认。
场景二:上游服务在状态变化时没有发出事件
上游服务每个关键状态变更都可能触发下游服务的一些逻辑链,因此上游服务发布的事件对于下游服务是非常重要的,但这些事件并不影响上游服务自身逻辑,也不影响自身数据状态的变化,因此通常不会设计成阻碍业务流程,那么在事件服务或事件载体(通常是消息队列)与上游服务之间的通信异常,就会导致上游服务的事件发布失败。
这种场景下,上游服务的业务流程已经成功,不可能有再次触发事件的场景,这个事件就丢失了,下游服务因为没有收到上游服务的事件,数据没有做对应的变化而导致数据不一致。
场景三:下游服务没有办法正常消费上游服务的事件
同样,下游服务在消费事件时也很有可能因为一些原因,导致事件的消费失败,这些原因可能包括:
-
上游服务发布事件的内容格式发生变化
-
上游服务发布事件的格式没变,但某些字段的可选值空间变化了,比如一些枚举值的扩充
-
下游服务内部逻辑异常(数据库、跨服务调用等)
上游服务并不关心下游的消费者,所以对于发布出去的事件,上游系统也不关心下游服务是否消费成功,更不会有因某个下游服务消费失败而重发事件的逻辑,这同样会导致类似于场景二的数据不一致。
2、如何解决微服务架构下数据不一致的问题?
根据CAP理论,分区容错性、可用性和一致性里面必须要牺牲掉一个,而在实际实现过程中,分区容错性和可用性是很难舍弃的,所以通常会舍弃一致性,取而代之会用最终一致性保证数据在可容忍的时长内达到最终一致。
微服务架构也不例外,在服务内部,可以通过本地事务保证数据的强一致性;而当业务发生在多个服务中,我们追求最终一致性。那么怎么解决上面提到的问题,做到跨服务的最终一致性呢?
避免同时跨服务的写操作
这是个业务问题,在微服务的架构下,每个服务都是独立的,如果有一个业务功能需要同时修改两个服务的数据,往往这个业务可以拆分成两个步骤,比如场景一种提到的订单和库存的例子,如果我们可以先锁定库存,然后再确认订单看上去这个问题就迎刃而解了。
因此在业务中发现一个功能需要同时修改两个服务的数据,我们首先可以来讨论这个业务设计是否合理;如果业务上很多场景都要求两个服务的数据保持强一致,那可能我们需要看看微服务的划分是否合理。
最大努力通知 + 最大努力处理
为了解决场景二和场景三的不一致性问题,需要上游服务和下游服务的共同努力:
上游服务需要尽可能将事件发送出去,比如:先同步发送,如果失败改为异步重试,重试多次仍然失败可以先持久化,通过定时任务来重发或者人工干预重发。
下游服务也要尽可能的把事件处理掉,收到事件后可以考虑先将事件持久化,消费成功后标记事件,如果消费失败可以通过定时任务重试消费。
保证幂等性
当我们提到重试,就不得不考虑幂等性的问题,这里的幂等性包括以下两个场景:
-
上游服务接口的幂等性,保证下游系统的重试逻辑可以得到正确响应
-
下游服务消费事件保证幂等性,避免因上游多发事件或事件已消费成功后再次重试产生的问题
核心业务数据补偿机制
在分布式系统的执行链路上,每个节点都有可能失败,加上业务的复杂度,即便我们做了很多我们认为万全的准备,数据不一致的情景也很难彻底解决,而对于那些小概率发生但技术解决起来成本昂贵的问题,我们可以尝试通过对业务的深刻理解设计一些后台的维护功能,保证在核心业务数据异常时,可以在一定的规则内进行修复,从而保证业务的顺利进行。
3、设计过接口API吗?你是怎么做的?
这里给大家列举一个规范,可以进行参考。
-
URI 命名是否通过聚合根和实体统一
-
URI 命名是否采用名词复数和连接线
-
URI 命名是否都是单词小写
-
URI 是否暴露了不必要的信息,例如
/cgi-bin
-
URI 规则是否统一
-
资源提供的能力是否彼此独立
-
URI 是否存在需要编码的字符
-
请求和返回的参数是否不多不少
-
资源的 ID 参数是否通过 PATH 参数传递
-
认证和授权信息是否暴露到 query 参数中
-
参数是否使用奇怪的缩写
-
参数和响应数据中的字段命名统一
-
是否存在无意义的对象包装 例如
{"data":{}'}
-
出错时是否破坏约定的数据结构
-
是否使用合适的状态码
-
是否使用合适的媒体类型
-
响应数据的单复是否和数据内容一致
-
响应头中是否有缓存信息
-
是否进行了版本管理
-
版本信息是否作为 URI 的前缀存在
-
是否提供 API 服务期限
-
是否提供了 API 返回所有 API 的索引
-
是否进行了认证和授权
-
是否采用 HTTPS
-
是否检查了非法参数
-
是否增加安全性的头部
-
是否有限流策略
-
是否支持 CORS
-
响应中的时间格式是否采用
ISO 8601
标准 -
是否存在越权访问
4、数据库三范式你是怎么理解的?
第一范式就是属性不可分割,每个字段都应该是不可再拆分的。比如一个字段是姓名(NAME),在国内的话通常理解都是姓名是一个不可再拆分的单位,这时候就符合第一范式;但是在国外的话还要分为FIRST NAME和LAST NAME,这时候姓名这个字段就是还可以拆分为更小的单位的字段,就不符合第一范式了。
第二范式就是要求表中要有主键,表中其他其他字段都依赖于主键,因此第二范式只要记住主键约束就好了。比如说有一个表是学生表,学生表中有一个值唯一的字段学号,那么学生表中的其他所有字段都可以根据这个学号字段去获取,依赖主键的意思也就是相关的意思,因为学号的值是唯一的,因此就不会造成存储的信息对不上的问题,即学生001的姓名不会存到学生002那里去。
第三范式就是要求表中不能有其他表中存在的、存储相同信息的字段,通常实现是在通过外键去建立关联,因此第三范式只要记住外键约束就好了。比如说有一个表是学生表,学生表中有学号,姓名等字段,那如果要把他的系编号,系主任,系主任也存到这个学生表中,那就会造成数据大量的冗余,一是这些信息在系信息表中已存在,二是系中有1000个学生的话这些信息就要存1000遍。因此第三范式的做法是在学生表中增加一个系编号的字段(外键),与系信息表做关联。
5、SQL语句优化怎么做的?
原文参考地址:https://www.cnblogs.com/Little-Li/p/8031295.html
1.’对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。
2.应尽量避免在 where 子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描。
3.应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描,如:
select id from t where num is null
可以在num上设置默认值0,确保表中num列没有null值,然后这样查询:
select id from t where num=0
4.应尽量避免在 where 子句中使用 or 来连接条件,否则将导致引擎放弃使用索引而进行全表扫描,如:
select id from t where num=10 or num=20
可以这样查询:
select id from t where num=10
union all
select id from t where num=20
5.下面的查询也将导致全表扫描:
select id from t where name like '%abc%'
若要提高效率,可以考虑全文检索。
6.in 和 not in 也要慎用,否则会导致全表扫描,如:
select id from t where num in(1,2,3)
对于连续的数值,能用 between 就不要用 in 了:
select id from t where num between 1 and 3
7.如果在 where 子句中使用参数,也会导致全表扫描。因为SQL只有在运行时才会解析局部变量,但优化程序不能将访问计划的选择推迟到运行时;它必须在编译时进行选择。然而,如果在编译时建立访问计划,变量的值还是未知的,因而无法作为索引选择的输入项。如下面语句将进行全表扫描:
select id from t where num=@num
可以改为强制查询使用索引:
select id from t with(index(索引名)) where num=@num
8.应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描。如:
select id from t where num/2=100
应改为:
select id from t where num=100*2
9.应尽量避免在where子句中对字段进行函数操作,这将导致引擎放弃使用索引而进行全表扫描。如:
select id from t where substring(name,1,3)='abc'--name以abc开头的id
select id from t where datediff(day,createdate,'2005-11-30')=0--'2005-11-30'生成的id
应改为:
select id from t where name like 'abc%'
select id from t where createdate>='2005-11-30' and createdate<'2005-12-1'
10.不要在 where 子句中的“=”左边进行函数、算术运算或其他表达式运算,否则系统将可能无法正确使用索引。
11.在使用索引字段作为条件时,如果该索引是复合索引,那么必须使用到该索引中的第一个字段作为条件时才能保证系统使用该索引,否则该索引将不会被使用,并且应尽可能的让字段顺序与索引顺序相一致。
12.不要写一些没有意义的查询,如需要生成一个空表结构:
select col1,col2 into #t from t where 1=0
这类代码不会返回任何结果集,但是会消耗系统资源的,应改成这样:
create table #t(...)
13.很多时候用 exists 代替 in 是一个好的选择:
select num from a where num in(select num from b)
用下面的语句替换:
select num from a where exists(select 1 from b where num=a.num)
14.并不是所有索引对查询都有效,SQL是根据表中数据来进行查询优化的,当索引列有大量数据重复时,SQL查询可能不会去利用索引,如一表中有字段sex,male、female几乎各一半,那么即使在sex上建了索引也对查询效率起不了作用。
15.索引并不是越多越好,索引固然可以提高相应的 select 的效率,但同时也降低了 insert 及 update 的效率,因为 insert 或 update 时有可能会重建索引,所以怎样建索引需要慎重考虑,视具体情况而定。一个表的索引数最好不要超过6个,若太多则应考虑一些不常使用到的列上建的索引是否有必要。
16.应尽可能的避免更新 clustered 索引数据列,因为 clustered 索引数据列的顺序就是表记录的物理存储顺序,一旦该列值改变将导致整个表记录的顺序的调整,会耗费相当大的资源。若应用系统需要频繁更新 clustered 索引数据列,那么需要考虑是否应将该索引建为 clustered 索引。
17.尽量使用数字型字段,若只含数值信息的字段尽量不要设计为字符型,这会降低查询和连接的性能,并会增加存储开销。这是因为引擎在处理查询和连接时会逐个比较字符串中每一个字符,而对于数字型而言只需要比较一次就够了。
18.尽可能的使用 varchar/nvarchar 代替 char/nchar ,因为首先变长字段存储空间小,可以节省存储空间,其次对于查询来说,在一个相对较小的字段内搜索效率显然要高些。
19.任何地方都不要使用 select * from t ,用具体的字段列表代替“*”,不要返回用不到的任何字段。
20.尽量使用表变量来代替临时表。如果表变量包含大量数据,请注意索引非常有限(只有主键索引)。
21.避免频繁创建和删除临时表,以减少系统表资源的消耗。
22.临时表并不是不可使用,适当地使用它们可以使某些例程更有效,例如,当需要重复引用大型表或常用表中的某个数据集时。但是,对于一次性事件,最好使用导出表。
23.在新建临时表时,如果一次性插入数据量很大,那么可以使用 select into 代替 create table,避免造成大量 log ,以提高速度;如果数据量不大,为了缓和系统表的资源,应先create table,然后insert。
24.如果使用到了临时表,在存储过程的最后务必将所有的临时表显式删除,先 truncate table ,然后 drop table ,这样可以避免系统表的较长时间锁定。
25.尽量避免使用游标,因为游标的效率较差,如果游标操作的数据超过1万行,那么就应该考虑改写。
26.使用基于游标的方法或临时表方法之前,应先寻找基于集的解决方案来解决问题,基于集的方法通常更有效。
27.与临时表一样,游标并不是不可使用。对小型数据集使用 FAST_FORWARD 游标通常要优于其他逐行处理方法,尤其是在必须引用几个表才能获得所需的数据时。在结果集中包括“合计”的例程通常要比使用游标执行的速度快。如果开发时间允许,基于游标的方法和基于集的方法都可以尝试一下,看哪一种方法的效果更好。
28.在所有的存储过程和触发器的开始处设置 SET NOCOUNT ON ,在结束时设置 SET NOCOUNT OFF 。无需在执行存储过程和触发器的每个语句后向客户端发送 DONE_IN_PROC 消息。
29.尽量避免向客户端返回大数据量,若数据量过大,应该考虑相应需求是否合理。
30.尽量避免大事务操作,提高系统并发能力。
其他面试问题参考:
简单介绍项目,怎么做的,为什么要做这个,用到了什么技术;
常见的协议有哪些?
TCP,UDP区别;
TCP三次握手;
TCP可靠传输;
http,https区别;
http数据格式;
post,get区别;
长连接短连接区别,使用;
redis单线程为什么这么快;
乐观锁、悲观锁、举例讲讲,CAS;
死锁举例说明;
CAS怎么实现的(底层源码);
异步同步、阻塞非阻塞,他们之间有联系吗,说说区别,再从内核的角度说说看;
synchronized,放在各个地方锁的是啥,一个synchronized静态方法,一个普通的synchronized方法,访问静态方法后还能访问普通方法吗,锁的对象是一样的吗;
synchronized的底层实现,知道多少说多少,和ReentrantLock的区别;
cookie,session区别,session保存 ;
数据结构:数组,链,队列,栈,二叉树,AVL树(树高与节点数关系),红黑树;
一道场景题设计数据结构;
数据库:索引有哪些类型,分别应用场景;
b+和红黑树用来做索引区别,b+和b树区别;
场景题:根据需求设计数据库表,写sql语句;
java基础:java语言特性(封装继承多态);
举例子说明以上三种特性;
接口和抽象类区别。