kafka

- mq使用场景#

  • 异步
    • 上游不需要同步拿到结果,执行过程又耗时
    • 好处:异步可以让服务端的线程不同步阻塞,提高服务端的吞吐量
    • eg: 视频制作
  • 解耦
    • 多个下游相同依赖
    • 好处:不与依赖方耦合,不用额外开发
    • eg: 下单成功消息,不同的依赖方做不同的事情
  • 削峰
    • 请求高峰期
    • 好处:保护下层存储
    • eg: 下单

- kafka如何保证消息顺序性#

针对消息有序的业务需求,还分为全局有序和局部有序。

  • 全局有序:一个Topic下的所有消息都需要按照生产顺序消费。 要满足全局有序,需要1个Topic只能对应1个Partition。consumer内部单线程。
  • 局部有序:一个Topic下的消息,只需要满足同一业务字段的要按照生产顺序消费。例如:Topic消息是订单的流水表,包含订单orderId,业务要求同一个orderId的消息需要按照生产顺序进行消费。
    在发消息的时候指定Partition Key,Kafka对其进行Hash计算,根据计算结果决定放入哪个Partition。这样Partition Key相同的消息会放在同一个Partition。consumer内部单线程消费或接亲缘型线程池(内存队列)。此时,Partition的数量仍然可以设置多个,提升Topic的整体吞吐量。

- kafka如何保证消息可靠性(不丢消息)#

  1. 多副本写入 + 数据持久化,通过后台线程将消息持久化到磁盘
  2. 生产者ack机制,可设置写入副本情况,ack=0 可不写入就成功,ack=1 写入leader副本,ack=all 写入全部副本
  3. 生产者重试机制,出现网络问题可自动重试,可设置重试间隔
  4. 消费者使用手动提交,可保证消费一次

- kafka如何保证高可用#

副本机制。
Kafka在0.8版本之前是没有HA机制来确保高可用的,当某一个broker挂掉,partition就挂了。即存在单点故障。
0.8之后提供了副本机制,副本机制会将 一个broker下某个topic的一个partition放入到另外一个broker里,这个备份的分区和原分区都叫做副本(replica)。在所有的副本里,只能有一个leader,其余的副本都作为follower,同一时间内只有leader负责读写,follower不起任何作用。这样做的原因是为了确保消费者单调读 且 确保能立即读取写入的信息(Read-your-writes)。其他所有的follower会异步的拉去leader消息,拉的快的会进入ISR里,拉的慢得超过replica.lag.time.max.ms 配置的超时值的,会被踢进OSR里,这个过程是动态的,如果一个follower开始没跟上leader的消息写入速度,被踢出了ISR,等到跟上后又会重新进入ISR。当leader挂掉之后,为了保证高可用性,从ISR中获取一个副本,升格为leader。如果ISR全挂了,有两种策略,一是等待ISR第一个恢复的副本,二是开启unclean从OSR中选择一个副本作为leader。为保证高可用 可以选择第二种牺牲一致性的方式。

- kafka整体架构#

- kafka服务端架构#

服务端是reactor模式,selector + threadPool
acceptor里有个selector, 每个processor中有一个selector,都是NIO,IO多路复用
processor会通过注册/取消 OP_READ _ 事件,保证每个连接上只有一个请求和一个对应的响应,从而实现每个连接请求的顺序性。

- kafka 基本特性#

  1. 高吞吐量、低延迟
    kafka每秒可以处理几十万条消息,它的延迟最低只有几毫秒,每个topic可以分多个partition, consumer group 对partition进行consume操作。
  2. 可扩展性
    kafka集群支持热扩展
  3. 持久性、可靠性
    消息被持久化到本地磁盘,并且支持数据备份防止数据丢失
  4. 容错性
    允许集群中节点失败(若副本数量为n,则允许n-1个节点失败)
  5. 高并发
    支持数千个客户端同时读写

- kafka 如何保证高性能#

  • 顺序读写。Kafka的message是不断追加到本地磁盘文件末尾的,而不是随机的写入,这使得Kafka写入吞吐量得到了显著提升 。
  • 页缓存。kafka重度依赖页缓存技术,kafka只是将数据写入页缓存(内存)中而不直接操作磁盘,由操作系统决定什么时候把页缓存中数据刷到磁盘上。同时写入页缓存的数据是按照磁盘顺序去写入的,因此刷到磁盘上的速度也较快。当读操作发生时,先从页缓存中查询是否有所需信息,若没有才会调度磁盘。
  • 零拷贝。这里的零拷贝并非指一次拷贝都没有,而是避免了在内核空间和用户空间之间的拷贝。页缓存 -> socket缓冲区。
  • 分区分段 + 索引。通过这种分区分段的设计,Kafka的message消息实际上是分布式存储在一个一个小的segment中的,每次文件操作也是直接操作的segment。为了进一步的查询优化,Kafka又默认为分段后的数据文件建立了索引文件,就是文件系统上的.index文件。这种分区分段+索引的设计,不仅提升了数据读取的效率,同时也提高了数据操作的并行度。
  • 批量压缩。批量的消息可以通过压缩的形式传输并且在日志中也可以保持压缩格式,直到被消费者解压缩,减少网络IO.

- controller broker机制#

  1. 本身也是一个普通的Broker,Kafka集群中始终只有一个Controller Broker。
  2. controller broker具体作用:
    创建、删除主题,增加分区并分配leader分区
    集群Broker管理(新增 Broker、Broker 主动关闭、Broker 故障)
    preferred leader选举
    分区重分配
  3. Broker选举:第一个启动的broker会在zk中创建临时节点/controller,利用zk的强一致性,成为唯一的控制器节点,而其余的broker会监听该节点。随后由controller决定每个broker中副本的master,每当broker挂掉,controller会检测其副本,重新决定出master。当控制器broker断开时,/controller临时节点会删除,其余连接的broker将收到事件通知,抢占式注册为新的controller。

- rocketmq & kafka#

  • 性能
    Kafka单机写入 TPS 号称在百万条/秒;
    RocketMQ 大约在10万条/秒。
    结论:追求性能的话,Kafka单机性能更高。
  • 可靠性
    RocketMQ支持异步/同步刷盘;异步/同步Replication;
    Kafka使用异步刷盘方式,异步Replication。
    结论:RocketMQ所支持的同步方式提升了数据的可靠性。
  • 实时性
    均支持pull长轮询,RocketMQ消息实时性更好
    结论:RocketMQ 胜出。
  • 支持的队列数
    Kafka单机超过64个队列/分区,消息发送性能降低严重;
    RocketMQ 单机支持最高5万个队列,性能稳定
    结论:长远来看,RocketMQ 胜出,这也是适合业务处理的原因之一
  • 消息顺序性
    Kafka 某些配置下,支持消息顺序,但是一台Broker宕机后,就会产生消息乱序;
    RocketMQ支持严格的消息顺序,在顺序消息场景下,一台Broker宕机后,
    发送消息会失败,但是不会乱序;
    结论:RocketMQ 胜出
  • 消费失败重试机制
    Kafka消费失败不支持重试
    RocketMQ消费失败支持定时重试,每次重试间隔时间顺延。
  • 定时/延时消息
    Kafka不支持定时消息;
    RocketMQ支持定时消息
  • 分布式事务消息
    Kafka不支持分布式事务消息;
    RocketMQ支持分布式事务消息
  • 消息查询机制
    Kafka不支持消息查询
    RocketMQ支持根据Message Id查询消息,也支持根据消息内容查询消息
  • 消息回溯
    Kafka理论上可以按照Offset来回溯消息
    RocketMQ支持按照时间来回溯消息,精度毫秒,例如从一天之前的某时某分某秒开始重新消费消息.

rocketmq功能性和可靠性更强一些。

zk2021

定义&特点#

定义:分布式协调服务。中心化的服务,维护配置信息,命名服务,分布式同步器(锁、屏障、队列等),分组服务(组成员检测)。
抽象一下,即分布式协调服务。自己本身没有太大意义,用来协调分布式应用,类比一种进程间通信方式。

特点:纯内存结构,吞吐高,单机 1w 写qps,4w读 qps。(参考值,和配置相关)

总结: 在粗粒度分布式锁,分布式选主,主备高可用切换等不需要高 TPS 支持的场景下有不可替代的作用

数据模型#

树形结构,类文件系统。每个树节点znode,每个节点即是文件,也是文件夹,修改操作只能整体 set,不支持部分修改。

ephemeral znode. 临时节点,和 session 生命周期一致,可以用来检测机器在线状态。
sequence znode. 顺序节点,自动编号,可以用来实现统一命名。(redis incr 也可以做到)
watch 机制。znode 新增、删除、修改、子节点变更及时通知 client。

分布式实现方式#

zk 使用 replication,统一收口写,读水平扩展。适用于读多写少场景。

zk选主#

zab协议:ZooKeeper atomic broadcast,zk原子广播协议。论文
规则是 majority vote,超过半数认可的 server 成为 leader。FastLeaderElection。
大致过程,每台 server 初始广播 (serverEpoch, zxid, serverId),默认选择自己为 leader。判断逻辑先比较 epoch 最大,再比较 zxid 最大,最后比较 serverId 最大。
初始第一次很快是 serverId 最大的成为 leader(没有出现断网断电等异常情况时)

zk主从同步#

leader 连接所有的 follower,发送操作指令,2阶段提交,先 proposal 再 commit,超过半数 proposal 就进行 commit。commit 失败怎么办?2阶段提交都有的问题,概率小不做考虑。
同样会有主从延迟问题,一致性保证客户端看到的数据视图一致性,变更顺序一致性,并且在有限时间内保证 server 数据达到最新状态。

zk性能瓶颈#

  • 单机处理写请求
    majority vote 要求超过一半的 server 存活,server 数量 = 2*n + 2,n越大容错性越强,可以支持n台机器挂掉情况下服务依然可用。
    同时,n越大,写扩散越大,写能力会变弱,查询能力会线性增长。单机 5w qps级别. (1w 写qps,4w读 qps)

  • 事务日志落盘
    事务日志保证 client 写成功后数据不会丢失,write ahead,最好配置单独的磁盘。顺序写,所以依然可以有 1w 级别的写 qps。

  • 纯内存操作树结构
    纯内存,容纳数据有限,线上服务器内存,比如32G。不能用于通常的数据存储,主要用于元数据。单节点现在最大 1m,太大会严重影响读写 qps,对于网络压力也很大。

zk在mysql应用#

mysql 主从库切换怎么做的?
直接用的 zk 的能力。数据同步通过 binlog 追加,写操作必须主库完成,一台从库 binlog 完成才返回。
从库应用binlog会有时延,所以会有主从延迟。日常10ms到100ms左右。后面的从库延迟更多。

zk是否适合服务注册发现#

  1. 服务注册发现规模巨大,服务规模 =F{服务 pub 数, 服务 sub 数},几千个服务,大服务集群机器上w台,比如淘宝交易系统就2w台机器,支撑200w qps。发布过程中会有很大的写入压力和监听扩散。

  2. 注册中心不需要很强的一致性,也不需要持久的事务日志。短暂的服务列表不一致只是造成部分的负载不均衡,影响不大。另外持久化事务日志保证宕机后快速恢复对于服务发现来说没啥用,只关注实时情况。

MVCC-2021

总结#

MVCC的核心实现主要基于两部分:多事务并发操作数据与一致性读实现。

RC的本质:每一条SELECT都可以看到其他已经提交的事务对数据的修改,只要事务提交,其结果都可见,与事务开始的先后顺序无关。
RR的本质:第一条SELECT生成ReadView前,已经提交的事务的修改可见。

多事务并发操作数据#

多事务并发操作数据核心基于Undo log进行实现,Undo log可以用来做事务的回滚操作,保证事务的原子性。
同时可以用来构建数据修改之前的版本,支持多版本读。

InnoDB中,每一行记录都有两个隐藏列:DATA_TRX_ID和DATA_ROLL_PTR。(若没有主键,则还有一个隐藏主键)
DATA_TRX_ID:记录最近更新这条记录的事务ID(6字节)
DATA_ROLL_PTR:指向该行回滚段的指针,通过指针找到之前版本,通过链表形式组织(7字节)
DB_ROW_ID:行标识(隐藏单增ID),没有主键时主动生成(6字节)
当存在多个事务进行并发操作数据时,不同事务对同一行的更新操作产生多个版本,通过回滚指针将这些版本链接成一条Undo Log链。

操作过程如下:
1、将待操作的行加排他锁。
2、将该行原本的值拷贝到Undo Log中,DB_TRX_ID和DB_ROLL_PTR保持不变。(形成历史版本)
3、修改该行的值,更新该行的DATA_TRX_ID为当前操作事务的事务ID,将DATA_ROLL_PTR指向第二步拷贝到Undo Log链中的旧版本记录。(通过DB_ROLL_PTR可以找到历史记录)
4、记录Redo Log,包括Undo Log中的修改。
INSERT操作:产生新的记录,其DATA_TRX_ID为当前插入记录的事务ID。
DELETE操作:软删除,将DATA_TRX_ID记录下删除该记录的事务ID,真正删除操作在事务提交时完成。

一致性读实现#

在InnoDB中,对于不同的事务隔离级别,一致性读实现均不相同,具体如下:
READ UNCOMMITED隔离级别:直接读取版本的最新记录。
SERIALIZABLE隔离级别:通过加锁互斥访问数据实现。
READ COMMITED和REPEATABLE READ隔离级别:使用版本链实现。(ReadView,可读性视图)
对于RC与RR隔离级别,实现一致性读都是通过ReadView,也就是今天的重点,什么是ReadView?

MVCC ReadView
ReadView是事务开启时,当前所有活跃事务(还未提交的事务)的一个集合,ReadView数据结构决定了不同事务隔离级别下,数据的可见性。

up_limit_id:最先开始的事务,该SQL启动时,当前事务链表中最小的事务id编号,也就是当前系统中创建最早但还未提交的事务
low_limit_id:最后开始的事务,该SQL启动时,当前事务链表中最大的事务id编号,也就是最近创建的除自身以外最大事务编号
m_ids:当前活跃事务ID列表,所有事务链表中事务的id集合
注:ID越小,事务开始的越早;ID越大,事务开始的越晚

1、下面所说的db_trx_id,是来自于数据行中的db_trx_id字段,并非开启了一个事务分配的ID,分配的事务ID只有操作了数据行,才会更新数据行中的db_trx_id字段
2、ReadView是与SQL绑定的,而并不是事务,所以即使在同一个事务中,每次SQL启动时构造的ReadView的up_trx_id和low_trx_id也都是不一样的
up_limit_id表示“低水位”,即当时活跃事务列表的最小事务id(最早创建的事务),如果读取出来的数据行上的的db_trx_id小于up_limit_id,则说明这条记录的最后修改在ReadView创建之前,因此这条记录可以被看见。

low_limit_id表示“高水位”,即当前活跃事务的最大id(最晚创建的事务),如果读取出来的数据行上的的db_trx_id大于low_limit_id,则说明这条记录的最后修改在ReadView创建之后,因此这条记录肯定不可以被看见。

如果读取出来的数据行上的的db_trx_id在low_limit_id和up_limit_id之间,则查找该数据上的db_trx_id是否在ReadView的m_ids列表中:

如果存在,则表示这条记录的最后修改是在ReadView创建之时,被另外一个活跃事务所修改,所以这条记录也不可以被看见。
如果不存在,则表示这条记录的最后修改在ReadView创建之前,所以可以看到。

REPEATABLE READ下的ReadView生成
每个事务首次执行SELECT语句时,会将当前系统所有活跃事务拷贝到一个列表中生成ReadView。

每个事务后续的SELECT操作复用其之前生成的ReadView。

UPDATE,DELETE,INSERT对一致性读snapshot无影响。

示例:事务A,B同时操作同一行数据

若事务A的第一个SELECT在事务B提交之前进行,则即使事务B修改记录后先于事务A进行提交,事务A后续的SELECT操作也无法读到事务B修改后的数据。
若事务A的第一个SELECT在事务B修改数据并提交事务之后,则事务A能读到事务B的修改。
针对RR隔离级别,在第一次创建ReadView后,这个ReadView就会一直持续到事务结束,也就是说在事务执行过程中,数据的可见性不会变,所以在事务内部不会出现不一致的情况。

READ COMMITED下的ReadView生成
每次SELECT执行,都会重新将当前系统中的所有活跃事务拷贝到一个列表中生成ReadView。

针对RC隔离级别,事务中的每个查询语句都单独构建一个ReadView,所以如果两个查询之间有事务提交了,两个查询读出来的结果就不一样。

redis_2021

- redis支持的基础数据类型#

SDS (Simple Dynamic String)是 Redis 最基础的数据结构。直译过来就是”简单的动态字符串“。
链表:自定义双向链表:listNode
跳跃表:skipList. 链表的一种,是一种利用空间换时间的数据结构。跳表平均支持 O(logN),最坏O(N)复杂度的查找。
压缩链表:为了尽可能节约内存设计出来的双向链表。
字典: hash
int, intset: 数字集合

- redis支持的对象及实现方式#

String

最常规的get/set操作,value可以是数字和string
一般做一些复杂的计数功能的缓存
sds

string类型控制在10KB以内,hash、list、set、zset元素个数不要超过5000

hash

value存放的是结构化的对象
比较方便的就是操作其中的某个字段
在做单点登录的时候,就是用这种数据结构存储用户信息,以cookieId作为key,设置30分钟为缓存过期时间,能很好的模拟出类似session的效果

Redis 的 hash 表 使用 ziplist 和 字典 实现的。
键值对的键和值都小于 64 个字节, 键值对的数量小于 512。
都满足的时候使用 ziplist,否则使用字典。

list

使用List的数据结构,可以做简单的消息队列的功能
还有一个是,利用lrange命令,做基于redis的分页功能,性能极佳,用户体验好
一个场景,很合适—取行情信息。就也是个生产者和消费者的场景。LIST可以很好的完成排队,先进先出的原则。

底层是一个 ziplist 或者 linkedlist。
当列表对象保存的字符串元素的长度都小于64字节。保存的元素数量小于512个。

set

set堆放的是一堆不重复值的集合。所以可以做全局去重的功能
为什么不用JVM自带的Set进行去重?因为我们的系统一般都是集群部署,使用JVM自带的Set,比较麻烦,难道为了一个做一个全局去重,再起一个公共服务,太麻烦了
另外,就是利用交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好等功能

Redis 的集合底层是一个 intset 或者 一个字典(hashtable)。
这个比较容易理解:
当集合都是整数且不超过512个的时候,就使用intset。
剩下都是用字典。

zset (sorted set)

sorted set多了一个权重参数score,集合中的元素能够按score进行排列。可以做排行榜应用,取TOP N操作。

Redis 的有序集合使用 ziplist 或者 skiplist 实现的。
元素小于 128 个
每个元素长度 小于 64 字节。
同时满足以上条件使用ziplist,否则使用skiplist。

- rediis 为什么快#

官方给出的数字是读写性能可以打到10W/秒

  1. Redis所有数据都是放在内存中的
  2. Redis使用了单线程架构,预防了多线程可能产生的竞争问题
  3. Redis的IO多路复用,实现Reactor模型

- redis的过期删除策略#

定时删除,用一个定时器来负责监视key,过期则自动删除。虽然内存及时释放,但是十分消耗CPU资源。在大并发请求下,CPU要将时间应用在处理请求,而不是删除key,因此没有采用这一策略.

定期删除,redis默认每个100ms检查,是否有过期的key,有过期key则删除。需要说明的是,redis不是每个100ms将所有的key检查一次,而是随机抽取进行检查(如果每隔100ms,全部key进行检查,redis岂不是卡死)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除

于是,惰性删除派上用场。也就是说在你获取某个key的时候,redis会检查一下,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除。

- 跳表#

https://blog.csdn.net/qq_34412579/article/details/101731935

- redis的内存淘汰机制#

在Redis内存使用达到设定上限时,触发满容淘汰策略释放内存

Redis 目前提供8种策略:

volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰

volatile-random:从已设置过期时间的数据集中任意选择数据淘汰

volatile-lfu:从已设置过期时间的数据集中挑选最不经常使用的数据淘汰

volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰

allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个是最常用的)

allkeys-random:从数据集中任意选择数据淘汰

allkeys-lfu:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的key

no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧!

- 如果redis有热点key怎么解决#

本地缓存, 拆分:eg: 按日期拆分、根据业务拆分

- redis缓存击穿、缓存穿透、缓存雪崩怎么解决#

缓存穿透

定义: 访问一个不存在的key,缓存不起作用,请求会穿透到DB,流量大时DB会挂掉。

解决方案:

  1. 缓存空值 (值少时)
  2. 布隆过滤器, 特性: 没有的肯定没有,有的不一定有

缓存击穿

定义:并发性,一个存在的key,在缓存过期的一刻,同时有大量的并发请求,这些请求都会击穿到DB,造成瞬时DB请求量大、压力骤增。

解决方案:

  1. 分布式锁,在访问key之前,采用分布式锁SETNX(set if not exists)来设置另一个短期key来锁住当前key的访问,访问结束再删除该短期key
  2. 双重缓存,设置不同过期,其实同时解决了key过热 和 缓存击穿问题,如果更新的操作确实很耗时,返回的有损请求比较多,那确实需要双重缓存了,再放一份到本地、分布式缓存的另一个key

缓存雪崩

定义:大量的key设置了相同的过期时间,或者某台服务器宕机,导致大量缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩。

解决方案:

  1. 提前预防,主从加集群,主从可以一个实例挂了,另一个可以顶上。集群数据分片,即使一个分片上的主从都挂了,打到db的量也不会是全部
  2. 将key的过期时间设置时添加一个随机时间,分散过期时间
  3. 如果是热点key,可以加分布式锁,减少并发量
  4. 二级缓存(本地缓存),减少db压力

击穿是单个,必定是热点key,雪崩是很多,不一定是热点key,对应的解决方案也有不一样的地方,雪崩有一个设置过期时间加随机数,雪崩用二级缓存比较好使,但击穿就没太大必要

- redis执行一条命令的过程#

整体:

  1. 第一步是建立连接阶段,响应了socket的建立,并且创建了client对象;
  2. 第二步是处理阶段,从socket读取数据到输入缓冲区,然后解析并获得命令,执行命令并将返回值存储到输出缓冲区中;
  3. 第三步是数据返回阶段,将返回值从输出缓冲区写到socket中,返回给客户端,最后关闭client

细节:

  1. 客户端向服务端发起建立 socket 连接的请求,那么监听套接字将产生 AE_READABLE 事件,触发连接应答处理器执行。处理器会对客户端的连接请求进行应答,然后创建客户端套接字,以及客户端状态,并将客户端套接字的 AE_READABLE 事件与命令请求处理器关联。
  2. 客户端建立连接后,向服务器发送命令,那么客户端套接字将产生 AE_READABLE 事件,触发命令请求处理器执行,处理器读取客户端命令,然后传递给相关程序去执行。
  3. 执行命令获得相应的命令回复,为了将命令回复传递给客户端,服务器将客户端套接字的 AE_WRITEABLE 事件与命令回复处理器关联。当客户端试图读取命令回复时,客户端套接字产生 AE_WRITEABLE 事件,触发命令回复处理器将命令回复全部写入到套接字中

个人理解:

  1. 所有事件都是注册在一个aeEventLoop上,aeEventLoop里面是事件数组
  2. 事件监听的是socket的文件描述符,socket分server和client,连接事件监听serversocket, 建立client之后注册读事件监听clientsocket, 执行完之后注册写事件监听clientsocket
  3. 每个client有个输入输出缓冲区方便数据就绪
    关键点就是他娘的用了IO多路复用,提升执行效率

- select epoll#

select 是轮询所有的文件描述符
epoll 是走回调,能支持的数量更多,效率更高

- redis主从同步过程#

对于master

  1. 首先slave向master发起sync同步命令,这一步在slave启动后触发,master被动的将新的salve加入到自己的主备复制集群。
  2. master在收到sync后开启BGSAVE命令,BGSAVE是Redis的一种全量模式的持久化机制
  3. BGSAVE完成后,master将快照信息发给salve。
  4. 发送期间,master收到来自客户端新的写命令,除了正常的相应之外,都存入一份back-log队列
  5. 快照信息发送完成后,master继续发送backlog命令
  6. backlog发送完成后,后续的写操作同时发送给salve,保持实时的异步复制

对于slave

  1. 发送完sync命令后,继续对外提供服务。
  2. 开始接收master的快照信息,此时,slave将现有数据清空,并将master快照写入内存
  3. 接收backlog内容并执行,也就是回放,期间对外提供读请求
  4. 继续接收后续来自master的命令副本,并继续回放,保持数据和master数据一致。

如果有多个slave节点并发发送sync命令给master,企图建立主备关系,只要第二个salve的sync命令发生在master完成BGSAVE之前,第二个salve将收到和第一个salve相同的快照和后续backlog,否则第二个salve的sync将触发master的第二次BGSAVE

断点续传 (部分重同步)
Redis支持PSYNC用于替代SYNC,做到基于断点续传的主备同步协议,master和slave两端通过维护一个offset记录当前已经同步过的命令,slave断开期间,master的客户端命令保存在缓存中,salve重连之后,告知master断开时的最新offset,master则将缓存中大于offset的数据发送给slave。

- redis主从 故障转移 failover sentinel Raft协议#

failover决策
当一台master宕机后,要发起failover流程。其中只有一个sentinel节点可以作为failover的发起者,让他作为leader主导选举。
Redis的sentinel leader选举机制采用类似raft协议实现这个选举算法。

  1. sentinelState的epoch变量类似于raft协议中的term(选举回合)
  2. 每一个确认了master“客观不可用”的sentinel节点都会向周围广播自己的参选请求
  3. 每一个收到参选请求的sentinel节点如果还没人向他发送过参选请求,它就将本回合的意向置为第一个参选sentinel并回复他,如果本回合内回复过意向,那么拒绝所有参选请求,并将已有意向回复给参选的的sentinel
  4. 每个发送参选请求的sentinel节点,如果收到了超过一半的意向同意某个的sentinel参选(可能是本人),那么将成为leader,如果回合内进行了长时间还没有选出leader,那么进行下一个回合。

leader sentinel确定了之后,从所有的slave中依据一定的规则选一个新的master,并告知其他slave连接这个新的master。
新master的选举规则

  1. 过滤不健康(主观下线,断线),5秒内没有回复过sentinel节点ping相应,与主节点失联超过
    down-after-millisecond*10 时间的
  2. 选择slave-priority最高的从节点列表,如果存在返回,否则继续
  3. 选择复制偏移量最大的从节点,
  4. 选择runid最小的从节点

如果多个回合还没有选出leader sentinel,怎么办?
选举过程非常快,一般谁先判断出主观下线谁就是leader

- redis高可用#

高可用(主从、哨兵、集群)

- RDB 和 AOF#

RDB 将数据库的快照(snapshot)以二进制的方式保存到磁盘中。
AOF 则以协议文本的方式,将所有对数据库进行过写入的命令(及其参数)记录到 AOF 文件,以此达到记录数据库状态的目的。

- AOF重写机制#

what: 创建一个新的 AOF 文件来代替原有的 AOF 文件, 新 AOF 文件和原有 AOF 文件保存的数据库状态完全一样, 但新 AOF 文件的体积小于等于原有 AOF 文件的体积。
why: 有些被频繁操作的键, 对它们所调用的命令可能有成百上千、甚至上万条, 如果这样被频繁操作的键有很多的话, AOF 文件的体积就会急速膨胀, 对 Redis 、甚至整个系统的造成影响
how:
AOF 重写并不需要对原有的 AOF 文件进行任何写入和读取, 它针对的是数据库中键的当前值。

重写使用子进程
子进程进行 AOF 重写期间,主进程可以继续处理命令请求。
子进程带有主进程的数据副本,使用子进程而不是线程,可以在避免锁的情况下,保证数据的安全性。

主进程工作

  1. 处理命令请求。
  2. 将写命令追加到现有的 AOF 文件中。
  3. 将写命令追加到 AOF 重写缓存中。

当子进程完成 AOF 重写之后, 它会向父进程发送一个完成信号, 父进程在接到完成信号之后, 会调用一个信号处理函数, 并完成以下工作:

  1. 将 AOF 重写缓存中的内容全部写入到新 AOF 文件中。
  2. 对新的 AOF 文件进行改名,覆盖原有的 AOF 文件。

mysql

- 什么情况下索引失效#

  1. 不符合联合最左匹配
  2. like前导模糊查询 (可以通过 REVERSE()函数来创建一个函数索引解决)
  3. 字段类型不匹配
  4. 索引字段施加函数
  5. or的多个字段都要索引(只要有一个没有索引,就无法使用索引)
  6. != , <> is null, is not null, not in都会使索引失效
  7. 索引情况不好(索引列数据区分度太小,例如性别列),范围太大,扫表.

- InnoDB 支持的索引类型#

  1. B+树索引:
  2. 全文索引:就是倒排索引
    仅能再char、varchar、text类型的列上面创建全文索引
    但是面对高级的搜索还是略显简陋,且性能问题也是担忧。
  3. 哈希索引:
    哈希索引在InnoDB中只是一种系统自动优化的功能
    Hash 索引仅仅能满足”=”,”IN”和”<>”查询,不能使用范围查询。
    Hash 索引无法被用来避免数据的排序操作。
    在MySQL运行的过程中,如果InnoDB发现,有很多SQL存在这类很长的寻路,并且有很多SQL会命中相同的页面(page),InnoDB会在自己的内存缓冲区(Buffer)里,开辟一块区域,建立自适应哈希所有AHI,以加速查询。InnoDB的自使用哈希索引,更像“索引的索引”

- innodb 锁#

innodb 锁

- 平时怎么创建索引的#

  1. 选择区分度高的列做索引

  2. 根据查询场景、查询语句的查询条件设计索引

  3. 如果要为多列去创建索引,遵循最左匹配原则使用联合索引去创建

  4. 在长字符类型的字段上使用前缀索引,减少空间占用

  5. 扩展索引的时候尽量做追加,不是新建

- B+树和 B 树的区别,和红黑树的区别#

为什么不用B树?:
因为B树的所有节点都是包含键和值的,这就导致了每个几点可以存储的内容就变少了,出度就少了,树的高度会增高,查询的 时候磁盘I/O会增多,影响性能。由于B+Tree内节点去掉了data域,因此可以拥有更大的出度,拥有更好的性能。

和红黑树:
B+树跟红黑树不用比,B+树的高很低,红黑树比不了。

- 如何优化sql#

  1. 让sql使用索引,如果查看sql使用索引情况,explain查看执行计划
    通过explain命令可以得到下面这些信息: 表的读取顺序,数据读取操作的操作类型哪些索引可以使用哪些索引被实际使用,表之间的引用,每张表有多少行被优化器查询等信息。 rows是核心指标,绝大部分rows小的语句执行一定很快。
    Extra字段几种需要优化的情况
    Using filesort 需要优化,MYSQL需要进行额外的步骤来对返回的行排序。
    Using temporary需要优化,发生这种情况一般都是需要进行优化的。mysql需要创建一张临时表用来处理此类查询
    Using where 表示 MySQL 服务器从存储引擎收到行后再进行“后过滤”
  2. 如果让sql使用到索引(符合建索引的几个原则)
    • 最左前缀匹配原则
    • 选择区分度高的列作为索引,区分度的公式是count(distinct col)/count(*)
    • 索引列不能参与计算,保持列“干净”, 原因很简单,b+树中存的都是数据表中的字段值
    • 尽量的扩展索引,不要新建索引
    • =和in可以乱序

- 什么是覆盖索引#

覆盖索引就是把要查询出的列和索引是对应的,不做回表操作

- 为什么尽量不要用select *?#

  1. 返回了太多不需要的数据
  2. 无法覆盖索引(select * 走的是聚簇索引),需要回表
  3. 一个好的应用程序设计应当能够在 sql 中有准确的定义,从而减少歧义或者不必要的更改

- 百万级数据分页查询优化#

  1. 纯扫表:记录主键游标,记录上次最大主键Id,从该Id处开始扫

    1
    select * from t where id > max_id limit 100;
  2. 不记录游标只根据主键扫库, 不带where条件, 主键覆盖索引 + 子查询

    1
    select * from t where id in (select id from usertb limit 7000000,100);
  3. 带where条件,联合覆盖索引 key(type, id) + 子查询

    1
    select * from t where id in (select id from usertb where type=1 limit 7000000,100);
  4. 带where和orderby条件,联合覆盖索引key(a, b) + 子查询

    1
    select * from t where id in (select id from usertb where a=1 order by b limit 7000000,100);
  5. 尽量保证不要出现大的offset,加一些条件过滤一,不应该使用limit跳过已查询到的数据,offset做无用功。实际工程中,要避免出现大页码的情况,尽量引导用户做条件过滤。

mysql隔离级别#

- MVCC, 什么是快照读,什么是当前读#

快照读: 即普通SELECT语句,既然是快照读,故 SELECT 的时候,会生成一个快照。

生成快照的时机:事务中第一次调用SELECT语句的时候才会生成快照,在此之前事务中执行的update、insert、delete操作都不会生成快照。

不同事务隔离级别下,快照读的区别:

READ COMMITTED 隔离级别下,每次读取都会重新生成一个快照,所以每次快照都是最新的,也因此事务中每次SELECT也可以看到其它已commit事务所作的更改;

REPEATED READ 隔离级别下,快照会在事务中第一次SELECT语句执行时生成,只有在本事务中对数据进行更改才会更新快照,因此,只有第一次SELECT之前其它已提交事务所作的更改你可以看到,但是如果已执行了SELECT,那么其它事务commit数据,你SELECT是看不到的。

RC的本质:每一条SELECT都可以看到其他已经提交的事务对数据的修改,只要事务提交,其结果都可见,与事务开始的先后顺序无关。
RR的本质:第一条SELECT生成ReadView前,已经提交的事务的修改可见。

- 什么是当前读#

https://mp.weixin.qq.com/s/w1DwsDDSxKfmFxcLOgG3Dw

- mysql数据库事务隔离级别,分別解決什么问题,next-key锁原理、如何解决幻读?#

READ-UNCOMMITTED(未提交读): 可能会导致脏读、幻读或不可重复读
READ-COMMITTED(提交读): 可以阻止脏读,但是幻读或不可重复读仍有可能发生
REPEATABLE-READ(可重复读,mysql默认隔离级别): 可以阻止脏读和不可重复读,幻读通过mvcc解决了快照读,next-key锁解决了当前读
SERIALIZABLE(串行化读): 该级别可以防止脏读、不可重复读以及幻读

- RR RC区别#

区别就在于rr解决了不可重复读和幻读,怎么解决的,通过MVCC和next-key锁.

不可重复读:rc级别下的mvcc总是读取数据行的最新快照,而rr级别下的mvcc,会在事务第一次select的时候,为数据行生成一个快照,后面每次都读这个快照,除非自己更新,所以rr下是可重复读,别的事务提交也无法影响你的事务。

幻读:快照读下rr级别不会出现幻读,因为rr级别的mvcc读的是事务第一次读取时的快照;在当前读下rr级别使用了next-key锁(临键锁),临键锁包括行锁+间隙锁, 来避免两个当前读时有其它事务插入数据,所以当前读使用next-key锁解决的幻读。 最后备注下:如果是先快照读再当前读,影响行数不一致是否属于幻读,是有争议的但大多认为并不是幻读。

RC的本质:每一条SELECT都可以看到其他已经提交的事务对数据的修改,只要事务提交,其结果都可见,与事务开始的先后顺序无关。
RR的本质:第一条SELECT生成ReadView前,已经提交的事务的修改可见。

mysql其他#

- 分库分表#

根据userid取模做的分库分表

两个场景,业务解耦垂直拆分,读写性能瓶颈做水平拆分。

  1. 垂直切分

业务维度切分,解耦

  1. 水平切分

读写性能遇到瓶颈,分库分表

- mysql的乐观锁、悲观锁实现#

MySQL乐观锁的实现完全是逻辑的,也就是自己去实现。 比如给每条数据附带版本号或者timestamp。更新引起数据的版本号改变,两次select判断版本号是否一致可以判断是否发生改变

MySQL悲观锁的实现需要借助于MySQL的锁机制。
行锁
常见的增删改(INSERT、DELETE、UPDATE)语句会自动对操作的数据行加写锁,查询的时候也可以明确指定锁的类型,SELECT … LOCK IN SHARE MODE 语句加的是读锁,SELECT … FOR UPDATE 语句加的是写锁
行锁的实现方式
1) Record lock 锁记录
2) Gap lock 锁两个记录之间的 GAP,防止记录插入
3) Next-key lock 锁一条记录及其之前的间隙

- 主从延迟怎么办#

主从延迟的原因:
主库写入数据并且生成binlog文件, 从库异步读取更新

解决方案:
一、更新操作,做SQL优化,减少批量更新操作
二、查询场景,

  1. 强制读主,对主库压力大,谨慎使用
  2. 延迟读从, 将要更新的key先放到一个本地延迟队列中,做延迟处理。
  3. 聚合表, 如果是1:1的两张数据,可以先订阅更新到一张聚合表,再订阅聚合表的binlog
  4. 订阅全部从库的binlog, todo

jvm

- Java内存区域#

线程私有的

程序计数器
存放当前线程所执行的字节码的行号指示器。如果线程执行的是一个Java程序,计数器记录正在执行的虚拟机字节码指令地址;正在执行的是native方法,则计数器的值为空

Java虚拟机栈

描述Java方法执行的内存模型,每个方法执行的同时都会创建一个栈帧来存储局部变量表(编译期可知的各种基本数据类型,对象引用【reference类型】,内存空间的分配是在编译期完成的,方法运行期间不会改变局部变量表的大小)、操作数栈、动态链接、方法出口等。

本地方法栈

为虚拟机使用到的native方法服务

线程公有的

Java堆(GC堆)

存放对象实例,所有的对象实例和数组都在堆上分配。程序运行时分支可能不一样,只有运行期间才能知道创建哪些对象,这部分内存的分配和回收是动态的

metaspace

存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码

直接内存

不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存。NIO使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,避免了Java堆和Native堆来回复制数据。直接内存的回收是在虚拟机进行full gc的时候顺带进行的,并不会自己触发垃圾回收

- 谈谈java的类加载机制#

  1. 过程

    • 加载
      通过全类名获取定义此类的二进制字节流
      将字节流所代表的静态存储结构转换为方法区的运行时数据结构
      在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口
    • 连接
      验证
      文件格式、元数据、字节码、符号引用验证
    • 准备
      正式为类变量分配内存并设置类变量初始值的阶段,设置数据类型默认的零值
    • 解析
      解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程
    • 初始化
      真正执行类中定义的 Java 程序代码
      初始化阶段是执行类构造器 ()方法的过程
  2. 双亲委派模式
    Java的类加载使用双亲委派模式,即一个类加载器在加载类时,先把这个请求委托给自己的父类加载器去执行,如果父类加载器还存在父类加载器,就继续向上委托,直到顶层的启动类加载器。如果父类加载器能够完成类加载,就成功返回,如果父类加载器无法完成加载,那么子加载器才会尝试自己去加载。这种双亲委派模式的好处,一个可以避免类的重复加载,另外也避免了java的核心API被篡改。

    BootstrapClassLoader
    ExtensionClassLoader
    AppClassLoader(应用程序类加载器)

- gc对象存活判定算法#

1.1引用计数算法

给对象添加一个引用计数器,当有地方引用它时加1,当引用失效时计数减1,任何时刻计数器为0时的对象都不会再被引用。

缺点:存在两个对象不会被继续访问,但是两者循环调用的情况,由于存在循环调用导致两个对象都不会被回收。

1.2可达性分析算法

通过一系列被称作gc roots对象作为起点,从这些起点向下搜索,搜索所走过的路径成为引用链,当一个对象到gc roots没有任何引用链时证明此对象不可用。

Java中gc roots的对象包括:

虚拟机栈中引用的对象
native方法中引用的对象
类静态属性、常量中引用的对象

对于一个 Java 程序而言,对象都位于堆内存块中,存活的那些对象都被根节点引用着,即根节点 GC Roots 是一些引用类型,自然不在堆里,那它们位于哪呢?它们能放在哪呢?
答案是放在栈里,包括:
Local variables 本地变量
Static variables 静态变量
JNI References JNI引用等

- gc垃圾收集器及其优缺点#

垃圾回收器种类:Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS(ConCurrent Mark Sweep)、G1(Garbage First)

- 标记算法和复制算法的区別,用在什么场合#

CMS 标记清除、Serial Old,Parallel old 标记整理:适用于存活对象比较多的场景
Serial、ParNew、PS 收集器 复制算法:用在那种不可达对象比较多的场合

- cms垃圾收集器的回收步骤及其优缺点?#

cms回收器的标记过程:

CMS以获取最短回收停顿时间为目标的收集器,使用“标记-清除”算法,分为以下6个步骤

1.STW initial mark:第一次暂停,初始化标记,从root标记old space存活对象(the set of objects reachable from roots (application code))

2.Concurrent marking:运行时标记,从上一步得到的集合出发,遍历old space,标记存活对象 (all live objects that are transitively reachable from previous set)

3.Concurrent precleaning:并发的标记前一阶段被修改的对象(card table)

4.STW remark:第二次暂停,检查,标记,检查脏页的对象,标记前一阶段被修改的对象 (revisiting any objects that were modified during the concurrent marking phase)

5.Concurrent sweeping:运行过程中清理,扫描old space,释放不可到达对象占用的空间

6.Concurrent reset:此次CMS结束后,重设CMS状态等待下次CMS的触发

或者4个大步骤:
1,initial mark 2,concurrent mark 3,remark 4,concurrent sweep

CMS缺点:cpu敏感,浮动垃圾,空间碎片

1.CMS收集器对cpu资源非常敏感,在并发阶段对染不会导致用户线程停顿,但是会因为占用一部分线程导致应用程序变慢,总吞吐量会降低。CMS默认启动的收集线程数=(CPU数量+3)/4,在cpu数比较少的情况下,对性能影响较大。

2.CMS收集器无法处理浮动垃圾,可能会出现“Concurrent Mode Failure”失败而导致另一次Full GC,原因是CMS的并发清除阶段用户线程还是在运行,所以还会有新的垃圾不断产生,这些垃圾CMS只能在下次GC时再清理掉,这部分垃圾被称为“浮动垃圾”。所以CMS不能像其他收集器那样在老年代几乎完全被填满了在开始收集,需要预留一部分空间。JDK1.6中CMS将这个阈值提高到了92%,要是CMS运行期间预留的内存不足,会出现一次“Concurrent Mode Failure”,这是虚拟机启动备用方案,临时启用Serial Old收集器充满新进行老年代垃圾收集,所以这个阈值不宜设置的过高

3.CMS基于标记-清除算法,这意味着垃圾收集结束后会有大量的空间碎片,空间碎片过多会造成老年代有很大空间空余但是无法存放大对象的情况。通过参数UseCMSCompactAtFullCollection(默认开启)开关参数来开启内存碎片的合并整理。

- G1回收器的特点#

G1的出现就是为了替换jdk1.5种出现的CMS,这一点已经在jdk9的时候实现了,jdk9默认使用了G1回收器,移除了所有CMS相关的内容。G1和CMS相比,有几个特点:

  1. G1把内存划分为多个独立的区域Region
  2. G1仍然保留分代思想,保留了新生代和老年代,但他们不再是物理隔离,而是一部分Region的集合
  3. G1能够充分利用多CPU、多核环境硬件优势,尽量缩短STW
  4. G1整体采用标记整理算法,局部是采用复制算法,不会产生内存碎片
  5. 控制回收垃圾的时间:这个是G1的优势,可以控制回收垃圾的时间,还可以建立停顿的时间模型,选择一组合适的Regions作为回收目标,达到实时收集的目的。控制G1回收垃圾的时间 -XX:MaxGCPauseMillis=200 (默认200ms)
  6. G1跟踪各个Region里面垃圾的价值大小,会维护一个优先列表,每次根据允许的时间来回收价值最大的区域,从而保证在有限事件内高效的收集垃圾
  7. 大对象的处理: 在CMS内存中,如果一个对象过大,进入S1、S2区域的时候大于改分配的区域,对象会直接进入老年代。G1处理大对象时会判断对象是否大于一个Region大小的50%,如果大于50%就会横跨多个Region进行存放

- G1执行步骤#

  1. 初始标记阶段:暂停应用程序,标记可由根直接引用的对象。
  2. 并发标记阶段:与应用程序并发进行,扫描 1 中标记的对象所引用的对象。
  3. 最终标记阶段:暂停应用程序,扫描 2 中没有标记的对象。本步骤结束后,堆内所有存活对象都会被标记。
  4. 筛选回收(首先对各个Regin的回收价值和成本进行排序,根据用户所期待的GC停顿时间指定回收计划,回收一部分Region)

- 内存泄漏与内存溢出的区别#

  • 溢出: OOM,除了 PC 剩下的区域都会发生 OOM,是由于内存不够,或者是代码中错误的分配太多对象导致的。
  • 泄漏:是指 内存分配后没有回收,导致内存占有一直增加,最后会导致溢出。

- 有几种gc fail?#

Allocation Failure 新生代没有足够的空间分配对象 Young GC

GCLocker Initiated GC 如果线程执行在JNI临界区时,刚好需要进行GC,此时GC locker将会阻止GC的发生,同时阻止其他线程进入JNI临界区,直到最后一个线程退出临界区时触发一次GC。 GCLocker Initiated GC

Promotion Failure 老年代没有足够的连续空间分配给晋升的对象(即使总可用内存足够大)

Concurrent Mode Failure CMS GC运行期间,老年代预留的空间不足以分配给新的对象

- 什么情况会触发fullgc?#

1.metaspace空间不足

2.Promotion Failure 老年代没有足够的连续空间分配给晋升的对象(即使总可用内存足够大)

3.Concurrent Mode Failure CMS GC运行期间,老年代预留的空间不足以分配给新的对象

4.System.gc

5.jmap -histo:live [pid]

- 栈内存溢出#

对虚拟机栈这个区域规定了两种异常状况:

(1)如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError 异常;
(2)如果虚拟机栈可以动态扩展(当前大部分的 Java 虚拟机都可动态扩展,只不过 Java 虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出 OutOfMemoryError 异常。
(3)与虚拟机栈一样,本地方法栈区域也会抛出 StackOverflowError 和OutOfMemoryError 异常。

- 系统内存多大,留给操作系统2G,够吗?#

16G内存 虚拟机 推荐配置12G堆, 更大的堆容易引起SWAP,SWAP使用过多会造成宕机

各分区的大小对GC的性能影响很大。如何将各分区调整到合适的大小,分析活跃数据的大小是很好的切入点。

活跃数据的大小是指,应用程序稳定运行时长期存活对象在堆中占用的空间大小,也就是Full GC后堆中老年代占用空间的大小

例如,根据GC日志获得老年代的活跃数据大小为300M,

总堆:1200MB = 300MB × 4 新生代:450MB = 300MB × 1.5 老年代: 750MB = 1200MB - 450MB

https://tech.meituan.com/2017/12/29/jvm-optimize.html

其他

- tcp如何保证可靠传输,tcp、udp区别#

tcp与udp区别

  1. 是否建立连接, udp不建立连接,tcp三次握手
  2. 是否可靠,udp不需要确认, tcp会有确认、重传、窗口、拥塞等机制
  3. 应用场景, udp一般用于即时通信,qq,直播; tcp用于文件传输,收发邮件、登录等

- 为什么要三次握手,四次挥手#

为什么要三次握手
确认双发收发功能都正常

为什么要四次挥手
确认双方都没有数据再发送

- 四次挥手的closewait, timewait分别在哪,为什么timewait要等待2msl#

2MSL是两倍的MSL(Maximum Segment Lifetime),MSL指一个片段在网络中最大的存活时间,2MSL就是一个发送和一个回复所需的最大时间

如果直到2MSL,Client都没有再次收到FIN,那么Client推断ACK已经被成功接收,则结束TCP连接

TIME_WAIT状态就是用来重发可能丢失的ACK报文。在Client发送出最后的ACK回复,但该ACK可能丢失。Server如果没有收到ACK,将不断重复发送FIN片段。所以Client不能立即关闭,它必须确认Server接收到了该ACK

- 设计模式六大原则#

其中设计模式的SOLID原则(Principles)如下:

单一职责原则(Single Responsibility)
开闭原则(Open Closed)
里氏代换原则(Liskov Substitution)
接口隔离原原则(Interface Segregation)
依赖倒置原则(Dependency Inversion)

- 线上需要关注哪些机器参数#

指标 阈值

cpu.iowait 所有进程因为等待IO完成而被阻塞,导致CPU idle所花的时间的百分比

disk.io.util 如果这个指标较高,代表io遇到瓶颈

cpu.busy 60

cpu.load 1以下比较好,1.5 会引起程序响应时间变慢,应触发报警

mem.swapused 一般情况下使用swap,代表物理内存已不足,当系统没有足够物理内存来应付所有请求的时候就会用到 swap 设备。使用 swap 的代价非常大,如果系统没有物理内存可用,就会频繁 swapping,如果 swap 设备和程序正要访问的数据在同一个文件系统上,那会碰到严重的 IO 问题,最终导致整个系统迟缓。

cms gc后的老年代大小

jvm.fullgc.count 5

jvm.yonggc.count 70

jvm.yonggc.meantime(一分钟内的每次年轻代gc的平均时间) 500

- 有哪些处理线上问题的经验#

实际是在考雪崩,限流,降级等措施

xxx ES导致雪崩,bc端未分离,b端下游超时引发的血案
事件: 调用ES持续大量超时,服务bc端都在调用,b端超时时间设置长,占用了很多线程资源,影响C端服务响应,造成C端上游雪崩,影响二十度个服务
(调ES超时的原因?查询请求的返回响应太大了)
事中: 无熔断降级错误,现上线加熔断ES降级,
(降级措施是?返回有损服务,创建活动失败)
事后: bc端分离,对下游添加熔断降级,根本上还是要让es调用查询粒度更小一点,减少es调用的返回量

xxx 慢查询引发的血案1
事件:下单的时候先去删除用户未支付订单并归还库存,但未支付订单表竟然没有给orderid添加索引,导致delete操作锁全表,在当天有大量未支付订单,导致下单接口越来越慢,最终把订单db中的连接数占满,db的整体不可用。

分析原因: DB的客户端在超时的时候会断开连接,但DB服务器还是在执行该操作,或阻塞着,客户端新的调用再申请新的连接,直到把DB的连接池打满,DB完全阻塞在这个查询上,导致不可用。

事前: 1. 学习数据库知识,尤其是索引和锁相关 2. 定时检查服务的索引是否覆盖全 3. 提前做好限流熔断等降级措施
事中: 1. 限流降级, 2. 问题排查,数据库压力没有增加很多,但数据库响应缓慢,应该分析出是慢查询的原因,当很多的慢sql出来时
事后: 1. 对服务整体重新排查

xxx 慢查询引发问题,慢sql导致mq积压
事件: 新表去更新库存,但索引创建的时候没覆盖全场景,后续开发有sql未走到索引,某个周六出现MQ积压告警,看监控发现是慢sql更新,有条update语句未使用到索引,导致更新时锁全表
事中: 临时增加索引
事后: 检查了新表所有设计的sql没有未使用索引的情况。
(慢查询是有可能占用过多的DB资源,把整个DB打垮)(是有可能出现卖超的)

java并发

- Java并发容器有哪些#

concurrentHashmap: 线程安全的HashMap, Node数组+链表 + 红黑树
copyonwriteArrayList: 线程安全的List,在读多写少的场合性能非常好
concurrentLinkedQueue: 高效的并发队列,使用链表实现。
可以看做一个线程安全的 LinkedList
BlockingQueue(接口)
1 ArrayBlockingQueue: 有界队列实现类 底层Object数组
2 LinkedBlockingQueue: 单向链表
3 PriorityBlockingQueue: 支持优先级的无界阻塞队列

- ArrayBlockingQueue和LinkedBlockingQueue的实现原理#

ArrayBlockingQueue是object数组实现的线程安全的有界阻塞队列
1.数组实现:使用数组实现循环队列
2.有界:内部为数组实现,一旦创建完成,数组的长度不能再改变
3.线程安全:使用了 ReentrantLock 和 两个 condition(notEmpty, notFull) 来保证线程安全
4.阻塞队列:先进先出。当取出元素的时候,若队列为空,wait直到队列非空(notEmpty);当存储元素的时候,若队列满,wait直到队列有空闲(notFull)

LinkedBlockingQueue是一个单向链表实现的阻塞队列
1.链表实现: head是链表的表头, last是链表的表尾。取出数据时,都是从表头head处取出,出队; 新增数据时,都是从表尾last处插入,入队。
2.可有界可无界:可以在创建时指定容量大小,防止队列过度膨胀。如果未指定队列容量,默认容量大小为Integer.MAX_VALUE
3.线程安全:使用了 两个 ReentrantLock 和 两个 condition,putLock是插入锁,takeLock是取出锁;notEmpty是“非空条件”,notFull是“未满条件”。通过它们对链表进行并发控制
4.阻塞队列:先进先出。当取出元素的时候,若队列为空,wait直到队列非空(notEmpty);当存储元素的时候,若队列满,wait直到队列有空闲(notFull)

PriorityBlockingQueue
PriorityBlockingQueue(具有优先级的无限阻塞队列). 是一个支持优先级的无界阻塞队列,内部结构是数组实现的二叉堆
线程安全的排序。其数据结构是二叉堆(分为最大堆和最小堆)
二叉堆本质是一颗二叉树:
二叉堆是一种特殊的堆,二叉堆是完全二叉树或者是近似完全二叉树
如果节点在数组中的位置是i(i是节点在数组中的下标), 则i节点对应的子节点在数组中的位置分别是 2i + 1 和 2i +2, 同时i的父节点的位置为 (i-1)/2(i从0 开始)
通过一次上浮可以把符合条件的元素放到堆顶,反复进行上浮操作,可以将整个堆进行有序化。下沉操作可以把替代堆顶后的元素放到该放的位置,同时堆顶元素是符合条件的元素(前提是二叉堆已经是有序的)

SynchronousQueue
SynchronousQueue (SynchronousQueue是一个不存储元素的阻塞队列。每一个put操作必须等待一个take操作,否则不能继续添加元素), 每一个线程的入队操作必须等待另一个线程相应的出队(take)操作,相反,每一个线程的出队操作必须等待另一个线程相应的入队操作。

Executors.newCachedThreadPool() 使用的SynchronousQueue, 实际就不想往队列里放元素,就是想有多少任务就生成多少线程去做。线程拉满,不做任务堆积。 但newCachedThreadPool(). maxThreadSize 是Integer.MAX_VALUE, 容易内存溢出。

白话
ArrayBlockingQueue和LinkedBlockingQueue都是线程安全的阻塞队列。 ArrayBlockingQueue底层是数组实现,是有界的队列,线程安全是使用一个 ReentrantLock 和 两个 condition实现的。 LinkedBlockingQueue底层是单向链表实现,是可有界可无界的队列,线程安全是使用两个 ReentrantLock 和 两个 condition实现的。

为什么ArrayBlockingQueue是一个ReentrantLock,LinkedBlockingQueue是两个?
ArrayBlockingQueue在插入、取出的时候,都会对index做变更,有并发问题,需要同步处理。
LinkedBlockingQueue从队头取出,从队尾插入,链表结构所以两种操作可并行。所以用两个lock, 提升并发能力,还用了一个AtomicInteger count; // 记录总数

- 阻塞队列原理#

核心思想就是,何时阻塞:空不让你取,满不让你加。
进行操作的方法,在操作前,都必须加锁。
主要就是一个 ReentrantLock,和两个由它创建的 Condition:notFull、notEmpty。
然后,在take、put、enqueue、dequeue 四个方法里面对这两个信号条件进行控制。

- 谈谈对ConcurrentHashMap的了解#

1.7 结构: 分段的数组+链表;
并发:分段锁,锁一段数组 segment,默认有16个segment, 并发度只有默认是16
线程安全: segment上锁,继承ReentrantLock可重入锁

1.8 结构: Node数组+链表 + 红黑树;
并发: 并发控制使用 synchronized 和 CAS
线程安全: CAS和synchronized来保证并发安全, synchronized只锁定当前链表或红黑二叉树的首节点, 只要hash不冲突,就不会产生并发,效率又提升N倍

get是怎么做的

  1. hash。 hashcode高低16位异或得到hash值,(h ^ h >>> 16) & 2147483647;
  2. 桶位节点就是要找节点,直接返回该节点
  3. 如果节点hash值小于0,说明是已迁移的节点或者红黑树bin,调用node.find(), TreeBin 和 ForwardingNode 都继承Node重写find方法
  4. 剩下情况就是链表,遍历判断是否节点存在

注:ForwordingNode的hash值为-1,红黑树的根结点的hash值为-2。 TreeBin 和 ForwardingNode 都继承Node重写find方法

put是怎么做的

  1. hash。 hashcode高低16位异或得到hash值,(h ^ h >>> 16) & 2147483647;
  2. 桶位为空,cas设置新node,U.compareAndSetObject
  3. 如果hash值为-1,则helpTransfer,else 加synchronized锁住头结点,判断是树,调用添加到树中方法,如果是链表,添加到尾端。
  4. 判断是否到了8个,链表变树; 判断size是否到了阈值,触发扩容。size总数增加cas (U.compareAndSetLong)

resize是怎么做的

  1. newTable, 大小是旧表的2倍
  2. 头结点加锁,不允许其他线程更改,但可get,
  3. 判断桶中各节点位置,复制一模一样的节点到新表(非直接移过去,还要get),到新表可能在旧桶位也可能在新桶位
  4. 分配结束之后将头结点设置为fwd节点(指向新表)

白话:线程安全的map。1.7版本是使用分段锁实现的线程安全,segment上加锁,在构建的时候可以设置大小,默认是16,就是并发度默认只有16. 1.8版本的线程安全是用synchronized+cas实现的,使用synchronized锁住链表或红黑树的头结点,使用cas来进行size++和首个节点入桶。1.8的并发度是随着桶位增加而增加的,所以并发效率会随扩容提升很多倍。

- 谈谈对Java内存模型的了解#

总结:
jmm定义了线程间通信的方式,jmm使用共享内存的方式通信,jmm规范多线程下的执行顺序和多线程下共享变量的可见性问题,规定了一套happens-before规则。

jmm是为了解决线程间通信问题,线程间通信通常有两种解决方法,共享内存或通知机制, jmm使用了共享内存的方式。
jmm定义了一套happens-before规则来规范多线程下的执行顺序和多线程下变量的可见性问题,规则底层是通过禁止部分编译器和处理器的指令重排序实现。
happens-before有八个,分别是:
程序顺序规则:一个线程中的每个操作,发生在该线程中任意后续操作之前
监视器锁规则:对一个锁的解锁,发生在随后对这个锁的加锁之前
volatile变量规则:对一个volatile域的写,发生在任意后续对这个volatile域的读之前
传递性:如果A发生在B之前,B发生在C之前,那么A一定发生在C之前
线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作;
线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;

- volatile原理#

  1. volatile的特征
    A、禁止指令重排(有例外)
    B、可见性
  2. Volatile的内存语义
    当写一个volatile变量时,JMM会把线程对应的本地内存中的共享变量值刷新到主内存。
    当读一个volatile变量时,JMM会把线程对应的本地内存置为无效,线程接下来将从主内存中读取共享变量
  3. Volatile的重排序
    两个volatile变量操作不能够进行重排序;

白话:volatile有两个特性,可见性和禁止指令重排序。可见性,当一个线程更新volatile变量后,这个变量会立即写入到主内存中,其他线程读取时会从主内存中读取最新的值。禁止指令重排序,两个对volatile变量的操作不能被重排序,底层是通过内存屏障实现的。

- java的乐观锁CAS锁原理#

CAS英文全称Compare and Swap,直白翻译过来即比较并交换,是一种无锁算法,在不使用锁即没有线程阻塞下实现多线程之间的变量同步,基于处理器的读-改-写原子指令来操作数据,可以保证数据在并发操作下的一致性。

CAS包含三个操作数:内存位置V,预期值A,写入的新值B。在执行数据操作时,当且仅当V的值等于A时,CAS才会通过原子操作方式用新值B来更新V的值(无论操作是否成功都会返回)。

CAS的含义是:我认为V的值应该为A,如果是,那么将V的值更新为B,否则不修改并告诉V的值实际为多少

CAS是怎么获取到预期值的? 通过unsafe类获取到的

1
2
3
4
5
6
7
8
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

return var5;
}

CAS修改的值为什么要是volatile? 可见性

在compareAndSet的操作中,JNI借助CPU指令完成的,属于原子操作,保证多个线程在执行过程中看到同一个变量的修改值
unsafe:操作内存内存空间
valueOffset:value在内存中的偏移量(把存储单元的实际地址与其所在段的段地址之间的距离称为段内偏移),这里我们可以简单认为是内存地址,在类加载的时候,通过unsafe获取到value的偏移量。
value: 顾名思义代表存储值,被volatile修饰,保证在线程间的可见性

CAS问题:
1.ABA问题。CAS需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。但是如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变化,但是实际上是有变化的。ABA问题的解决思路就是在变量前面添加版本号,每次变量更新的时候都把版本号加一,这样变化过程就从“A-B-A”变成了“1A-2B-3A”。
JDK从1.5开始提供了AtomicStampedReference类来解决ABA问题,具体操作封装在compareAndSet()中。compareAndSet()首先检查当前引用和当前标志与预期引用和预期标志是否相等,如果都相等,则以原子方式将引用值和标志的值设置为给定的更新值。
2.循环时间长开销大。CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销。
3.只能保证一个共享变量的原子操作。对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的。
Java从1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。

白话:CAS是Compare and Swap,比较并交换,是一种乐观锁实现线程安全的方式,更加轻量。底层是通过cpu的原子指令实现的比较并替换。使用的时候参数有内存位置,预期值,写入的新值,当通过内存位置拿到的值和预期值相等时,就用新值进行替换,整个操作是原子的,cpu指令保证。

- sychronized使用及原理#

  • 修饰实例方法:作用于当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁
  • 修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁。
  • 修饰代码块: 对应的锁则是,传入的synchoronzed的对象实例。

synchronized原理
对象分为3部分:对象头,对象数据,填充内容。
对象头又分为:Mark Word,类型指针、数组长度。
synchronized的锁依赖java对象头, 通过对象头中的mark word 和 monitor实现锁机制。
mark word主要会记录对象关于锁的信息(偏向锁、轻量锁、重量锁)。Monitor是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的线程同步。

其中有两个队列 _EntryList和 _WaitSet,它们是用来保存ObjectMonitor对象列表, _owner指向持有ObjectMonitor对象的线程。
当多个线程访问同步代码时,线程会进入_EntryList区,当线程获取对象的monitor后(对于线程获得锁的优先级,还有待考究)进入 _Owner区并且将 _owner指向获得锁的线程(monitor对象被线程持有), _count++,其他线程则继续在 _EntryList区等待。若线程调用wait方法,则该线程进入 _WaitSet区等待被唤醒。线程执行完后释放monitor锁并且对ObjectMonitor中的值进行复位。

上面说到synchronized使用的锁都放在对象头里,大概指的就是Mark Word中指向互斥量的指针指向的monitor对象内存地址了。
由以上可知为什么Java中每一个对象都可以作为锁对象了。

白话:
sychronized可以使用在实例方法、静态方法和代码块上。
synchronized依赖java对象头中的mark word和monitor实现线程同步。mark word会记录对象关于锁的信息(偏向锁、轻量锁、重量锁)。Monitor依赖于底层的操作系统的Mutex Lock(互斥锁)实现的线程同步。
synchronized有多种锁类型,偏向锁、轻量锁、重量锁。
偏向锁,通过对比Mark Word里存储的锁偏向的线程ID解决加锁问题,最多执行一次CAS操作。
升级:当一个线程正持有偏向锁,被另外的线程所访问获取锁失败,偏向锁就会升级为轻量级锁。
轻量级锁,线程通过线程栈帧与对象mark word之间的多次CAS操作和自旋,尝试获取轻量级锁。
升级:若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。
重量级锁:是将除了拥有锁的线程以外的线程都阻塞。依赖操作系统的metex lock, 存在用户态和内核态切换,消耗较大

- 锁升级#

偏向锁通过对比Mark Word里存储的锁偏向的线程ID解决加锁问题,避免执行CAS操作。
升级时机:当持有偏向锁的时候,被另外的线程所访问获取锁失败,偏向锁就会升级为轻量级锁。
轻量级锁是通过用CAS操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能。轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令即可。

  • 先在栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝
  • 拷贝对象头中的Mark Word复制到锁记录中。拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock Record里的owner指针指向对象的Mark Word。
  • 如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,表示此对象处于轻量级锁定状态。
    升级时机:若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。
  • 重量级锁*是将除了拥有锁的线程以外的线程都阻塞。依赖操作系统的metex lock, 存在用户态和内核态切换,消耗较大。

- sychronized缺点#

  1. 效率低:锁的释放情况少,试图获得锁时不能设定超时,不能中断一个正在试图获得所得线程。
  2. 使用synchroinzed修饰一个代码块时,如果一个线程获取了对应的锁,并执行改代码块,其他线程只能一直等待。等待获取锁的线程释放锁,但是获取锁的线程执行释放锁只有2种方式(要么是执行完该代码块,正常释放。要么是.线程执行发生异常,JVM自动释放)一旦这个锁被别人获取,如果我还想获取,那么我只能选择等待或阻塞,只得到别的线程释放,如果别人永远不释放锁,那我只能永远等待下去。不能设定超时等待,无法做到响应中断。
  3. 不够灵活(多个线程只是做读写操作,线程直接就发生冲突。)
  4. 非公平。使用synchroinzd,非公平锁使一些线程处于饥饿状态,对于一些线程,可能长期无法抢占到锁。对于某些特定的业务,必须使用公平锁,这时synchronized无法满足要求
  5. 无法知道是否成功获取到锁

白话:

  1. 无有限等待。没有tryLock(带时间参数)
  2. 不可中断。没有lockInterruptibly(调用后一直阻塞到获得锁 但是接受中断信号)
  3. 读写锁不分离。没有读写锁,读读固定互斥,影响并发
  4. 不支持公平锁。
  5. 获取锁无返回值。无法知道线程当前有没有成功获得到锁,没有tryLock的返回值
  6. 无多路通知机制。lock.condition

- sychronized reentranlock 的区别#

  1. 有限等待:需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),这个是synchronized无法办到,Lock可以办到,由tryLock(带时间参数)实现;
  2. 可中断:使用synchronized时,等待的线程会一直阻塞,一直等待下去,不能够响应中断,而Lock锁机制可以让等待锁的线程响应中断,由lockInterruptibly()实现;
  3. 有返回值:需要一种机制可以知道线程有没有成功获得到锁,这个是synchronized无法办到,Lock可以办到,由tryLock()方式实现;
  4. 公平锁:synchronized中的锁是非公平锁,ReentrantLock默认情况下也是非公平锁,但可以通过构造方法ReentrantLock(true)来要求使用公平锁(底层由Condition的等待队列实现)。
  5. 读写分离,提高多个线程读操作并发效率:需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,这个是synchronized无法办到,Lock可以办到。
  6. 可实现选择性多路通知(锁可以绑定多个条件)

- sychronized 和 valotile 的区别#

  1. 作用范围。volatile更轻量,性能更好,但volatile只能用于变量而synchronized关键字可以修饰方法以及代码块
  2. 是否阻塞。多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞
  3. 是否保证原子性。volatile关键字能保证数据的可见性,但不能保证数据的原子性 (eg: i++).synchronized关键字两者都能保证
  4. 是否保证同步。volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized关键字解决的是多个线程之间访问资源的同步性

- 弱引用#

强引用
任何被强引用指向的对象都不能被垃圾回收器回收。
软引用
如果有软引用指向这些对象,则只有在内存空间不足时才回收这些对象(回收发生在OutOfMemoryError之前)。
弱引用
如果一个对象只有弱引用指向它,垃圾回收器会立即回收该对象,这是一种急切回收方式。
虚引用
虚引等同于没有引用,拥有虚引用的对象可以在任何时候被垃圾回收器回收。

弱引用的出现就是为了垃圾回收服务的。它引用一个对象,但是并不阻止该对象被回收。
如果使用一个强引用的话,只要该引用存在,那么被引用的对象是不能被回收的。
弱引用则没有这个问题。在垃圾回收器运行的时候,如果一个对象的所有引用都是弱引用的话,该对象会被回收

- ThreadLocal原理#

用多线程多份数据,来避免线程不安全

每个Thread 维护一个 ThreadLocalMap 映射表,这个映射表的 key是 ThreadLocal 实例本身,value 是真正需要存储的 Object。也就是说 ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。

由于每一条线程均含有各自私有的ThreadLocalMap容器,这些容器相互独立互不影响,因此不会存在线程安全性问题,从而也无需使用同步机制来保证多条线程访问容器的互斥性。

ThreadLocalMap 是使用 ThreadLocal 的弱引用作为 Key的,弱引用的对象在 GC 时会被回收。
Entry 继承 WeekReference<ThreadLocal<?>>,也就是说,一个Entry对象是由ThreadLocal对象和一个Object(ThreadLocal关联的对象)组成。

为什么选择弱引用?
为了应对非常大和长时间的用途,哈希表使用弱引用的 key
由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal key不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除

内存泄漏问题
在ThreadLocalMap中,只有key是弱引用,value仍然是一个强引用。当某一条线程中的ThreadLocal使用完毕,没有强引用指向它的时候,这个key指向的对象就会被垃圾收集器回收,从而这个key就变成了null;然而,此时value和value指向的对象之间仍然是强引用关系,只要这种关系不解除,value指向的对象永远不会被垃圾收集器回收,从而导致内存泄漏!

解决办法:
ThreadLocal提供了这个问题的解决方案,
其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。
但是这些被动的预防措施并不能保证不会内存泄漏:
使用static的ThreadLocal,延长了ThreadLocal的生命周期,可能导致的内存泄漏。
分配使用了ThreadLocal又不再调用get(),set(),remove()方法,那么就会导致内存泄漏。

ThreadLocal最佳实践
综合上面的分析,我们可以理解ThreadLocal内存泄漏的前因后果,那么怎么避免内存泄漏呢?
每次使用完ThreadLocal,都调用它的remove()方法,清除数据。
在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。

- AQS同步器原理?tryAcquire的过程?#

**AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作。AQS通过CAS完成对state值的修改

核心思想是,如果被请求的共享资源空闲,将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态;如果共享资源被占用,将暂时获取不到锁的线程加入到队列中, 需要一定的阻塞等待唤醒机制机制来保证锁分配。这个机制主要用的是CLH队列实现的**

AQS是通过将每条请求共享资源的线程封装成一个节点来实现锁的分配。通过简单的几行代码就能实现同步功能,这就是AQS的强大之处。

自定义同步器实现的相关方法也只是为了通过修改State字段来实现多线程的独占模式或者共享模式。自定义同步器需要实现以下方法(ReentrantLock需要实现的方法如下,并不是全部):

ReentrantLock这类自定义同步器自己实现了获取锁和释放锁的方式,而其余的等待队列的处理、线程中断等功能,异常与性能处理,还有并发优化等细节工作,都是由AQS统一提供,这也是AQS的强大所在。对同步器这类应用层来说,AQS屏蔽了底层的,同步器只需要设计自己的加锁和解锁逻辑即可

一般来说,自定义同步器要么是独占方式,要么是共享方式,它们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。ReentrantLock是独占锁,所以实现了tryAcquire-tryRelease。

独占与共享最大不同就在各自的tryacquire里,对于独占来说只有true或false,只有一个线程得以执行任务;而对于共享锁的tryAcquireShared来说,线程数没达到限制都可以直接执行。
但本质上都是对AQS同步状态的修改,一个是0与1之间,另一个允许更多而已

应用:
1.ReentrantLock
使用AQS保存锁重复持有的次数。当一个线程获取锁时,ReentrantLock记录当前获得锁的线程标识,用于检测是否重复获取,以及错误线程试图解锁操作时异常情况的处理。
2.Semaphore
使用AQS同步状态来保存信号量的当前计数。tryRelease会增加计数,acquireShared会减少计数。
3.CountDownLatch
使用AQS同步状态来表示计数。计数为0时,所有的Acquire操作(CountDownLatch的await方法)才可以通过

白话:AQS是jdk提供的一个同步器,可以很方便的生成自定义同步器。AQS内部使用一个volatile的state来表示同步状态,通过一个FIFO队列来做多线程获取资源的排队操作,AQS通过CAS来做state变量的修改。实现AQS只要实现其中判断获取锁和释放锁的方法即可,AQS内部会去做队列入队出队等复杂逻辑处理。使用AQS实现的同步器有ReentrantLock,Semaphore,CountDownLatch。

- CountDownLatch、Semaphore、CyclicBarrier含义及实现原理#

CountDownLatch
一个或多个线程等待其他线程完成一些列操作
CountDownLatch是一个同步辅助类,当CountDownLatch类中的计数器减少为0之前所有调用await方法的线程都会被阻塞,如果计数器减少为0,则所有线程被唤醒继续运行。

典型应用场景

开始执行前等待n个线程完成各自任务:例如有一个任务想要往下执行,但必须要等到其他任务执行完毕后才可以继续往下执行。假如这个想要继续往下执行的任务调用一个CountDownLatch对象的await()方法,其他的任务执行完自己的任务后调用同一个CountDownLatch对象上的countDown()方法,这个调用await()方法的任务将一直阻塞等待,直到这个CountDownLatch对象的计数值减到0为止。

CyclicBarrier
多个线程相互等待,直到到达同一个同步点,再继续一起执行。CyclicBarrier适用于多个线程有固定的多步需要执行,线程间互相等待,当都执行完了,在一起执行下一步。

CyclicBarrier和CountDownLatch的异同
CountDownLatch 是一次性的,CyclicBarrier 是可循环利用的
CountDownLatch 参与的线程的职责是不一样的,有的在倒计时,有的在等待倒计时结束。
CyclicBarrier 参与的线程职责是一样的。

个人理解:

  1. CountDownLatch 是当前线程等着别人做好再开始做。像做饭一样,买好菜。
    CountDownLatch内部是AQS做的同步,共享模式,共享释放,只有减到0才能获得
    countdownlatch
    await的时候,判断当前state是否为0,为0可以获取共享锁,非0则加入阻塞。
    countdown的时候,将state -1 , 并判断是否减到0, 减到0就唤醒阻塞的线程。
  2. Semaphore 是多个线程去获取,有的话就有,没有就等着。 像买房摇号。
    Semaphore 内部是AQS做的同步,非0就可获得,0就不行了
  3. CyclicBarrier 是各个线程都达到某个预设点的时候, 可以执行一段逻辑,然后打开所有线程的限制。 像赛马.
    CyclicBarrier 底层是依赖Reentrantlock保证同步 和 一个condition 来阻塞和放开早到达的多个线程,其实也是AQS

java异步#

- Callable、Future、FutureTask、CompletableFuture分别是什么#

Callable是一个接口,提供一个回调方法,可以放到executorService中

Future接口提供了三种功能:
1)判断任务是否完成;
2)能够中断任务;
3)能够获取任务执行结果。

FutureTask
可以看出RunnableFuture继承了Runnable接口和Future接口,而FutureTask实现了RunnableFuture接口,那就可以得出FutureTask即可以作为一个Runnable线程执行,又可以作为Future得到Callable返回值。
FutureTask是Future的唯一实现类

listenableFuture
ListenableFuture和JDK原生Future最大的区别是前者做到了一个可以监听结果的Future
void addListener(Runnable listener, Executor executor);

CompletableFuture
在JDK8中开始引入的,这个在一定程度上与ListenableFuture非常类似。比如说ListenableFuture的listener监听回调,在这个类中,相当于thenRun或者whneComplete操作原语

线程池#

- 线程池的创建使用、有哪些参数#

为什么要用线程池?
1. 降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消耗 (重复利用)
2. 提高响应速度。 当任务到达时,任务可以不需要的等到线程创建就能立即执行 (提前开始任务)
3. 提高线程的可管理性。 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控 (控制线程数量)

如何创建线程池
1. Executors - 不允许
CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM
FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE ,可能堆积大量的请求,从而导致OOM
2. ThreadPoolExcutor
参数: 核心线程数, 最大线程数,非核心线程空闲存活时间,阻塞队列,拒绝策略,有必要的话还有ThreadFactory线程工厂.
好处:
- corePoolSize, maximumPoolSize 弹性控制线程数量,可伸缩,可扩容可释放
- keepAliveTime, TimeUnit.SECONDS 设置后在回收前可让其他任务使用,减少重新创建线程的开销
- BlockingQueue, 设置队列大小,起到缓冲的作用
- rejectHandler

- 线程池的原理#

  1. 先讲构造参数
    corePoolSize: 线程池核心线程数最大值
    maximumPoolSize: 线程池最大线程数大小
    keepAliveTime: 线程池中非核心线程空 闲的存活时间大小
    unit: 线程空闲存活时间单位
    workQueue: 存放任务的阻塞队列
    threadFactory: 用于设置创建线程的工厂,可以给创建的线程设置有意义的名字,可方便排查问题。
    handler: 线城池的饱和策略事件,主要有四种类型。

  2. 再描述提交任务后的执行过程

    • 提交一个任务,线程池里存活的核心线程数小于线程数corePoolSize时,线程池会创建一个核心线程去处理提交的任务。
    • 如果线程池核心线程数已满,即线程数已经等于corePoolSize,一个新提交的任务,会被放进任务队列workQueue排队等待执行。
    • 当线程池里面存活的线程数已经等于corePoolSize了,并且任务队列workQueue也满,判断线程数是否达到maximumPoolSize,即最大线程数是否已满,如果没到达,创建一个非核心线程执行提交的任务。
    • 如果当前的线程数达到了maximumPoolSize,还有新的任务过来的话,直接采用拒绝策略处理。
  3. 四种拒绝策略

AbortPolicy(抛出一个异常,默认的)
DiscardPolicy(直接丢弃任务)
DiscardOldestPolicy(丢弃队列里最老的任务,将当前这个任务继续提交给线程池)
CallerRunsPolicy(交给线程池调用所在的线程进行处理)

如何自定义拒绝策略:实现RejectedExecutionHandler接口,实现rejectedExecution方法
public interface RejectedExecutionHandler {
void rejectedExecution(Runnable r,
ThreadPoolExecutor executor);
}

- 怎么配置参数#

线程数:

如果是CPU密集型应用,则线程池大小设置为N+1

如果是IO密集型应用,则线程池大小设置为2N+1

系统负载: 一个进程或线程正在被cpu执行或等待被cpu执行,则系统负载+1, 单核cpu负载小于1表示cpu可以在线程不等待的情况下处理完

IO密集:通常指网络IO

spring

- 谈谈对springIoc的了解#

如何完美阐述你对IOC的理解?

// 最底层map: Map<String, Object> singletonObjects = new ConcurrentHashMap(256);

个人理解:
1.控制反转是一种设计思想,将对象生命周期和依赖的控制由对象转给了ioc容器,对象由主动创建其它对象变为被动由ioc容器注入
2.springioc容器控制了对象的创建、销毁及整个生命周期,,可以动态注入依赖的对象
3.将对象与对象间解耦,对象不再直接创建依赖其它对象,都依赖springioc容器

扩展:springIoc的初始化过程

它负责业务对象的构建管理和业务对象间的依赖绑定。

- 谈谈对springAop的了解#

AOP为面向切面编程,底层是通过动态代理实现,实质上就是将相同逻辑的重复代码横向抽取出来, 拦截对象方法,对方法进行改造、增强!比如在 方法执行前方法返回后方法前后方法抛出异常后 等地方进行一定的增强处理,应用场景: 事务、日志、权限、监控打点

基于动态代理来实现,在容器启动的时候生成代理实例。默认地,如果使用接口的,用 JDK 提供的动态代理实现,如果没有接口,使用 CGLIB 实现

优点:每个关注点现在都集中于一处,而不是分散到多处代码中;服务模块更简洁,服务模块只需关注核心代码

@Pointcut(切点):用于定义哪些方法需要被增强或者说需要被拦截
@Before、@After、@Around、@AfterReturning、@AfterThrowing(增强): 添加到切点指定位置的一段逻辑代码

注:@AfterReturning 比 @After 的方法参数多被代理方法的返回值

参考:
Spring AOP 使用介绍,从前世到今生
Spring AOP就是这么简单啦

- 动态代理和静态代理的区别#

静态代理在运行之前就已经存在代理类的字节码文件了(.class文件),而动态代理是在运行时通过反射技术来实现的

静态代理,如果不同接口的类想使用代理模式来实现相同的功能,将要实现多个代理类,但在动态代理中,只需要一个代理类就好了

只有实现了某个接口的类可以使用Java动态代理机制,对于没有实现接口的类,就不能使用该机制。使用cglib。

静态代理:代理类与委托类实现同一接口,并且在代理类中需要硬编码接口

JDK动态代理:代理类与委托类实现同一接口,主要是通过代理类实现InvocationHandler并重写invoke方法来进行动态代理的,在invoke方法中将对方法进行增强处理

CGLIB动态代理:代理类将委托类作为自己的父类并为其中的非final委托方法创建两个方法,一个是与委托方法签名相同的方法,它在方法中会通过super调用委托方法;另一个是代理类独有的方法。在代理方法中,它会判断是否存在实现了MethodInterceptor接口的对象,若存在则将调用intercept方法对委托方法进行代理

- Java反射的原理#

反射就是在运行时才知道要操作的类是什么,并且可以在运行时获取类的完整构造,并调用对应的方法

Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能就称为反射机制。

通俗一点反射就是把Java类中的各种成分映射成一个个的Java对象。

操作步骤:

  1. 获取类的 Class 对象实例
  2. 根据 Class 对象实例获取 Constructor 对象
  3. 使用 Constructor 对象的 newInstance 方法获取反射类对象
  4. 获取方法的 Method 对象
  5. 利用 invoke 方法调用方法

为什么类可以动态的生成?
这就涉及到Java虚拟机的类加载机制了,推荐翻看《深入理解Java虚拟机》7.3节 类加载的过程。
Java虚拟机类加载过程主要分为五个阶段:加载、验证、准备、解析、初始化。其中加载阶段需要完成以下3件事情:

通过一个类的全限定名来获取定义此类的二进制字节流
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据访问入口

关于第1点,获取类的二进制字节流(class字节码)就有很多途径
动态代理就是想办法,根据接口或目标对象,计算出代理类的字节码,然后再加载到JVM中使用。但是如何计算?如何生成?情况也许比想象的复杂得多,我们需要借助现有的方案

通常使用反射,是想灵活的获取或操作obj的属性或方法信息,而不和具体的某个类耦合。这个时候,只要拿到类的Class对象,就可以获取对象的属性,和执行方法,或者new一个instance。eg: excel导出, 泛型属性解析。

- spring中的设计模式#

简单工厂模式#

简单工厂模式的实质是由一个工厂类根据传入的参数,动态决定应该创建哪一个产品类。
spring中的BeanFactory就是简单工厂模式的体现,根据传入一个唯一的标识来获得bean对象,但是否是在传入参数后创建还是传入参数前创建这个要根据具体情况来定。

工厂方法模式#

工厂方法将创建产品的代码与实际使用产品的代码分离, 从而能在不影响其他代码的情况下扩展产品创建部分代码。
例如, 如果需要向应用中添加一种新产品, 你只需要开发新的创建者子类, 然后重写其工厂方法即可。

一般情况下,应用程序有自己的工厂对象来创建bean.如果将应用程序自己的工厂对象交给Spring管理, 那么Spring管理的就不是普通的bean,而是工厂Bean。
理解:通过扩展工厂子类的方式,调用其工厂方法创建新对象。

单例模式#

保证一个类仅有一个实例,并提供一个访问它的全局访问点。
spring中的单例模式完成了后半句话,即提供了全局的访问点BeanFactory。但没有从构造器级别去控制单例,这是因为spring管理的是是任意的java对象。

Spring下默认的bean均为singleton,可以通过singleton=“true|false” 或者 scope=”?”来指定。

代理设计模式#

Spring AOP 功能的实现。

适配器模式#

springaop中的advice,需要适配成 MethodInterceptor 接口,才能组成一条拦截器链,做依次的织入。
不同的Advice需要通过 不同适配器(XXXAdviceInterceptor) 适配成一个个 MethodInterceptor。使用适配器模式,持有advice实例,实现MethodInterceptor 接口。

我们知道 Spring AOP 的实现是基于代理模式,但是 Spring AOP 的增强或通知(Advice)使用到了适配器模式,与之相关的接口是AdvisorAdapter 。Advice 常用的类型有:BeforeAdvice(目标方法调用前,前置通知)、AfterAdvice(目标方法调用后,后置通知)、AfterReturningAdvice(目标方法执行结束后,return之前)等等。
每个类型Advice(通知)都有对应的拦截器:MethodBeforeAdviceInterceptor、AfterReturningAdviceAdapter、AfterReturningAdviceInterceptor。
Spring预定义的通知要通过对应的适配器,适配成 MethodInterceptor接口(方法拦截器)类型的对象(如:MethodBeforeAdviceInterceptor 负责适配 MethodBeforeAdvice)。

参考文档:https://www.jianshu.com/p/57d3d1beeb62

模板方法模式#

JDBCTemplate使用了模板方法 + 回调模式。把直接执行statment的操作延迟到子类实现,通过回调函数的方式传递进来,父类实现对连接和异常的处理等操作。
execute中接收一个 StatementCallback 的回调函数,子类只要传递自己处理statment获取结果的回调函数即可,不需要再写过多的重复代码。

1
2
3
4
5
@FunctionalInterface
public interface StatementCallback<T> {
@Nullable
T doInStatement(Statement var1) throws SQLException, DataAccessException;
}

execute实现(调用回调函数StatementCallback#doInStatement)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Nullable
public <T> T execute(StatementCallback<T> action) throws DataAccessException {
//参数检查
Assert.notNull(action, "Callback object must not be null");
//获取连接
Connection con = DataSourceUtils.getConnection(this.obtainDataSource());
Statement stmt = null;
Object var11;
try {
//创建一个Statement
stmt = con.createStatement();
//设置查询超时时间,最大行等参数(就是一开始那些成员变量)
this.applyStatementSettings(stmt);
//执行回调方法获取结果集
T result = action.doInStatement(stmt);
//处理警告
this.handleWarnings(stmt);
var11 = result;
} catch (SQLException var9) {
//出现错误优雅退出
String sql = getSql(action);
JdbcUtils.closeStatement(stmt);
stmt = null;
DataSourceUtils.releaseConnection(con, this.getDataSource());
con = null;
throw this.translateException("StatementCallback", sql, var9);
} finally {
JdbcUtils.closeStatement(stmt);
DataSourceUtils.releaseConnection(con, this.getDataSource());
}
return var11;
}

update方法详解(使用lambda函数传递更新的实现操作)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
protected int update(PreparedStatementCreator psc, @Nullable PreparedStatementSetter pss) throws DataAccessException {
this.logger.debug("Executing prepared SQL update");
return updateCount((Integer)this.execute(psc, (ps) -> {
Integer var4;
try {
if (pss != null) {
pss.setValues(ps);
}
int rows = ps.executeUpdate();
if (this.logger.isTraceEnabled()) {
this.logger.trace("SQL update affected " + rows + " rows");
}
var4 = rows;
} finally {
if (pss instanceof ParameterDisposer) {
((ParameterDisposer)pss).cleanupParameters();
}
}
return var4;
}));
}

JDBCTemplate使用了很多回调。为什么要用回调(Callback)?
如果父类有多个抽象方法,子类需要全部实现这样特别麻烦,而有时候某个子类只需要定制父类中的某一个方法该怎么办呢?这个时候就要用到Callback回调了就可以完美解决这个问题,可以发现JDBCTemplate并没有完全拘泥于模板方法,非常灵活。我们在实际开发中也可以借鉴这种方法。

参考文档:https://juejin.cn/post/6844903847966703624#heading-5

- spring aspect advice pointcut理解#

1、adivisor是一种特殊的Aspect,Advisor代表spring中的Aspect
2、区别:advisor只持有一个Pointcut和一个advice,而aspect可以多个pointcut和多个advice

  • 方/切 面(Aspect):一个关注点的模块化,这个关注点实现可能另外横切多个对象。事务管理是J2EE应用中一个很好的横切关注点例子。方面用Spring的Advisor或拦截器实现。
  • 连接点/织入点(Joinpoint):程序执行过程中明确的点,如方法的调用或特定的异常被抛出。
  • 通知(Advice):在特定的连接点,AOP框架执行的动作。各种类型的通知包括“around”、“before”和“throws”通知。
  • 切入点(Pointcut):指定一个通知将被引发的一系列连接点的集合。AOP框架必须允许开发者指定切入点,例如,使用正则表达式。

Springboot启动简要流程#

  1. SpringApplication 初始化,收集所有ApplicationContextInitializer和ApplicationListener。
  2. SpringApplication run
    调用started事件
    创建并准备Environment(PropertySource、Profile)
    调用environmentPrepared事件
    创建ApplicationContext,遍历调用ApplicationContextInitializer的initialize方法
    调用contextPrepared 事件
    创建并配置BeanDefinitionLoader
    调用contextLoaded 事件
    applicationContext的refresh()方法, 完成ioc容器初始化
    调用started事件
    callRunners(ApplicationRunners,CommandLineRunners)
    调用running事件

Springboot启动总结#

Springboot的启动,主要创建了配置环境(environment)、事件监听(listeners)、应用上下文(applicationContext),并基于以上条件,在容器中开始实例化我们需要的Bean
自动装配核心:@EnableAutoConfiguration中的AutoConfigurationImportSelector中的SpringFactoriesLoader 提供一种配置查找的功能支持,即根据@EnableAutoConfiguration的完整类名org.springframework.boot.autoconfigure.EnableAutoConfiguration作为查找的Key,获取对应的一组@Configuration类。并在后续加载到ioc容器。

系统设计

- 高并发系统提性能#

读优化
多级缓存:让数据靠近计算
链路优化:合并batch请求缩短整体耗时

写优化
流量漏⽃:缓存预扣减少数据库的⽆效访问
分库分表:提升数据库的整体写⼊能⼒( 窄表设计下单分片物理机可承载4500,云服务器可承载3000,仅供参考,具体以业务测试为准 )
并⾏扣减:降低库存扣减的响应时间
分库存:解决单行更新能力不足问题,(一般单行更新的QPS在500以内)
合并请求:异步批量,将多次扣减累计计数,集中成一次扣减,从而实现了将串行处理变成了批处理。大大减轻更新压力。

扩展

  1. 如果某个sku_id的库存扣减过热,单台实例支撑不了(mysql官方测评:一般单行更新的QPS在500以内),可以考虑将一个sku的大库存拆分成N份,放在不同的库中(也就是说所有子库的库存数总和才是一件sku的真实库存),由于前台的访问流量非常大,按照均分原则,每个子库分到的流量应该差不多。上层路由时只需要在sku_id后面拼接一个范围内的随机数,即可找到对应的子库,有效减轻系统压力。
  2. 单条sku库存记录更新过热,也可以采用批量提交方式,将多次扣减累计计数,集中成一次扣减,从而实现了将串行处理变成了批处理,也可以大大减轻数据库压力。
  3. 引入RocketMQ消息队列,经过前置校验后,如果有剩余库存,则把创建订单的操作封装成消息发送给MQ,订单系统从RocketMQ中以特定的频率消费,创建订单,该方案有一定的延迟性。

- 请求幂等性#

请求幂等性总结:
1.是否需要幂等。比如查询,insert含唯一索引的表,update set数据,delete by id 等简单场景是天然幂等。不需要额外做幂等操作。无法这样的比如要做数据的累加,就要做幂等。
2.如何判断重复。
业务唯一键,查记录表或流水表,根据记录的有无和状态来判断。
3.实现。

  1. 简单的话接口直接实现, 但通常幂等逻辑是有通用性的
  2. 如果服务多接口使用写幂等工具类
  3. 如果多服务使用一套幂等逻辑,写sdk
  4. 最复杂的多服务间幂等,还无法都获取到记录结果,就要在sdk统一读写幂等表,读写相同的幂等记录做幂等
  5. 并发处理
  6. 单机情况下,使用java锁synchronized
  7. 使用数据库锁,悲观锁:select for update,乐观锁:update set XXX and version = 2 where version = 1
  8. 使用分布式锁:
    redis锁:1.过期时间导致并发问题,2.主从切换时锁丢失
    zookeeper锁:写能力不如redis

幂等设计

- 分布式锁#

方案1、基于数据库唯一主键
原理:在数据库中创建一个表,表中包含方法名等字段,并在方法名字段上创建唯一索引,想要执行某个方法,就使用这个方法名向表中插入数据,成功插入则获取锁,执行完成后删除对应的行数据释放锁。

方案2、基于redis实现分布式锁
原理:通过redis 中的命信setNx 来实现 (当key不存在则设置值并返回true,否则返回失败)

方案3、基于Zookeeper来实现分布式锁
原理:1、先建一个目录lock 2、线程A想获取锁就在lock目录下创建一个带顺序的临时节点 3、然后获取比当前顺序号小的顺序号,获取到则获取锁失败,获取不到则获取锁成功。 4、解锁则是删除这个临时节点。

- 分布式主键生成方案选择#

分布式自增ID的实现

1.uuid
组成部分:当前日期和时间、时钟序列、机器识别码

缺点:
UUID长度128bit,32个16进制字符,占用存储空间多,且生成的ID是无序的

对于InnoDB这种聚集主键类型的引擎来说,数据会按照主键进行排序,由于UUID的无序性,InnoDB会产生巨大的IO压力,此时不适合使用UUID做物理主键。

2.号段模式,底层proxy服务+数据库分段获取id
结合数据库维护一个Sequence表,每当需要为某个表的新纪录生成ID时就从Sequence表中取出对应的nextid,将其+1后更新到数据库中以备下次使用。

缺点:由于所有的插入都要访问该表,很容易造成性能瓶颈。
在高并发场景下,无法保证高性能。

3.Snowflake
使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID),最后还有一个符号位,永远是0。

长度为64bit,占用存储空间多
存在时钟回拨问题, leaf已解决

- 如何实现延迟队列#

delaymq

- 高并发系统的稳定性保证#

限流,单机限流动态扩容,集群限流保护存储
隔离,业务与业务隔离,避免多业务之间互相影响。读写服务隔离,关注点不一样。环节隔离,查询和交易环节在流量和响应时间上有很大差异(api, order)。
监控,业务量级、成功失败原因、活动量级上报,尤其是失败原因。
降级

  • 非核心功能,容量问题,非核心功能。比如压力过大,先限流降级查询功能,减少入口流量,避免雪崩。
  • 非核心依赖,功能异常问题,熔断降级。eg: 直播视频制作,依赖算法,mmu,ytech,音视频,区分非核心依赖:mmu,ytech,即可做区别降级处理。发第三方券。
    全链路压测
    定期进行全链路压测评估链路流量负载能力
    总结
    事前:链路梳理,明确强弱依赖
    性能评估,容量评估,负载评估
    完善监控,明确因果,建设大盘
    降级预案,区分场景,区分等级
    事中:扩容、限流、降级、熔断
    事后:性能优化,问题修复,复盘总结

redis监控:读写qps,内存使用率,响应耗时,缓存命中率,慢查询,大key
kafka监控:消费延迟量,消费失败数量,生产&消费速率

- 双写服务迁移#

  1. 迁移模式:老服务代理新服务完成内部迁移,再推业务方做sdk迁移
  2. 双写操作:以谁为主,就同步写为主一侧,异步写另一侧,减少耗时
  3. 路由策略
    查询以灰度为准,下单以灰度为准
    退款、回滚固定以下单为准。防止一单交易不同环节分别走到新老,新老数据可能不一致。 退款回滚时,请求新老流水,根据流水路由信息,判断退款路由策略
  4. 实时响应比对:双写完, 整合两侧结果上报mq, 消费mq做结果比对,打点,告警,问题及时发现
  5. 离线数据最终一致校正:离线任务跑新老流水,判断以谁为主,以谁为主就用谁的流水校正另一侧的流水

- 领域划分#

营销平台领域划分
业务领域:触达,感知,转化,留存。
工具域:立减,拼团,积分,优惠券
基础域:选品,选人,规则,库存,预算

基建平台领域划分
前台: 代理商平台、crm平台、财务平台
业务编排:代理商引入、广告主引入、转账充值
基础域:客户域、财务域、报表分析域、销售管理域
支撑域:upm、计费、dsi、passport、审核

直播剪辑的整体流程
近实时、配置化、异步制作

  1. 自动剪辑配置,开启时段,剪辑时长,字幕,配乐,片头片尾
  2. 自动剪辑,定时任务时间驱动,每10min, 异步制作
    定时任务

-> mq异步 ( 算法视频切片 -> mmu语音转文字 -> ytech视频特效 )
-> mq复用 ( 音视频片头片尾)
注:mmu、ytech、音视频可降级。

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×