架构杂记(1)

  1. 数据库设计的时候,有些时候可考虑横向分表,把不常用,占用空间多,并且这张表的数据可能超过千万级别,这种情况吧一个表分成基础表和拓展表,可以明显的提高数据检索效率,Btree 的结构决定每行的数据越少,每页能放的行数越多,数据结构就越矮胖,查询效率就越高

  2. 纵向分表也是一种常见的策略,用类似的也可以分库

    • 比如通过Id的访问拆分

    • 比如通过hash模运算拆分

    • 比如通过一致性哈希算法拆分

  3. 读写分离也是常见提高数据库查询能力的方式

  4. 浏览器端强制缓存,http响应头expires 和 cache-control 是设置缓存的重要参数

    • expires 设置到期时间点,这是http1.0的产物,对于客户端时间和服务器端有时差的情况,会导致缓存失效,或者缓存时间变长。

    • cache-control 设置多久以后过期,http1.1的的优化版本,解决了时差的问题,建议用这个。

    • 目前的浏览器两个参数都是生效的,那个先过期那个生效,建议只使用cache-control

    • 强制缓存有请求记录,但是不会真实的发送请求,响应结果直接是200.

    • 这种固定缓存时间适合那些几乎永远不会变动的资源。如果有一定变动的不适合。

  5. 浏览器端协商缓存,由响应头ETag 和 Last-Modified控制

    • 第一次请求资源的时候服务器响应头返回这两个参数,ETag是资源None值,一般是文件hash,Last-Modified是最后一次修改时间

    • 第二次请求的时候浏览器端默认在If-None-Match 和 If-Modified-Since,带上ETag和Last-Modified,然后由服务端程序比对这两个值是否变动。

    • 服务端比对过程,If-Modified-Since如果和后端最后一次修改时间不相等那就文件一定被改过,如果相等可能是一个时间精度内改了多次,所以要比对资源的none值。如果还一样,服务器端认为客户端是最新资源,返回304和空的响应体,浏览器就会去读取本地缓存。如果比对不上,那么就正常返回服务器资源,状态码200.

    • 协商缓存适用于需要前端缓存,但是有一定变动可能的资源。

  6. 客户端发起请求的时候,可以通过请求头,决定是否要走缓存。

    • no-cache:带了这个请求头的请求不在走强制缓存,但是还可以走协商缓存。

    • no-store:带了这个请求的不走强制缓存和协商缓存,相当于这个请求禁用缓存。

  7. 互联网应用用户分布在全国各地,需要CDN网络缓存资源加速,如果企业内部用的我们可以考虑ngxin缓存静态资源

  8. 考虑缓存的情况应该是不常变化,高频率使用的数据,并且缓存会一定程度的带来数据不一致,这是使用缓存需要考虑的一个点

  9. 通过id范围分库分表,会导致尾部热点问题

    分表为例:预估10亿数据,分10 份,0-1亿放表1,1-2亿放表2,一次类推。必定有一个统一的地方维持Id生成,正常Id应该是递增的,如果我们写到1.5亿的时候,表2所在的数据库一般就压力就比较大。要解决这个问题我们可以轮询的生成10个区间的递增数据,比如第一次生成1,第二次生成1亿零1,第三次生成两亿01,这样能解决热点问题,或者直接是不通过范围分配,直接通过hash来分配储存位置,hash模或者hash环

  10. 分库分表的情况雪花算法之类的与生成Id,比自增Id更加适合。

  11. 布隆过滤器的使用,可以减少缓存穿透

    • 缓存穿透:读取不存在的Id的对应的数据,然后缓存层没有,然后loadDB,然后如果有大量这种请求,数据库直接搞死。解决办法2种,

    • 1 缓冲层直接缓存不存在的Id直接缓存null值,不去loadDB。

    • 2 使用布隆过滤器,在缓存前面加一个布隆过滤器,初始化系统的时候告诉布隆过滤器那些Id是存在的,读取数据的时候去布隆过滤器查询Id是否存在,如果存在才查询缓存,如果缓存存在直接放回,如果缓存不存在loadDB,加入缓存然后返回。

  12. 布隆过滤器只能准确的判断不存在,不能判断准确的存在。

  13. 布隆过滤器是二进制数组,然后过滤值比如Id,通过hash模运算得到下标,里面二进制的值是0,表示不存在,1表示存在。

  14. 布隆过滤器的数组长度越长重复概率月底,hash次数越多,精确度越高,但是hash 次数也会让更多的位置变成1,多次hash只是相当于变相的增加hash 长度,只有增加布隆数组长度才是根本。多次hash完全可以用 更长的hash 算法替代。在有限的数组长度下,更长的hash不一定有意义。

  15. 因为hash模运算的下标冲突,布隆过滤器不能100%的过滤无效Id缓存穿透,一般我们做到重复概率低于1%就行了,这样即便被恶意攻击别人需要100倍的请求才能压垮我们的系统。

  16. 在id删除的时候,是不能直接把布隆过滤器对应位置1改成0 的,因为这个位置可能被别的id使用,所以只能通过定时重建布隆过滤器或者在对应下标做计数的方法实现。缓存null的方式可以解决这个问题,删除对应Id,对应的缓存设置成null就行了。

  17. 计数布隆过滤器会成倍的占用空间,我们扩大布隆过滤器数组长度也能一定程度的解决重复问题,相对来说计数布隆过滤器效果比增加数组长度更好。

  18. 生产中要避免使用IP,域名会是更好的选择,能让物流机器和地址解耦,但是随之而来的依赖DNS服务器是否问题,否则偶尔会出现DNS无法解析的异常,外部的如果不值得信赖,我们可以考虑内部的DNS解析服务。

  19. DNS可以一个 域名下面挂多个IP,可以并且只能轮询访问。

  20. DNS 有个明显的缺陷,只能提供物理机和访问地址的解耦,不能提供故障发现,如果一个域名下面的 3 个IP对应的物理机挂了一个,是无法感知的。这种情况通过网关和注册中心是一种解决方案。

  21. CAP的理解,CAP 三者不可兼得

    • C:强一致性,同意时间,任意节点的数据都是一样的

    • A:可用性,系统一定能在指定时间内返回,不会出现因为等待数据同步出现超时

    • P:网络分区以后,系统依旧能正确对外提供服务,分布式系统这个必选

    • 网络节点通信存在不确定性,要强强一致性那么处理的时间就可能会很长,所以在有网络节点通信的情况下,CA只能满足一个。只有不存在网络同步的时候才能同时满足CA。

    支持分布式的应用只能是AP 或者CP,一般AP 的居多。分布式软件数据同步的过程中如果是全部节点数据写入成功才返回成功的是CP模式,如果是配置的部分节点写入成功就返回成功的是AP模式,分布式应用没有CA模式的,并且很多软件支持可以通过配置来切换AP 和 CP模式。

  22. AP不是完全的舍弃一致性,如果是完全舍弃一致性,那么久没有事务的概念,分布式事务的XA模式是 CP 模式,全程持有锁,没有脏读,强一致性。别的类型TCC之类的事务都是AP模式,如果能一起完成就一起完成,如果实在完成不了就都不做,如果已经做到了confirm阶段那么只能无限重试。不管是正常做完,还是无限重试以后做完这个事务,过程中有脏读的情况,只保证最终一致性。

  23. 我们ACID,分布式事务CP 模式可以满足ACID,AP模式存在中间状态,存在脏读,所以隔离性,一致性,原子性都没有保留。

  24. 在说说 事务的隔离性通过什么手段实现的

    • 读未提交,出现脏读。不能接受,可以通过写快照来解决,有了写快照就不会出现出现脏读,变成了读已提交。

    • 读已提交,出现不可重复读,可以通过读快照来解决,加入了快照就是可重复读。

    • 可重复读,会出现幻读问题可以通过序列化来解决,mysql中使用快照和间隙锁也可以解决幻读。

  25. 间隙锁解决幻读的原理是锁定一定区间,不允许插入

  26. 4层负载均衡和7层负载均衡

    • lvs是常用的4层负载均衡方案,效率高,支持的负载均衡算法比较少只支持轮询,加权轮询,源地址hash和目标地址hash 之类的简单算法,支持的协议是tcp/udp这样的基础协议,位于传输层。

    • nginx是常用的7层负载均衡方案,效率相对lvs低一些,支持的负载均衡算法丰富,比如通过响应时间来负载均衡,并且可以自动屏蔽问题节点等高级功能,支持协议也比较具体高级协议,位于应用层。

  27. 为啥要禁用外键,只能在程序中做数据检查,保证数据完整性

    • 想有外键的子表中写入数据的时候,会检查主表的数据,要求数据存在,并且要获取到外键对应主表的记录的共享锁,才能插入子表数据,如果如果另外一个事务在对主表这条记录做修改,那么会出现等待。在这种外键关系复制的情况中锁定的主表可能会挺多,大大的增加了锁的力度,效率也就低了。

    • 删除数据的时候,如果不级联删除那么被删除的数据不是手动控制的,风险比较高,可控性低。

    • 开发测阶段,外键约束要求的数据完整会带来额外工作量。

    • 如果数据量达到一定级别,需要数据迁移到非关系型数据库,依赖数据库外键保证数据完整的将是灾难级别的。

  28. 触发器也有类似的问题,不建议在项目中使用,数据级联应该由程序来做。

  29. 前后端分离的好处

    • 数据样式解耦,分工明确,独立变化

    • 支持同一份接口支持多种客户端

    • 如果有mok,前端的开发可以完成和后端同时完成,不需要等待

    • 同时也带来了跨域问题

    • 也可能会影响搜索引擎的对数据数据爬取,减少自然流量

  30. 如果需mysql数据到别的数据库的准实时同步,我们可以考虑canal,canal类似一个mysql 从库,读取主库binlog传过来的内容,然后重放到别的类似es,MongoDB之类的数据仓库中。如果是需要在多种数据仓库中写入,大概可以在canal那边直接发送消息发哦kafka之类的mq,然后不同消费者组里面的取过去决定要怎么处理这种数据。

  31. 简历问题

    • 简历要简单,但是不能太短

    • 简历要体现出你基本实力,最常用的技术,你学过的最新的技术,最深入透彻的技术,最近几年过时的东西不要写,大部分人都歧视刚刚过时的东西,但是不歧视远古的东西。

    • 简历项目经验要体现自己负责模块的具体功能,使用的技术,最好有业务难点,技术难点,如果项目没有亮点,可以把学的过程中一些比较常见,但是你又研究的比较深入的问题写进去。

    • 简历需要适当美化,但是要在自己可以把控的基础上美化,美化多了就是吹牛逼。

  32. 锁的本质是抢占,占用的效果是同步。

  33. mysql的 高可用方案可以考虑MHA(Master High Availability),目前在 MySQL 高可用方面是一个相对成熟的解决方案.

  34. 架构应该从实际需求出发,考虑项目成本,考虑项目用途,而不应该一味的堆砌技术栈,如果你只需要一个鸡窝,那么绝对不需要打一个几十米深的地基,只需考虑这个鸡窝以后可能改成牛棚,不需要考虑它会变成摩天大楼。

  35. 不炫技,不堆砌技术栈,并不是摆烂,左一个鸡窝,一个能让用户用的满意,问题少的简单程序也是很重要的。

  36. 话又说回来,有些东西必须实践才能更加深入的体会,如果总是考虑满足需求就够了,也是一种不思进取的表现,怎么把控中间的尺度是一个架构师应该考虑的问题。

  37. 听懂客户要做什么想做什么是一门很重要学问,有些东西可能客户表达不清楚,有些东西客户不想或者不能明说,这时候要在和客户的沟通过程中揣测客户潜在的意思。

  38. redis主从复制的过程,主节点执行bgsava,生成rdb,传输给从,然后从节点读取到rdb内容,后面的同步就是同步的每一次操作指令。

  39. 哨兵模式中哨兵和master维持心跳,如果一个哨兵一定时间没有得到master的回应,就把master标记成主观下线,然后它让别的哨兵发送心跳包,如果大部分哨兵都丢失了和master的心跳。就把master标记成客观下线。然后哨兵在连接情况比较好的从节点中选出偏移量最多的作为新的master,然后开始新的一轮数据同步,并且修改各节点磁盘配置文件的主从信息。master后面如果连上会作为从节点,并且数据会被rdb包覆盖。

  40. 哨兵之间的算法是过半raft算法,并且向master注册自己,并且从master获取到所有哨兵的地址列表,这样他们之间就彼此发现了。

  41. @GlobalLock 可以在 AT模式下开启一个全局锁定的没有脏读的事务,类似XA事务。

  42. 对后服务接口幂等的处理,建议通过唯一Id来实现,对于前端接口,也可以考虑让前端带一个唯一Id,如果不容易带上,那么可以考取接口名字+请求参数Id(最好带上用户Id)取取摘要作为唯一Id,唯一Id算法这时候可以随机,不要用类似雪花算法的东西,这个不要求id递增,而且客户端很多,带有当前时间递增的算法都不合适,用redis锁的情况毫不犹豫的考虑UUID。

  43. 作为幂等条件的锁标志一定要设置过期时间,请求完成吧这个锁状态改成成功,并且依旧保留一段时间。完成操作key多久过期,请求过程中多久过期更加业务需要幂等时间长度决定。有些业务要求永久幂等的,我们可以考虑吧幂等锁标志放在数据库记录中。有过期时间的,用redis 更加方便高效。

  44. Spring的注解 @Retryable 自动重试,需要开启才生效@EnableRetry。一般配合 @Recover 记录失败重试也失败的记录。在乐观锁,调用远程服务需要重试的时候非常好用。

  45. 阿里开发规范,禁止三张以上的表关联原因,多表关联会产生大量笛卡尔积,类似循环,时间复杂度提升效率大大降低,然后join对以后数据量太大分库分表带来麻烦。

  46. 使用表关联我们需要注意什么

    • 我们在表关联的时候要保证被关联的数据用上索引。并且表关联一定要保证有作为查询条件的外键有索引。比如用户->用户详情.用户详情的user_id必须要有索引,比如用户->用户角色->角色表,如果我们需要查询用户有那些角色,需要在用户角色表的user_id上建索引。如果我们需要查询指定角色有哪些用户,那么需要在用户角色的 role_id 上建索引。

    • 要小表驱动大表,虽然结果都一样,但是中间过程不一样,越早的明确筛选掉大量无效数据,效率越高。

    • 三表关联,有两种情况,多对多,或者一对多对多。超过上面的两种情况我们就尽量不要用了。

  47. 不用多表join以后我们应该怎么做。

    • 我们可以分成两段查询,前一段查询主动表,后一段通过in 查询驱动表,但是对于分表情况in和id主动表查询的记录太多的时候in就有些力不从心了。

    • 我们可以额外生成倒排索引表,比如订单和子订单 ,在子订单分片的时候,通过子订单Id分片的数据不利于订单Id数据查询,我们可以用 订单ID分片,这样同一个订单Id就会落入一个分片,便于查询。数据分片的时候配合in 使用。

    • 我们可以在生成这些小表数据的时候吧他们写入到一张大表里面去。这个大表装着我们想要的数据,只用于查询,这种数据是实时的。很适合单机,分布式情况也能用,写到比较合适的一边的库里面就行。

    • 我们可以,当天晚对数据抽取(extract)、转换(transform)、加载(load),生成前一天各种情景需要的数据(数据湖,或者数据集市)。这种方式数据是T-1的。不是实时数据。这种方式叫做ETL。

  48. 阿里开发规范禁止使用存储过程,存储过程难以调试扩展,可移植性差。银行等老行业大量使用,是因历史遗留问题,并且那套系统可以满足他们稳定的使用,并且不易,也不能轻易改动。并且银行等企业起步早,那时候缺有技术的人员有限,更加愿意用钱来购买成套稳定的解决方案。互联网一般都是小公司起步晚,并且这个阶段可用的技术人员很多,并且其他解决也能很好的解决这些问题。

  49. 所谓去“IOE”,是对去IBM、Oracle、EMC的简称,三者均为海外IT巨头,其中IBM代表硬件以及整体解决方案服务商,Oracle代表数据库,EMC代表数据存储。去IOE的本质是数据库国产化。

  50. oracle数据到别的数据尚能迁移,存储过程跨数据库里面的东西基本都要重写。存储过程只能处理本地数据,互联网应用数据多了需要数据分片。

  51. token验证的两种方式,网关验证和服务节点验证。

    • 网关验证,要求所有的服务节点处于内网,可能对网关造成压力,优点是验证逻辑只在网关执行,别的服务无感。

    • 服务节点认证,不用所有服务节点处于内网,验证压力均摊,但是需要在每个节点引用验证逻辑。服务认证可以通过拦截器,过滤器,或者注解的方式来解决。

  52. 如果是使用JWT 方式的 token,过期策略有两种

    • 依赖jwt的负载区域的过期时间,这种事固定的不可更改的,过期验签就会不通过。

    • 设置不过期的jwt,然后手动在缓存中维护一个过期时间。一般设置2组对应关系,其一是jwtId->jwt,然后指定过期时间,这个关系是检查用户token是否还有效的。第二是userId->jwtId,这个关系是用来维持用户多端登陆,强制用户下线,获取指定用户在线状态的。或者直接就是 token里面有userId,缓存里面存 userId 作为key,token作为值,并且设置过期时间,这种做法不支持多端登录。

  53. token续期方式

    • 不用token自己的过期时间,用自己维护时间,这种适合内部系统,自己下发token,自己的子系统用,各个子系统之间可以共享缓存里面的过期时间。

    • 如果是认证服务器是三方的,比如微信,不可能给我们共享缓存里面的过期时间,并且也不愿意让这个权限永久下放,这时候我们只能更换token的方式实现。oath2.0 协议中,放回资源token的同时返回来一个刷新token,可以通过刷新token,重新请求一个新的token。

  54. jwtId 应该和 userId有关,然后唯一,考虑到安全可以带上客户端特征,比如IP,mac地址等,然后建议不要用MD5之类的摘要算法对 jwtId签名,免得哈希碰撞导致出现奇怪的问题,如果要对这些数据 模糊处理,用普通的对称加密算法比较好,缓存在后端,我觉得加密是不需要的。

  55. token 续期使用 access_token + refresh_token 的作用。正常我们访问一个程序登录有效期是30分钟,如果30分钟没有操作,就会过期,如果有操作,就能一种用。这种情况access_token会设置成如15分钟,refresh_token设置成30分钟,access_token过期以后,refresh_token还是有效的,后端这时候会拿refresh_token 去请求一对新的token,可用时间重置,然后返回给前端。做到类似的需求效果,如果超过30 分钟没有请求,那么这个对token就彻底的过期了。如果只有一个token,那么如果过期了能续期,就和不设置过期时间一样了,没有意义,refresh_token是一个比access_token时间长,并且判断是否可以续期的标志。两者除了过期时间不同,里面的内容一般是一样的。这种做法有个明显的缺点没有精确做到30 分钟过期,过期时间可能是15-30 分钟,如果需要精确过期时间,需要每次请求的时候更换token,就看有没有这种需求了。

  56. 如果我们需要比较精确的时间,但是又不想每次刷新token,可以access_token 设置1分钟,refresh_token设置31分钟,这样过期时间就是30-31分钟,token重新生成的时间.也变成了最多一分钟一次。

  57. 分布式需要级联查询额外数据解决方案,比如我们只能拿到userId,需要加载用户的别的信息。

    • 我们定义CascadeData接口,表示这样的对象可以级联查询。

    • 我们定义一个CascadeDataLoader接口,来处理CascadeData对象的查询。

    • 我们需要有一个配置类存着CascadeData类型和处理这个加载的实现类。比如说是一个Map<CascadeData.class,CascadeDataLoader>这样的配置。CascadeDataLoader加载的方法一定要支持批量。

    • 我们可以在返回值方法上用一个自定义注解 @Cascade 表示返回值需要解析级联关系。

    • 对象和对象的 成员 构成一个tree,解析级联关系设计一个下探多次层的问题,我们可以在定义一个注解@CascadeDelivery,表示需要向下传递解析关系。

    • 然后我们按照CascadeData类型收集需要级联对象类型和参数,放到类似 Map<CascadeData.class,List>这样的数据结构中。

    • 然后按照类型执行批量查询,把结果放到需要加载的对象里面去。下面是怎么吧数据放回去方案。

    • 我们也可以只要是CascadeData接口的参数都去做批量查询,并且填充,但是这样不够精确,不建议。

    • 我们可以优化处理,通过注解 @Padding 描述那些是需要加载的,比如在set方法上面加上指定注解,这样的才收集,并且批量查询完成以后通过set方法设置进去。

    • 我们也可以在get方法上做注解 @Resolve 标记,解析的过程中通过 CascadeData 把最终的结果对象预先放到 CascadeData 实现类中 ,批量查询完成以后,让CascadeData里面对应的 get方法通过 类型和参数去获取值。

    • 优化,类通常是固定的,那些成员变量应该收集这些可以在第一次解析以后缓存起来,后面通过这种缓存可以节省每次都都去反射解析那些成员变量应该收集过过程。

  58. ngxin的高可用策略

    • VIP+keepalive+DNS方式,通过 vip 和 keepalive 防止nginx单节点故障,用户通过DNS哪里得到的IP访问到物理机,物理机通过 VIP 请求分发给nginx1,这时候nginx2是完全空闲的,并且和nginx1配置一样,只是VIP指向了nginx1,如果nginx 挂了,keepalive识别到到nginx1连不通,会把VIP指向nginx2,保证高可用。我们可以通过DNS上配置2个ip实现轮询,并且通过2个VIP相互备份。
    • 我们也可以通过LVS实现nginx负载均衡实现搞可用。
  59. redis的三种模式

  • 主从,主写数据,同步到从,从可以读数据,分散读压力,但是主挂了从不能顶上来。

  • 哨兵,至少要3个节点。在主从的基础上,由哨兵监听主的状态,主挂了以后,选一个从顶上来。

  • 集群,至少要6个节点,分成三组,数据分片到三个组,然后下面有主从两个节点,主从数据一样。

  1. redis 分数据分片算上是hash槽。redis有16384(2^14)个hash槽。

  2. 为什么redishash槽是16384个?目前来说没有人需要超过1.6w个redis服务器节点,然后16384个二进制位的bitmap可以2K的数据来表示一个字节有8个二进制位,2k数据就是16K个二进制位。redis集群中会不间断的通过这2K的数据在集群中同步槽分配的信息。

  3. redis集群模式的扩容,是把之前节点分配的槽位,每个拿出一部分来给新节点,并且把对应的数据同步过去。只需要部分数据的迁移。缩容类似,把删除节点的槽位平均分配给保留的节点,然后把数据移动过去。

  4. 数据隔离级别。

    • 脏写:所有数据库都解决了脏写,脏写是指写入的时候没有加锁,然后两次并发写入不会排队,然后丢失一次修改的更新。即便是redis之类的非关系型数据库也通过CAS乐观锁来解决了脏写问题。

    • 脏读:读了没有提交的事务的修改记录,这种数据可能会回滚,这种数据是不能用的。

    • 不可重复读:两次读取读到的内容不一样,在脏读已经解决的情况下,不可重复读如果读到的是已经提交的数据,其实第二次读到的是最新的,一定程度上来说,有些时候我们希望读到这种数据。

    • 幻读:幻读是不可重复读的一种特殊情况,不可重复读描述的行内数据,幻读描述的整行数据的添加或者删除,两次读取的数量不一样(一般都是这么说,但是我认为不准确)。

    • mysql通过多版本并发控制(MVCC)解决了脏读和不可重复读,mysql通过多版本并发控制,让当前事务不会读到未提交的事务,mysql利用同一个ReadView让二次查询不会查到新提交事务的结果,我们查询一般查的快照而不是当前数据。

    • mysql通过事务隔离级别来平衡mysql 效率和可以接受的上述数据问题。隔离级别越高,上述问题出现的越少,效率越低。

    • mysql innodb 使用 间隙锁,一定程度上解决了幻读。但是需要程序员利用间隙锁,锁定区间不让另外一个事务写入或者删除。

  5. mysql读取数据有2种情况。

    • 一种是当前读,所有带有锁的读取,或者写入读到的数据都是当前读,比如删除,比如更新,比如 for update ,lock in share model。

    • 一种是快照读,正常的 select 是快照读,读取的时候要使用ReadView,mysql只有在快照读的时候才会使用MVCC,创建ReadView。

  6. mysql的MVCC,的版本控制类似下面的图

    在undo日志中对同一条数据的修改记录写成一个版本链表,里面记录着数据Id,修改信息,修改事务Id,回滚版本Id。版本链和读试图配合起来可以解决脏读,不可重复读问题。

  7. undo日志不是在事务提交以后就立即删除的,事务提交以后,别的事务可能还需要使用它,而是在所有事务Id比它小的都提交以后才考虑删除它。

  8. ReadView 的结构。 读视图里面记录着的信息

    • m_ids:当前活跃的事务Ids

    • min_trx_id:最小的事务Id

    • max_trx_id:当前最大的事务Id+1。

    • creator_trx_id:创建这个读视图的事务Id。

  9. 读视图的工作原理

    通过ReadView的4个参数参数,在版本链中做一下的判断,如果条件满足那么直接返回当前版本,如果不满足就和版本链的下一条比对。

    ReadView和版本链里面记录可以访问有2种情况。

    • 第一是版本链事务Id等于视图事务Id,自己写的数据可以读。

    • 第二是在视图创建以前就已经提交了的数据,这种可以读。这种数据可以看出三段,小于min_trx_id的都是已经提交的,可以读。大于等于max_trx_id都是未来的,不能读。唯一两者之间的又分两种情况,m_ids里面存在的事务没提交,不可以读。m_ids里面不存在的已提交,可以读。

  10. 不可重复读和可重复读的区别在于第二次查询是不是要生成一个新的ReadView,不可重复读每次查询生成一个ReadView,可重复读第二次查询使用第一次的ReadView。

  11. 两次快照读之间,只有要当前读的操作(修改,删除,for update,lock in share mode,这里能在版本链写入新记录,说明拿到了写锁,并且前面的版本链的事务已经提交),就会让版本链的最后一个的事务Id,变成当前事务Id,然后另外的事务提交到版本链里面的记录就能被当前事务读到,可重复读被打破。所以这种可重复报被打破的情况算是幻读还是不可重复读?

    • 例子1,事务1读取全表数据,读到1条,事务2删除全部数据,并且提交,事务1再次查询全表,还是会得到1条数据。这种情况的数据删除或者添加不会出现幻读问题。

    • 例子2,事务1查询全表,得到1条数据,事务2删除全表然后提交,事务1插入一条记录,事务1查询全表依旧有2条数据。没出现幻读。删除那个版本的事务Id,高于事务1里面第一读取数据产生的实物图里面的事务Id,所以查询不到删除,但是添加的那条记录是自己事务,可以查到,所以是7条。

    • 例子3,事务1查询指定Id的记录不存在,事务2查询指定Id的记录也不存在,事务2插入指定Id的数据然后提交,事务1插入指定Id的记录然后就会报错。查询不存在,但是我插入就报错,是这幻读吗?我认为是。如果查询条件不是id,之类的唯一索引,而是userName这种,就会插入两个一样的。

    • 例子3.1,事务1查询指定userName=a的记录有2天,事务2插入了一条userName=a的记录然后提交,事务1 使用当前读,比更新 userName为一的字段的更新时间为当前时间,然后事务1查询userName=a的发现有3条。两次查询中嵌入了当前读,这个应该比例子3的更加容易明白。

    • 例子4,事务1查询了一条记录,然后事务2修改了这条记录的A字段然后提交,事务1然后修改了这条记录的B字段,事务1在查询这条记录的 A 字段已经变了,这是幻读还是不可重复读?我认为是幻读

    • 所以幻读不是指的插入删除,读取记录不一样,而是指快照读中嵌入了当前读?让可重复读被打破了。

    • 个人对幻读的理解,很多资料都是说幻读是插入删除数据,前后查询的数量不一致。通过例子1和2,我们证明事务1两次查询之间,即便事务2添加,删除并且提交了数据,事务1依旧不会出现两次读取到的内容不一致。例子3,前后不一致,这就是通常我们说的插入数据导致的幻读问题(在用户注册情况下经常出)。例子4我觉得原理和例子3是一样的,都是快照读和当前读的结果不一样,我认为这两种都是幻读。

  12. 个人认为当前读for update,lock in share mode ,可能给版本链添加了一个当前最新版本数据一样的数据,并且事务Id是当前事务。

  13. for update 不能命中索引的时候是表锁,可以命中索引的时候是行锁,如果命中的是不存在的数据(比如不存在的userName),那么是间隙锁,间隙锁是共享锁,在修改数据的时候升级成写锁,锁升级,就可能出现死锁问题。mysql处理这种死锁的方案是,默认自动选择一个事务回滚,然后完成另外一个。

  14. 容器服务和虚拟机的区别

    • 虚拟机需要安装操作系统,容器服务不需要安装操作系统

    • 虚拟机和容器都是允许在操作系统上,都是软件

    • 虚拟机一般逻辑上隔离硬件资源(vm 上装windows是直接占用指定内存的,装linux是动态占用内存的),容器里面的应用之间是硬件资源是共享的。

    • 虚拟机软件上面要装应用,必须要装操作系统,容器上面可以直接安装打成镜像包的软件,比虚拟机省了虚拟机内操作系统允许的资源。

    • 可以把docker 类比为虚拟机软件,然后它里面运行程序会默认提供一个类似虚拟机的隔离环境。

    • 虚拟机和容器的都可以做到应用运行环境隔离。

  15. docker容器的优点和缺点

    1. 提供了一种统一的程序打包的方式,让自动化打包,发布,运行变得简单。

    2. 比虚拟机来占用的资源少。

    3. docker内部的环境内有操作系统运行环境直观。

  16. 项目发布方式

    • 红黑发布:使用双倍的主机,在同样的配置的集群上重新部署一套新版本,然后把网关的地址指向新集群

    • 蓝绿发布:在用户低估,网关断掉一半集群的服务,然后在这一半集群中部署新版本。然后把网关地址指向新版本的那一半集群,然后再老版本的集群上部署新版本,然后加入到集群。

    • 灰度发布:灰度发布是在小范围的集群上部署新版本,然后让一定量的用户走新版本,并且要让走新版本的这部分用户以后每次请求都走新版本,一般在负载均衡算法选用客户端ip的hash取模算出来。如果稳定,慢慢增加新版本集群的比例。

      灰度发布,需要数据库兼容新旧版本,上线前不止要测试新版本,还要测试就版本,对软件要求比较高。灰度发布需要通过监控指标来判断系统是否可以增加新版的比例。

  17. 索引过滤性差有时候mysql会选择不走索引,可以使用force index强制选择索引。

    select * from test force index(index_name) where name = "name7"

  18. 不要轻易使用左模糊和全模糊的like查询,除非你确定你的表数据是有限的,最多不会超过1W条。

  19. Java序列化的速度比json序列化,耗时多,数据量比较小的时候,耗时大概多200%,并且序列化的数据也更加大,数据量小的对象上能大一倍以上。Java序列化,性能不高,不能兼容不同语言.所以不被通用。

  20. 可靠消息,一个消息是否可靠需要考虑那些点

    • mq server 需要支持配置先刷盘然后再返回ack信息,避免消息丢失。

    • 生产者发送消息的时候要支持选用有ack的模式,只发不管结果的模式是不可靠的

    • 生产者发送消息可以重试,避免网络波动导致消息发送失败

    • mq server 能过滤这些因为网络波动重试发出的消息,也就是幂等消息。重试加幂等能实现精准一次性消息。

    • 生产者发出的消息应该有序,一般mq做的都是队列内有序,然后就是投递有序,要投递有序,那么一个主题下面的任意一个队列,只能被同一个消费者组里面的一个消费者消费,如果多个就不能实现有序。

    • 一般mq都支持事务消息,能保证事务完成消息才会发送出去,除了rocketmq是用的2端确认的消息以外,别的mq好像都是挤压消息到事务提交的时候发送,怎么保证事务提交消息就一定能发送出去的呢?或者说是事务达到可以提交的这个点,再去发送消息,消息发送成功,事务提交,消息发送是吧,事务回滚。

    • 消费者端的ack应该支持完成业务以后手动ack。

  21. 事务在达到可以提交这个点,undo日志可以回滚,并且redo日志也记录完整的修改数据,如果突然挂了,重启以后的依旧支持数据恢复到完整提交一样的状态。这是分布式事务所有子事务都达到可提交状态以后,大家一起提交不会出现部分提交也是依赖于此。

  22. 自动化部署一般常见做法。使用jenkins可视化的执行脚本->把程序打包成Docker镜像->把镜像上传到docker私有仓库(harbor)->使用k82拉去docker镜像->拉去对应的环境的配置文件并且部署。

  23. 项目中不要轻易的使用业务主键,也许主键在修改,添加重复记录等情况会有巨大的麻烦。

  24. jvm常见参数的 -开头的是标准参数,-X开头的是非标准参数.-XX开头的是不稳定参数

  25. -Xms:内存尺寸,-Xmx:内存最多此处(x表示最大),-Xss:栈的尺寸,-Xmn:内存新生代尺寸,-Xloggc:改成文件输出位置,-XX:+printGCDetails:打印GC详情,-XX:PrintGCTimeStamps:打印GC时间,-XX:MaxGCPauseMillis:gc时候STW的时间。

  26. 服务器我们需要稳定,而不是吧内存省下来干别的事情,不希望堆内存频繁的伸缩带来的效率影响,所以我们一般 设置 -Xms 和 -Xms相等。

  27. -Xss在jdk1.5以后默认是1M,1.5之前是256K,-Xss影响能够调用的方法深度。

  28. jstat -gcutil 进程Id 时间间隔 次数 可以每个一定时间打印内存信息。

  29. expain extended 可以看到一些额外的索引失效的信息。在mysql8.0以前可以用,8.0以后删除了,需要在 explain 以后执行 show warnings。

    explain SELECT * from test where name = 77;
    show warnings;
    
  30. rabbitmq的6种工作模式

    • simple模式,一个队列,一个消费,没有交换机

    • work模式,一个队列,多个消费者,没有交换机

    • publish/subcribe模式,发布订阅,有交换机参与,交换机无条件的把消息分别投递给下面的所有队列,然后被多个消费者消费。

    • routing模式,交换机通过routingKey吧消息分发到队列,然后被队列下面的消费者消费

    • topic模式,主题模式,和路由模式类似,只是routingKey支持通配符。*表示一段模糊,#表示一段或者多端模糊。

    • RPC模式,用两个队列来处理请求,相应连个过程,全程柱塞。

  31. rabbitMQ的死信队列和死信交换机
    死信队列,死信交换机和正常队列,正常交换机几乎一样。我们可以给队列配置死信交换机,当这个队列里面的消息指定时间内没有被消费的时候们就会进入死信交换机,然后分发给死信队列。

  32. 死信队列做延时/重试队列
    我们设置一个队列,但是不给它设置消费者,并且给消息或者队列设置一定的过期时间,然后消息就会在指定时间以后被送入死信队列,我们消费死信队列的消息就得到了延时消息的效果。

  33. 队列和消息都可以设置过期时间,小的那个生效。

  34. 死信队列里面过期时间比较短的消息会被过期时间比它长并且排在它前面的消息阻塞吗?前面消息不被消费,肯定会阻塞后面的,但是,过期直接从队伍中移除,应该不会被前面的阻塞!待确认。

  35. 死信不能根本上解决,消息积压问题,最多做到给消息设置过期时间,然后移动到死信队列,然后再死信中取出放到别的地方积压(比如数据库),然后等洪峰过去,在从数据库读出来,放回队列中。mq带有持久换,本来就能积压很多消息,把太久没消息的消息积压在死信或者原始队列也是可以的,只是占用硬盘。

  36. 消费者端消费能力是关键,如果消费者不给力,有消息积极,那么消息可以积压的地方。

    • 积压在原始队列,那么新来的任务一定会被阻塞,按顺序消费。

    • 积压在死信,这样原始队列新来的消息还有一部分很快被消费。一部分放到死信等空下来在消费。

    • 死信的消息也可以放到数据库,然后mq完全不积极,但是消息积压在数据库了,意义有限。

  37. UUID虽然不适合做数据库这种有序数据结构的索引,但是用来做通过hash方式检索唯一key很合适。

  38. UUID的几种算法

    • 基于设备号+时间算法,理论可以保证不同设备唯一,但是同一台设备上时间回拨可能出现问题。

    • DEE安全的UUID,需要获取用户的一些唯一标志+有损时间戳,涉及用户隐私,有些地方不让用。

    • 基于hash,命名空间+MD5或者 sha1,可以认为不同名空间几乎不会重复,然后就是hash可能冲突。

    • 完全随机,Java的UUID用的这种,完全随机,重复概率极小。和设备时间无关。可以认为是不重复的。

  39. UUID不合适做数据库有b+tree索引的原因是它不是递增的,乱序插入会频繁的导致也分裂。mysql一页数据16K,当这页中间插入数据超过16K的时候,就会分裂成2个约8K的页。这种操作需要移动数据比较多,为了满足平衡二叉树,移动的可能不止这2页的数据。所以磁盘IO,内存,CPU资源都会明显增加。

  40. Java UUID的 结构 32位 16进制数。一个16进制数是4个二进制位,半个字节。所以Java的UUID是128位的。

    下面的图可以看到,默认Java的随机UUID是完全随机,没有固定的唯一标志。

    但是也是支持手动传入高低64位(两个long代替)的指定内容的UUID的。

  41. Sha1 是 160位的。SHA(Secure Hash Algorithm)的缩写,安全hash算法。更加长的还有SHA256,SHA384,SHA512。

  42. MD5一般都是128位的(32个16进制字符表示),也有64位的(16个16进制字符表示)。

  43. 关于机械硬盘,SSD,内存随机读写和顺序读写性能对比图

    机械硬盘差的是随机读写的能力,它需要巡道。顺序读写能力和ssd 区别不大。并且机械,ssd,内存的 顺序读取都远远块于随机读写速度。

  44. kafka吞吐量高的几个因素

    • 顺序读写,kafka的消息都是顺序存储,kafka的被删除的消息,被处理的消息,不是实时物流删除的,每次通过offset定位应该消费的位置,通过这种方式保证读取硬盘消息是顺序读写。

    • 页缓存,kafka内存里面的消息不是放在jvm里面的是放在操作系统页缓存里面的。减少了JVM垃圾回收的影响。

    • 零拷贝,磁盘数据到操作系统页缓存以后,不会进入用户态,而是直接发送给网卡,减少了到用户态的拷贝,这个和页缓存是一起的。需要操作系统支持。

    • 批量操作,数据压缩,批处理等方式可以减少IO的开销,提升效率。这个大家都有,不算kafka独有的特点。

  45. redis 高效的几个因素

    • 基于内存 ,基于内存的都比基于磁盘的块,redis和mysql比块,和momenyCache比是一个数量级的。

    • 单线程,redis的 执行命令的模块是单线程的,减少也锁定资源,线程切换带来的性能消耗,但是别的模块也是多线程的,不能说富人坐了一次公交车就变成穷人了吧。

    • 高效的自定义数据结构,redis的数据结构很好用,但是和memoryCache之类的比,一个级别的,只是支持了数据通持久,类似说相声里面唱歌最好的,很多时候,我们确实需要的就是这种能说会唱的组件。

  46. 列式存储块对比行式存储查询块的原因,列式存储的存储方式类似多个数组,每个数组存一种属性。行式存储类似数组里面字节存对象。

    • 行式结构,比如我们查询年级=3年级的学生,通过年级索引找到学生ids,然后通过ids,id对应数据行存的位置,他们不是连续的,如果有1000个id,至少要进行1000次寻址,然后读取出用户数据的行出来。类似于通过索引得到1000个数组下标,然后要寻址1000次拿到完整的数据。磁盘,尤其是机械硬盘寻址这个过程需要磁盘机械臂摆动到指定位置,然后等磁盘转到指定位置才能读,很慢。随机寻址的次数直接关系到读取数据的效率。

    • 列式存储,还是查询年级=3年级的学生。列存数据库,同一列的数据是放在一起的,然后去其他几个需要的列存空间找出别的数据。正常看来说列的数量,远比行小。类似每次只用从几个属性数组取出固定位置的值拼起来。

    • 但是列存的写入效率和修改效率比行式数据库低,一般写入是多列并发写入,修改是通过版本控制,删除也是假删除。

  47. 行式存储机构示意图

  48. 列式存储结构示意图

  49. mysql老版本一条sql只能一个cpu执行,mysql5.6以后一条sql也能使用多个cpu,mysql8.0 添加了新参数innodb_parallel_read_threads,指定一个sql允许的并行读取线程数量。

  50. mysql对cpu的依赖,一般多用户情景,cpu数量比单核新能重要,对于用户量不多,报表统计等复杂计算的情景,单核性能比核心数量重要。

  51. mysql对内存的依赖,mysql会把索引和部分数据缓存到内存,所以这样可以大大提高效率。我们如果分库分表的时候可以把热点数据放在一个数据库,然后给他配置比较大的内存,提高写效率,非热点数据放在配置低的数据库,因为它用的比较少,速度慢点影响不大。

  52. mysql非常依赖磁盘随机读写能力,所以优先考虑sdd,目前ssd和机械的价格差距已经不是特别大了。普通家用固态1T容量约450块,普通机械1T约280块。机械硬盘不要选择叠瓦盘。并且如果是服务器硬盘,要考虑容灾,要考虑磁盘阵列,比如raid10.

  53. raid (redundant arrays of indepandent disk) 直译过来 冗余阵列作为一个磁盘,简称磁盘阵列。

    • RAID0 多个硬盘,分片存储数据,读写速度倍增,硬盘故障率也倍增。

    • RAID1 冗余硬盘,作为备份,容量倍减,写入速度不变,读取速度倍增。有备份,允许一半的磁盘损坏。

    • RAID10 在RAID1下面搞了一个RAID0,速度有提高,容量减少,但是硬盘坏了部分数据不会丢。缺点容量减半。

    • RAID5 在RAID0的基础上增加机构检验,而不是备份,容量略微缩减,读写速度几乎倍增。但是如果校验盘和数据盘都坏了,就会丢数据。

  54. 用户关心的信息更新,消息的推拉模式带来的读扩散和写扩散问题

    • push 模式,一般做法是发送消息到MQ,然后消费消息的时候推送给用户。如果关注这条消息的人很多,那么要写入的消息数据也很多,这时候可能会给mq带来比较重的压力。对于超大关注量的,比如1亿用户关注,我们可以考虑延长推送时间,分批推送,比如延长到10 分钟,没分钟唤起一个定时任务,推送一定量的消息。或者直接通过拉取方式。

    • pull 模式,一般做法是前端定时轮询来查询动态。如果大量用户集中一个时间点pull,那么可能出读扩散问题,压垮我们的服务器,可以通过多级缓存,限流等方式保护我们的服务器,在秒杀等用户行为集中,并且意愿强烈的情况,可以考虑验证码来分散用户瞬时高峰。服务器也应该由预热等手段。

  55. 提高我们程序的一个重要因素动静分离,动静分离以后,可以通过CDN,服务器缓存等方式提高静态文件的读取数据。

  56. 对于㤇大并发的页面,实时服务器端生成动态页面是应该避免的。这也是jsp,freemark等模板语言用户量越来越少的原因之一。

  57. mysql之类的关系型数据库到现在都不官方支持分布式集群的原因之一是它数据库表之间有严重的关联。

  58. mongdb对比mysql的优势在于,支持TB-PB的数据量,并且支持分布式集群。他的数据格式和mysql类似,并且数据之间没有强依赖。每条数据都是完成的。

  59. MongoDB从 3.0版本引入WiredTiger存储引擎之后开始支持事务,MongoDB 3.6之前的版本只能支持单文档的事务,从MongoDB 4.0版本开始支持复制集部署模式下的事务,从MongoDB 4.2版本开始支持分片集群中的事务

  60. 随着monggdb对事物的支持,在有些业务场景中mongoDb也是作为数据库的选择。对于一些复结构复杂的数据,设计一堆关系型表结构,可以考虑直接存mongodb,比如excel数据。有些结构随时变化的数据,比如自定也首页之类的需求,随时变动。或者数据量很大,但是我们对数据一致性要求不高的情况。

  61. mysql ngram_token_size指定全文检索,分词数(多少个字分成一个词),配合full_text实现简单的全文检索。ngram_token_size默认值是2。

  62. mysql的索引类型

    • UNIQUE 唯一索引,一般不建议使用,唯一可以通过程序来控制,而且假删除需要额外处理

    • NORMAL 普通索引,也就是我呢吧你常用的那种。

    • FULLTEXT 全文检索,配合 ngram_token_size 可以做简单的全文检索。

      spatial 空间索引,多坐标类型的支持。

  63. mysql索引方式

    • btree:常用的就是这个,使用的二分查找。

    • hash:使用的hash算法,对范围查询支持比较差。

  64. mysql ngram_token_size 是对中文,日文 等单字语言的分词器,默认用的英文分词器,使用空格分词,单字语言通过ngram_token_size指定简单的分词规则,并且需要在定义索引的时候指定 ngram分词器。

FULLTEXT INDEX name(name) with parser 'ngram'

  1. ngram_token_size=2的意思是2个字一分词,比输入“黑色游戏机”分词的结果就是"黑色",“色游”,"游戏","戏机"四个词,然后到数据库中用类似or的方式查询,这种分词简单,效率低,并且不符合日常习惯,有大量无效词,只有在有限的情况才有使用的必要。对于大段的文本,这种方式会分出来很多词,而且这次词很多都是无效的,带入数据库中查询效率也很低。输入文本长度不高的时候可以代替es使用。

  2. mysql全文检索 使用match(字段名,字段名) against("关键字") 代替了 字段名 like "%g关键字%",是左like的的一种替代方案。更具出现的频率和出现的字段算出一个评分,然后默认按照评分高低排序。ngram_token_size=2的时候是查不到单个字的。

  3. 使用全文索引检索的时候支持 自然语言模式 和 布尔模式,两种方式。

  4. 个人认为架构的基础是 熟练的开发+经验的积累+独立的思考。

  5. 分布式raft(Replicated And Fault Tolerant)选举算法原理

    • 分成三个角色,领导者,跟随者,和候选者。

    • 在没有领导者的时候和领导者联系不上的时候。跟随者通过自身随机超时发起选举。

    • 选举的过程,把自己变成候选人,把自身的任期+1,然后偷自己一票,然后请求其他节点给把当次任期的那票投给自己一票。

    • 每个节点在一轮任期中只有一票。给了先来要票的候选者,后来的就得不到了。

    • 跟随者收到投票邀请以后,如果自己的任期比当前投票任期小,更新自己的任期,否则就拒绝。

    • 随机超时 时间是为了减少大家一起扎堆成为候选人发起选举的情况。

    • 候选者得到大多数的票以后变成领导者。并且通知大家自己成为领导。

    • 如果这时候前领导回来了,收到新领带任期比自己大的心跳,就会默默地把自己降级成跟随者。

    • 如果当前领导心因为网络不通,丢失了其他节点的心跳,那么其他节点在随机时间以后开始重复走选举的流程。

    • 选举平票会重新选举。

    • 如果领导者能一直保持心跳,那么它会终身任命。

  6. 明显raft算法只有在没有利益关系的时候才能公平选举,否者不管缩短随机时间,还是发出高任期的选举请求,都有利于自己成为领导者。

  7. 分布式锁常见方案,基于redis ,基于数据库锁,基于zk等等

  8. zookeeper设计思想是CP模式,强一致性,舍弃了高可用效率不高,zk更适合分布式系统间的强一致控制,zk可以保持心跳,redis没有类似的功能,比如统计并分配雪花算法的 workId 和 dataId就很适合用zk做,分布式锁建议不要用zk。

  9. 分布式锁可能不太适合高并发场景,或者说悲观锁都不太适合高并发场景,但是乐观锁也不一定就适合高并发场景,CAS大量重试也是非常消耗资源的。我觉得只有读数据用到读锁的情况乐观锁才能比悲观锁有明显的优势,所以减小锁的粒度也行是更加有效的办法。

  10. zookeeper的四种节点

    • 持久节点 适合做持久锁

    • 持久有序节点 适合做持久公平锁

    • 临时节点,适合做锁

    • 临时有序节点,适合做公平锁,服务注册列表等

  11. 那些系统不需要微服务,预估业务量不大的系统,比如某卖茶叶的系统官网下面的留言系统。公司内部使用的一些管理系统,公司也就那多人人用,丹徒足够。比如要求实时性比较高的系统,分布式系统网络请求延时都比较高,如果是串行的并且调用深度比较高,那么响应时间就会明显变慢。

  12. 微服务业务流程除了串行调用,有些业务场景也可以考虑并行调用,这样可以减少系统响应时间,但是也多数据一致性有了更大的挑战。

  13. BASE的解释,BASE是对 AP模式的一种补充说明,或者说AP模式应该怎么去做。

    • BA:Basically Available 基本可用,要求快速完成并响应用户的基本诉求, 客户的业务有主次之分,保证主业务正常进行并且快速响应,次业务慢慢完成也是可以的。(游戏里面第一个杀死boss的全区通告,这里对于玩家来说,boos血归零,boos就应该死了,至于全区通报,可以迟一点,不能因为等全全区通报结果,就让boss0血不死卡在哪里)。

    • S:Soft state 软状态,也就有中间状态,脏读/不可重复读之类的,比如AT模式事务,TCC模式的事务,都是有软状态的,XA这种强一致性的事务是没有软状态的。

    • E:Eventully consistent 最终一致性,之后要把数据对上,保证数据最终是一致的

  14. 最终一致性的解决方案

    • 重试,也是最常见的方式,两段式事务的补偿机制都是重试

    • 数据校对程序:定时检查那些数据,然后得到不一致的数据,然后修正,比如有个垃圾app,维持了用户存储的照片,和这些照片的存储空间,然后用户信息里面的已经使用的容量和真是上传照片的数量和照片文件真实的占用空间对不上,我们可以通过定时任务,每天晚上偷偷核对真实照片的数量和空间,修正用户存储空间信息。

    • 人工介入, 人虽然会犯错,但是也是最智能的,计算机只能按照指定的程序执行,只有人才能做出最合理的判断。分布式事务里面除了XA模式以外别的方案都会出一定的问题,比如TCC,AT,saga 都有脏读问题,如果脏读数据被使用( 可能被回滚,也就是依赖了没有提交的数据),程序是不能自动修正的。事务消息,本地事务提交,在消费者没有处理这个消息之前,这份数据也是不一致的,这个数据虽然不是脏数据,但是是老数据,用了可能会出问题。

  15. 基于最终一致性的事务解决方案,都有概率会出问题,不能100%的保证数据的一致性,只有XA才是永远的神,但是我们有时候不需要神,大概率能解决小概率才出现的问题就行了。而且,重程序设计的角度来说也可以避免一些类似的,比如程序校对,能发现这种问题,比如独立消息这种情况可以延长再查询,脏读那几种应该是最麻烦的,因为不能区分你读到的是否是脏数据。

  16. 缓存写入,缓存更新应该怎么保证数据一致性? Cache Aside Pattern 和 延时双删

    • 新增以后下一次查询就会loadDB,并且加入缓存,所以一般不用维护缓存,除非有特殊要求。比如你缓存了null值,这种处理逻辑和缓存更新一样。

    • 数据库更新的时候缓存应该怎么更新?在数据更新前更新缓存都是不合理的,因为数据库数据可能回滚,缓存却被写入了,不会级联回滚。

    • 比较理想的就是数据库事务提交的那个点,我们保证缓存一起更新了。但是这显然是做不到的。数据库和缓存db的修改不具有强一致性。

    • 然后我们只能在数据库事务提交以后修改缓存,但是如果线程1在事务提交以后,缓存修改以前,中间嵌入了一个套线程2,做了类似的事情, 修改数据库->然后修改缓存,那么这时候线程2的缓存修改就被线程1的覆盖了,明显线程2的修改才是最新的。缓存很长一段时间都是错误的。这种情况是不能接受的。

    • 好像任意阶段修改缓存都要出大问题?那么我们考虑不是通过修改缓存,而是让缓存失效呢?线程1事务提交以后,然后删除缓存,等另外要使用缓存的时候loadDB,缓存就是数据库的最新值,在线程1事务提交到删除缓存的过程中,即便线程2做了同样的操作修改数据库->删除缓存,打乱了缓存失效的顺序,loadDb的数据依旧是数据库里面的最新数据,不一致只位于线程1事务提交到缓存删除的这一小段时间。看是没什么大问题,但是又一种极端情况,线程2在线程1删除缓存以后写入了一次缓存,并且这个缓存的内容是在线程1事务提交之前loadDB得到的数据,这份数据明显是旧的,然后这份错误的缓存很长的时间都会是错误的。这种事务提交以后 通过删除缓存来达到缓存更新效果的方式叫做 cache aside pattern。

    • 我们可以通过 延时双删 来解决这个问题,我们可以考虑线程1 在删除缓存一段时间以后再次删除一次缓存,这个时间间隔如果大于线程2 loadDB完成到写入缓存的时间,那么这个错误的缓存很快就被修正了。正常缓存不会loadDb以后写入花费2秒,所以这个延时双删2秒大概就已经够了。

    • 所以这样第一次删除缓存还有意义吗?当然有,如果第一次不删除,2秒以后删除,那么必然有2秒的缓存不一致,如果事务提交以后立马删除,2秒后在删除一次,结果就是大概率不会出现2秒的缓存不一致,即便出现了,第二次的延时删除也能大概率的解决这个问题。

    • 上面延时双删两个大概率解决问题,有没有100% 能避免缓存数据不一致的办法?有,但是效率低,我们一般不用,我们读取缓存的时候加读锁,修改数据前加写锁,吧修改数据和修改缓存放到一个写锁的同步区域里面去,如果拿到了写锁,缓存修改过程中,别的线程不能修改,也不能查询缓存,更加不会loadDB然后并发写入。写锁释放后才能读取缓存,这时候数据库数据缓存数据已经是一致的了。我们要保证的只是写锁内,如果数据库提交成功,一定要保证缓存写入,如果写入失败就要重试,并且一致占用这锁。并且这个锁阻止着读,效率明显就低了,这种一般不选,这种事强一致性的解决方案也就是CP模式,我们一般为了追求高吞吐,都会使用AP的思路来进行取舍。

  17. 一个电商秒杀情景的数据库设计

    • 秒杀情理考虑,如果只有100个以内的库存,海量用户请求,前端直接验证码,分散用户请求,后端直接限流,假如系统每秒能处理20个,那么就限流20,20个领取以后正常扣减库存就行,海量用户,5秒搞定。多的直接返回没有库存,让他重新抢,输入验证码,重看看看还有没库存基本5秒就过去了,如果还有让他在抢一次,10秒过去总没有库存了吧。

    • 20个每秒太少了,假如有10000个库存,每秒20个要500秒才能抢完,不停提示没有库存,一看还有库存,甚至500秒以内都有库存,用户很烦,不停的提示重新抢,然后还一直有货(京东抢茅台就是这样情况,很烦),这种情况考虑库存锁力度拆分。

    • 单独一个表存100段库存,后端接口限流20*100=2000个每秒,即便10000库存也只要5秒,具体做法查询有库存的库存分片,然后用户id取模有库存集合数量(最初是100),得到指定用户库存的分片以后行锁扣库存,锁力度就低了。如果当前分片修改数据的时候提示没有库存,直接告诉用户没库存,这时候一般一个分库没了别的分片库存也快没了,用户重新请求就去别的分片。一个库存分片没了,剩下的估计1秒不到所有的库存都会清空。每秒2000,一分钟能卖出去10w对于一个商品来说已经完全足够了,一小时600W订单,够了吧。

    • 考虑到随时洪峰,我们可以在接受限流改成漏桶,2000个每秒,缓存5秒的,1W请求。别的都扔掉,告诉他们没库存。

    • 然后这种漏桶我们可以做成异步的,直接放回抢购中,这个状态前端能看到,然后让用户转页面圈圈,我们把处理结果推送给用户就行了。进入了漏桶的和直接拒绝的不一样,是有概率能抢到的。或者为了用户体验,全都转圈,也行。

    • 这时候程序的瓶颈变成了数据库cpu内存能否承受2000用户每秒的用户炒作,而不是锁的力度。如果单库做不到那就分库吧,10个库,每个库200每秒总可以了吧

    • 分段锁不止秒杀情况能用,减少锁力度,提高并发是很多情况通用的。

  18. 电商秒杀数据库外缓存库存的做法。

    • 库存可以不用设置在数据库,我们秒杀开始前预设值秒杀库存在redis。

    • 秒杀开始的时候nginx通过LUA脚本调用 redis 库存自减。调用自减之前需要判断当前库存是大于0的。这里也可以是在程序里面读取redis,不一定要在ngxin中。

    • 判断剩余库存是否>=0,满足就生成订单号,然后把订单号和userId交给mq,然后返回订单Id给用户。不满足就提示售罄。

    • 用户界面转圈圈,定时轮询订单Id的订单结果。这时候这时候查询方法坐缓存,并且缓存null值。

    • 通过mq缓存消息,缓冲瞬时洪峰,根据数据库的写入能力,限流消费消息,真实的为用户生成订单,这里的生成订单不会扣减库存。然后写入订单缓存,把null值得缓存删除。

    • 用户转圈圈的页面后台轮询脚本通过订单Id查询到订单信息以后,提示用户抢购成功。

    • 用户拿到订单以后,如果30分钟不支付,我们把订单取消(可以通过延时消息,触发+查询订单被动触发保证),取消订单以后调用redis库存自增,归还库存。

    • 还有一个细节就是用户mq消息生成订单落库的时候失败,甚至重试也失败,我们这时候可以把缓存里面对应订单id的信息改成指定状态,前端轮询查到提示抢购失败,并且归还redis库存,这应该是小概率,正常应该通过限制数据库读取mq消息频率,保证数据不出问题。

    • 前端轮询可以改成消息推送,用额外的消息服务器来做,减轻轮询订单服务器压力。

    • 通过上面的例子,我们可以保证高效的秒杀,并且可以保证库存不会被超卖。但是可能出现mq消息没有发送出去的问题( rocketMQ 的两段式消息可以解决,但是 别的mq 事务消息不能解决,redis改库存 和 mq 发送消息的一致性问题 ),这时候可能会出现库存减了,订单创建消息丢了的情况,但是这种问题不严重。在redis库存归零以后,检查数据库订单数量如果一致就没问题。

    • mq消息丢失补偿过程,如果不一致,可以补偿丢失的库存数,这个补偿机制,补偿前,我们拦截关闭用户下单通道,检查mq消息全都被消费,才能开始补偿查询数据数量,然后再redis库存里面补齐,然后开放通道。

    • 补偿丢失库存不是必须的,很多时候不需要补偿,甚至业务就允许出现少了量没有卖出去。少买比多买问题小。我们也可以通过rocktet特殊的事务消息解决,用rocket事务消息的时候,补偿机制是不需要的。

  19. 我么可以考一个问题,商品的库存是不是一定要数据库才是最准的?数据库存一个最大库存,一个已用库存,然后请求库存数量的时候,通过减法算出当前剩余库存然后缓存redis,后面扣库存直接扣除redis也是可以接受的,数据的已经售出数量完全可以晚点更新,比如10 分钟通过对比redis数据算,挥着订单数量算出来。

  20. 什么事当天单体架构,什么事分布式,什么事分布式微服务

    • 单体架构,操作在一个进程内完成,不需要经历多个进程。也可以叫做单步式架构。单体架构可以用集群方式部署,每个节点都能完整的提供服务。横向拓展集群不会带来分布式数据的一致性问题。

    • 分布式架构,纵向拆分业务为多个服务程序,每个服务做不一样的事情,一个操作可能是分多步,在多个进程中完成的。这其实已经是一个分布式架构了,大家协同才是一个完整的服务,只有完整的节点才能提供完整的服务。都做到纵向拆分了,为啥不在做简单很横向拓展,让程序支持高可用呢?

    • 分布式微服务架构,一般说分布式架构,指的就是分布式微服务架构。这一种可以横向拓展,并且可以纵向拓展的架构方式。并且服务器中部分节点宕机也是可以对完提供完整的服务器的。

    • 纵向拆分了,这个程序就已经有了纵向拓展和横向拓展的能力。所以不用严格分划分什么事分布式程序什么是分布式微服务程序。就像单体架构,只要前面做了负载均衡,它就能以集群的方式对外提供服务(需要session共享)。

  21. 分布式架构没有直接提高程序负载能力,而是提供了拓展的方向数据交互规范,拓展能力变强了,间接的提高负载能力,并不是用了分布式微服务并发就能上去,负载能力主要还是依赖程序的内部实现,提高集群数量和提升单机配置都是硬件升级。

  22. 我们发现很多时候我们都要流控,不管做人还是做软件,有多少能力做多少事情,超过自己能力的事情做多了,就会被累死,软件设计的一个核心价值观,如果不能给所有人提供服务,那给部分人提供服务比程序崩溃大家都不能用更好。自己在程序里面写流控麻烦而且不通用,想想sentinel这种可以通过外部可视化界面做流控的东西,真是太优秀了。

  23. 分布式微服务有那些需要重视的东西

    • 服务注册和发现

    • RPC调用方式和效率

    • RPC调用负载均衡

    • RPC调用数据一致性(分布式事务)

    • RPC调用数据幂等(自动或者手动重试等情况)

    • RPC调用日志全局日志追踪

    • RPC调用链路追踪

    • 统一的配置中心

    • 限流,熔断(保护系统,避免雪崩的重要方式)

    • 服务网关(专用网关,或者 ngxin 也行)

    • 单点登录,授权认证机制

    • 分布式锁

    • 分布式数据存储

    • 集群高可用

  24. 服务降级和服务熔断,在早期服务熔断和降级sentinel 和 Hystrix 表示的意思不一样,sentinel早期版本 降级和熔断感觉和 hystrix是相反的,老版本的 sentinel的流控配置界面就能看出来是反着叫的,目前都统一了含义了,没啥争议的了。

    • 服务降级,针对当次请求返回默认处理方案

    • 服务熔断,针对一段时间内的请求,返回默认处理方案。熔断一般是触发服务降级的此次或者评率达到一定阈值触发的。

  25. sentinel是通过拦截器链做流控的。

  26. 什么事雪崩效应?雪崩效应是怎么产生的?怎么避免雪崩?

    • 雪崩效应,因为一个服务或者节点故障,导致整个服务器集群都不可用的现象。

    • 雪崩是怎么产生的,在分布式系统中,因为一个服务节点响应慢,或者无相应,然后导致依赖它的服务都阻塞,后面依赖这些服务的服务也阻塞,滚雪球一样阻塞的越来越多,连接池等资源得不到释放,可用资源越来越少,然后整个系统就不能接收新的请求了,服务器进入假死状态或者直接崩溃。

    • 怎么避免雪崩:流控是避免雪崩的有效办法。一个木桶能装多少水有最短的木板决定,请求链路中的一个不通一直阻塞这是cp的做法,AP的做法我们我们可以做一些默认的处理,然后让程序正常执行。比如记录返回一个默认值,比如记录这个应该做什么回头再来补偿。

  27. 在购买具有使用价值的商品的时候,人们通常会购买便宜的。在购买具金融价值的商品的时候人们更愿意购买价格贵的。

  28. 分布式架构,除了要考虑开发人员技术储备,对运维部署人员要求也更高,要求有自动化部署方面的能力,并且对硬件成本也更加浪费,有些负载很低的程序也需要一个单独的进程跑着,分布式建议做法是一个服务配有固定的人员组来维护,以保证服务的稳定,对人力的浪费也更多。使用分布式以后人力成本,开发工期,硬件成本都会暴增,所以不应该盲目的使用分布式微服务架构。

  29. 那些项目可以审核分布式

    • 实验性质的边缘应用,目的是做技术沉淀,为核心业务的微服务改造积累经验

    • 预算充足的新项目,并且这个项目有或者未来有真实的高负载场景的业务需求。

    • 拥有明确业务边界的项目和技术团队。微服务是以业务边界来拆分的,没有业务边界不便于系统和人员的拆分,改造就会困难重重。

  30. redis的单机QPS大概在5W

  31. 分布式集群缓存数据倾斜问题

    • 单节点缓存都在一台redis机器上,不存在缓存倾斜,能顶住就顶,顶不住就升级集群。

    • redis集群有数据分片,只要我们数据分片的时候带上数据id,而不只是业务类型,一般认为缓存数据存储也是均匀的。

    • 缓存数据是均匀的,但是用户行为不是均匀的,有些数据是用户高频使用的热点数据,这种就会把大量的请求分发给固定的保持了热点数据的缓存节点。这个缓存节点的压力就很大了,这时候不是集群抗压不够,而是数据倾斜压垮固定的部分集群。

    • 除了全量缓存集群以外,还可以建立热点缓存集群,只要是标组了热点数据的请求,都去热点数据缓存请求。比如刚发布的热门商品,做活动的商品,最近大家关注度比较高的商品,销售排行比较高的商品,这类都可以放到热点缓存集群去。

    • 这种通过添加额外硬件的做法是比较有效的,如果觉得成本高,可以降低全量集群的配置,然后配置热点集群,降低缓存节点的单点压力。

  32. 除了服务器端缓存上的优化以外,我们还能优化别的地方。

    • 在客户端做热点数据的前置缓存。安卓苹果可以对一些不常变动的热点数据做一个短时间的本地缓存,比如一分钟。浏览器静态资源可以本地缓存,不常变动的动态资源可以用h5 的 localStorage来保存一个比较短的时间,比如一分钟。前端缓存会带来用户数据不一致,这个时间不能设置太长。

    • 应用内做的热点数据的闪电缓存。服务器端全局缓存的某个节点因为保存了某个热点数据Id,有了缓存倾斜容易被压垮,但是应用内缓存不存在这个问题,请求一个商品Id可以去任意一个服务器端节点,而不是把热点数据的缓存请求发给保存了对应id的全局缓存节点。

    • 应用内缓存,有个明显的弊端,缓存更新的时候,它是不会被更新的,因为每个节点都有,所以一般设置的时间都很短比如5秒,以此降低数据不一致影响时长。应用内缓存指的是escache之类的缓存框架,或者就是一个应用捏定时过期的内存。

  33. 无监控不运维,只有分析监控数据,分析出问题才能做出正确的部署调整。凭感觉应该怎么干就是很盲目的,用户习惯,社会情况都决定了用户行为的走向,这些只能通过监控分析得出结论,认为猜测可能偏离真实情况。

  34. 日志收集

    • ELK,(ElasticSearch+ LogStach + Kibana ), 分别查询统计,日志收集,和数据图表化展示。

      由于Logstash做了日志收集和解析比较耗CPU和内存资源,所以我们可以把日志传递到指定的机器去让logstash分析,避免和应用服务器抢资源,但是也带来Logback里面要写死logstash地址的问题,logstash服务器地址和程序耦合了。毕竟logstash原生不支持注册到注册中心。

    • KEFK

      上面的方式有带了了程序和日志分析服务耦合的问题,然后有了FileBeat,FIleBeat是监听日志文件变化,和程序没有任何耦合。然后它把收集到的书籍发送给Logstash或者ES,或者其他的组件来分析。

      如果有多接受源接受日志的需求,可以把FileBeat的数据发送到kafka,然后需要日志的组件来kafka拉取消息就行了。

      话有说回来,越复杂月可靠的东西越贵,KEFK的方案不止要ELK,还要有kafka集群,成本比较高,没钱没资源的,小破公司小集群,还是把所有日志都怼到一台机子上手动查吧,ELK需要的资源也不少,es就是吃内存的大户。

  35. spring @Transation 默认只会在RuntimeException及其子类的时候回滚,如果有直接是Exception直接子类的不会回滚,同样Error及其子类也不会回滚。如果想任何异常都会滚,那么rollbackFor设置为Throwable。

  36. SPA(单页应用)和MPA(多页面应用)的区别,单页面不利于SEO(Search Engine Optimization),但是整体性,维护性都比MPA好,能做到连贯的过度效果。

  37. 索引覆盖 要求select 后面 只能出现一条索引的字段,但是可以出现主键Id。

  38. 如果是查询全表,没有where语句或者只有假删除这种过滤性不强的不能作为索引的字段,也就是没有索引可以用,这种情况的深分页效率很低,我们我们可以考虑通过索引覆盖优化查询效率。

    • 第一种情况,没有排序字段,也就是id排序。
      select ids from test limit 1000000,20;
      select * from test where id in(ids);
      上面的第一行值查询了id,走的索引覆盖,内存索引扫描一次就行,不用全表扫描。第二行,通过id查询,走索引。效率也很高。

    • 第二种,有排序字段,比如所示创建时间。
      select ids from test order by create_date limit 1000000,20;# 这里 用的是 create_date 的索引,不是主键索引。
      select * from test where id in(ids);

      下图是name字段作为排序字段的例子,create_date作为索引也一样。

  39. mysql应该加索引的地方

    • 一对多,如果查询方向是1->多,那么需要在多上建立外键索引。如果查询方向是多->1,那么不需要在多上建外键索引。

    • 多对多,也是类似,中间表里面有两个Id,如果查询方向是A->B那么应该在中间表的中间表的AId上建立索引,如果查询方向是B->A那么应该在中间表的BId上建立索引,如果双向查询,两个分别建立索引。

    • 有列表需求,并且按照时间倒序或者正序排列的,应该考虑在时间字段上建立索引。或者别的排序字段也要考虑加索引。

    • 假删除字段,永远不要建索引,很多时候我们都是查位删除的数据,我们数据库90%以上的数据都是未删除,过滤性太差。过滤性低于10%的我们几乎不用考虑索引。

    • 登录账号,用户昵称或者名字,商品名字,有搜索需求的应该建立索引,并且优先考虑联合索引。

    • 数据量低于1000的表可以不用考虑建索引。但是如果如果有里面有频繁并发修改的字段,需要用到行锁,那么可以通过查询字段查询出id,然后通过id加锁,避免索引不命中,锁表。

    • 分组字段要考虑索引。

  40. 很多时候分页是不需要知道总页码的,而且select count(*) from test ,在数据量比较多的时候会耗时比 业务查询耗时多得多。如果可以,我们要干掉查询总页数的逻辑,直接查询数据。

  41. count(*) count(id),count(别的索引字段),那么效率最高?答案是count(星),效率最高的一定是btree结构最矮胖的,id 所在的聚簇索引,带有数据节点,它一页存的数据有限,那些有索引,并且索引字段文本少,查询就更加矮胖,而且没查数据不用回盘读数据,这时候只看索引矮胖和量小,count(星)就是自动选择,一般来说是最矮胖的那个。

  42. VIP+keepalive 配置图

    双VIP互为主备,最佳配置文件 VI_2

keppalive默认情况主挂了,从顶上,主恢复,又会把从顶下去。可以通过preempt配置,让主恢复的时候不要把从顶下去,这样可以减少IP漂移。

  1. keepalive脑裂的情况会两个节点都会认为知己是主,都会向外广播自己的VIP,导致网络混乱,这个问题我们可以只能在硬件层面尽量避免网络波动,正常局域网如果没有恶意攻击,或者超负荷网络应该是很稳定的。出现脑裂,然后两个主的情况以后,我们首先应该恢复网络,然后重新从节点的网卡服务就行了。

  2. kill -9 keepalive不会回收网卡的VIP配置,V也会一直广播自己的VIP。所以只能kill 不能 -9.

  3. XSS攻击,全程Cross Site Scripting, 安全专家们通常将其缩写成XSS,原本应当是css,但为了和层叠样式表(Cascading Style Sheet,CSS )有所区分,故称XSS。

    攻击方式把html 和 js脚本通过没有验证的输入框,写入数据库,然后这段脚本被显示到html页面的时候会执行,通过执行这段脚本可以获取用户信息,跳转一些钓鱼网站等等,目前依旧大范围的存在。

  4. XSS攻击的预防手段是直接在html中显示的文本一定要检查有没有类似的字符。hutool 的 HtmlUtil.escape()和spring的HtmlUtils.escape()方法都能把里面的敏感字符转移。避免XSS攻击。

  5. 如果你知道结果只有一个,但是用的索引不是唯一索引,那么sql 后面带上 limit 1,可以提升效率,相当于告诉mysql 找打一行就立马返回,不用继续向下找。

  6. sql 查询的结果默认是按照id 排序的,使用查询条件能命中的索引排序比用默认的id排序方式节省资源。

  7. sql优化策略

    • 查看执行计划

    • 查看检查类型,是否命中索引,使用的那种索引

    • 查看使用的那个索引,是不是我们想用的

    • 查看预估的结果行数

    • 查看是否有 文件排序这种效率极端的东西,没有使用索引的并且结果数量很多的时候就会出现这个问题,效率很低。

    • 查看是否有 分组不能命中索引的情况 一般是显示零时表

    • 查看是有有应为数据过滤性差索引失效

    • 多表关联的时候要注意是否每张表都使用了索引

    • 多表关联的时候要注意,驱动表的数据过滤性是否够高(小表驱动大表)

  8. mq之类的分布式和数据库之类的分布式实现完全不同,mq 之类的写,分发就可以了,数据库要CRUD,所以数据库一定要明确的知道这个数据在那个分片,MQ不需要知道,不管给谁,下游的消费者都能收到。所以mq集群能够轻易做到多个写入节点,数据不容易做到。集群模式的mq的从节点只是为了在主挂掉以后能,积压的消息还能被消费,所以这种从一般来说一个就够了。

  9. rocketMq 集群部署关系图

    rocketMQ 主从同步有两种方式一种是同步复制,一种是异步复制,异步复制有丢包的可能,重要消息必须要走同步复制。

    集群模式下面,如果一个主节点挂了,那么积压消息还可以被消费,后面的新消息都会发送给其他可以broker,从挂了,啥影响都没得。rocketMQ里面主挂掉,从是不会自动升级成主的。

    如果一个节点的主从都挂了。新的消息还能发送给其他节点,挂掉节点的积压消息就不能被消费了。

namserver挂掉一个的情况,生产者消费者回去另外一个nameServer获取 broker节点信息。

有一种特殊情况,生产者访问的nameserver可以连接所有broker,消费者使用的nameserver只能连接部分集群,这时候有一部分消息会没有消费者消费。

零时解决就是通过配置中心把消费者那个不网络不全通的nameserver提出掉。让消费者使用另外一个,短时间的网络分区问题不大,长时间的网络不通,应该想办法解决网络问题。

  1. spring cloude contract 是一个 契约测试框架。使用测试通过的测试用例的数据,生产mock文件,然后跟随jar包发布到maven 仓库,然后被调用方下载下来使用。测试通过的数据比普通mock生产的数据更加真实,而且也能应对接口变化。

  2. 本地消息模式的分布式事务

    • 本地服务数据库有一个消息表,发送消息的时候不是真实的发送,而是在本地消息表里面插入一条消息记录

    • 插入贝蒂消息记录的时候通过当前把这个消息放到ThreadLocal里面去

    • 通过aop监听本事务方法已经提交的时候读取到事务里面插入的消息,把它发送出去,如果发送不去出去就放弃。

    • 后台启动一个定时任务轮询位发送的本地消息表,找到位发送的消息重新发送

    • 一般来说这样就完了,但是这样消息只能支持同时完成,不能支持同时回滚,也就是发出消息以后,只能做完,不能消费者发起回滚,带动生产者这边的事务也回滚。

    • 消息被传递到消费者的时候,如果消费成功,通过另外一个队列向生产者那边回复一个消费成功的消息,然后生产者把把本地消息表里面对应记录修改成完成。

    • 如果消费失败,可以发一个消息,让生产者把本地消息表里面对应记录的状态改成失败,然后把前面生产者本地事务里面写入的数据最undo,或者是状态改成已删除。并且,指定时间内没有完成消息发过来的也要默认被滚回。

    • 个人觉得,双队列消息风险很高,延时很重,尤其是通过重试发出的消息。如果一定要这么做,我建议 生产者那边的本地事务只做资源预留,类似TCC事务的try阶段,然后回消息的时候才是做确认阶段的事情。不然这段比较长时间可见的脏数据,很影响用户体验。

    • 只做正向一起成功,不做一起回滚,做到一起提交,已经极大程度的保证了事务的一致性,并且避免了脏读。数据一致的延迟,脏数据的危害要大得多。分布式事务很多时候都是取舍的选择,如果需要理论上绝对不出错,直接XA。

    • 目前为止只有AX事务能保证完全的一致性。别的分布式事务伦理上都会出问题。

  3. 生产常用的 指标收集框架是 Prometheus 和它配到的展示工具一般用 grafana。

  4. 分布式全链路压测,理论上很优秀,但是实施难度非常大,风险非常高,成本非常大,如果不是对自己团队的技术非常有信心,并且也有相应的实践经验,不要轻易尝试。

    • 挑战1,测试数据的产生调配分发,测试数据来源可以是随机生成的测试数据,也可以是线上录制的用户行为,要产生这么大量的测试数据,需要几十甚至几百台自动化测试节点(jmeter,loadRunner之类的集群),他们之间如果有中心话的调配分发任务,中心节点是不容易撑下来的。并且为了模拟真实的网络情况,这些服务器必须是分布到全国各地,很多问题都是网络引起的。

    • 挑战2,测试数据的隔离。生产数据肯定不能和这些测试数据混在一起,否则一些统计指标,活动数据全都无效了。重放的用户行为,也会对用户的数据错乱,数据隔离做不好,会对系统造成致命的打击,错乱的数据会让用户的信任度大打折扣。并且恢复这种大规模的数据错乱也是极其困难的。所哟的三方接口,也都有类似的问题。

    • 挑战3,全系统,全链路,全分支的请求识别。我们必须区分测试请求和真实的用户请求,然后再后面的分支中做出响应的数据隔离,如果在调用链路的传递过程中任意环节丢失了识别标志,这种数据就会混入真实数据,对系统造成极其严重的危害。

    • 上面三点测试数据的产生,测试数据的隔离,老系统的测试数据的识别和处理,是最明显的三个方面的挑战,细微只处很有很多东西。

    • 挑战巨大,优点也很明显也很明显,只要做到了全链路压力测试,很多问题都可以提前发现,先比单点的压测,全链路压测能够相对真的的测出基础组件的压力和系统之间资源争抢带来的问题(资源的争抢不止限于内存,磁盘,CPU,还有网络带宽,还有对其其他服务提供服务能力的争抢)。发现问题解决问题的过程中,系统的稳定性会得到极大的提示。

  5. log4j2 2.x rc1已经爆发重大bug,似乎该漏洞可以控制服务器?2.x rc2以后修复了。

  6. 在没有自己机房,没有独立带宽的情况下,不要轻易使用云服务器存储文件,ECS为例带宽最多200兆,换算成数据也就是25兆每秒。现在普通手机拍摄的照约片3兆一个。每秒8张照片就把这ECS最高配的带宽占满了,这时候我们可以考虑云存储,或者CDN。

  7. CDN 可以为云存储(OSS等),指定IP或者域名的主机,指定函数计算 进行 缓存加速。

  8. 如果让我来做天府通健康码的。

    • 百度一下四川有0.84亿人。我们假设他们每天都会核酸,健康嘛每天8点到9点查看2次(一次上地铁,一次进入写字楼)。

    • 0.84单做1一算。每天录入一亿条核酸茶几结果。mysql 可以用吗?当然可以按照日分表就行了。实际上,四川不能能全员每日核酸核酸。我们就假定1亿次核酸。

    • 一亿的写入,按照4小时,没每秒7000次,如果用mysql必须分表了(mysql单机写入量批量执行可以有几万,单词执行只有数千左右),如果用mongodb,轻松撑下。

    • 些搞定以后我么考查询的问题,查询比较集中一般是早上半个小时打开2次。我们做查询缓存到本地所以大概只有一次,也就是1亿一次查询30 分钟。大概每秒5600次,一个redis节点够呛,,然后还有瞬时压力,所以一定要用redis集群。可以考虑2-10台redis集群。按照峰值20倍估算,10台大概也够了,redis号称10W次每秒的查询速度.

    • 检查结果录入的时候考虑批量,效率就高了,然后同时写入缓存,并且,我们只写入黄马和红马的缓存,绿码都一样,缓存可写可以不写考虑要下发,前端缓存时间,所以还是写缓存吧。

    • 然后就是前端缓存,请求到健康码以后,如果没用用户主动发起的刷新请求,那么每次打开都用第一次打开的缓存。

    • 这样顺表几台机子就能撑起四川一亿人的每天核酸数据。分支业务如果要做, 可以通过收集日志的方式得到,或者异步的触发,统计分级这类的东西,流失几条数据无关紧要,所以不邀请同步,异步是最有效做高效的做法。

  9. 分布式链路追踪两种模式

    • 基于日志 ,比如 sleuth+zipkin

    • 基于java agent 实时收集,比如SkyWalking

  10. OpenTracing 格式示意图 sleuth 就是用的这种格式的全局链路追踪标志

  11. mysql5.6以后对联合索引的检索有了优化,对所索引第一段右模糊然后对后面的一段筛选有优化,5.6以后的做法是尽量在索引筛选,得到最少的id,然后再去回盘。5.6以前如果联合索引前面匹配部分不是连续的了,后面的部分索引就无效,这时候直接通过id去回盘。

  12. 数据库事务默认是没超时机制的,默认直到连接断开,数据库等锁默认超时时间是50秒。

  13. 2PC 和 3PC的 区别,3PC 解决了什么问题,带来了什么问题?

    • 2pc在提交阶段如果网络不通,会导致通知不到的子事务长时间占用排它锁,这份数据长时间锁定,容易引发雪崩。

    • 解决办法就是提交阶段加入超时机制,到期自动提交或者回滚。

    • 3pc事务就是加入了超时机制,提交拆分成两段,执行执行阶段完成告知协调中心以后,如果指定时间内没有预提交就回滚,如果一个子事务等待预提交超时回滚了,后面预提交请求肯定会失败,大家一起回滚。

    • 如果预提交都完成,那么就进入设置超时自动提交,如果指定时间以内,没有收到确认提交的消息,那么就自己提交。这种做法解决了长时间锁定问题。

    • 超时自动提交带来了数据不一致问题,比如第一个子事务收到预提交通知以后就断网了,结果协调者通知第二个子事务预提交失败,然后协调者通知整个事务回滚,正常网络通信的都回滚,自事务1如果在在即超时提交以前连上网,也能一起回滚,如果连不上超时时间一到,自己就自动确认了,这样数据就不一致了。

    • 3PC解决了确认通知没有送达导致的长时间锁定问题,3PC解决了长时间锁定问题,但是带来了数据不一致问题。既然会数据不一致,那还用个毛的3PC ,TCC 等更加 高效的分布式事务解决方案是更好的选择。

  14. 一些东西的负载

    • nginx单机的负载是5w/s左右

    • redis 官方号称读写次数大概是10W次/s

    • lLVS是10万级别,据说可以到80万/s

    • F5负载均衡器可以达到200w/s

    • mysql单机的多线TPS大概是2000-4000,单线批量大概2.5W条数据每秒,多线批量大概5W条每秒。

    • RabbitMq 万级别

    • RocketMQ 10W级别

    • Kafka 10W级别,我试过家用PC可以轻松40W以上的吞吐量

  15. 硬件的读写延时,和读写速度

    • 读取寄存器,1纳秒

    • 读取缓存1到10纳秒,一级缓存速度2TB/s,二级缓存1TB/s,三级缓存500G/s

    • 读取内存 10-60纳秒,ddr4 家用PC 60纳秒,内存读写速度大概,50G/s

    • 读取硬盘 毫秒级别。机械硬盘stata3接口 500MB/s上限,固态更具接口不同0.5G/s 到 8G/s

    • 网络 毫秒级别,但是存在不稳定因素,不同网线类型速度不一样,还要受到网卡的限制。


  16. TCC 预留资源和提交阶段具体应该怎么做

    • 如果是插入操作,那么预留阶段插入中间状态,提交阶段改中间状态位正常启用启用状态。

    • 如果是删除操作,预留阶段可以检查ID存在与否(有要求删除不存在提示异常的这里就可以抛出),也可以啥都不干,提交阶段直接删除。

    • 修改数据分2种,幂等的修改操作,和非幂等的操作

    • 幂等的修改,比如修改昵称,如果有检查重复的要求,预留阶段检查,如果没有重复要求,只检查格式,如果有必要,可以分布式锁定这个名字,不然别人用,提交阶段直接修改。

    • 非幂等的修改,比如修改库存,金额等,预留阶段需要冻结金额,生成冻结记录,如果冻结失败,直接回滚。提交阶段,找到冻结记录,解冻冻结记录,扣除金额字段对应金额。

  17. 银行业务分为核心业务和配套业务,配套业务是不能直接获取核心业务数据的,不管是从安全还是性能的角度来说都是不允许的,一般的做法是通过ETL(导出转换导入)让配到系统拿到数据。银行核心系统挑选系统闲置时间,把单日数据按照一定格式打包成数据文件(CVS文件或者其他格式),然后传输到FTP文件服务器,需要这些数据文件的配套服务,通过定时任务去获取这些数据,然后解析,转化导入到自己的系统。这种方式可以大大的降低核心业务的压力,并且规避很多数据安全方面的风险,代价就是数据不是即时的,是延迟一天的。

  18. 存放数据文件的地方,叫做数据仓库或者数据中台或者数据集市或者数据湖。ODS(Operation Data Store)是数据湖的第一层。后面还有DWD(data warehouse details),DWM(Data Warehouse Middle),DWS(Data Warehouse Service)等等.

  19. 为什么不建议使用默认的线程池?

    • Executors的线程池newFixedThreadPool(n),线程数量是固定的,但是积压任务的队列是Integer.MAX_VALUE,消费能力有限的时候会导致大量任务积压,OOM。

    • Executors.newCachedThreadPool(),最大线程数量上限很高,是Integer.MAX_VALUE,来多少任务,创建多少线程,太多的线程,cpu时间都浪费到频繁的上下文切换,CPU的性能被大量浪费。

    • 更好的做法确定一个明确和最大线程数,然后确定一个明确的积压队列数。不能无限积压,和无效线程数量。假如设定10个线程的情况下,每秒100个任务,并要求任务10秒内必须被处理,那么我们的设置核心线程数位2个(1/4-1/5),最大线程数10个,阻塞队列100*10个。如果积压队列满了,应该明确处理策略,比如排除异常。

    • 核心线程数大概设置为最大线程数量的1/4。

    • 最大线程数的设置多少看任务类型,如果是计算密集型的任务,最大线程数设置为CPU数量的2倍+1个,现在的CPU一般都是带有超线程的,一个CPU2个线程,所以是 2N+1(N是物理核心的数量)。如果是IO密集型的任务线程数量是2N/(1-阻塞系数),如果一个任务耗时10秒,其中9秒在IO阻塞,name阻塞系数就是0.9,线程池数量就可以设置测逻辑核心数量的10倍。

  20. rocketMQ,kafka的有序都是队列内有序,如果要全局有序,那么只能一个主题下面只能有一个队列,正常部分有序就够了,只要保证需要有序的消息发送到同一个队列,并且消费者端,保证一个队列只能被一个消费者消费。

  21. 怎么考虑系统集群各个节点需要部署多少台服务?

    • 首先单接口的QPS和TPS,可以通过压测软件得到。整个系统的话,要根据用户使用习惯,和单接口负载能力计算出。

    • 通过压测和指标监控,我们可以发现那些地方是系统负载能力低的短板,这时候考虑缓存,分治等办法解决这届负载能力低的地方。

    • 然后是根据估算得到用户需要的平均并发量的,还有带宽数据。

    • 瞬时并发量一般要考虑平均并发量的20倍。如果是秒杀之类的要单独评估。

    • 计算负载的时候,按照单机的80%计算,并且预警指标也设置为80%,80%可以留给系统扩容,处理问题的时间。

  22. mysql主从复需要注意的问题

    • 异步复制,本地事务commit了,响应了用户操作以后早通过binlog把数据异步同步到从服务器,异步的东西slaver能不能收到看运气。myq5.3以前只有异步复制,后面的版本异步复制也是默认方案。

    • 半同步复制,数据同步在响应用户之前,如果同步数据失败,会抛出异常,并且至少有一个节点同步完成以后才不会抛出异常。提交发起数据同步的时间节点有2个,默认是commit以后,如果要使用MHA需要给出commit以后( after_sysnc ),5.5以后通过插件支持,半同步复制有两种情况.感觉这里的默认配置在commit以后,响应用户以前同步做半同步没啥意义呀?

    • 全同步复制,要求所有节点都收到数据以后才commit才算数据写入成功。5.7.17以后才支持。

  23. mysql高可用的2中常见方案

    • MHA(MasterHigh Availability),依赖三方软件,这种方式兼容性好,对数据库版本,配置等没有太多的限制。使用的半同步机制。支持一主多从的搞可用,主节点挂掉以后,通过VIP的IP漂移,把VIP指向新的主主节点,因为是半同步复制,所以我们只要选出那个同步数据最多的那个从节点来做主就行。

    • MGR(MySQL Group Replication),依赖mysql 5.7.17的全同步复制,支持多写入节点。因为是全节点同步才算写入,所以不存在数据不一致的问题,因为多节点的主写入,可用性也会更高,最多支持9个节点。但是限制很多,对必须使用innnoDB,隔离级别必须是read commited,外键约束不能使用,日志格式必须是rows,不能锁表,DDL语句不支持事务,不支持GAP lock等等一些列的限制。

  24. G1的 G1HeapRegionSize 需要是1M^n倍,最多是 32M,region 的数量是更具 regionSise算出来的。mixGC以后如果老年代的比例依旧超过45%那么会再次mixGc,如果连续8次mixGC的结果老年代超过45% ,那么就会进行fullGC,fullGC是单线程,全region回收。效率极低,会导致STW的时间过程,我们要避免STW 。

posted on 2023-01-01 14:00  zhangyukun  阅读(58)  评论(0编辑  收藏  举报

导航