Java
2022-01-24 16:54:04 4 举报
AI智能生成
登录查看完整内容
包含Jvm、Jdk、redis、mq、mysql及一些方法论的沉淀,覆盖大部分面试考点
作者其他创作
大纲/内容
Netty
四大组件
Pipeline
Tomcat
Consumer
Producer
Registry
Monitor
各组件作用
服务暴露发现过程
消费者到提供者的调用过程
流程
跨语言、高效、二进制
hession
文本形式,性能差
json
尚不成熟
dubbo
性能差
java
性能好
protobuf
性能好,包含序列化之外的功能
Thrift
方案对比
序列化
负载均衡
CP
zk
etcd
consul
如果一个服务器出问题,不需要任何类型的选举,客户端会自动连接到一个新的Eureka服务器Eureka有一个服务心跳的概念,可以阻止过期数据:如果一个服务长时间没有发送心跳,那么Eureka将从服务注册中将其删除。但在出现网络分区、Eureka在短时间内丢失过多客户端时,它会停用这一机制,进入“自我保护模式”。网络恢复后,它又会自动退出该模式。这样,虽然它保留的数据中可能存在错误,却不会丢失任何有效数据。Eureka在客户端会有缓存。即使所有Eureka服务器不可用,服务注册信息也不会丢失。缓存在这里是恰当的,因为它只在所有的Eureka服务器都没响应的情况下才会用到。Eureka就是为服务发现而构建的。它提供了一个客户端库,该库提供了服务心跳、服务健康检查、自动发布及缓存刷新等功能。使用ZooKeeper,这些功能都需要自己实现。
特性
AP
Eureka
Nacos
在注册中心的场景中,注册中心不可用 对比 数据不一致不一致:会导致客户端拿到的服务列表不一致,导致流量不均衡,但是若有最终一致的保证及failover机制,实际影响不大;不可用:例如同机房 业务服务提供节点要注册(或更新、缩容、扩容等)到zk集群,该机房和其他机房出现网络分区,由于无法连接zk的leader,导致本机房业务消费房无法感知本机房的提供方的变化,导致无法调用,这是难以容忍的。所以针对注册中心可能ap会更合适
注册中心
各种选型
Dubbo
Broker
NameSever
queue数量=consumer数量时,queue与consumer一对一指定。queue数量>consumer数量时,其中一些消费者会消费多个队列。queue数量<consumer数量,queue与consumer一对一指定,多出来的消费者空闲。
消费策略
见 https://juejin.cn/post/6844903511235231757
单机高队列数
数据可靠
支持消费失败重试
分布式事务
支持消息轨迹
如订单状态变化的消息,每种状态只处理一次,那么可以以订单号+状态作为幂等key,可以先用redis作为前置校验,通过后再插入数据库唯一索引,保证幂等
以订单场景为例
消息消费端幂等处理
每个状态单独分出一个或多个独立的字段。消息来时只更新其中一个字段,会有短暂的不一致,但是最终会一致。
方案 1 宽表
方案 2 消息补偿机制
方案3 利用同一个分区实现顺序消息
如订单有支付、收货、完成、退款等消息,程序中的顺序并不一定符合预期
消息顺序问题
RocketMQ主要通过MappedByteBuffer对文件进行读写操作。其中,利用了NIO中的FileChannel模型将磁盘上的物理文件直接映射到用户态的内存地址中(这种Mmap的方式减少了传统IO将磁盘文件数据在操作系统内核地址空间的缓冲区和用户应用程序地址空间的缓冲区之间来回进行拷贝的性能开销),将对文件的操作转化为直接对内存地址进行操作,从而极大地提高了文件的读写效率(正因为需要使用内存映射机制,故RocketMQ的文件存储都使用定长结构来存储,方便一次将整个文件映射至内存)
零拷贝
OS使用PageCache机制对读写访问操作进行了性能优化,将一部分的内存用作PageCache。对于数据的写入,OS会先写入至Cache内,随后通过异步的方式由pdflush内核线程将Cache内的数据刷盘至物理磁盘上。对于数据的读取,如果一次读取文件时出现未命中PageCache的情况,OS从物理磁盘上访问读取文件的同时,会顺序对其他相邻块的数据文件进行预读取。
页缓存
为什么快?
rocketmq的master/slave角色是固定的,没有选举,NameServer只需要提供topic/queue的路由。而kafka的需要依赖zk选主
为什么kafka需要zk而rocket不需要?
topic+分区进行组织,多副本机制,且flower和leader不会在同一个机器
这样的组织虽然单文件是顺序追加写,但是当topic很多时,消息高并发写入的情况下,IO就会显得零散,因为要同时写入多个文件,就相当于随机写,即Kafka的写入性能在IO增加时性能会先上升后下降;并且扩容比较复杂,涉及老数据迁移
Kafka
消息和消费进度分开,消息都放在commitLog,消费进度在consumeQueue且按topic/queue的形式组织,副本是以commitLog复制
追求极致顺序写,只写一个commitLog文件,但这样也比较浪费,无法充分发挥磁盘IO性能,但是扩容比较简单,只影响新消息,运维成本低
RocketMQ
文件布局
sendfile 系统调用相比内存映射多了一次从用户缓存区拷贝到内核缓存区,但对于超过64K的内存写入时往往 sendfile 的性能更高,可能是由于 sendfile 是基于块内存的
基于sendfile
基于mmap
数据写入方式
消息在客户端进行组织并插入一个双端队列,按批次发送,由另外的线程去获取队列中的批次,会增加响应时间但是提高了吞吐量
在客户端路由到某个队列,发送到服务端进行组织、持久化
消息发送方式
Kafka 和 RocketMQ 之性能对比 https://mp.weixin.qq.com/s/KzMPPZ0NNHJkHiqbx6v1sw
Kafka 在性能上综合表现确实要比 RocketMQ 更加的优秀,但在消息选型过程中,我们不仅仅要参考其性能,还有从功能性上来考虑,例如 RocketMQ 提供了丰富的消息检索功能、事务消息、消息消费重试、定时消息等。span style=\
Kafka对比
RabbitMQ 对比
「空间预分配」:当一个sds被修改成更长的 buf 时,除了会申请本身需要的内存外,还会额外申请一些空间。
「惰性空间」:当一个sds被修改成更短的 buf 时,并不会把多余的内存还回去,而是会保存起来。
空间预分配和惰性空间释放来提升效率,缺点就是耗费内存
struct sdshdr { int len; //长度 int free; //剩余空间 char buf[]; //字符串数组};
结构
string
链表被广泛用于实现 Redis 的各种功能,比如列表键、发布与订阅、慢查询、监视器
struct listNode { struct listNode * prev; //前置节点 struct listNode * next; //后置节点 void * value;//节点的值};
list
struct dict { ... dictht ht[2]; //哈希表 rehashidx == -1 //rehash使用,没有rehash的时候为-1}
每个字典有两个hash表,一个平时使用,一个rehash的时候使用。rehash是渐进式的
触发:1. serveCron定时检测迁移 2. 每次kv变更的时候(新增、更新)的时候顺带rehash。
rehash
采用单向链表的方式解决hash冲突,新的冲突元素会被放到链表的表头
hash冲突
hash
struct zskiplistNode { struct zskiplistLevel{ struct zskiplistNode *forward;//前进指针 unsigned int span;//跨度 } level[]; struct zskiplistNode *backward;//后退指针 double score;//分值 robj *obj; // 成员对象};
结构(ziplist or skiplist)
方便范围查找
实现简单
插入元素方便,只需要修改相邻元素的指针
为什么跳表而不是红黑树?
zset
set 的底层为了实现内存的节约,会根据集合的类型和数目而采用不同的数据结构来保存,元素都是整数时intset(整数且元素不多)、非整数时dict。无序,不重复
struct intset { uint32_t encoding;//编码方式 uint32_t length;//集合包含的元素数量 int8_t contents[];//保存元素的数组(哈希表实现)};
set
数据结构
内存型数据库
预分配、跳表、渐进式rehash等
简单/特定的数据结构
redis 的主体模式还是单线程的,除了一些持久化相关的 fork。单线程相比多线程的好处就是锁的问题,上下文切换的问题。官方也解释到:redis 的性能不在 cpu,而在内存。(注:并不是整个reids进程就一个线程,例如处理请求的是一个IO线程,执行命令也有一个线程)
单线程
IO 多路复用就是多个 TCP 连接复用一个线程,redis采用reactor模型,一个IO多路复用线程监听多个socket(利用操作系统的epoll),Redis4.0开始支持多线程,主要体现在大数据的异步删除方面,例如:unlink key、flushdb async、flushall async等。而Redis6.0的多线程则增加了对IO读写的并发能力,用于更好的提升Redis的性能。
如果采用多个请求起多个进程或者多个个线程的模式还是比较重的,除了要考虑到进程或者线程的切换之外,还要用户态去遍历检查事件是否到达,效率低下
通过 IO 多路复用技术,用户态不用去遍历fds集合,通过内核通知告诉事件的到达,效率比较高。
IO多路复用
快的原因?
定期删除,redis默认每100ms检查是否有过期的key,有过期的key则删除。需要说明的是redis不是每个100ms将所有的key检查一次,而是随机抽取进行检查(如果每100ms,全部key进行检查,redis岂不是卡死了)。因此,如果只采用定期策略,会导致很多key到时间没有删除。惰性删除:获取key的时候,redis会检查一下,这个key如果设置过期时间那么是否过期了?如果过期 此时就删除。淘汰策略:因为前两者都无法保证删除所有过期的key,所以需要兜底方案
为什么不定时删除(对key设置过期时间的定时器,轮询到时间后删除)?因为定时删除需要使用到CPU,但高并发下需要CPU都尽量用来处理请求,所以不用这种方案
策略:定期删除+惰性删除+淘汰策略
淘汰时机:内存不足会触发我们设置的淘汰策略
noeviction:当内存使用超过配置的时候会返回错误,不会驱逐任何键allkeys-lru:通过LRU算法驱逐最久没有使用的键(一般用这个)volatile-lru:通过LRU算法从设置了过期时间的键集合中驱逐最久没有使用的键allkeys-random:从所有key中随机删除volatile-random:从过期键的集合中随机驱逐volatile-ttl:从配置了过期时间的键中驱逐马上就要过期的键volatile-lfu:从所有配置了过期时间的键中驱逐使用频率最少的键allkeys-lfu:从所有键中驱逐使用频率最少的键
淘汰策略
删除机制
SAVE 是手动保存方式,它会使 redis 进程阻塞,直至 RDB 文件创建完毕,创建期间所有的命令都不能处理。
save
与 SAVE 命令不同的是 BGSAVE,BGSAVE 可以不阻塞 redis 进程,通过 BGSAVE redis 会 fork 一个子进程去执行 rdb 的保存工作,主进程继续执行命令。
bgsave
redis 没有专门的用户导入的命令,redis 在启动的时候会检测是否有 RDB 文件,有的话,就自动导入。
关于导入
rdb
aof 先是写到aof_buf的缓冲区中,redis 提供三种方案将 buf 的缓冲区的数据刷到磁盘,当然也是 serverCron 来根据策略处理的。1. appendfsync always2. appendfsync everysec3. appendfsync no
描述:aof由于是类似日志追加的形式保存,其数据会越来越大,所以进行压缩存储,如多条命令合并为一条,这样存储就节省了很多。重写也不是分析现有 aof,重写就是从数据库读取现有的 key,然后尽量用一条命令代替
通过fork子进程重写,重写时机由文件大小来控制
aof重写
aof
将AOF和RDB的数据放到同一个文件,在bgrewriteaof时生成,文件前半段是rdb后半段是aof,恢复时比较快,虽然还是会丢数据,所以这个方案主要还是针对afo恢复速度慢和rdb数据不全的问题
混合持久化(4.0之后)
持久化
针对这种批处理命令,为了减少往返的开销,于是管道pipeline诞生了,通过管道我们可以把两条命令合并发送,只需要建立一次连接,但pipeline非原子性的
pipeline的好处
本质上是个业务问题,不是技术问题。技术上有标准答案,就不存在这个问题了所以需要结合业务场景看业务容忍度来确定方案,强一致的方案建议弃用缓存,避免造成业务损失例如,针对营销的场景:在商品详情页/确认订单页的优惠计算时使用缓存,而在下单时不使用缓存。这可以让极端情况发生时,不产生过大的业务损失。针对库存的场景:读取到旧版本的数据只是会在商品已售罄的情况下让多余的流量进入到下单而已,下单时的库存扣减是操作数据库的,所以不会有业务上的损失。
1. 一般的写数据库+写缓存 / 写缓存写数据库 并发更新和非原子的问题 X 不采纳
2. 先删缓存 脏读问题 X 不采纳
1. 直接删缓存
2. 删缓存动作放到MQ(可以通过事务消息)
3. 监听Master 的 binlog,然后删缓存
删缓存方案
存在删除后,读slave库,slave复制延迟的问题,1. 可以通过延迟双删(发mq,设定估计的salve复制延迟时间的double)2. 可以起个定时任务扫表兜底同步(表可以是数据库操作记录表,写数据和操作记录同一个事务)或每周全量同步3. 起个JOB进行数据校验对比redis和mysql
还存在删缓存后缓存穿透db的问题,可以使用版本号更新缓存解决
3. 更新数据库+删缓存
和方案3. 更新数据库+删缓存区别在于数据库数据要加版本号信息缓存组件要判断版本号较新的进行更新,旧的忽略避免缓存穿透
4. 更新数据库 + 带版本号更新缓存
方案
一致性
高可用
可重入性
公平性
是否阻塞
性能
锁时间到了,业务还没执行完?解决方案:1. 提前给足时间2. 设置监控线程,每1/3时间来check一次,若业务还没处理完(业务执行),则延长锁时间,可以通过在设置redis key时,把key维护在内存如通过map维护,然后由另外的线程去监控这个map,自动去续期;锁删除前删除map中key,防止删除redis的key失败,若map不删,则监控线程无限续期。3. 异常回滚,解锁时检查当前线程是否持有,否则回滚业务。
redLock存在的问题
存在的问题
watch dog自动续期
可重入(通过hash结构,filed为线程id,value为重入数量,通过Lua脚本)
优点
判断加锁Key存不存在,不存在就新建hash结构,通过hincrby给指定的filed+1,KEYS[1] 是加锁key,KEYS[2]是客户端id(锁实例的UUID属性+线程id),加锁成功返回null
例子:127.0.0.1:6379> HGETALL myLock1) \"285475da-9152-4c83-822a-67ee2f116a79:52\"2) \"1\"
通过Lua脚本
加锁
当第二个线程来尝试加锁,会判断\
锁互斥
锁续期
第一步:删除锁,对field值-1第二步:若减到0,则del删除key,并广播释放锁的消息,通知阻塞等待的进程(向通道名为 redisson_lock__channel publish 一条 UNLOCK_MESSAGE 信息)。第三部:取消 Watch Dog 机制,即将 RedissonLock.EXPIRATION_RENEWAL_MAP 里面的线程 id 删除,并且 cancel 掉 Netty 的那个定时任务线程。
锁释放
原理
1. Redis Master-Slave 架构的主从异步复制,master宕机的情况若没有把锁及时同步到slave,会导致锁状态丢失,多个客户端获取到锁2. 有个别观点说使用 Watch Dog 机制开启一个定时线程去不断延长锁的时间对系统有所损耗(这里只是网络上的一种说法,仅供参考)。
缺点
redisson
redis
数据库
由于zk是阻塞锁,等待线程会监听锁定线程的释放命令,若等待的JVM很多,那么锁释放时就会有可能会造成我们ZkServer端阻塞
羊群效应
性能不好
问题
分布式锁
业务名:表名:id 例子 o2o:order:1
Key设计
拒绝big key
打散过期时间(加上随机数)
Value设计
KV设计
1. 字符串类型 超过10KB 2. 集合类型 元素数量超过5000?
定义
1. redis阻塞 redis单线程,bigkey删除耗时长,也消耗cpu,bigkey序列化反序列也消耗应用的cpu
危害
--bigkeys 命令
离线方式 对rdb文件进行分析,不够实时
如何定位?
删除大key(非字符串的bigkey,不用 del 删除,用 hscan、sscan、zscan 方式渐进式删除,如每次扫500个元素,再一个个删除)
短期解决
拆:big list: list1、list2、…listNbig hash:可以将数据分段存储,比如一个大的key,假设存了1百万的用户数据,可以拆分成 200个key,每个key下面存放5000个用户数据
查少量:若大key不可避免 那么不要一下子全查出来,例如有时候仅仅需要hmget,而不是hgetall
过期时间设置为非业务高峰期,否则过期触发del,造成阻塞,或者用redis 4.0的lazy free特性,但是默认不开启
如何优化bigkey?
BigKey问题
所谓热key问题就是,突然有几十万的请求去访问redis上的某个特定key。那么,这样会造成流量过于集中,达到物理网卡上限,从而导致这台redis的服务器宕机。那接下来这个key的请求,就会直接怼到你的数据库上,导致你的服务不可用
1. 凭借业务经验,进行预估哪些是热key其实这个方法还是挺有可行性的。比如某商品在做秒杀,那这个商品的key就可以判断出是热key。缺点很明显,并非所有业务都能预估出哪些key是热key。
2. 在客户端进行收集这个方式就是在操作redis之前,加入一行代码进行数据统计。那么这个数据统计的方式有很多种,也可以是给外部的通讯系统发送一个通知信息。缺点就是对客户端代码造成入侵。
3. 在Proxy层做收集有些集群架构是下面这样的,Proxy可以是Twemproxy,是统一的入口。可以在Proxy层做收集上报,但是缺点很明显,并非所有的redis集群架构都有proxy。 client -> proxy ->redis cluster
4. 用redis自带命令(1)monitor命令,该命令可以实时抓取出redis服务器接收到的命令,然后写代码统计出热key是啥。当然,也有现成的分析工具可以给你使用,比如redis-faina。但是该命令在高并发的条件下,有内存增暴增的隐患,还会降低redis的性能。(2)hotkeys参数,redis 4.0.3提供了redis-cli的热点key发现功能,执行redis-cli时加上–hotkeys选项即可。但是该参数在执行的时候,如果key比较多,执行起来比较慢。
5. 自己抓包评估Redis客户端使用TCP协议与服务端进行交互,通信协议采用的是RESP。自己写程序监听端口,按照RESP协议规则解析数据,进行分析。缺点就是开发成本高,维护困难,有丢包可能性。
如何发现Hot Key?
1. 预热,首先热 key 肯定是要缓存的,提前把热数据加载到缓存中,一上线就直接读取缓存。
2. 备份,缓存至少集群架构,保证多个从,这样就算一个从挂了,还有备份。
3. 二级缓存(热点发现,本地缓存),使用机器的内存再做一道拦截。比如像秒杀的商品基本信息可以直接使用机器的内存,可以通过增加proxy层,topK统计高频访问key,达到阈值后通过mq或者zk通知客户端,客户端本地缓存对应kv
4. 限流,预估支持的 qps,拦截多余的请求。
解决方案
热Key问题
redis阻塞,使用info commandstats命令分析,展示每个命令的次数,总时间,平均时间
慢日志查看 slowlog get 128
info 状态
bigkey排查 redis-cli提供了--bigkeys来查找bigkey,会给出每种数据类型的最大key,原理是用scan扫描所有key,所以有影响性能,建议在从库执行
常用命令
现象:访问一个非法的数据(数据库和缓存中都不存在)出现这种情况,每次必然是要去数据库请求一次不存在的数据,这时候因为没有数据,所以也不会写入缓存,下一次同样的请求还是会重蹈覆辙。
1. 前端校验: 例如根据用户id查询数据,针对id如负数等可以直接拦截
2. 后端校验: 在接口的开始处,校验一些常规的正负数,比如负数的user_id直接返回报错。
3. 空值缓存: 有时候我们也对于数据库查不到的数据,也做个缓存,这个缓存的时间可以短一些。
4. hash 拦截: hash 校验使用一些数据量不多的场景,比如店铺的商品信息,上架一个商品的时候,我们商品做下hash标记(map[“商品ID”]=1),这样如果请求的商品 id 都不在 hash 表里,直接返回了。
5. 位图标记:类似 hash,但是使用比特位来标记
6. 布隆过滤器:当我们关心的数据量非常大的时候 hash和位图那得多大,不现实,这时可以用布隆过滤器,布隆过滤器不像hash和位图那样可以做到百分百的拦截,但是可以做到绝大部分的非法的拦截。布隆过滤器的思想就是在有限的空间里,通过多个hash函数来定位一条数据,当只要有一个hash没中,那么一定是不存在的,但是当多个hash全中的话,也不一定是存在的,这一点是需要注意的。
1. 缓存穿透
现象:热点数据在某一时刻缓存过期,然后突然大量请求打到 db 中,这时如果 db 扛不住,可能就挂了,引起线上连锁反应。
1.分布式锁:分布式系统中,并发请求的问题,第一时间想到的就是分布式锁,只放一个请求进去(可以用redis setnx、zookeeper等等)
2. 单机锁:也并不一定非得需要分布式锁,单机锁在集群节点不多的情况下也是ok的(golang 可以用 synx.mutex、java 可以用 JVM 锁),保证一台机器上的所有请求中只有一个能进去。假设你有 10 台机器,那么最多也就同时 10 个并发打到db,对数据库来说影响也不大。相比分布式锁来说开销要小点,但是如果你的机器多达上千,还是慎重考虑。
3. 二级缓存:当我们的第一级缓存失效后,也可以设置一个二级缓存,二级缓存也可以拦截下,二级缓存可以是内存缓存也可以是其他缓存数据库。
4. 热点数据不过期:某些时候,热点数据就不要过期。
2. 缓存击穿
现象:当某一些时刻,突然大量缓存失效,所有的请求都打到了 db,与缓存击穿不同的是,雪崩是大量的 key,击穿是一个 key,这时 db 的压力也不言而喻。
1. 缓存时间随机些:对于所有的缓存,尽量让每个 key 的过期时间随机些,降低同时失效的概率
2. 上锁:根据场景上锁,保护 db
3. 二级缓存:同缓存击穿
4. 热点数据不过期:同缓存击穿
3. 缓存雪崩
缓存穿透、击穿、雪崩
采用的是虚拟槽分区算法。其中提到了槽(Slot)的概念。这个槽是用来存放缓存信息的单位,在 Redis 中将存储空间分成了 16384 个槽,也就是说 Redis Cluster 槽的范围是 0 -16383(2^4 * 2^10)。此时 Redis Client 需要根据一个 Key 获取对应的 Value 的数据,首先通过 CRC16(key)%16383 计算出 Slot 的值,假设计算的结果是 5002。将这个数据传送给 Redis Cluster,集群接受到以后会到一个映射表中查找这个 Slot=5002 属于那个缓存节点。节点间通过gossip协议同步映射数据,每个master都知道其他master负责哪些slot,基于多master,通过client路由对应的slot,其缓存了 key - slot 映射,如果请求的node不存在对应的slot,会返回给客户端一个重定向命令,明确该slot由哪个master负责,然后更新client的缓存
1. 集群主库半数宕机
2. 某一个节点主从全部宕机
不可用条件
扩容:CLUSTER MEET命令或使用redis-trib.rb工具让新节点加入集群rehash指定节点的数据(目的是迁移到新节点)添加从节点缩容类似:迁移数据下线节点
集群缩/扩容过程
redis cluster
proxy-based
采用一层无状态的proxy,分布式逻辑写在proxy上,逻辑上将key分成1024个slot(hash算法是crc32(key)%1024),proxy无状态方便横向拓展,不会成为qps的瓶颈,支持hashtag预发,如对于同于个用户查多个信息 uid1age,uid1sex,uid1name,那么可以通过{uid1}age,{uid1}sex,{uid1}name,这样就保证这些key分布在同一个机器上。另外codis强依赖zk,proxy通过zk来监听的redis集群变化等。关于主从,codis本身并不负责主从切换,主从复制依赖redis本身的replication,手动切换;另外也提供一个codis-ha在master挂掉后,提升slave为master
codis
集群方案
首先要定义慢,要对你的生产redis进行基准测试
使用复杂度过高命令 方式:SLOWLOG命令排查,可能是消耗cpu的命令,如sort,或O(n)命令且N很大,导致IO瓶颈
BIG KEY 申请、释放内存慢
集中过期 变慢的时间点很有规律,例如某个整点,或者每间隔多久就会发生一波延迟,由于redis主进程中存在定时任务去删除过期的key,会导致同时删除大量的KEY,若有BIG KEY 那么删除就会更耗时,并且这个删除不会出现在SLOW LOG
内存达到上限 也会导致频繁删除KEY,延时原因同上
FORK 耗时太长当 Redis 开启了后台 RDB 和 AOF rewrite 后,在执行时,它们都需要主进程创建出一个子进程进行数据的持久化。主进程创建子进程,会调用操作系统提供的 fork 函数。而 fork 在执行过程中,主进程需要拷贝自己的内存页表给子进程,如果这个实例很大,那么这个拷贝的过程也会比较耗时。
redis变慢排查
Redis
单一职责
开闭原则
李氏替换
迪米特法则(最少知道)
接口隔离
依赖倒置
6大原则
单例
原型
抽象工厂
builder
建造者 ✅
创建型
代理
对接外部系统
适配器 ✅
桥接
装饰
外观
享元
组合
结构型
减少if/else的代码,可以把适配方法放在策略中,通过遍历策略通过上下文匹配出对应的策略
策略 ✅
命令
例如 登录校验,分为基础登录非空校验、门店权限校验、获取角色数据 等handler
责任链 ✅
动作封装在状态里
状态 ✅
观察者
中介者
迭代器
访问者
备忘录
例如 结算,不同的结算方共同的步骤,1. 数据校验 2. 创建购物车(默认实现)3. 结算方结算单 4. 结算后置处理方法(发消息等)
模板 ✅
行为型
23种模式
设计模式
性能优化(缓存(车辆等)、异步化(如商详页面))、压测、应急预案制定(限流降级)、故障演练、各种研发规范流程、预发环境测试、日志统一接入sdk、利用注解、统一格式、慢SQL优化、集成监控报警
事前
快速响应、通知业务、上下游等、第一时间止损,需要有三个角色:通讯员,处理人,决策者,看监控灭火错误日志
事中
复盘,整改优化、经验总结
事后
稳定性
集群部署
多级缓存
分库分表索引优化
异步化
限流
削峰填谷MQ
并行处理
预计算
缓存预热
减少IO次数
减少IO数据大小
程序逻辑优化
池化
JVM优化
锁选择(分段锁、乐观锁)
高性能
对等节点的故障转移
非对等节点的故障转移,如MySql主从切换,redis哨兵等
接口层面的幂等、超时、重试策略等
接口层面降级,非核心接口熔断、核心接口有备选链路(针对调用方)
1. 固定窗口(计数器),管理不够细
2. 滑动窗口,窗口越多越平滑
3. 漏桶,流量经过漏桶后速度恒定,无法应对突发流量
4. 令牌桶,可以通过控制令牌发放速度应对突发流量
4种限流算法对比
限流(对超过接口处理能力的请求直接返回错误码或拒绝请求,针对被调用方)
MQ场景的可靠性保证,Producer的重试机制,Broker的持久化机制,Consumer的ACK机制
灰度发布,支持按机器维度小流量发布,观察日志后平稳后全量上线
日志接入监控平台,监控平台根据正则统计,例如10秒周期内下单成功数,失败数等等
业务指标监控怎么做?
监控报警:全方位的监控体系,基本的如CPU、内存、磁盘、网络,另外像JVM、中间件、数据库等监控及业务指标的监控
灾备演练,类似当前的“混沌工程”,对系统进行一些破坏性手段,观察局部故障是否会引起可用性问题。
高可用的方案主要从冗余、取舍、系统运维3个方向考虑,同时需要有配套的值班机制和故障处理流程,当出现线上问题时,可及时跟进处理。
但是要平衡服务多了之后的性能问题(网络上多了一跳)
合理的分层架构
存储层的拆分(分库分表)
业务流程分,如电商场景的商品服务、订单服务这种
按核心、非核心接口拆分
按请求源,如ToC、ToB或App、H5这样分
业务层的拆分
高扩展
高并发
平均响应时间
TP90 TP99等
吞吐量
性能指标
CPU Load 过高
OOM
响应慢(FULL GC)
问题排查
设计整体流程
理流程
确定实体和状态流转
定单据
增删改查
填功能
项目开发
如果两个披萨喂不饱一个团队,那么这个团队就太大了,6-10人
TWO PIZZA(两个披萨原则)
团队管理
第一步:企业画像:理清使用你SaaS产品的核心企业画像。例如所处行业、企业规模、地域等。第二步:组织架构:识别企业中的关键角色及其职责,了解组织架构是一个比较快捷的方法。第三步:核心业务流程:梳理出企业的核心业务流程。前面识别出的角色和人物就像一个个的点,流程会讲这些人错综复杂的关系联系起来。第四步:诊断和定位企业的核心需求和问题,这是真正开始发挥SaaS价值的第一步第五步:针对问题提出对应有效的解决方案,并进行可行性验证
理解SaaS
方法论
做成营销活动,比如可以提前报名,那么开发就可以提前知道用户量,做好准备和系统预热
业务隔离
运行时隔离,模块部署单独的集群
系统隔离
启动单独的cache和db存放热点数据,目的也是不想0.01%的数据影响另外99.99%
数据隔离
隔离
通过增加验证码或者答题等,把峰值的下单请求给拉长了,从以前的1s之内延长到2~10秒左右后端处理时,可以通过 a. 线程池等待 b. 自定义队列存放 x c. 请求序列化到数据库或者mq x
削峰
CDN缓存静态内容
动静分离
CDN
如用户资质、商品状态、秒杀状态等校验热点可以利用本地缓存做二级缓存,过期时间可以比较短,使其被动失效,读脏数据这里没有大问题,数据库会做一致性校验,不会超卖
读逻辑
库存校验这里可以全部放在缓存,如库存校验和扣库存,后台定时任务刷db
写逻辑
不能超售
DB
分层校验
采集后可以发mq等让下游做好准备,下游可以准备、预热缓存
实时热点分析
限流、降级、熔断(兜底)
超卖(兜底)
链接参数加入随机数,前端轮询接口,秒杀开始时才返回
防提前刷
安全
秒杀系统
唯一ID生成系统
12306购票系统
一次浏览器访问URL的过程
一般父子结构表设计存在的问题是,多层的查询没法一下查询出来所有的叶子,需要多次递归;一个解决方案是增加一个链接字段(如下图level,含义是从根节点到当前节点父节点的所有节点的拼接)通过like 'XX%',可以跨层级查询到叶子,不需要递归
子主题
多层级组织架构表设计
重构是有代价的,引入风险(BUG),投入资源(业务推进放缓);重构首要目的一定是能够推进业务发展的,然后才是性能等问题
1. 明确重构的目标
吃透代码和架构的情况下才可以重构,最好有之前了解业务的同事,比只看代码好,能够了解设计的初衷
2.明确当前系统状态
列出重构的要点(范围or边界),确定PRD,在涉及的团队中达成一致,包括上下游,前端,测试
3. 重构的目标需要被量化
通过log的形式建立业务流转记录,通过埋点或者手动等,目的1. 能看出对于数据的处理,存储是否有影响2. 重构中或重构后能通过数据验证效果,后续可以不断优化
4. 重构中必须建立或维护数据流
将一次大的重构拆分拆多次迭代上线,小迭代的风险比较可控,每次重构不应该超过一个正常的迭代周期(2周),并且重构过程对bug的容忍度比较低,上一次重构的结果影响下一次重构迭代的进行
5. 采用迭代式重构
6. 重构首选团队熟悉的技术
7. 重构前务必和业务方沟通
如何有效、正确执行?
重构
1. 定时任务扫表
2. 业务主动发MQ
3. 业务数据库binlog
1. 多源触发,查询db最新数据发送MQ到缓存组件,触发缓存更新缓存消费MQ,查缓存数据,若不一致(可以对比更新时间,然后对比数据内容)则更新
2. 并发控制,解决并发多次更新的情况下,多个请求更新db和更新cache顺序不确定的问题触发源A old val = 1 diff cache update 1触发源B old val = 2 diff cache update 2以上出现不一致那么可以把这diff和update加分布式锁或lua来保证两步的原子性
最终一致
强一致
【携程】缓存最终一致和强一致方案总结
系统设计
堆
永久代在物理上是在堆上的,只是逻辑上是分开的,导致老年代和永久代无论谁满了都会触发fullgc(老年代gc),故java8去掉老年代,设置元空间,作为方法区、常量池等的空间,使用的是本地内存,满了不会触发gc,可以通过参数设置其上限
为什么Java8取消永久代?
元空间
栈
本地方法栈
PC
内存结构
是一种抽象的模型,并不真实存在,被定义出来屏蔽各种操作系统和硬件的 内存访问差异
背景(意义)
有序性
原子性
可见性
JMM
2)本地方法栈中引用的对象
3)方法区中类静态属性引用的对象(static)
4)方法区中的常量引用的对象(final static)
GCRoot
复制
标记清理
标记整理
垃圾回收算法
以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合,适用老年代(响应时间优先)
标记GC Roots直接关联的对象以及年轻代指向老年代的对象
1. STW 初始标记(initial mark)
从GC Root向下追溯,标记所有可达的对象
2. 并发标记(Concurrent marking)
减少下一阶段重新标记的处理时间,对上一步由于用户线程并行,对象可能有变化
3. 并发预清理(Concurrent precleaning)
4. STW 重新标记(remark)
产生浮动垃圾,因为和用户线程并行,用户线程可能不断产生垃圾
5. 并发清理(Concurrent sweeping)
6. 并发重置(Concurrent reset)
过程
1. 低延迟(STW在初始标记和重新标记阶段)
2. 并发收集(与用户线程)
优点:
1. 产生内存碎片,加参数可以解决,会整理内存,但会STW,因为整理的过程无法并发
2. CPU 资源敏感,并发阶段占用一部分用户线程,导致应用程序变慢,吞吐量降低
3. 无法处理浮动垃圾(初始标记活着但并发标记中死亡的对象,remark无法纠正,只能等下一次gc)
导致CMS的问题就是一个死循环,内存碎片过多,空间利用率低,又要预留给用户线程,碎片问题加剧了空间问题,最终导致有可能降级为 Serial Old,卡顿时间更长,不过技术实现本身就是一种 trade-off(权衡),不可能都完美
4. 空间需要预留:CMS可以一边回收垃圾,一边处理用户线程,这个过程需要保证有足够的内存空间给用户使用
缺点:
CMS
优先处理那些垃圾多的内存块,G1只有并发标记才不会stop-the-world 其他都会停下来, 新、老同时用(吞吐量优先)
1. 初始标记(stop the world事件 CPU停顿只处理垃圾);
2. 并发标记(与用户线程并发执行);(不会触发stop the world事件)
4. 筛选回收(stop the world事件 根据用户期望的GC停顿时间回收); (注意:CMS 在这一步不需要stop the world)
1. 与CMS的“标记–清理”算法不同,G1从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的**。
2. 可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内。
1. G1 需要记忆集 (具体来说是卡表)来记录新生代和老年代之间的引用关系,这种数据结构在 G1 中需要占用大量的内存,可能达到整个堆内存容量的 20% 甚至更多。而且 G1 中维护记忆集的成本较高,带来了更高的执行负载,影响效率。
G1
垃圾回收器
现象:响应时间变慢,监控发现CPU负载变高,FGC频率飙升,内存占用提高,由两天1次变成两秒1次
以HttpHeaderMaxSize=100M为例(曾发生过)
FGC排查过程
GC
1. 无锁
JVM认为只有一个线程会执行同步代码(没有竞争),所以在MarkWord会直接记录线程ID,线程来执行代码时会对比,相等则执行;若不相等,则CAS修改锁的线程ID,如果CAS修改成功,那还是能获取到锁,执行同步代码(这种是两个线程交替执行的情况,并没有竞争)
2. 偏向锁
上一步CAS失败,升级为轻量级锁,当前线程会在栈帧下创建Lock Record,然后把Mark Word的信息拷贝进去,然后线程尝试CAS将对象头中的MarkWord替换为指向锁记录的指针,成功则执行同步代码,否则自旋一定次数,若还没成功则升级
3. 轻量级锁(少量竞争)
依赖系统mutex指令,需要用户态内核态切换,主要在阻塞或唤醒线程时,性能损耗十分明显,每个对象都和一个monitor队列关联,线程进入同步代码时会尝试获取monitor所有权1. 若monitor进入数为0,则进入,并将monitor进入数置为1,当前线程成为monitor的owner2. 若线程已拥有monitor,则可重入,进入数+13. 若当前进入数不为0,则需要等待变成0,才重新尝试获取
4. 重量级锁(竞争多)
锁升级
编译时,在static静态方法的flags中ACC_SYNCHRONIZED标志,默认将当前类的class作为锁
类锁(静态方法加synchronized修饰)
编译时,在同步代码块前后加入 monitorenter monotorexit 指令
对象锁
Synchronized
JVM
减少创建销毁线程的资源
提高系统吞吐量(同样的时间,处理更多事务),例如在活动中发送短信推送等
作用
workQueue(大小):一个任务的执行时长在100~300ms,业务高峰期8个线程,按照10s超时(已经很高了)。10s钟,8个线程,可以处理10 * 1000ms / 200ms * 8 = 400个任务左右,往上再取一点,512已经很多了。
maximumPoolSize:IO密集型业务,我的服务器是4C8G的,所以4*2=8。
CPU密集型任务(N+1):这种任务消耗的主要是CPU资源,可以将线程数设置为N(CPU核心数)+1,比CPU核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用CPU的空闲时间。
I/O密集型任务(2N):这种任务应用起来,系统会用大部分的时间来处理I/O交互,而线程在处理I/O的时间段内不会占用CPU来处理,这时就可以将CPU交出给其它线程使用。因此在I/O密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是2N。
1. 丢弃任务并抛异常(默认)
2. 丢弃任务不抛异常
3. 丢弃队列最前面的任务,并提交当前任务
4. 由提交任务的线程处理当前任务
拒绝策略
并不是一个真正的队列,只有一个线程在取元素时,才能放元素,只有无界线程池(maximumPoolSize无限)或者有饱和策略时才建议使用该队列
使用场景可能是任务间有依赖关系,必须先执行A再执行B,这种情况可以使用,在A被处理完时,B不会被提交
span style=\
SynchronousQueue
1. 同步移交队列(同步器)
ArrayBlockingQueue
FIFO
PriorityBlockingQueue
优先队列
2. 有界队列
fix线程池使用这个,会导致oom
LinkedBlockingQueue
3. 无界队列
newScheduledThreadPool使用了这个队列,元素按执行时间排序
DelayQueue
4. 延迟队列
阻塞队列选型
控制大于核心线程数量的线程超时时间
keepAliveTime
参数怎么确定
阻塞队列和非阻塞区别:当队列为空或者为满时,取/存元素是否会阻塞,若使用阻塞队列,可以让线程池获取任务时,当任务为空,会进入wait状态,等待有任务时才被唤醒,不至于消耗CPU;当然用非阻塞也可以,只是要自己实现这个逻辑
为什么用阻塞队列不用非阻塞?
corePoolSize 设置为0时,提交任务会走什么流程?
提供者使用FixedThreadPool,queue大小为0,默认使用的是SynchronousQueue拒绝策略自定义,方式是记日志并且dumpJstack生成jstack文件并且报错
dubbo线程池特点
tomcat线程池特点
面试问题
ol class=\"list-paddingleft-2\" style=\
shutdown
和shutdown()一样,先停止接收外部提交的任务忽略队列里等待的任务尝试将正在跑的任务interrupt中断返回未执行的任务列表
shutdownNow
调用shutdown或shutdownNow 它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。
如何优雅关闭
线程池
Thread -> ThreadLocalMap ->Entry(WeakReference) -> [Key(ThreadLocal):Value(Object)]
ThreadLocal被回收了,ThreadLocalMap的Key没有强引用,但Value还有。首先在非线程池环境不会有长期性的这个问题,因为Thread回收后,ThreadLocal也就被回收了,造成泄漏的情况是 线程被复用 && ThreadLocal 被回收 && 不再调用ThreadLocal的get set remove 等方法
为什么Key不是强引用? 因为若是强引用,当ThreadLocal置为null时,由于ThreadLocalMap还存在对其强引用,则导致无法回收。
内存泄漏问题
原因:线程池是复用的,导致提交任务时的线程上下文和执行任务的线程上下文不一致
使用TTL解决
线程池复用问题
使用完成后调用remove,会把key置为null,触发检测所有key==null的entry,把value置为nul,方便gc
使用要点
ThreadLocal
强 StrongReference
软 SoftReference
弱 WeakReference
虚 PhantomReference
引用类型
1. 多线程扩容时头插法(1.7)导致形成环,出现死循环
2. 多线程Put导致Key被覆盖,丢失的情况,例如hash到同一个空bucket位置,会同时new 一个bucket
3. 线程A扩容,线程B调用get,可能发生底层数据引用已经修改为新的数组但是数据并没有迁移,导致get得到null的情况
为什么是线程不安全的?
扩容过程
HashMap
数据结构:1. 分段锁 2. ReentrantLock (Segment继承自ReentrantLock)
扩容:1. 不会扩Segment数量 2. 是针对每个Segment进行扩容,扩两倍
1.7 实现
数据结构 1. 取消Segment,直接是Node数组+链表+RB tree 2. 并发控制使用 Synchronized和CAS
数据结构优化原因:1. jvm(1.6)对 synchronized的优化 2. ReentrantLock 内存的开销更大
put过程:entry为空就cas添加节点,不为空就synchronized节点进行追加节点
扩容:1. 每次(或每个线程)调用扩容时会进行数组从后往前若干个元素的迁移,每个线程迁移一部分,实现多线程扩容
1.8 实现
ConcurrentHashMap
设置前驱SIGNAL是表示后继需要被唤醒
acquire1. 判断state是否等于0,cas获取锁2. cas失败则判断是否当前线程持有,(可重入逻辑)3. 都失败则进入队列,判断前驱是否是头节点,是且获取到锁,则设置当前节点为头节点4. 没获取到锁,则判断前驱是否是SIGNAL,不是则找到合法前驱,CAS设置其状态为SIGNAL5. 最后调用park挂起自己
AQS
1. 支持公平锁
2. 支持超时时间
3. 需要手动解锁
Synchronized对比
Lock
Atomic
CyclicBarrier
CountDownLatch
LongAdder
Executor
通过自旋+队列实现等待,任务完成后唤醒
Future
J.U.C
JDK
本身作为容器必须的,保存初始化完成的bean
一级缓存
用于提前暴露的bean,只是完成实例化的
二级缓存
用于保存 beanName -> ObjectFactory,二级缓存获取不到时会从三级缓存的factoryBean中获取bean,并放到二级缓存,然后删除三级缓存的bean;在不考虑合理分层、可维护的情况下,实际上一级缓存或二级缓存就能解决这个问题,三级缓存的存在是为了解决AOP
三级缓存
循环依赖
InstantiationAwareBeanPostProcessor 在实例化前后发生作用
1. 实例化 Instantiation
2. 属性赋值 Populate
BeanPostProcessor 在初始化前后发生作用
3. 初始化 Initialization
4. 销毁 Destruction
总体是四阶段:
bean生命周期
IOC
事务实现
动态代理
常见BeanPostProcessor(作用)
AOP
1. 内置sevlet容器,如tomcat,直接打包jar通过java命令执行
2. starters pom简化maven配置
3. 尽可能自动配置应用
4. 没有配置文件的必要
和SpringBoot对比
Spring
A: atomicity 原子性 事务操作要么同时成功,要么同时失败。通过 undo log保证
C:consistency 一致性 是事务的目的,AID均是实现C的手段
I:isolation隔离性 事务并发执行时,他们内部操作互不影响
D:durability持久化 通过redo log保证,由于修改数据时,mysql是先把这条记录的页找到,然后加载到内存,将对应记录进行修改,为了防止内存修改完之后mysql挂掉,引入redo log,记录这次在某个页做了某个修改,及时mysql挂了也能根据redo log恢复
事务特性ACID
1. 相较于二叉树,一个Node节点存储信息更多,树高更低
2. 对比B树,B+非叶子不存数据,相同数据量下,B+数更加矮壮
3. 叶子节点之间组成一个双向链表
B+树优点:
由于主键需要有序,若使用随机会影响插入性能,因为生成的uuid,在插入时存在导致页分裂的可能,就需要移动数据页
为什么用自带自增主键?
脏读
RUC
不可重复读
RC
可以通过开启 next key lock 解决当前读(select for update)的幻读
幻读(当前读的幻读,快照读没有幻读的问题)
RR
S
隔离级别
通过给索引项加锁来实现,意味着只有通过索引条件检索数据才会被加上行锁,否则InnoDB将使用表锁
行锁实现原理
解决脏读、不可重复读、幻读问题(只是快照读的幻读问题,还有当前读的幻读问题需要通过next-key lock解决,通过锁定查询对应索引的行和间隙,锁范围)
creator_trx_id 当前事务id
m_ids 当前系统中所有的活跃事务的 id,还没提交
min_trx_id 当前系统中,所有活跃事务中事务 id 最小的那个事务,也就是 m_id 数组中最小的事务 id
max_trx_id 当前系统中事务的 id 值最大的那个事务 id 值再加 1,也就是系统中下一个要生成的事务 id
read view
通过read view + undo log实现
RC:总是读取当前记录最新版本号的数据,(版本号在事务commit之后才会生成),即每次都获取一个新的read view
RR:总是读取当前事务的版本,即使当前记录被其他事务修改了版本,也只会读取当前事务版本的数据,即每次事务只获取一个read view
原理:针对RC,生成语句级快照;针对RR,生成事务级快照
MVCC
事务提交前
Binlog
逻辑简单,影响性能,老数据写入才更新新数据
同步
由于是异步,要注意时序问题,可以老数据写入后发消息,异步线程收到消息后,直接查老库数据写入新库,或者设置version
异步
需要有一个任务定时对比两个库,diff的case用老数据覆盖新
1. 增量双写
双写diff符合预期后,进行存量迁移,通过db脚本或代码进行
2. 存量迁移
存量迁移后,diff全量数据比较困难,这时我们可以双读+异步diff。读老库 然后异步读新库,diff后上报,再在监控中发现问题
3. 双读
上面的稳定后,切读,但是双写不能停,防止出问题没有退路
4. 切读
稳定后
5. 下线双写双读
整体方案
迁移
1. 对于 like 'C%',由于在B+树结构的索引中,索引项是按照索引定义里面出现的字段顺序排序的,索引在查找的时候,可以快速定位到 ID 为 100的张一,然后直接向右遍历所有张开头的人,直到条件不满足为止。
3. 所以联合索引最左边最好是区分度最高的
最左匹配(前缀)
覆盖索引
索引下推
不对索引进行函数或表达式计算
排序没有走索引、使用union、子查询连接查询等case
Using temporary
没有用索引排序,利用排序列+行指针堆到排序buffer,然后进行快排;如果数据集大小超过buffer大小,那么会形成多个排序完成的小文件,再归并排序,消耗CPU,需要优化
Using filesort
Using index
Using index condition
extra
rows
all < index < range ~ index_merge < ref < eq_ref < const < system
type
key
执行计划说明
查执行计划
子查询优化大分页
读写分离
读IO瓶颈:热点数据多,数据库缓存放不下,查询时产生大量磁盘IO,查询速度较慢,导致活跃连接数变多,可以采用主从,读写分离,分库分表来解决
写IO瓶颈:出现大量写,只能分库分表
IO瓶颈
查询存在大量函数或非索引字段查询,可以分库分表
CPU瓶颈
单数据库连接数有限
原因
接入方便,代码耦合度低;需要单独部署,黑盒,排查问题复杂
1. MyCat
客户端接入,对业务有一定侵入,但排查问题方便
2. Sharding-Jdbc
如预期日订单1000W
确定重构的目标,对未来有个预期,以订单场景为例
确定Key,订单分库分表主要用于下单和查询,按user_id的频率最高,所以选择user_id;另外订单号查询频率也较高,所以在order_id中掺杂了user_id,比如order_id前半部分是user_id,后面是订单号,这样针对订单号查询可以先解析出user_id然后再查,
比如可以分成2的N次幂个库/表,可以使MOD和&结果一样,&效率更高
分库分表数量
另外直接查库效率不高,所以上层可以加redis,存储用户活跃的若干条订单,若用户查询的超过缓存?
步骤
可以不用简单的hash分表,而是通过一致性hash,这样数据倾斜时,只会影响一小部分数据
数据倾斜
将索引与数据存储隔离。可能参与条件检索的字段都会在ES中建一份索引,例如商家,商品名称,订单日期等。所有订单数据全量保存到HBase中。我们知道HBase支持海量存储,而且根据rowkey查询速度超快。而ES的多条件检索能力非常强大。可以说,这个方案把ES和HBase的优点发挥地淋漓尽致。看一下该方案的查询过程:先根据输入条件去ES相应的索引上查询符合条件的rowkey值,然后用rowkey值去HBase查询,后面这一步查询速度极快,查询时间几乎可以忽略不计
通过ES+HBASE
数据多维度查询
分库分表
例如大促时订单生成较多,将同步改为异步发消息,较小服务器压力,增大吞吐量,消费时可以放到redis中,每隔一段时间或者一定数量订单再写入数据库,可以配置开关进行降级
数据库写入降级方案
常见优化
走了索引还是慢,可能原因是数据确实大可以通过删除旧数据,旧数据可以放到hive也可以走缓存、或者es,通过空间换时间
表锁 行锁 gap锁 next key 锁都会有死锁的可能性
表现
1. 以固定顺序访问表、行,比如两个更新数据的事务,事务A更新数据的顺序为1,2;事务B更新数据的顺序为2,1。这样更可能会造成死锁。
2. 由于大事务造成死锁的可能性更大,尽量把大事务拆小,减少范围查询更新
3. 同一个事务中,尽量一次性获取所有需要的锁资源
4. 降低隔离级别,在业务场景允许的情况下,可以使用rc的隔离级别,避免gap锁死锁
5. 建立合适的索引,避免表锁
如何避免?
1. 通过应用业务日志定位到问题代码,找到相应的事务对应的sql,mysql自带的死锁检测 Deadlock found when trying to get lock 死锁被检测到后会回滚,报错会体现在业务异常日志中
2. 确定数据库的隔离级别,如RC,那么可以排除gap lock导致的死锁
3. 找dba 执行 show InnoDB STATUS看看最近死锁的日志。
如何排查?
死锁问题?
常见问题
负责建立连接等: Java -> 驱动 -> MySQL
MySQL驱动
维护一定的连接数,避免频繁建立、销毁连接
连接池,分为客户端的连接池和MySQL本身的连接池
解析SQL语句
查询解析器
即从磁盘把数据加载到内存,MySQL以页的形式读取数据,并不是单独读取某条记录,即局部性原理,所以IO成本主要和页的大小有关
IO成本
数据读到内存后还要确认是否满足条件和排序等消耗CPU的操作,CPU成本和行数有关
CPU成本
选择最优查询路径,根据成本最小(CPU/IO成本最小),如选择合适的索引,生成执行计划
查询优化器
执行器
执行SQL时会把数据加载到内存,即放到Buffer Pool
Buffer Pool
存储引擎(真正执行SQL)(INNODB)
MySQL线程处理
一条SQL执行的过程
删除表数据
DML
可以回滚
delete
DDL
不能回滚
truncate
删除表数据、表结构
drop
删表
MySQL
CAP原则
2PC
3PC
TCC
事务和发消息放在同一个事务
事务消息
缺点:业务需增加一个业务无关的表,高耦合,占用数据库资源
发消息改为插入数据库记录,单独的线程轮询表来发送。然后事务和插入消息表放一个事务
本地消息表
1. 发送半消息
2. 发送成功,执行事务
3. 成功失败告知mq
4. 没有结果告知mq的情况下,mq本身会轮询半消息,查业务的回调接口
5.若状态是提交,则投递消息;回滚则不提交
只是保证通知和事务的一致,无法保证多方事务一致
MQ事务消息
机制:两阶段提交的演变阶段1. 业务数据和解析出来的回滚日志放到同一个事务,在一阶段提交,释放本地锁和连接资源,一阶段提交前需要获取全局锁,否则不能提交阶段2. 提交异步化,快速完成; 回滚通过一阶段的回滚日志完成。
写隔离通过全局锁读隔离默认是读未提交,可以通过读锁(select for update达到读已提交,阻塞其他事务获取锁)
AT
属于两阶段,只是把commit和rollback的动作交给业务自定义
适用场景:流程长,参与者包含其他老系统,无法提供tcc要求的三个接口
优点:高性能,无锁,一阶段提交本地事务,高吞吐
缺点:不保证隔离性,主要是脏写的场景,发生脏写后无法回滚,例如,在事务中给A充值给B扣款,如果A充值成功,B事务提交前,A把钱消费了,那么就无法回滚了,业务前回不来了。需要通过业务去规避,要么遵循宁可长款,不可短款,事务总是优先扣款,发生回滚可以通过平台退款;要么通过向前回滚(重试扣除B的钱)达到最终一致
SAGA
利用数据源本身的XA协议支持
XA
SEATA
2. 事务开始时通过@GlobalTransactional开始全局事务,根据参数中的标记的唯一ID作为分布式锁的KEY,保证隔离性,如订单开单场景,这样其他线程就无法操作该订单
3. 接下来保证原子性,利用Saga T1->T2->T3[IF Error]... C3->C2->C1
1. 利用注解+AOP,@GlobalTransactional @BranchTransactional2. Spring初始化时,实现BeanPostProcessor,针对 @BranchTransactional,将方法注册到HashMap中
代码设计
事务、子事务保存到数据库,作为Saga Log
数据库设计
自研实现逻辑 仿Saga流程
Network Delay
Process Pause
Clock Drift
NPC
原服务未执行,补偿服务先执行,原服务请求丢失
解决:业务需要允许空补偿,返回成功,因为这种基本重试也没用
空补偿
补偿服务比原服务先执行,原服务来晚了
解决:业务应记录补偿日志,执行原服务时检查
悬挂
分布式事务主要问题在NP
分布式存在的问题
BIO
传统IO是一个字节一个字节(字节流)地处理数据,NIO是以块(缓冲区)的形式处理数据。最主要的是,NIO可以实现非阻塞,传统IO只能是阻塞的。
和传统IO对比
存储数据
Buffer
运输数据的载体
Channel
用于检查多个Channel的状态变更情况
Selector
JAVA NIO
NIO
AIO
Linux对文件的操作实际上是通过文件描述符(fd),IO多路复用指的是通过监听多个fd,一旦某个fd准备就绪,就去通知程序做相应的处理,优势在于可以处理更多的连接。(连接上并不代表有数据,可读可写的状态fd才有用)
支持最大连接数为1024或2048,取决于操作系统,那么select做的就是遍历fd集合,如果状态变化则通知程序,最大连接数限制可以通过多进程来解决
select
用链表保存fd,没有大小限制,触发式,就绪时会放到就绪列表,每次从就绪列表拿就好了,不需要再遍历fd集合,时间复杂度O(1)。
epoll
函数
以读操作为例一般情况(调用read,用户态 -> 内核态 -> 用户态 -> 内核态 -> 用户态, 4次拷贝,磁盘文件 DMA 拷贝到内核缓冲区,内核缓冲区 CPU 拷贝到用户缓冲区,用户缓冲区 CPU 拷贝到 Socket 缓冲区,Socket 缓冲区 DMA 拷贝到协议引擎):1. 会从用户态切到内核态2. 随后CPU会告诉DMA去把磁盘数据拷贝到内核空间3. 等到内核缓冲区真的有数据后,CPU会把内核缓冲区的数据复制到用户缓冲区4. 此时用户会获取到数据零拷贝情况不用read,取而代之的是:mmap(内核缓冲区与用户缓冲区共享(用户态 -> 内核态 -> 用户态 -> 内核态 -> 用户态,3次拷贝,磁盘文件DMA拷贝到内核缓冲区,内核缓冲区 CPU 拷贝到 Socket 缓冲区,Socket 缓冲区 DMA 拷贝到协议引擎):将「内核缓冲区拷贝到用户缓冲区」省去,提高效率和性能. )1. 已经从磁盘复制到内核缓冲区的数据不需要复制到用户缓冲区,而直接与应用程序共享sendfile(系统底层函数支持,全程不经过用户缓冲区,用户态 -> 内核态 -> 用户态, 2次拷贝,磁盘文件 DMA 拷贝到内核缓冲区,内核缓冲区 DMA 拷贝到协议引擎):1. 调用sendfile,磁盘数据被copy到内核缓冲区2. 从内核缓冲区copy到内核中socket缓冲区3. 将socket相关的缓冲区copy到协议引擎
IO
TCP/IP
HTTP
客户端需要确切知道服务端是否真实,所以https中会有一个CA(公信机构)的概念,服务端在使用https之前回去CA申请数字证书,数字证书包含证书持有者、证书有效期、服务器公钥等信息,CA机构也有公私钥,发布证书前用私钥对证书加密,等客户端请求服务端时,服务端返回证书给客户端。客户端用CA的公钥对证书解密(因为CA是公信机构,会内置到浏览器,所以客户端有公钥)。那么这时,客户端会判断证书是否可信,有无被篡改。私钥加密,公钥解密 -- 称为数字签名,以这种方式查看有无被篡改。自此解决认证的问题。
认证问题,如何证明服务端是真实的?
客户端拿到证书后,就能拿到服务端的公钥,这时客户端生成一个Key作为堆成加密的密钥,用服务端的公钥加密传给服务端,服务端就可以用自己的私钥解密,得到堆成加密的密钥,之后就可以用这个对称加密密钥收发消息了。
保密问题,如何保证客户端和服务端的通讯内容不会在传输中泄漏给第三方?
HTTPS
协议
网络
HBase
Spark
Flink
大数据
状态机、流程引擎
1. 生产流量正常请求,服务器正常响应
2. tcpcopy 服务在生产机器上复制流量,并修改流量包的源 ip 地址为我们指定的伪网络段(-c 参数指定),之后将流量转发到测试服务器,(源端口不变)
3. 测试服务器,接受到流量,但包的源地址为伪网络段的地址,回包时根据提前配置好的伪路由,将回包导流到辅助服务器。
4. 辅助服务器接收测试服务器的回包,但是并不转发。而是解包,只返回部分必要的信息给 tcpcopy,以便完成 tcpcopy 和测试服务器之间的 tcp 交互。
在tcp协议层
tcpCopy
1. 比tcpCopy简单,仅仅是重新构建一个http请求,再用新端口和测试服务器交互
在http协议层面
goReplay
RDebug(滴滴开源)
ByteCopy
jvm sanbox + repeater
现有方案
原来利用拦截器复制请求转发,新服务监听老服务表binlog触发diff
现在老服务返回后才复制请求
项目中
流量复制/回放
稳定性:优先情况下 要选择多进程,万一挂掉,不影响后继
性能:进程:占用内存多,切换复杂,CPU利用率低
规模:多进程方便横向拓展,如果一台机器不够,拓展多台机器也比较简单
多线程、多进程区别?
OS
Java
0 条评论
回复 删除
下一页