MQ

- mq使用场景#

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

- 如何保证消息顺序性#

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

  • 全局有序:一个Topic下的所有消息都需要按照生产顺序消费。 要满足全局有序,需要1个Topic只能对应1个Partition。consumer内部单线程。
  • 局部有序:一个Topic下的消息,只需要满足同一业务字段的要按照生产顺序消费
    • producer按shardingkey分片的方式发到不同的分区
    • consumer单线程消费分区或保证相同的sharding规则下顺序线程模型串行消费

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

刷盘+重试、确认

rocketmq

  • 多副本机制、持久化机制
  • 生产者发消息重试&ack,生产者发送消息自动重试到最大次数,发送时可配置同步刷盘再返回ack
  • broker发消息重试&ack,消费者接收到消息后会发送ack到broker否则broker也会重试

kafka

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

- 如何保证高可用#

  • 分布式部署多分区
  • 主从复制, 副本机制
  • 同步复制,可配置broker节点之间同步复制
  • 故障切换与恢复,自动检测触发故障转移

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

rocketmq

  • 顺序读写。
  • 可异步刷盘
  • 零拷贝。
  • 高效的消息存储结构。commitlog,cq indexfile,简化了存储优化了读写
  • 批量处理。支持批量发送和消费,减少网络交互

- rocketmq & kafka#

rocketmq功能性更丰富一些。kafka性能在topic不多的时候更优一些。

功能性
kafka不支持延时消息,不支持消息轨迹追踪和消息过滤,最新版本才开始支持事务消息。
rocketmq支持消息过滤,在broker做过滤,减少网络压力;在consumer端可自定义过滤条件。

性能
kafka因为是partition维度的文件存储,当topic多的时候随机写变多,性能下滑明显;相比rocketmq因为都是顺序写入commitlog,在topic多时对性能也不会有影响。

分布式锁

1、 基于MySQL的分布式锁实现
原理:直接创建一张锁表,然后通过操作该表中的数据来实现了.
。利用唯一索引的排他性来实现分布式锁,只有抢到锁的线程才能插入成功,释放锁删除该条记录即可

缺点:
1、强依赖数据库的可用性,存在单点问题。
2、数据库在高并发的场景下,性能上表现欠佳

2、基于Redis的分布式锁实现
可以通过set nx的特性来实现分布式锁

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

临时节点是根据客户端维护的session会话来维持生命周期的,当客户端不在维护session时,或者说客户端断开连接时,session消失,那么临时节点就会消失,这种特性正好满足了分布式锁要有过期时间的需求,不会产生死锁问题

缺点:抗不了多少并发,内存无法横向扩展

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参数设置,通过appendonly参数设置为yes开启AOF,同步策略参数默认是appendfsync everysec每秒将aof缓冲区的内容同步到磁盘的aof文件中。
也可以配置参数apendfsync为always,这样每次写入都会立即同步磁盘,但是性能影响大,我们使用的是默认的everysec每秒。

RDB快照默认是开启的,默认每15分钟进行一次快照。快照的实现是通过fork父进程,生成一个子进程,父进程继续处理client请求,子进程把内存数据写到临时rdb文件,因为os的copy on write, 父子进程共享物理页面,父进程写的时候会为父进程创建副本去写,子进程的地址空间内数据是fork时刻的整个数据库的一个快照。当写入完毕后,用临时文件替换原文件,子进程退出。

so,rdb快照生成是不影响master处理client请求的,是通过fork子进程的方式操作的,aof的重写也是用了这种fork子进程的方式,最后将临时文件替换原文件。

- 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 文件。

java

### - 常见Java集合类

非线程安全
ArrayList: 底层数组
LinkedList: 底层双向链表
HashMap: (重要必考) 数组加链表,链表长度 到 8 转化为红黑树

- ArrayList 和 LinkedList的区别#

  1. 数据结构 数组 / 双向链表
  2. 是否支持快速随机访问
  3. 随机插入删除速度, ArrayList需要整体位移

- 项目中用到过哪些数据结构#

queue, map, set, list, 堆(priorityQueue)

- BIO、NIO、AIO 的区別和联系#

BIO:同步、阻塞。服务器实现模式为一个连接一个线程,服务器端为每一个客户端的连接请求都需要启动一个线程进行处理

NIO:多路复用技术,同步非阻塞。服务器实现模式为客户端的连接请求都会注册到多路复用器上,用同一个线程接收所有连接请求

AIO:异步非阻塞IO

BIO里用户最关心“我要读”,NIO里用户最关心”我可以读了”,在AIO模型里用户更需要关注的是“读完了”

- 谈谈对Java NIO的了解#

NIO(Non-blocking I/O,在Java领域,也称为New I/O),是一种同步非阻塞的I/O模型,也是I/O多路复用的基础,已经成为解决高并发与大量连接、I/O处理问题的有效方式。
传统的BIO模型严重依赖于线程。但线程是很”贵”的资源。创建和销毁成本很高,本身占用较大内存,切换成本是很高。

NIO由原来占用线程的阻塞读写变成了单线程轮询事件,找到可以进行读写的网络描述符进行读写。大大地节约了线程,为处理海量连接提供了可能
NIO的主要事件有几个:读就绪、写就绪、有新连接到来。

首先需要注册当这几个事件到来对应的处理器。然后在合适的时机告诉事件选择器
;其次,用一个死循环选择就绪的事件,会执行系统调用(Linux 2.6之前是select、poll(轮询),2.6之后是epoll(通知))

总结NIO带来了什么:

  • 事件驱动模型
  • 避免多线程
  • 单线程处理多任务
  • 非阻塞I/O,I/O读写不再阻塞
  • 基于block的传输,通常比基于流的传输更高效(buffer)
  • 更高级的IO函数,zero-copy
  • IO多路复用大大提高了Java网络应用的可伸缩性和实用性
  • JavaNIO使用channel、buffer传输,更高效

白话:nio 的核心在selector,相比于BIO一连接一线程,nio 一线程可以监听多个网络连接的文件描述符,监听处理其中的事件。这样很好的节省了线程资源,会增加服务端的并发能力和吞吐。

- select epoll#

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

- 谈谈对hashMap的了解#

基本:底层数据结构, 数组 + 链表,长度到8变为红黑树,非线程安全
注1:HashMap在put时, 经过了两次hash,一个是JDK自带的对对象key的hash,一个是内部扰动函数hash,做高16位与低16位异或,加大散列的效果
注2:HashMap 的长度为什么是2的幂次方。1. 散列值用之前要对数组长度取模,求桶位(n - 1) & hash 更高效;2. 扩容的时候也可以用 hash值与扩容后的最高位 & 判断节点是在高位还是低位,更高效。
注3:HashMap 非线程安全,多线程操作导致死循环问题,1.7 并发下头插形成循环链表,1.8 用尾插修复, 但仍可能造成丢失数据。

HashMap.Node<K, V>[] table;

get是怎么做的

  1. hash。 hashcode高低16位异或得到hash值, (h = key.hashCode()) ^ h >>> 16
  2. 根据hash值拿到桶位第一个节点,判断是否可以直接返回
  3. 判断节点中结构类型是树还是链表,树则调用树查询方法,链表则遍历链表比较

put是怎么做的

  1. hash。 hashcode高低16位异或得到hash值, (h = key.hashCode()) ^ h >>> 16
  2. 根据hash值拿第一个桶位的节点,如果为空则新建节点插入
  3. 判断节点结构是树还是链表,树则调用树插入的方法, 链表则添加到表尾,如果链表节点到了8个,则触发treeifyBin() !如果整体节点数量 > threshold, 则resize()

resize是怎么做的

  1. new HashMap.Node[newCap],遍历每一个桶位
  2. 如果只有一个节点直接赋到新桶位,新桶位计算方式:e.hash & newCap - 1
  3. 如果节点数据结构为树,则走树的split()
  4. 如果节点数据结构为链表,遍历链表,判断属于新桶还是老桶,通过:e.hash & oldCap(2^n), 通过最高位的0,1值

白话:hashmap是非线程安全的集合。底层是数组+链表,链表长度到8会转成红黑树。hashmap中table的长度总是2的幂次方,这样可以使用&运算代替取余运算(%)来提高效率,在求桶位和扩容时获取节点新位置在左边还是右边的时候。

- loadFactor和threshold的关系?#

size: 当前共有多少个KV对
capacity: 当前HashMap的容量是多少, 桶位数, table大小, 默认是16
loadFactor: 负载因子,默认0.75
threshold: loadFactor* capacity,

白话:负载因子就是控制碰撞几率的,调的小,碰撞的概率小,调的大,碰的概率就大。阈值就是扩容时机。阈值等于负载因子乘以table大小

分布式事务

分布式事务#

分布式事务,主要为了实现分布式系统下一组操作的全部成功或全部失败,
主要分为数据库层的和业务层的分布式事务,

一、数据库层的主要是基于XA协议的分布式事务,实现的强一致性,基于XA模型,主要有全局的事务管理器和本地资源管理器,事务管理器会调度本地资源管理器的本地事务的提交和回滚,达到全局的强一致性

二、业务层的,基于CAP和BASE理论,实现的最终一致性,来支持的分布式事务,主要有tcc模型的补偿性事务,tcc模型主要将操作分为三个,try, confirm, cancel, try会预留该事务的资源,即使当前事务出问题,也不会影响其它事务,支持高并发, 当一组服务的try都成功,tcc会保证所有服务的confirm最终成功,如果一组服务中有try失败,tcc会保证所有的服务最终cancel成功。

事务#

核心:修改多个数据时,保证对多个数据操作一致性

如何保证一致性: 两阶段

  • 让失败的部分操作 全部成功 ( x )
  • 让成功的部分操作 全部回退 ( √ )

事务一致性的基本要求#

在提交之前,要保证所有的操作都可以回滚
条件:

  • 对每个操作,要知道如何回滚
  • 为保证能正确回滚,需要控制事务的并发
    在并发场景下,不能让两条rollback之间相互影响
    (可以是阻塞等待,也可以是抛出异常、返回失败。只要能避免事务往不一致的方向发展就好。)

tcc的一致性保证#

cancel/confirm 失败怎么办?– 重试到成功为止
如何重试? – 事务协调器 tc

tc#

TC 是一个独立的服务,用于协调分布式事务内的多个操作一起提交或者回滚。
存储了事务执行的中间状态

rocketMq事务#

先执行 DB 操作,再发消息,难以保证消息一定发出去。

RocketMQ 就反过来,先发一个半消息(Half Msg,类似一个 Prepare 操作),这个半消息是不会投递给消费者的,半消息发送成功再执行 DB 操作,DB 操作成功以后,提交半消息,这个时候半消息就变成一个普通消息送到消费者那里。

对于 RocketMQ 消费者而言,事务消息和非事务消息是没有区别的。

  1. 第 1 步发生异常,半消息发送失败,那么本地 DB 事务根本就不会执行,整个操作失败了,但是 DB/消息的状态是一致的(都没有提交)。
  2. 第 2 步发生异常,或者返回超时,生产者以为失败了,因此 DB 操作不会执行,也就没有后续了。另一边 Broker 存储半消息成功了,
    却迟迟等不到后续的提交操作,等了好久(超时)以后,Broker 就会跑来问生产者(回查,也就是第 4 步),这个半消息到底是要提交还是回滚?
    此时生产者去数据库中确认本地 DB 事务的完成状态(第 6 步),给 Broker 一个回答(第 7 步),然后 Broker 就知道要怎么办。
  3. 第 3 步 DB 操作失败,生产者可在第 4 步,告知 Broker 回滚半消息。另外生产者也可以报告状态为”未知”,然后由 Broker 稍后触发回查来决定提交还是回滚半消息。
  4. 第 4 步提交/回滚半消息失败,Broker 等不到这个操作,会在一段时间以后触发回查,与上面的第 2 步异常类似。
  5. 第 5、6、7 步回查失败,如果回查发生异常或者回查仍然返回”未知”,或回查失败,RocketMQ 稍后会重新调度,最多会回查 15 次

tcc和rocketMq事务的比较#

tcc优势

  1. rpc事务,将多个rpc组合在一起
  2. tcc可以无限去加更多的操作来判断整体的提交或回滚。事务中的操作有3个及以上,要根据其中的结果判断是否要回滚其中的前两个,这种rocketmq就无法去做。(rocketmq 比较被动,当本地事务提交了,后面的一定要提交,缺少判断多个操作的结果来判断整体是否提交的能力)
  3. rocket mq的异步消息在逻辑上一定是成功的,但tcc的其他操作也可以是失败的可以参与到整体成功与否的判断的。如果rocket mq中的异步操作不一定会成功不能使用rocket mq

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启动原理#

springboot在启动时除了spring context容器启动的流程,还加入了通过spi实现的根据依赖自动装配的机制。
springboot容器启动的流程,先初始化事件监听器,加载环境信息,创建applicationContext容器,执行applicationInitializer的initialize方法,在容器refresh时,会通过spi机制获取到所有的autoConfiguration类(解析spring.factotries文件)并加载配置类中相关bean注入context容器,完成自动装配。

Springioc解决循环依赖问题#

使用中间缓存,使用二级缓存earlySingletonObjects,三级缓存singletonFactories一起解决的循环依赖问题。
earlySingletonObjects就是一个临时存放初始bean的缓存。
singletonFactories是会在获取依赖的A时,调用一个wrapIfNessasory来获得一个aop后的A, 从而解决循环依赖aop bean的问题。
注:springioc只能解决set注入的循环依赖问题,无法解决构造方法注入的循环依赖问题,因为A在构造时依赖B,B在构造获取A的时候,A都还没有创建出来,没有能放到一个中间缓存解决循环依赖的机会。

系统设计

- 高并发系统提性能#

读优化
多级缓存:让数据靠近计算
链路优化:合并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、音视频可降级。

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

Your browser is out-of-date!

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

×