文章目录
- jmeter压测websocke指南,接入arms分析,以及mysql,jvm,tomcat性能调优
- hadoop(2017年4月份分享带案例)
- Redis
- ehcache
- MQ
- Tomcat todo
- spring-cloud todo
- mysql
- 搜索引擎
- RPC
- 分布式锁
- 一致性
- 迁移
- 十种分布式id生成
- 常见场景
- 架构与系统设计
- 如何定位线上问题?
- Gossip流言协议
- im解决消息重复
- 如何提高单个接口性能?
- 性能优化
你不知道的越多,你懂得也就越多.
你知道的是一个点,不知道的是点的外面.因为你是一个点,你接触的外面有限,所以你就会产生自己啥都知道的心理.然后活在自己的世界里.直到某一天,兴许是某个契机,你突破了这个点,当你知道的越多,点就会形成圆,圆外的是未知的,圆内是已知的.而随着你的经验阅历学习,随之而来的圆的尖角增长,当尖角突破到了外面大圆的边界,就那么一丢丢的小点.那么恭喜你,你已经在某个领域成为了专家.
程序=算法+数据结构
为什么要去看源码了,实际上是从代码层面解剖它的算法+数据结构
借此博客,大概罗列下整个知识体系.有部分的后续补充.
jmeter压测websocke指南,接入arms分析,以及mysql,jvm,tomcat性能调优
hadoop(2017年4月份分享带案例)
Redis
事前:Redis 高可用,主从+哨兵,Redis cluster,避免全盘崩溃。
事中:本地 ehcache 缓存 + Hystrix 限流+降级,避免MySQL被打死。
事后:Redis 持久化 RDB+AOF,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。
如下三种情况造成结果,大量请求压入数据库,造成数据库炸了
雪崩:缓存失效
随机设置过期时间,或者不过期
双缓存机制,缓存A的失效时间为20分钟,缓存B没有失效时间,从缓存A读取数据,缓存A中没有时,去缓存B中读取数据,并且启动一个异步线程来更新缓存A。
缓存穿透:访问没有缓存的数据,如用户id-1
设置校验
缓存为null之内的
布隆/布谷鸟过滤器
缓存击穿:热点数据不停访问,当key失效
加互斥锁,缓存中没有热点key对应的数据时,等待100ms,由获得锁的线程去读取数据库然后设置缓存。这样后续的请求,就不会落在DB.
key永不过期
本地缓存,hashMap,或者ehcache
Hash分key,一个key拆分多个key,分布不同节点,防止单点过热.
这种方法只能缓解当一个key失效时,DB压力.
比如说,我现在用redis缓存秒杀活动订单号,存储在同一个key上面,所有请求
都落在这个key上压力过大,我可以根据活动id分key,又比如如果每个秒杀活动
配置的秒杀商品id过多而且每个商品的秒杀数量也很多
我可以再次根据活动id+商品id组合分key
redis持久化方式
一:rdb快照持久化
工作原理
每隔[N分钟]或[N次]写操作后,从内存dump数据形成rdb文件[压缩]放在[备份目录],
[]里面的内容可以配置
二:aof(日志)
对每条写入命令作为日志,以 append-only 的模式写入一个日志文件中,因为这个模式是只追加的方式,所以没有任何磁盘寻址的开销,所以很快,有点像Mysql中的binlog。
master down之后
哨兵必须用三个实例去保证自己的健壮性的,哨兵+主从并不能保证数据不丢失,但是可以保证集群的高可用。
一:手工配置
1:指定任意一台slave设置slaveof no one,表示角色更换为master
2:设置slave-read-only no:表示可写
3:其他slave,新的master
设置:slaveof IP地址 端口
二:使用sentinel监控主服务器
1:cp 源代码目录下sentinel.conf 到redis目录
2:sentinel.conf配置信息
sentinel monitor 定义的名字 ip 端口 次数 :监视那个地址,一定的失效次数没有连接上,确定失效
sentinel auth-pass 名字跟上面定义的名字一致 012_345:设置密码
sentinel down-after-milliseconds 名字同上 30000:30秒没有连接上,失效次数增加1
sentinel can-failover 名字同上 yes:是否允许故障转移(slave更改为master),建议只有一台设置为yes,因为监控的sentinel多了之后,配置的yes也多了之后,会导致一台服务器失效后,一起修改。
sentinel parallel-syncs 名字 数量:当master死机时候,指定新的master时,指定一定的数量slave启动连接,防止masterio剧增
sentinel failover-timeout 名字 时间:超过时间没有完成设置,监控失败。
3:开启
./bin/redis-server --help,开启server帮助
开启哨兵模式
./bin/redis-server ./sentinel.conf --sentinel &
4:master死机后,哨兵随机指定新的master,手动配置
每一个redis.conf里面配置slave-priority优先级,数字越小越靠前,也就是越有限指定为master
redis为什么可以做分布式锁?
- Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系。
redis性能为什么高
- 完全基于内存,绝大部分请求是纯粹的内存操作
- 数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;如整数集合,压缩列表
- 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
- 使用多路I/O复用模型,非阻塞IO;
- 使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;
过期策略
定时删除:创建定时器删除
定时器数据结构为无序链表,查找O(N),并不能高效处理定时
定期删除:默认100ms就随机抽一些设置了过期时间的key,去检查是否过期,过期了就删了。
惰性删除:获取的时候,判断是否过期,过期则删除
如果过期没删除,且没查询,redis过期数据过多?则涉及内存淘汰机制
noeviction:返回错误当内存限制达到并且客户端尝试执行会让更多内存被使用的命令(大部分的写入指令,但DEL和几个例外)
allkeys-lru: 尝试回收最少使用的键(LRU),使得新添加的数据有空间存放。
volatile-lru: 尝试回收最少使用的键(LRU),但仅限于在过期集合的键,使得新添加的数据有空间存放。
allkeys-random: 回收随机的键使得新添加的数据有空间存放。
volatile-random: 回收随机的键使得新添加的数据有空间存放,但仅限于在过期集合的键。
volatile-ttl: 回收在过期集合的键,并且优先回收存活时间(TTL)较短的键,使得新添加的数据有空间存放。
如果没有键满足回收的前提条件的话,策略volatile-lru, volatile-random以及volatile-ttl就和noeviction 差不多了。
当内存占用满了,会删除所有过期数据.
数据结构
常见数据结构:
- 简单动态字符串
- 链表
- 字典
- 跳跃表
- HyperLogLog
redis自己实现的数据结构:
- 整数集合
- 压缩列表
redis封装提供给外在使用的数据结构:
使用场景
hash
集中像数据库一样,存储如用户全部信息.
string
- 计数,譬如:秒杀场景下记录商品库存数。限制用户点击数等
- 分布式锁(利用单进程的特性)
- 共享session
- 首页展示数据
- 利用可设置过期时间,限制譬如发送短信间隔60秒
list
阻塞消息队列,栈
实现新浪消息列表
lpush+lpop=Stack(栈)
lpush+rpop=Queue(队列)
lpsh+ltrim=Capped Collection(有限集合)
lpush+brpop=Message Queue(消息队列)
sortedList
- 点赞,取消点赞,获取区间点赞最多
- 排行榜
set
- 求交集,并集,差集(二度关系)
- 标签(给用户打tag,标签打用户,计算用户共同感兴趣的标签)
- 黑白名单拦截
- 实现抽奖
- 基于集合运算,实现电商商品筛选
HyperLogLog
如何保证高可用的?
当单台redis读写压力过大时,可通过主从复制解决数据读压力和数据同步问题.但是当master宕机了,redis将不能提供写服务.解决办法sentinel进行主从切换.
- 实现流程:2.8之前,每次发送RDB文件给从服务器
2.8之后,从服务器发送偏移量给主服务器,若偏移量之后数据在 master复制积压缓冲区中,则发送之后数据给slave.否则完全重同步.
通过sentinel弥补了主从的局限性,但是当数据量过大,会出现单台服务器存不下问题,解决办法分片.也就是集群模式.跟数据库分库分表差不多.解决容量上限问题.
- 实现流程:sentinel这里需要至少3台,要不然2台竞争后,一人一票.就选不出主了.当master宕机了,检测宕机的sentinel会竞选.竞选条件取决于网速,谁先抢到票就是谁的,竞选leader的会再次从slave选举出master.这里会首先根据slave优先级还有slave最新的复制偏移量进行决策.
集群
- 给每个服务器分配槽,访问时,会判断键值对是否在这个服务器上,在的话就返回数据,不在的话就告诉你应该访问那个服务器.
如何保证数据极小丢失的?
具体参考这篇文章,后续会上传相关源码解析
通过RDB和AOF.服务器重启时,会看AOF是否开启了,开启就AOF恢复,若没开启则RDB恢复.
RDB:
- 在SAVE期间,线程阻塞,RDB期间拒绝所有命令
- 在BGSAVE期间,会新开线程进行RDB.同时接收命令处理.
AOF:
- 新开子进程将aof文件内容写入内存,在重写期间,引入一个AOF重写缓冲区,记录重写期间追加的命令,重写完成后,向父进程发送信号,父进程接收到之后,将重写缓冲区写入新AOF中,写完后,原子替换现有AOF.
为什么不用redis做消息队列?
- 没有可靠性和持久性,redis接收到msg就发送,不管有没有订阅者.且没有消息确认机制,第二它的持久依托于RDB和AOF,但是二者都是持久整个Redis缓存命令的.而MQ不一样,MQ收到一条消息,会持久,然后消息发送(这里可选择只消费一次,或者多播形式),等待接收方确认,在删除持久数据.
- 实时性,因为丢失了可靠性和持久性,操作完全基于内存,所以redis吞吐比mq高.
- 消费者负载均衡,redis广播的形式发送,不管有没有订阅者.而MQ不一样,可以根据消费者负载能力,批量拉取.
Redis发布订阅与rabbitmq的区别
也就是冒的可靠性(确认)+持久(落全部)+监控+消息广播机制
但是实时性高,因为基于内存
1. 可靠性
redis :没有相应的机制保证消息的可靠消费,如果发布者发布一条消息,而没有对应的订阅者的话,这条消息将丢失,不会存在内存中;
rabbitmq:具有消息消费确认机制,如果发布一条消息,还没有消费者消费该队列,那么这条消息将一直存放在队列中,直到有消费者消费了该条消息,以此可以保证消息的可靠消费。
2. 实时性
redis:实时性高,redis作为高效的缓存服务器,所有数据都存在内存中,所以它具有更高的实时性
3. 消费者负载均衡:
rabbitmq队列可以被多个消费者同时监控消费,但是每一条消息只能被消费一次,由于rabbitmq的消费确认机制,因此它能够根据消费者的消费能力而调整它的负载;
redis发布订阅模式,一个队列可以被多个消费者同时订阅,当有消息到达时,会将该消息依次发送给每个订阅者,她是一种消息的广播形式,redis本身不做消费者的负载均衡,因此消费效率存在瓶颈;
4. 持久性
redis:redis的持久化是针对于整个redis缓存的内容,它有RDB和AOF两种持久化方式(redis持久化方式,后续更新),可以将整个redis实例持久化到磁盘,以此来做数据备份,防止异常情况下导致数据丢失。
rabbitmq:队列,每条消息都可以选择性持久化,持久化粒度更小,更灵活;
5. 队列监控
rabbitmq实现了后台监控平台,可以在该平台上看到所有创建的队列的详细情况,良好的后台管理平台可以方面我们更好的使用;
redis没有所谓的监控平台。
6. 性能
性能上,对于RabbitMQ和Redis的入队和出队操作,各执行100万次,每10万次记录一次执行时间。测试数据分为128Bytes、512Bytes、1K和10K四个不同大小的数据。实验表明:入队时,当数据比较小时Redis的性能要高于RabbitMQ,而如果数据大小超过了10K,Redis则慢的无法忍受;出队时,无论数据大小,Redis都表现出非常好的性能,而RabbitMQ的出队性能则远低于Redis。
总结
redis: 轻量级,低延迟,高并发,低可靠性;
rabbitmq:重量级,高可靠,异步,不保证实时;
rabbitmq是一个专门的AMQP协议队列,他的优势就在于提供可靠的队列服务,并且可做到异步,而redis主要是用于缓存的,redis的发布订阅模块,可用于实现及时性,且可靠性低的功能。
Redis发布订阅与ActiveMQ的比较
1. ActiveMQ支持多种消息协议,包括AMQP,MQTT,Stomp等,并且支持JMS规范,但Redis没有提供对这些协议的支持;
2. ActiveMQ提供持久化功能,但Redis无法对消息持久化存储,一旦消息被发送,如果没有订阅者接收,那么消息就会丢失;
3. ActiveMQ提供了消息传输保障,当客户端连接超时或事务回滚等情况发生时,消息会被重新发送给客户端,Redis没有提供消息传输保障。
总之,ActiveMQ所提供的功能远比Redis发布订阅要复杂,毕竟Redis不是专门做发布订阅的。但是如果系统中已经有了Redis,并且只需要基本的发布订阅功能,可以考虑使用Redis的发布订阅机制以满足需求。
布隆/布谷鸟过滤器 todo
Redis 单key值过大 优化方式
- 整存整取:单个key拆分成多个key,采用RedisTemplate的multiGet的操作
- 每次获取部分数据,按照属性拆分成多个key-map,存储hash中 hash分key
ehcache
使用场景
- 基于jvm虚拟机运行,所以效率最高,速度最快。
- 支持缓存共享(通过RMI或者Jgroup多播方式进行广播缓存通知更新,不推荐)
- 支持缓存共享(复杂,维护不方便,涉及到缓存恢复,大数据缓存,不推荐)
- 主要用于多级缓存。
与redis的区别?
- 单体应用,效率要求最高,ehcache
- 大型系统,存在缓存共享、分布式部署、缓存内容很大的,数据结构复杂,建议用redis。
MQ
使用场景
- 削峰填谷
- 解耦各个模块之间的依赖。比如用户现在登录了,需要加积分,这个积分操作完全不重要,可以完全MQ实现。而且能提高登录访问时间。
- 安全,不暴露接口
开线程做不就可以了区别是什么?
- MQ接收消息的时候会落地,然后保证接收方能收到,以及还提供广播以及延时队列形式。而线程做则不一样,首先需要维护线程池的生命周期,第二无法保证接收方是否能收到(因为无法得知消息方是否在线),以及当接收方没接收,会造成消息丢失。甚至没有可视化监控界面,无法广播等等。都是缺点。
顺序执行
rocketmq 根据唯一标识hash取模,然后把消息确保投递到同一条queue
使用场景
上游不需要关注结果
关心结果,但执行时间过长
消息必达
超时(可采用指数退避,第一次超时2S重试,第二次超时2S^2...)+确认+重发+落地
MQserver收到消息首先落地
MQserver发送client时,超时重传
client发送失败,超时重传server
重复消费
重复消费就是为了保证幂等的问题
如insert更改为update,,update可重复set值,而值是一样的.如果为了确定顺序,可添加版本号.
发送端的幂等
发送端发送消息给server时,server内部生成id,全局唯一,且MQ生成与业务无关.
当发送端没有收到server消息,重试的时候,server根据id判断唯一
接收端的幂等
消息体中存在一个业务场景全局唯一,如订单号,流水号.接收端根据唯一id判断重复.
强校验
新增订单的时候,增加一个流水.下次消息重试,去查询流水
弱校验
Id+场景唯一id缓存起来,加上过期时间
削峰填谷
生产速率>消费速率
client根据自身处理能力,间隔一定时间或者每次拉取N条消息处理
达到限速执行目的.批量拉取消息,减轻MQ队列压力.
延时
启动一个timer,循环遍历超时处理
timer触发
针对每个请求生成一个timer监控超时
时间轮
还是比较简单实现的,工作中实现过.就一个环形队列大小为3600,
转完一圈相当于一个小时.每一秒指针移动到下一个位置.位置中存储对应的任务和圈数,
圈数相当于记录的多少小时之后执行,2圈则2个小时.每次遍历一圈后,
减少每个遍历格子里面的圈数,然后圈数为0,则执行格子任务.
RocketMQ
如何顺序消息?
如何定时消息?
如何解决重复消费?
如何HA?
MQ刷入磁盘过程?
Tomcat todo
spring-cloud todo
mysql
binlog恢复数据库
mysql 的sleep线程过多处理方法
存储引擎
innodb和MyISAM存储引擎都是B+TREE.不同的是innodb根节点存储数据,且根节点之间相互关联,这也保证他能够范围查询.而MyISAM则只存储地址.
对比
innodb:支持事务,主键
myISAM:读+插入
Memory:存RAM,对表大小有限制,其次需要保证可丢失
Merge:将MyISAM逻辑方式组合一起
explain重要指标?
mvcc
innodb,在每行隐藏保存2列,分别记录行创建版本号,和行删除版本号.每次开启事务,当前系统版本号为事务版本号,系统版本号则递增.
可重复读隔离级别下,分别执行的各种操作是如何影响版本号的?
SQL优化
导入优化
1:主键顺序插入
2:关闭唯一性校验,导入后在设置
3:手动提交事务
insert优化
1:批量插入
2:事务中插入
3:有序插入
orderBy
2种排序方式
fileSort:不走索引排序
using index:走索引排序,不需要额外排序
根据索引排序
复合索引排序时,排序规则需要与复合索引顺序一致
当多个表join,orderby子句引用的字段全部为第一个表时走索引
当组合索引中,第一个列where时为常量,则使用组合索引第二列,也走索引.
FileSort优化
可设置 SQL_BIG/SMALL_RESULT指定
排序算法
1:二次扫描法,取出行指针和需要排序字段,进行排序.
然后根据排序结果读取所需要的数据行.
sort buffer排序,如果buffer不够,则在临时表存排序结果
2:一次扫描法:取出所有数据在sort buffer排序
mysql通过比较max_length_for_sort_data和query语句查询字段总大小对比,从而选择哪种排序算法.如果max_length_for_sort_data大,则第二种.
通过修改max_length_for_sort_data和sort buffer大小提高排序速度
group by
实际也会进行排序,然后在分组
可order by null 禁止排序
嵌套查询
in+子查询用exists或者join改写
join代替子查询
join不需要在内存中创建临时表
分页查询
select * from tab1 limit 10000,10
存在问题,排序前10000数据,然后获取10条数据
解决办法
索引上完成排序,然后关联原表查询
select * from tab1,(select * from tab1 order by id limit 10000,10)tab2 where tab1.id=tab2.id
如果主键自增
select * from tab1 where id>10000 limit 10
select * from tab1 where id between 10000 and 10010
count
统计列时,要求列非空.
MyISAM当没有where时,速度快是因为可以利用存储引擎直接获值
select * from tab where id >5
改
select (select count(*) from tab) - count(*) from tab where id <=5
如果近似值可以,则直接返回explain结果中的rows值
如果知道结果集只有一条记录,limit 1,当查询一条满足后,会提前终止查询.
如何避免索引失效
全值匹配
最左前缀
范围查询不走索引
索引上不要运算
字符串不加单引号
尽量使用覆盖索引(只访问带索引的列)
or前有索引,后没有,会造成索引失效
or连接的条件尽量全部命中索引,而且不能使用复合索引
解决办法union
模糊查询放结尾,col%,也可以通过覆盖索引解决
当mysql评估使用索引比全表更慢,则不适用索引.
假设有10条数据,只有一条col1=1.其余都为0.如果查询col1=0的,索引查还不如全表了
is NULL/is NOT NULL索引会失效
当null数据占据绝大部分,则is Null走索引
当null数据少部分,则is Not null走索引
in走索引,not in失效
尽量复合索引,因为复合索引相当于创建了3个索引,当然需要满足最左前缀
多个or时,in代替,因为in会将列表数据排序,
然后通过二分查找确认列表中值是否满足要求,复杂度为O(N)变成O(log n)
锁
行锁:锁当前操作行
表锁:锁表
读/写锁
MYISAM支持表锁
innodb支持行表锁
CUD:自动加写锁
R:不加锁
二级索引,读写锁
主键索引,排它锁
无索引行升级表锁
间隙锁:范围条件内数据加锁,其中不存在的数据叫做间隙,当间隙数据添加时,会阻塞
索引整个存储和执行流程
B+树的算法没写过,但是写过234树,原理差不多,就多一个节点罢了.
点击这里访问
innodb聚簇索引,采用根节点存储数据的方式.二级索引也按照B+Tree树结构存储,但是根节点存储的是主键,为什么根节点存主键了?而不是具体的数据行了?因为当innodb修改数据行时,就无须修改二级索引的位置了.
myISAM二级和主键索引一样,存储的都是行索引值,所以与innodb相比较,少了一次根据主键定位数据的操作,也是他快的原因.
为什么主键建议有序?
那我举个例子,就拿自增id和uuid作为对比,自增id因为有序,所以数据存储的时候,内存页整齐排放,避免了碎片化的问题.
uuid因为无序,存入的主键值不一定比之前插入的大,可能插入中间,这样涉及到很多操作,不得不频繁页分裂,造成频繁移动数据以及碎片化.
而且因为UUID长度,极大占用内存空间.
分库分表
主从复制,解决的是数据库集群数据同步问题
读写分离,解决的是读操作效率以及并发量的问题
而分库分表是在数据库级别,解决单表单库数据过量的问题
分库:不同业务数据表部署在不同数据库集群上,垮库不能join)
分表:一张表拆开分别存储在多个数据库中,cobar,amoeba)
数据迁移(利用hash一致算法)
路由算法
hash取余:命中率低至(N/N+1)
一致性hash算法(一致性hash环):通常使用二叉查找树实现。命中率高至99%
缺点:新加入节点只影响附近最近的节点,其他节点还是负载那么多
添加虚拟层,每个物理服务器节点虚拟为一组缓存服务器,然后根据hash值放置hash环上
命中高达75%,同时解决资源分配不合理问题
地理位置
时间范围
数据范围1-1000 A库,1001-2000 B库
单个库太大
垂直切分:表多而数据多,根据业务切分成不同的库。
水平切分:将单张表的数据切分到多个服务器上去,每个服务器具有相应的库与表,只是表中数据集合不同。
单个表太大
垂直拆分,字段多,拆分成多个表这些表还在同个数据库中
水平拆分,数据多,按照规则将数据分别存储多个相同结构表,
分库分表的顺序应该是先垂直分,后水平分。 因为垂直分更简单,更符合我们处理现实世界问题的方式。而且垂直拆分后,系统资源翻倍,连接数也有提升.
存在问题
分布式事务
范围查询面临问题
多库结果集合并(group by,order by)
冗余分库
当订单数据,用户要用,商家也要用,这个时候商家分库,那么用户查就需要扫描多次,同理用户分库也是
冗余分库,解决的是同一个库,按照不同的维度查询的问题,涉及的主要问题就是分库的数据冗余以及数据一致问题问题
保证数据冗余
方案一:
server同步写到冗余数据库
优点:简单实现
缺点:一次写,改成2次.因为不是原子性,写入库1后断电,库2丢数据
方案二:
MQ同步
优点:请求时间简短
缺点:引入MQ,系统复杂.
数据同步存在延时
MQ丢失数据,无法保证数据同步可达
方案三:
异步任务binlog同步
优点:解耦,请求时间短
缺点:存在时间空窗期,但保证了最终一致
保证数据一致
线下全量扫描,不停对比2个表的数据,不一致则修复
扫增量
写入A库后,写入log1,写入B库后,写入log2.线下对比log1和log2,就行了,为了以防万一,添加一个全量扫描
线上检测消息对
新增消费者,分别监听A库写入和A1库写入完成消息,如果二者接收间隔时间长,则启动补偿修复
垮库join
全局视野法
order by time offset x limit y;
如果数据均匀分布2个库中
offset x/2 limit y/2
数据来源一个库
每个库数据包含部分
order by time offset x limit y
修改为
order by time offset 0 limit x+y
假设N个库,则有N*(x+y)条数据
排序后,x开始取y条数据返回.
缺点:
每个分库,返回更多数据,增大网络带宽
数据库排序后,服务层内存还要排序
随着页码增加,性能下降
禁止跳页查询
只能一页一页查询,从第一页开始
order by time offset x limit y;
改成
order by time where time>0 limit y;
从每个库中获取数据,然后内存排序,获取第一页数据
查询下一页,将上一页最大时间作为条件查询.
order by time where time>$time_max limit y;
优点:数据传输量和排序量不会随着不断翻页而性能下降.
与全局视野法相比较,不用查询所有数据.
允许数据精度损失法(若业务可接受,极力推荐)
数据库分库-数据均衡原理
使用patition key分库,在数据量较大情况下,数据分布足够随机情况下
各分库所有非patition key属性在各库上,数据分布的概率一致
如:uid分库随机情况下,db0男性70%,则db1也差不多70%
order by time offset 100 limit 100;
改
order by time offset 50 limit 50;
每个分库均拿一半,得到数据并集.
优点:不需要返回更多数据
不需要服务内存排序
缺点:损失精度
二次查询
order by time offset 1000 limit 5;
假设有3库
order by time offset 333 limit 5;
获取三个结果集中最小的time,最小的那条数据不用去再次查询
order by time where time between min_time and 结果集中最大时间
由于查询条件放宽,数据量会有所增加
获取每个结果集中的offset,根据333往前倒推就行了.
每个结果集offset与333相加,可以得出min_time的索引.
然后根据第二次的结果集,排序limit就行了
如何跨库,依旧能路由到
路由算法
- hash取余:命中率低至(N/N+1)
- 一致性hash算法(一致性hash环):通常使用二叉查找树实现。命中率高至99%
- 一致性hash+每个节点加路由表
有意思的是P2P也是用的这种优化查找速率.
内存缓存原理
缓存存放在一个引用表中,通过hash值引用,这个hash值包括查询本身,查询数据库等.
若查询中包含任何不确定函数(如:NOW())以及缓存数据超过设置上限,则缓存不会命中,也不会缓存该结果.SQL语句有任何变更,也不会命中.当缓存的数据涉及的表变化,则缓存失效.
使用场景:
写少,读多,且跨表频繁,数据量少.
局限:
- 查询缓存时,会加排它锁,所以占用大量资源
- 事务执行前和提交后才会缓存,若大量事务存在,则会降低命中率.
- 存在碎片化问题,因为缓存的内存块大小是以query_cache_min_res_unit为单位,当在块中填充结果后,执行收缩后,节省的空间间隙<query_cache_min_res_unit,下次不会被分配.
如何分析和配置?
innodb如何防止内存缓冲区被污染? todo
数据库主从同步流程 todo
B树,B-树和B+树、B*树的区别? todo
innodb事务隔离级别如何实现? todo
死锁问题排查
select for update引发死锁分析
搜索引擎
Map<url,content> 根据url查询content就是正排索引
根据content查询url就是倒排咯
想要根据搜索关键字匹配content从而查找到url,提升效率这涉及到content的分词处理.
而根据关键字搜索肯定不止一个匹配的结果,于是倒排索引的数据结构可以理解为:
Map<content,list>
整个检索的过程就是,分词,查倒排索引,得出结果集交集
求交集方法
双重for循环,O(N^2)
有序list求交集,拉链法O(N)
就2个list遍历,因为有序所以2者只需要遍历一次就行了
分桶并行优化O(N)
将list1和list2按照范围划分N个桶,然后多线程N桶并行计算.
分桶并行+bitmap优化O(N)
因为list1和list2按照范围划分,假设按照桶[1,16]范围划分,建立16bit,集合中第一个元素x,在16位bit中x位置为1.然后2个16bit进行"与"操作
跳表O(log N)
有序链表求交集时,可根据集合长度,建立一级链表,比如list1(1,2,3,4,5,6,9)建立一级链表(1,5,9),二级链表就是普通链表.当采用拉链法时,就减少了很多中间的遍历.类似与采用2X树.
elasticSearch和数据库区别?
功能和Merge差不多,不过性能完全不能比.其次针对大量商品,elasticSearch理论上可以无限扩展,而且存储内存,查询效率比DB高
RPC
RPC小Demo
我个人的理解啊,一串程序由数据段+代码段组成
在进程的层面更加复杂了
而RPC就是跨进程的调用.也就是一个调用程序,跨进程的调用另外一个程序代码片段.
在这里有2个需要制定的东西
第一个就是请求报文协议,指定调用哪个方法,以及参数是啥.有原生的java序列化.除此之外,还可以用json,还有google那啥协议.
第二个传输协议,你可以采用tcp/udp,webservice,甚至java原生提供的RMI都阔以(RMI和webservice的代码我就不贴了).
异步RPC太复杂了,研究RPC源码时在写.
分布式锁
数据库分布式锁
乐观锁增加版本号
根据版本号来判断更新之前有没有其他线程更新过,如果被更新过,则获取锁失败。
悲观锁显示加锁
当想要获得锁时,就向数据库中插入一条记录,记录执行方法名(添加唯一约束),释放锁时就删除这条记录。如果记录具有唯一索引,就不会同时插入同一条记录。
这种方式存在以下几个问题:
锁没有失效时间,解锁失败会导致死锁,其他线程无法再获得锁。
只能是非阻塞锁,插入失败直接就报错了,无法重试。
不可重入,同一线程在没有释放锁之前无法再获得锁。
redis分布式锁源码解析
redis实现,借鉴Redlock
获得锁:setnx(id,过期时间)
exec
删除锁:del(key)
存在问题:
超时,但线程还在工作
开启一个守护线程监控任务是否完成,没完成添加过期时间
无法释放锁:设置超时时间
无法重入:
本地ThreadLocal计数
redis hash存储可重入次数,lub脚本加锁解锁
其他人删除锁
id为唯一标识,删除锁的时候判断是不是自己加的锁,不是不删除
var value=get lock_name
if value== 锁的时候填充的值
释放锁
else
释放锁失败
因为这个步骤不是原子性,所以需要lua脚本执行判断和释放锁
死锁:设置获取锁的优先级策略
存储一对key-value,value为当前时间+过期毫秒数
当cur_data>K-value时,获得锁
官方给出了Redlock算法,大致意思如下:
在分布式版本的算法里我们假设我们有N个Redis Master节点,这些节点都是完全独立的,我们不用任何复制或者其他隐含的分布式协调算法(如果您采用的是Redis Cluster集群此方案可能不适用,因为Redis Cluster是按哈希槽 (hash slot)的方式来分配到不同节点上的,明显存在分布式协调算法)。
我们把N设成5,因此我们需要在不同的计算机或者虚拟机上运行5个master节点来保证他们大多数情况下都不会同时宕机。一个客户端需要做如下操作来获取锁:
1、获取当前时间(单位是毫秒)。
2、轮流用相同的key和随机值(客户端的唯一标识)在N个节点上请求锁,在这一步里,客户端在每个master上请求锁时,会有一个和总的锁释放时间相比小的多的超时时间。比如如果锁自动释放时间是10秒钟,那每个节点锁请求的超时时间可能是5-50毫秒的范围,这个可以防止一个客户端在某个宕掉的master节点上阻塞过长时间,如果一个master节点不可用了,我们应该尽快尝试下一个master节点。
3、客户端计算第二步中获取锁所花的时间,只有当客户端在大多数master节点上成功获取了锁(在这里是3个),而且总共消耗的时间不超过锁释放时间,这个锁就认为是获取成功了。
4、如果锁获取成功了,那现在锁自动释放时间就是最初的锁释放时间减去之前获取锁所消耗的时间。
5、如果锁获取失败了,不管是因为获取成功的锁不超过一半(N/2+1)还是因为总消耗时间超过了锁释放时间,客户端都会到每个master节点上释放锁,即便是那些他认为没有获取成功的锁。
虽然说RedLock算法可以解决单点Redis分布式锁的高可用问题,但如果集群中有节点发生崩溃重启,还是会出现锁的安全性问题。具体出现问题的场景如下:
假设一共有A, B, C, D, E,5个Redis节点,设想发生了如下的事件序列:
1、客户端1成功锁住了A, B, C,获取锁成功(但D和E没有锁住)
2、节点C崩溃重启了,但客户端1在C上加的锁没有持久化下来,丢失了
3、节点C重启后,客户端2锁住了C, D, E,获取锁成功
这样,客户端1和客户端2同时获得了锁(针对同一资源)。针对这样场景,解决方式也很简单,也就是让Redis崩溃后延迟重启,并且这个延迟时间大于锁的过期时间就好。这样等节点重启后,所有节点上的锁都已经失效了。也不存在以上出现2个客户端获取同一个资源的情况了。
总之用Redis集群实现分布式锁要考虑的特殊情况比较多,尤其是服务器比较多的情况下,需要多测试。
/**
* 分布式锁的简单实现代码
* Created by liuyang on 2017/4/20.
*/
public class DistributedLock {
private final JedisPool jedisPool;
public DistributedLock(JedisPool jedisPool) {
this.jedisPool = jedisPool;
}
/**
* 加锁
* @param lockName 锁的key
* @param acquireTimeout 获取超时时间
* @param timeout 锁的超时时间
* @return 锁标识
*/
public String lockWithTimeout(String lockName, long acquireTimeout, long timeout) {
Jedis conn = null;
String retIdentifier = null;
try {
// 获取连接
conn = jedisPool.getResource();
// 随机生成一个value
String identifier = UUID.randomUUID().toString();
// 锁名,即key值
String lockKey = "lock:" + lockName;
// 超时时间,上锁后超过此时间则自动释放锁
int lockExpire = (int) (timeout / 1000);
// 获取锁的超时时间,超过这个时间则放弃获取锁
long end = System.currentTimeMillis() + acquireTimeout;
while (System.currentTimeMillis() < end) {
if (conn.setnx(lockKey, identifier) == 1) {
conn.expire(lockKey, lockExpire);
// 返回value值,用于释放锁时间确认
retIdentifier = identifier;
return retIdentifier;
}
// 返回-1代表key没有设置超时时间,为key设置一个超时时间
if (conn.ttl(lockKey) == -1) {
conn.expire(lockKey, lockExpire);
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
} catch (JedisException e) {
e.printStackTrace();
} finally {
if (conn != null) {
conn.close();
}
}
return retIdentifier;
}
/**
* 释放锁
* @param lockName 锁的key
* @param identifier 释放锁的标识
* @return
*/
public boolean releaseLock(String lockName, String identifier) {
Jedis conn = null;
String lockKey = "lock:" + lockName;
boolean retFlag = false;
try {
conn = jedisPool.getResource();
while (true) {
// 监视lock,准备开始事务
conn.watch(lockKey);
// 通过前面返回的value值判断是不是该锁,若是该锁,则删除,释放锁
if (identifier.equals(conn.get(lockKey))) {
Transaction transaction = conn.multi();
transaction.del(lockKey);
List<Object> results = transaction.exec();
if (results == null) {
continue;
}
retFlag = true;
}
conn.unwatch();
break;
}
} catch (JedisException e) {
e.printStackTrace();
} finally {
if (conn != null) {
conn.close();
}
}
return retFlag;
}
}
zookeeper分布式锁 todo
总结
数据库锁:
优点:直接使用数据库,使用简单。
缺点:分布式系统大多数瓶颈都在数据库,使用数据库锁会增加数据库负担。
缓存锁: CP
优点:性能高,实现起来较为方便,在允许偶发的锁失效情况,不影响系统正常使用,建议采用缓存锁。
缺点:通过锁超时机制不是十分可靠,当线程获得锁后,处理时间过长导致锁超时,就失效了锁的作用。
zookeeper锁: AP
优点:不依靠超时时间释放锁;可靠性高;系统要求高可靠性时,建议采用zookeeper锁。
缺点:性能比不上缓存锁,因为要频繁的创建节点删除节点。
性能角度:缓存>zookeeper>数据库
可靠性:zookeeper>缓存>数据库
接触的项目中,基本没用过zookeeper锁.
业内大佬也说实际项目中为了保证吞吐都不用zookeeper.
相反,金融,保险和涉及财务行业用的比较多,因为场景不一样,数据强一致>最终一致.而且如果性能低下,有的是钱提升硬件性能.
一致性
session一致性
tomcat广播
优点:简单容易实现
缺点
当服务器多的时候,广播占用大量带宽,而且每台服务器保存session占用内存
客户端存储
优点
服务器不需要存储
缺点
每次请求需要携带session,占用带宽
安全问题
session存储客户端,受限于cookie限制
nginx固定分配一台服务器处理
4层代理hash,使用ip作为hash分配服务器
7层代理hash,使用业务属性分配,如order_id,user_id等
优点:
只需要修改nginx
支持水平扩展
缺点:
重启,容易造成session丢失
当服务器水平扩展后,服务器访问时重新分配新的主机处理,造成访问不了session
存储内存
找到一个所有服务器都会访问的地方,如数据库,redis.可把session存储在此
优点:
存储服务端,没安全隐患
水平扩展
只要存储session服务器不重启,web-server重启无所谓
缺点:
增加一次网络调用
修改业务代码
建议:redis,因为sessin作为一个高访问的玩意儿,而且丢失也没事,所以存储redis.
分布式事务一致性
一致性协议
二阶段提交(2PC)
引入一个协调者,统一掌控所有参与者,并指示他们是否操作结果提交还是回滚
分为2个阶段
投票阶段:参与则通知协调者,协调者反馈结果
提交阶段:收到参与者反馈后,协调者再向参与者发出通知,根据反馈情况决定每个参与者到底还是提交还是回滚
例子:老大BOSS说要吃饭,统计下面员工意见,有员工ABC同意去,但D没回复,则BOSS依旧处于接受信息状态,ABC则阻塞.当统计了ABCD员工意见后,如果有一个员工不同意,则通知ABCD不去(回滚),然后ABCD收到后ACK BOSS,这个阶段如果BOSS没收到会一直阻塞
缺点
执行过程中,所有节点处于阻塞,所有节点持有资源锁定状态,当有一个节点出现问题无法回复,则会一直阻塞,就需要更复杂的超时机制处理.
协调者有限状态机
参与者有限状态机
看得出来整个过程有3个阻塞,协调者WAIT状态,参与者INIT和READY状态.当某个进程卡住的时候,导致其他进程等待.
解决办法
3PC???有点东西
超时判断
解决协调者WAIT
一定时间尚未接收全返回消息,则广播参与者取消事务
解决参与者INIT
一定时间内没等到协调者发出询问消息,则简单终止本地事务,并向协调者发送取消事务消息
参与者互询
解决参与者READY
此时参与者在等待协调者发出的全局表决消息,即便超时,参与者也不能中止事务,因为它并不知道协调者发出的啥表决消息.如此一来会出现数据不一致问题.
解决办法,问隔壁的参与者.
那么存在4种状态
- 隔壁的参与者Commit,说明协调者发出的是commit,只是我没收到,commit即可
- 如果隔壁收到abort,同理我取消事务即可
- 如果init,说明协调者和隔壁老哥存在通讯问题,而我是ready,说明协调者应该在init或wait状态,也就是表决阶段,若考虑超时机制,协调者在WAIT状态下等待隔壁老哥反馈,即便通讯畅通,也必定ABORT状态,因此我置为ABORT是安全的.
- 如果READY,就询问其他参与者.若所有参与者READY,则阻塞不可避免啊.
三阶段提交,性能更加比较差.
将提交阶段拆分预提交和提交阶段.怎么感觉跟预发布和发布一样= =
引入超时,解决2PC当TM宕机或网络问题出现阻塞问题,他就会自动提交事务.
能提交不?
能
准备提交
ack
执行提交
收集返回结果
协调者有限状态机
参与者有限状态机
向量时钟 todo
RWN todo
Paxos todo
Raft 选举协议
记得第一次接触这个协议,是学hadoop与zookeeper做HA时,接触过.
在redis和Consul里面也有用上
领导人选举
Term:整个系统执行时间划分为若干个不同时间间隔长度的时间片段构成的序列,每个片段就是Term
log复制
安全性
不同服务器的log中,若2个log项目具有相同的全局索引编号以及相同Term编号,则2个项目相等,且log之前的所有前驱log项目完全相同.
XA规范,java实现JTA todo
DTP事务模型
AP:应用程序
RM:存储数据的
TM:事务协调
补偿事务
在业务端执行业务逆向操作事务
flag1=执行事务一返回结果
if(flag1){
flag2=执行事务二返回结果
if(flag2){
...执行事务...
}else{
回滚事务二
}
}else{
回滚事务一
}
缺点
嵌套过多
不同业务需要写不同的补偿事务
不具备通用性
没有考虑补偿事务失败
后置提交优化
一个事务,分别为执行和提交2阶段
执行比提交耗时长
改变事务执行与提交时序,变成事务先执行然后一起提交
执行事务一,提交.执行事务耗时200ms,提交2ms
执行事务二,提交.执行事务耗时200ms,提交2ms
执行事务三,提交.执行事务耗时200ms,提交2ms
当执行事务二~三之间出现异常,数据就不一致,时间范围为202+202ms
通过改变时序后:
执行事务一,执行事务耗时200ms
执行事务二,执行事务耗时200ms
执行事务三,执行事务耗时200ms
提交事务一,提交2ms
提交事务二,提交2ms
提交事务三,提交2ms
后置优化后,在提交事务二~三阶段才会出现不一致情况,时间耗时4ms
虽然不能根本解决数据一致问题,但通过缩小时间概率,降低不一致概率
缺点:
串行执行事务提交后,连接就释放了,但后置提交优化后,所有库的连接,需要等待全部事务执行完才能释放,数据库连接占用时间加长,吞吐降低了
增量日志
确保最终一致性,延迟取决于日志对比时间
模仿mysql主从复制,binLog,涉及修改添加到日志,然后间隔多长时间进行正向反向表数据比对.
TCC(Try、Confirm、Cancel)
//尝试方法
function try(){
//记录日志
todo save A 转出了 100 元
todo save B 转入了 100 元
//执行转账
update amount set balacne = balacne-100 where id = 1
update amount set balacne = balacne+100 where id = 2
}
//确认方法
function confirm(){
//清理日志
clean save A 转出了 100 元
clean save B 转出了 100 元
}
//取消方法
function cancle(){
//加载日志
load log A
load log B
//退钱
update amount set balacne = balacne+100 where id = 1
update amount set balacne = balacne-100 where id = 2
}
TXC逆向SQL
- 拦截,并解析SQL
- 查询受影响的行数,并记录值
TXM通知事务提交,则删除记录
若回滚则,将记录的值创建逆向SQL,然后执行
最大努力通知
通知N次不再通知,允许一部分丢失.
本地消息表(ebay研发出的)
半消息/最终一致性(RocketMQ)
LCN
数据库主从一致性
当A修改主数据库时,主数据库在同步读数据库阶段,如果有B读取读数据库,因为主数据库还未同步,所以造成B读取的是旧值
解决方案
半同步复制
执行写操作后,等主从同步执行完,
在返回请求,这个时候读数据库读到的时最新的数据
优点:
利用数据库原生功能,简单实现
缺点:
写请求时间延长,降低吞吐
数据库默认异步复制,也就是接收到消息后立刻返回,然后私底下同步从库
全同步复制:所有从库同步完成才返回数据
半同步复制:至少一台从库同步完成就返回数据
强制读主库
优点:不需要修改
缺点:需要采用缓存优化数据库访问效率
数据库中间件
1:所有读走从库,写走主库
2:记录所有路由到主库的key
3:在主从同步时间内,如果有读请求访问,则读主库
4:主从同步时间过后,读请求访问从库
优点:有现成中间件可以使用
缺点:没发现啊- -
缓存记录写key
key缓存redis,设置超时时间(一般主从同步时间预估值)
当读请求,cache hit则说明这个key刚进行写操作,则读主库
cache miss,说明说明key近期没有写操作,则读从库
优点:轻量级实现中间件
缺点:引入cache,与业务绑定
数据库双从一致性
冗余写库保证高可用
当有2个请求分别写入AB主库,这个时候生成的ID都是一样的,A同步B,B同步A,就会同步失败,造成数据不一致
解决方案:
设置不同初始值,以及相同步长
设置id生成策略,参考10种分布式id生成
对外只提供一台写库,另外一台作为影子shadow
存在问题:当A库同步B时出现异常,KeepAlive虚ip漂移,切换到B库,在A库同步成功之前,插入一条数据到B库,造成数据不一致问题.
内网DNS探测
上述存在问题,本质需要同步成功后,在实施虚ip漂移
写一个小脚本轮询探测主库ip连通性,当连接失败,则delay一个X秒延时,等主库B同步成功后,在将B库ip连通
主从DB与缓存一致性
个人理解:在多机情况下,能放入缓存的数据本身不应具备实时性,强一致性。所以多数情况加上过期时间即可。针对实时性要求高以及强一致场景,还不如直接访问数据库。这样也不需要引入其他组件来二次淘汰,从而增加系统复杂性。
存在问题
方法一:
淘汰缓存
主从同步期间
休眠主从同步时间或者新开timer,再次淘汰缓存
缺点:所有的写请求阻塞固定时间,降低吞吐,增加处理时间
方法二:
写走库,读走缓存.另外开启一个线程读取库binlog,实时写入cache
缺点:实现比较复杂
方法三:MQ异步淘汰
淘汰缓存
写库
发送一条淘汰缓存的异步消息
直接返回
缺点:增加一次cache miss
方法四:读binlog异步淘汰
取消发送淘汰异步消息步骤
新增一个线下binlog的异步淘汰线程,读取binlog数据,异步淘汰缓存数据
方法五:写请求串行化 待定.
将写请求更新之前先获取分布式锁,获得之后才能更新,这样实现写请求的串行化,但是会导致效率变低。
库存扣减一致性
直接设置扣减
缺点:当重试时,导致重复扣减,主要原因是扣减是一个非幂等操作,不能重复执行
直接设置最新库存即可,是一个幂等操作
缺点:当存在多个线程设置的时候,容易造成覆盖
数据库CAS扣减
update store set num=$num_new where id=$id and num=$num_old
缺点:基于CAS的设计,当存在多个线程争抢的时候只有一个会成功执行,为了保证后续逻辑,需要增加重试机制,而因为数据库比较重量,所以可以采用redis优化
redis原子性扣减
redis缓存数据库真实库存,原子扣减.后MQ到数据库
需要同步到数据库,增加一次操作,而且当redis宕机,真实的数据还在redis没有来得及同步数据库,容易造成数据不一致问题
redis分布式锁
redis分段锁(借用CHM1.7的思想,同一个商品,假设有1000库存,分成20把锁,每把锁50库存,随机获取一把锁,若没库存,则获取下一把锁。)https://www.imooc.com/article/293531
MQ缓存请求
利用MQ,将请求线性维护,后台MQ接收端,执行数据库写请求.
迁移
机房迁移
单机房分层架构中,所有应用都在同一个机房,架构特点"全连接".
站点层调用业务层,业务层有多少分,上层就连接多少服务.
数据库冗余多少分,就要连接多少个数据库
全连接架构的负载均衡和高可用是通过连接池实现的.
1:所有服务在新机房部署一套,然后流量切过来
缺点:停止服务,丧失可用性
当服务器过多,部署一套成功率低
设计目标
平滑迁移,不停服务
可分批迁移
支持随时回滚
如果需要支持设计目标,迁移过程中必须涉及到多机房的架构
想要平滑迁移机房,势必存在中间过渡状态,二边机房都有流量,都对外提供服务,所以中间状态必不可少.
存在问题:如果单机房全连接架构复制到多机房,会有大量跨机房调用.极大增加延时.同机房则忽略不计.
解决方案:同机房连接
2:多机房多活架构,理想状态为,除了数据库同步需要跨机房通讯,
其他所有通讯都同机房连接.
优点:当机房故障,能将流量切到另一台机房,实现高可用
缺点:数据库同步存在延时,譬如像扣减余额需要保证数据强一致性,
所以不适合这套架构.
用户A在北京,访问北京的机房,扣减余额
用户A的老婆在广州,访问广州的机房,余额扣减.
存在的问题就是每个机房存在全局业务数据访问场景.
当每个机房都访问局部业务数据是可行的,
这些业务都具备数据聚集效应
如滴滴打车
下单用户/接单司机/交易订单都在同一个城市
3:伪多机房多活架构(最小化跨机房调用)
主机房部署主从库,从机房只部署从库
跨机房写主机房主库,同机房读
优点:
从机房挂了,能直接容错.
存在问题:
主机房挂了,写出现问题,DBA介入将从机房读库变为主库即可
迁移方案
自底向上
自顶向下
服务迁移(无状态)
新机房部署好子业务系统
新开专线访问旧机房如cache,DB,搜索引擎的
1:缓存和数据库未迁移,存在跨机房连接
2:新机房配置文件不要设置垮机房调用
3:最小粒度,部署服务.迁移范围不要过大.
灰度切流量
释放释放一定流量给新机房,观察是否能正常提供服务
出现问题,流量切回旧机房.
缓存迁移(有状态,但数据可重建)
部署与旧机房一样缓存服务器.
切换新机房使用缓存服务器
1:内网dns映射新机房ip
2:重启新机房依赖缓存服务器的服务
注意:
如ip直连,没设置域名dns映射,则重启服务器
流量低峰迁移,否则容易穿透数据库
数据库迁移(有状态,数据也要迁移)
新机房搭建数据库
新旧机房数据库同步(MM/MS架构同步数据或阿里云可用DTS)
数据源切换
注意事项:
保证数据库同步完成,但是数据库如果一直写入数据,
同步完成时间未知
需要修改数据库端口
旧机房数据库设置只读(ReadOnly),
新机房同步数据完成后,
修改端口,重启服务即可.
MQ迁移
1:升级消费方
同一个主题,新MQ也需要订阅,但新MQ-Server对外不提供服务.
此时对外提供服务的依然是旧MQ-Server和消费方.
2:生产方升级
旧MQ发布更换新MQ发布
3:消费方下线旧订阅
之所以不能统一升级,原因在于业务与底层MQ技术耦合
但更换MQ,业务层面也需要大量修改.
解决办法:
Spring cloud stream
和这个一样,也就是封装MQ底层实现方式,这样业务方只需要调用就行了.
更换MQ时,只需要修改封装MQ的那块
RocketMQ-client::sendMsg
封装为:
fengzhuangdiaoyong(){
RocketMQ-client::sendMsg
}
数据迁移
场景:
底层表结构变更,字段,属性,索引等
分库个数变换,user1和user2变更user1,user2,user3
存储介质变换,如mysql变更hbase
方案
1:停机方案
告知用户凌晨几点停机维护
开发离线数据迁移工具,进行数据迁移
恢复服务,流量切新库
表结构变更,服务升级到访问新表
数据库个数变更,修改寻库路由配置
存储介质变更,服务升级到访问新的存储介质
缺点:影响服务可用
固定时间范围需要升级完成,给人压力大.
平滑迁移
2:追日志法
服务升级,记录对旧库上的数据修改的日志.
主要记录,被修改库,表,唯一主键
特点
只需修改修改数据库的接口
升级只是增加一些日志,对业务没影响
研发数据迁移工具,进行数据迁移
特点:
旧库依旧能对外提供服务
出现任何问题,新库干掉即可
可限速慢慢迁移,没时间压力
数据追平
步骤一中记录的日志,进行数据追平
读取日志,获取那个库,表,主键的变化
获取旧库对应主键数据
替换新库对应主键数据(数据以旧库为基准)
特点:
出现问题,回滚步骤二重来
可限速慢慢重放日志,时间没压力
依旧可以对线上提供服务
数据校验
在追平数据过程中,新旧库数据对比,直到完全一致
特点:
依旧可以对线上提供服务
出现问题,回滚步骤二重来
可限速慢慢对比,时间没压力
如果数据始终不能一致,旧库设置秒级readOnly即可
流量切新库.
3:双写法
对旧库修改,对新库同样操作
数据迁移
数据对比
切新库
双倍扩容解决
1:停机维护
停止服务
新建2*N个库,做好高可用
数据迁移,将数据从N个库select然后insert到2*N库中
修改微服务数据库路由配置,取模N变成取模2N
重启微服务
迁移失败,配置改回旧库,即可.
缺点:时间限制
需要停止服务,
如果出现问题,难以回滚.如果回档,会丢失部分数据
双主+keepalived+虚ip保证高可用
2:
之前主从库共一个虚ip,现在增加1个虚ip
修改路由规则
即使路由同一个库,主从保证了数据同步
数据收缩
双虚ip修改成单虚ip
解除双主同步
新库新建高可用(新建主从+keepalived)
删除冗余数据(如虚ip1库中路由到虚ip2中的数据全部删除)
十种分布式id生成
分布式ID生成器(基于号段)源码解析
需要满足如下条件
- 全局唯一
- 高性能,高可用
- 趋势递增
UUID
简单,但id本身看不出特殊意义,且不能递增,且Mysql明确表示id越短越好,
取当前毫秒数
id趋势递增,id整数数据库查询效率高,本地生成即可,不需要远程调用,但因为毫秒1000范围内,所以当并发超过1000,容易重复.
数据库自增id
需要一个单独的mysql实例生成id,查询快,id递增,但单点容易宕机,且抗不出并发场景,可使用keepalived+vip做个影子备用,但因此引发新的问题,2个数据库使用率只有50% = =
数据库多主模式
多主解决了单点宕机风险,但id重复.但是可以设置步长,譬如A数据库步长为2,起始为1.B数据库步长为2,起始为2.但引发了新的问题,扩展集群后,需要修改步长,而且影响到之前的AB数据库起始值.
号段模式(发射器)
从数据库批量获取自增id,加载到内存(如redis),然后更改数据库最新起始值,当内存使用完id后在此获取.这里为了取段的时候出现并发场景,可表多设置一个字段,版本号.CAS修改.
redis
incr,但需要注意持久化问题.而且redis作为缓存服务器不应该与具体业务耦合,不推荐.
雪花算法
正数位+时间戳+机器id+数据中心+自增值,占用8字节.操作灵活,实现简单,只需要维护机器id就行了
TinyId(滴滴)
基于号段
Uidgenerator(百度)
基于雪花算法,
Leaf(美团)
同时支持号段模式与雪花算法,
常见场景
秒杀
核心技术
围绕秒杀,就6个字,缓存读,异步写
动静分离
squid/varnish/cdn/nginx做缓存代理
页面静态化(适用数据量不大,返回结果集有限)
读写分离(分组)
M主N从,主要解决读性能问题.
水平切分(分片)
如hash分库,解决单库数据量问题
前后台分离
如前后台数据库共用一个数据库,拆分前后台数据库,异步数据库同步
前后端分离
如:利用vue/php+java将前端和后端分离出来,这样后端只需要提供接口即可.更专业的人做专业的事.
缓存越贴近上游,下游服务器压力就越小
DNS轮询(只负责域名解析,无法保证提供的ip项目可用,不推荐)
CDN回源,判断是否加入了随机参数
lvs+keepalived+nginx/F5
nginx+keepalived(影子shadow,利用率50%)
nginx所在服务器双网卡,2个ip,cdn层面配置这2个ip,类似双主双从架构.占用率100%
js限制点击数(按钮置灰)
网关限流,如重复点击,返回同一个页面,对同一uid限制请求数和请求速度,以及黑名单拦截,极端情况下甚至不处理,直接抛弃.
服务降级
写
比如商品只有2000个,那么只允许2000请求进来,其余的请求返回已售完.MQ接收端可单个接受MQ,也可以批量拉取MQ请求.
数据库层面,如果订单数量是已知的,可提前生成好2000个订单数据,直接update订单和用户关联就行了,而不需要insert.这样也可以避免并发场景数据不一致.
读
redis缓存热点数据
redis缓存请求数,每次请求一个进来,原子减少请求数,当请求数=0,则抛弃后续所有请求.
数据库,主从复制,读写分离,分库分表
业务层面处理
下单和支付放2个环节
硬件层面
针对CPU密集型和IO密集型操作,提升相应的硬件配置.
(任务执行时间/任务执行时间-IO等待时间)*CPU内核数=启动线程数
固态硬盘(快速顺序读写,慢速随机读写) VS SSD硬盘
B+树 VS LSM树
RAID(改善磁盘访问延迟,增强磁盘可用性和容错能力) VS HDFS
终极解决方案:微服务架构
订单超时处理
方案一:定时器每分钟扫描数据库
方案二:每个订单单个线程处理
方案三:时间轮
方案四:利用redis过期回调
方案五:redis延迟队列,参考有赞
方案六:基于MQ延时队列,RabbitMQ死信队列
架构与系统设计
双机主备
双机热备
LVS
当多个系统之间相互依赖如何设计可扩展的微服务体系结构?
- 共用同一个service层
- 垂直拆分service层面.一个子业务一个service
- 数据垂直拆分,一个数据库或者一个数据表对应一个service
- 一个接口对应一个service
在这里本人觉得最好的是垂直拆分业务,一个子业务对应一个service对应一个数据库,比如与用户相关的业务,独立成一个service对外提供,另外只针对用户库调用.
引申出康威定律:一个系统的技术边界将反映组织的结构。换成白话来说就是微服务的粒度取决于团队的结构.随着团队的扩大而拆的更细,也随着团队聚合变得更粗.也就是低内聚高耦合.
当一个接口涉及到用户,订单,积分等系统,这个接口那个模块开发比较好?问题到这里,答案也就呼之欲出了.首先判断这个接口是啥业务类型,也就是根据他的功能判断,是否与用户业务相关,还是其他业务相关,然后对应模块开发就行咯.当然这是本人的真知灼见,欢迎拍砖.
如何设计返回给前端的数据格式?
msg
code
data
中台是啥? todo
别问我,我也不知道.
如何定位线上问题?
CPU100%排查
-
top -c 查找CPU占用多的进程
ps -aux | sort nk 3 也可以
-
top -Hp Pid 查找耗CPU的线程
-
查看堆栈,找到线程.
jstack Pid | grep ‘16位进制线程id’ -C 200
因为线程id用16表示的,线程对应PID转成16进制
printf “%x\n” 260 将260转成16进制
-C 200:列出符合行之外并列出上下各NUM行,默认值是2。
JVM系列
OOM
示例代码
内存泄漏:程序中动态分配内存给临时对象,对象不会被GC回收,它始终占用内存,虽然可达但已无用.无用对象持续占用内存得不到及时释放,从而造成内存空间浪费.
可达性分析算法
通过一系列名为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链
当一个对象到GC Roots没有任何引用链相连(用图论的话来说就是从GC Roots到这个对象不可达)时,
则证明此对象是不可用的,
java语言中,作为GC Roots的对象包括下面几种
1:虚拟机栈(栈帧中的本地变量表)中的引用的对象
2:方法去中的类静态属性引用的对象
3:方法区中的常量引用的对象
4:本地方法栈中jni(即一般说的native方法)的引用的对象
5:所有被同步锁(synchronized关键字)持有的对象。
6:·反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
产生原因
- 内存分配过小,使用了大容量内存.
- static字段引起,限制对象创建,可采用懒加载解决
- 大量常量引起,如String.intern(),扩大常量池或者堆大小即可
- 尚未关闭资源导致,使用finally或者try-with-resource关闭即可
- equals和hashCode比较对象不正确,导致存储大量重复对象
- 内部类持有外部类对象,当外部类已经不再用,也不会释放,将内部类转成static修饰即可
- 对象频繁创建,尚未释放.如ThreadlLocal泄漏问题
- 某一资源频繁申请,系统资源耗尽.如连接过多,资源耗尽.
确认内存分配过小
jmap -heap pid:查看堆使用情况
找到最耗内存对象
jmap -histo pid:查看堆中对象数量和大小
jmap -histo:live pid,这个命名会触发一次FUll GC,只统计存活对象
确认系统资源是否耗尽
查看进程创建线程数:pstree
查看网络连接数:netstat
查看句柄详情: ll /proc/pid/fd | wc -l
查看线程数:ll /proc/pid/task | wc -l
堆栈使用情况
ps -ef |grep java 或 jps 查找到pid
jmap -dump:live,format=b,file=D:\heap.hprof 这里输出pid
线程使用情况。
jstack pid > stack.log
数据库系列 todo
MQ系列 todo
Gossip流言协议
consul用的就是这个玩意,主要用来数据复制.
有时间研究源码实现.
消息传播模型
- 全部通知,当某节点有更新消息,则通知所有小伙伴,根据判断本地与接收到的消息要新,则不更新数据.存在单点问题,若某节点挂了.则集群所有节点无法获得最新消息
- 反熵[常用],随机选择一个节点交换信息,主要有push,pull,push-pull三种模式,更新效率也是从低到高
- 散布谣言,与反熵相比,增加传播停止判断,当P更新数据,随机选择Q时,如果Q有最新的数据,则降低P不在通知其他节点概率,当达到阀值则不在通知.
im解决消息重复
在硬件方面,我做的是幂等的,所以这块处不处理无所谓,就比如你开灯,你发送多个开灯指令,也是开灯,跟上个状态没关系.如果说压力过大,每个指令缓存唯一标识,重试的时候,根据唯一标识去重即可
在协议层面,添加一个标识.每次生成一个消息的时候,设置唯一标识.当消息重试的时候,服务端根据这个消息标识判断是否重复.
不在协议层面,根据消息所有内容生成唯一标识,然后去重,跟MQ差不多.
如何提高单个接口性能?
- FutureTask+Callable+多线程
- ExecutorCompletionService
源码解析地址
性能优化
jvm
- 最大最小堆内存(最大堆建议为可用内存80%,倘若直接内存内需要多,可适当减少点)
- 服务端模式启动
- 元空间最大最小值
- 新生代最大内存
- 年轻代和老年代比值(默认2,不建议修改)
- 伊甸园区和幸存者区比率 (默认8,不建议修改)
tomcat
- 最大连接数
- 最大线程数
- io/nio/apr协议
- 最大排队等待数
- 超时时间等