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