面经
三七
探迹
python 协程和golang 协程区别
python 协程单线程内切换,适用于io密集型,不适用cpu密集型,无法利用多核;不过也有个好处是因为协程单线程切换,不会并行,不需要考虑数据安全 golang的 协程gorouting,相对于python协程中协程和系统线程的N对1关系,go中的协程和线程是多对多关系,可以有效利用到系统多核,适用于cpu密集型和io密集型,因此需要考虑到并发操作过程中的数据安全,go中比较常用的是channel方式进行协程同步保证数据安全 调度方面的话,python协程通常依赖事件循环,go则是有自己的协程调度器,能够自动在多个协程间切换
go gmp模型
- g: gorouting,go协程
- m:操作系统线程
- P:执行上下文,负责管理gorouting队列,与M进行绑定,决定了并行执行gorouting数量,设置与cpu核数量挂钩
协程和线程区别
线程是操作系统调度的基本单位,由操作系统进行管理和调度;协程是一种轻量级的并发执行单位,由程序运行时进行调度,线程创建需要较大的系统资源,协程消耗极少;调度切换也是线程较大,因为线程切换时在内核态,协程则是在用户态
mysql 索引设置考虑
- 字段涉及数据查询的必要性
- 字段值的区分度,比如性别、状态等区分度小的则没有必要设置索引
- 索引类型选择,如联合索引/唯一索引等,联合索引还需要考虑字段顺序,按查询使用情况基于最左匹配关系设置
- 业务查询考虑索引覆盖,避免回表查询
mysql 主从同步,读请求3W,写请求5k,如何部署
操作过程: 主服务器上启用二进制日志,配置唯一服务器id;从服务器上配置主服务器连接信息,同时也设置不同的唯一服务器ID;启动从服务器同步进程; 主从同步自动切换:可以考虑使用一些第三方工具或框架,如MHA,通过监控主服务器的状态,出现故障时,自动选择切换(考虑的依据是数据延迟程度、服务器的负载)
kafka、rabbitmq消息队列底层、消息不丢失
kafka
顺序消费:kafka中的分区对于 保证了消息的顺序性,消息是按照他们发送的顺序进行存储和消费的 因此如果需要顺序消费,需要保证消费者组中的消费者实例只消费一个分区;生产者发送信息需要发送到同一个分区,比如订单处理系统,对同个订单的相关操作发送到同个分区
零拷贝:是一种用于优化数据传输性能的技术,常见的有mmap和sendfile;为了消息不丢失,kafka是将消息存储到磁盘中持久化的,传统的网络数据传输中,如果需要将磁盘中的数据进行网络发送,需要先从磁盘读取数据,拷贝到内核缓冲区,然后从内核缓冲区拷贝到用户空间缓冲区,再从用户空间缓冲区拷贝到网络缓冲区,最后网络缓冲区拷贝到网卡进行发送,kafka零拷贝则是通过sendfile系统调用,直接从磁盘文件拷贝到内核缓冲区,内核缓冲区拷贝到网卡进行发送,无需经过内核空间到用户啊空间到内存拷贝,减少了拷贝次数,而且拷贝操作都是在内核态内完成,提高了数据传输到性能和效率
常规发送:
sendfile 发送:
mmap 发送(RocketMQ)
磁盘 -> 内核缓冲区 —> 网络缓冲区 —> 网卡
消息不丢失
- 生产者设置ack=-1/all,确保消息被完整复制到所有副本后才确认发送成功,同时配置合适的重试机制和重试间隔,应对网络或者broker故障
- 消费者可以通过手动提交偏移量,处理完一批消息后进行提交;消息记录持久化,以便在出现事故时恢复
- broker 可以适当增加消息保留时间, 监控broker健康状况,及时处理故障节点
elastic部署节点类型,底层搜索原理
es数据节点类型有
- 主节点:负责管理集群状态,不处理数据和搜索操作
- 数据节点:负责存储数据, 对数据进行索引和检索操作
- 客户端节点:用于接受用户请求,并将请求转发到合适的数据节点进行处理
搜索原理
倒排索引
基于倒排索引,es倒排索引中主要包括结构有 词项表、文档列表等;在数据索引阶段,通过分词文档内容将文本内容分成对应词项,这些词项数据组成了词项表,文档列表则是存储了文档id,词频及词项在文本内容中偏移量,每个词项和包含该词项的文档id列表建立映射关系,通过词项快速匹配到对应文档列表;
词项前缀
拓展(词项前缀)但是词项表数据量比较大,是存储在磁盘中的,因此需要通过内存中的词项前缀加速定位,词项前缀是词项和词项之间相同的前缀组成的一个类似目录树的结构,方便快速定位到对应词项(比如follow和foward的前缀fo)
rpc通信,而不是http
- rpc通常使用二进制序列吧方式,如protocal buffer,减少数据量,提高传输效率;http常用json/xml等文本序列化,数据量较大;传输和解析开销相对较高;
- 现有的grpc框架大多能很好控制连接建立,服用关闭等,比如使用连接池,实时性较好
- 消息格式更灵活,http的请求和响应格式较为固定
- rpc框架内置了服务发现和负载均衡机制,更方便实现服务等自动发现和请求等负载均衡,也方便和其他组件一起实现zookper之类
- 强类型接口定义,有助于开发过程中进行类型坚持和错误预防
阿里(八股文居多)
redis其他数据结构使用情况,zset之类的
redis实现分布式锁
mysql
mysql 索引结构,B+树,如何分层,2000w数据多少层
联合索引最左匹配,范围查找生效情况
- 范围查找在最左的时候,从范围查找的位置开始,后面的字段无法进行精准匹配,但右侧可以进行范围查找及对左侧部分匹配
- 不在最左的时候,最左的连续匹配自动开始使用,但是范围查找位子开始及后面的都无法使用索引
es 搜索底层,group聚合时底层
浏览器输入一个网址后,发生了什么
如何提高网址加载速度
如何选择最近服务点
cdn底层实现
藏宝阁es部署情况
迅雷面试
mysql
创建一个包含id、lock_name、locked列的表,获取锁时开启事务,select lock_name by update,更新成功locked为锁定,更新成功则代表获取成功,释放锁时更新locked=不锁定
实现分布式锁
inner join和left join的区别
INNER JOIN只返回匹配的行,而LEFT JOIN返回左侧表的所有行,即使右侧表中没有匹配的行
join 查询注意
join查询使用时需要注意驱动表及关联表的索引设置,否则可能会有较高的系统资源如内存等消耗;mysql 中关于join查询有两种算法,一个是INLP 索引内嵌循环连接,一个是BLP 块嵌套循环连接;前者是可以当被驱动表相关字段有索引时可以基于索引进行查询减少获取行数加速查询;后者则是在没有合适索引情况下基于join buff优化批量查询驱动表数据到join buff内存中,再获取被驱动表中数据进行比较,有效减少驱动表数据获取io消耗,有效提高查询效率;可以通过select 语句分析具体走哪种方式,其中可以查看extra信息看是否有Block nested loop;mysql 5.6后也有对join算法的优化,mrr和bka,简单来说就是减少驱动表的数据到被驱动表数据的随机查磁盘,上面说的索引嵌套查询中使用join buff,对每次驱动表中普通索引树中查询到到的主键id进行缓存,然后排序后再到聚会索引上顺序查询,需要设置一下优化器配置optimizer_switch;如果对数据表有清晰认识,也可以通过straight_join绕过优化器手动选择被驱动表,无论如何使用到那种join算法,都需要使用小表作为驱动表有效提高整体查询效率
网络安全
防注入,跨域请求
golang
分片结构底层
slice 包括指针、长度、容量
- 指针是指向底层数组,
- 长度是当前包含的元素个数,
- 容量则是从切片起始指针到底层数组末尾到元素个数,也就是不分配内存情况下可以追加的元素个数
与数组的区别
- 切片是动态的,长度可以改变;数组是静态的,长度固定
- 切片是基于数组的抽象,提供了分片等更灵活的操作方式
切片扩容: append元素时当切片的长度等于其容量的时候,会触发扩容机制:
- 扩容大小通常取决于当前切片的容量,容量小于1024,新容量会是旧容量的2倍;大于等于1024,新则是旧的1.25倍
- 为新的容量分配内存
- 将原切片中的元素复制到新的内存区域
- 更新切片指针指向新的内存区域;切片长度和容量随之更新
- 返回新切片引用,旧的切片保持不动(内存回收机制会回收,怕内存浪费可以改变原切片容量=长度,减少内存浪费)
go实现面向对象类
// 1. 结构体struct
type Person struct {
Name string
Age int
}
// 定义方法来封装行为
func (p *Person) Intrdouce{
fmt.Printf("Hi, my name is %s", p.Name)
}
// 继承,使用嵌入/组合
type Employee struct {
Person
JobTitle string
}
// 多态,使用接口实现,该类型定义了接口中声明的所有方法,它就实现了该接口,可以用该接口存储任何实现了它的结构体
type Animal interface {
MakeSound()
}
type Dog struct{}
func (d *Dog) MakeSound(){
fmt.Printf("woof")
}
type Cat Struct{}
func (c *Cat) MakeSound() {
fmt.Printf("miao")
}
func main(){
var animal Animal
//因为上面MakeSound 值接收是 *指针类型,因此需要地址指针
animal = &Dog{}
animal.MakeSound()
animal = &Cat{}
animal.MakeSound()
}
协程同步
linux
统计词语出现数量
grep -o "apple" test.txt | wc -l
如果要忽略大小写进行统计,可以使用grep -i选项。例如:grep -io "apple" test.txt | wc -l
查看端口号占用情况
- netstat -tunlp
- lsof -i :8080
系统资源查看分析
100w个连续数中取出两个,然后打乱,找出被取出的两个数
- hashmap
- 平方和 + 和 二次方程求xy
虾皮物流
mysql
隔离级别
- 读未提交,读取最新数据
- 读提交
- 可重复读
- 串行读,基于锁实现
mysql默认可重复读,基于mvcc多版本实现,事务开启时生成快照,后续常规数据查询都是基于生成的快照读取数据
读提交也是基于快照读,但是生成快照时机是事务内每次查询都会生成一个快照,因此在事务期间,如果在其他事物更改数据并提交后,再查询,是会基于更改后的数据再次生成快照,因此可以读取到其他事物更改的数据
mvcc
mvcc 多版本并发控制基于undo日志实现,在mysql innodb事务中,每个事务开启时都会由innodb事务系统生成唯一id,按序递增;每行数据也是有多个版本,对于每行数据,除了记录定义的字段值,还会记录事务id和上一个版本的回滚日志undo log指针,undolog简单来说记录事务id+数据修改前信息+上一个undolog指针,组成undolog版本链;mvcc中还有个重要概念readview一致性视图,由当前事务id,当前系统活跃事务id数组(开启未提交),最小事务id(活跃事务id数组中最小事务id),当前系统最大事务id+1组成;通过readview加上上面说的undo log版本链完成多版本并发控制,事务在查询数据时,会先获取当前最新版本数据,通过review中的最大最小事务id和活跃事务id数组判断是否可读,如果最新版本事务id大于最大版本id,则是不可见,小于最小事务id则是可见;在最大最小之间,则判断是否在活跃事务id数组中,在的话,不可见,否则可见;查找版本的过程则是按照undolog版本链依次往回直到找到第一个可见版本;当然这些都是针对事务中的快照读,如果是事务中的for update或者update语句的则是当前读,获取最新版本数据(如果该版本未提交,则会锁阻塞); mvcc 对于不同的事务隔离级别是通过生成一致性视图时机不同实现
比如读可提交,在每次查询查询语句前都会生成新的一致性视图,因此如果事务a过程中,事务b 修改并提交了某个数据,这时事务a再查询,新生成的rearview中会可见该事务b修改的数据版本,因此可以读到该修改后数据
而可重复读,则是在事务开启时生成一致性视图,后续的查询数据快照读都是基于该视图,因此事务期间的其他事务修改提交的数据在该视图都是不可见状态
读未提交则是没有一致性视图概念,每次都是读最新的数据;串行读则是通过加锁的方式实现读写串行话
幻读和不可重复读
不可重复读只事务中多次读取同一行数据,由于其他事务修改并提交了该行数据,可能导致前后查询结果不一致,可以通过修改隔离级别为可重复读解决,可重复读隔离级别下,基于mvvm,事务开启时生成一致性视图,后续的快照读都基于该视图,因此不会读取到其他事务修改提交的数据
幻读是指事务过程中多次查询返回的数据集不一致,比如事务a开始时查询name=hello的数据,返回了3条,这个过程中事务b插入了一条name=hello的数据并提交,事务a再次查询,返回了4条;这种情况即使是事务隔离级别为可重复读也可能出现,上面说的例子第二次查询name=hello数据时加上for update进行当前读读时候;mysql innodb中提供了一个解决方式,临键锁即行锁+间隙锁,只要在第一次查询的时候使用for update 当前读方式,就会给name=hello的数据加上临键锁,锁定所有符合的数据行及对这些行所在的间隙加上间隙锁,这样事务b插入name=hello的时候就会阻塞直到事务a提交释放锁
锁
mysql 根据锁的范围主要有全局锁、表级锁、行锁
表级锁中又分为表锁,元数据锁mdl
表锁通过lock table read/write使用,读写互斥,写写互斥,读读不互斥
mdl锁,主要用于隔离dml和ddl操作之间的干扰,不需要显示使用,读写互斥,读读共享;简单来说就是修改表结构时,不允许数据增删改查;增删改查的时候,不允许修改表结构,确保了并发情况下的数据一致性
行锁是数据引擎层面支持的,mysql中innodb支持行锁,当不同事务需要更新同一行数据时,就会触发行锁,先获取锁的事务执行完后,后面的事务才会继续执行;innodb事务中行锁遵循二阶段锁协议,即行锁是在需要的时候才加上,比如update等语句,但是是在事务提交的时候才释放
二阶段锁协议
innodb事务中,行锁是需要的时候加上,但是需要等到事务结束时才释放;
如果你的事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放
mysql 查询内存使用
mysql 查询时是边查边发,因此不担心查询大表时机器内存oom mysql 查询修改数据都会先从磁盘中取出数据页到内存buff pool中,修后的数据也不是直接写回磁盘,而是等系统资源空闲或者内存满了需要刷脏页;mysql innodb管理内存使用lru 最近最少使用算法(哈希加双向链表组成),传统lru在扫描全表时,可能会导致内存命中率下降(因此内存中扫出的数据很少会第二次使用)影响其他查询线程;因此innodb做了优化,将lru 链表分为5:3,其中前5存young数据,后3存old数据,常规淘汰还是淘汰后3的old中的末端数据,新插入数据时old头部,如果old中数据存在超过一定时间(可调控)就移到整个链表头部也就是young头部,否则位置不变
索引结构
慢sql分析
redis
大key问题
redis大key就是大value问题,因为redis 核心处理是单线程的,所以在执行操作删除一些大value的缓存时,可能阻塞其他redis的读写操作,导致redis慢查询等
大key 删除
- 确定要删除的大key类型,字符串类型可以对字符串进行截取或修改,逐步减小其大小,然后再进行删除;集合类型,比如列表、集合、有序集合,可以使用sscan、hscan、zscan等命令分配逐步删除
- 如果redis版本在4.0以上的话,还可以使用unlink 方式异步删除,该方式底层也和上述所说的方式差不多,不过是redis帮忙操作,对于大key类型底层也会分不同结构采取不同方式处理,同时因为其异步处理,是由redis后台线程执行,不会阻塞核心线程处理其他redis读写操作;可以通过 INFO | grep ^io_threads 查看后台线程信息/或查看redis log方式查看执行情况
- 还可以修改redis配置,开启lazyfree 惰性删除,通过利用redis key清除策略实现被动删除,主要有过期惰性删除、超过最大内存惰性删除、服务端被动惰性删除
大key 发现
- redis 内置命令 scan + memory_usage 分析
- python 脚本通过+scan + memory_usage 分析
- 还可以使用第三方工具,rdbtools配合分析,首先通过redis bgsave方式导出rdb文件,然后执行 rdb -c memory dump.rdb --bytes 10240 > memory.csv
大key 避免
- 业务设计上,redis value只保持使用到的字段
- 应用层使用相关压缩算法,对value进行压缩存储,如lz4/zlib等
- valu设计越小越好,关联的数据使用不同key存储,比如一个玩家信息,包括玩家名称、玩家等级、玩家装备等,可以拆分为player-username、player-level、player-equipment等
- 对集合类型,比如hash,list,set,sorted set,可以将元素拆分,比如hash,原先是hset(hashkey,field,value),hget(hashkey,field),可以先确定一个桶数量,比如1000,每次存取的时候都先对field hash然后取模1000,确定field落到哪个newhashkey上,实现拆分效果
- 如果以上措施都无法减缓大key带来的问题,就要考虑redis集群扩容了
缓存一致性问题
缓存一致性问题主要原因有 第一个部分操作失败,第二是并发更新;
- 部分操作失败举个例子比如在更新数据库和更新缓存两个步骤,必须保证同时成功或者同时失败,其实就是个分布式事务问题了,这个情况比较难避免,因为目前常规缓存中间件包括redis不支持分布式事务,因此在因为部分操作失败导致不一致上,我们只能说尽量避免或者发生时进行相关补救操作,比如失败重试等保证最终一致性;
- 另外一个并发操作导致的缓存不一致,比如说多个线程同时更新数据库和缓存,先更新数据库的后更新缓存,就会导致不一致,这个是可以解决的;(缓存模式write_back,延迟双删、分布式锁、版本号更新、消息队列,mysql canal,一致性哈希)
比如缓存模式中的write back模式,但是这个模式可能导致数据丢失;或者在缓存命中率要求不是特别高的情况下,可以考虑延迟双删的方式;除此之外,还可以考虑分布式锁或者消息队列保证同一时刻只有一个线程更新同个数据,或者使用版本号的方式,每次数据更新,版本号增加,版本号小的无法覆盖高版本数据,当然也可以考虑使用更新时间作为数据版本;如果数据库是使用的mysql,还可以考虑引入开源产品canal,基于mysql binlog方式更新缓存;在分项目布式多实例的情况下,还可以考虑使用一致性哈希方式,客户端在发起请求操作相关key缓存的时候,通过一致性哈希算法,保证对同个key的操作只会落在一个实例上,极大降低多个实例操作同个key的问题;
如果是使用分布式锁的话,需要注意分布式锁获取释放和数据事务开启提交的顺序,常规思路有先数据库事务提交,再获取分布式锁删除缓存然后释放分布式锁;读请求则是先读缓存,未命中情况下加分布式锁,然后读数据库加载数据再写回缓存;还有个方案在事务提交前删缓存,就是先开启事务,更新数据,然后获取分布式锁,进行删除缓存,然后再提交事务,最后释放分布式锁,读数据则是和上一个保持一致;这种方案可无限接近强一致性,但是也有个很大的问题就是容易导致事务超时,因为获取分布式锁和删除缓存都是在事务内进行,容易引发超时等异常导致事务超时回滚等,违背了使用缓存提高系统吞吐等性能等初衷
redis 单线程
为啥其他缓存中间件比如memecahe使用多线程,但是redis则是单线程模式呢?
严格意义上来讲,redis并不是单线程,只是处理指令命令的时候是单线程,但是在数据持久化、同步等操作都是其他线程处理;而且redis 6.0版本后,io模型还改成了多线程模式,进一步发挥了cpu多核的性能 redis高性能的原因,是因为在处理命令的时候,完全是内存操作,同时在linux系统上采用了epoll和reactor模型结构的io模型,非常高效
linux io多路复用模型主要有select、poll、epoll,其中select/poll都需要对监听文件描述符fd轮询后才能获取出其中有效的fd,同时select api常规调用还有1024描述符数量限制,而epoll则是可以返回监听描述中的有效描述符,不需要再经过轮询;epoll数据结构主要有红黑树+双向链表也就是就绪链表;epoll方式在调用了epoll_create创建后,不需要在发起epoll_wait才将fd移动到就绪队列,而是有满足条件就会被移动到就绪队列,因此相比之下更加高效
reactor io模型,简单来说就是一个分发器 + 一堆处理器;在客户端和服务端的io交互主要有两类事件:连接事件和读写事件,reactor中分发器就是将连接事件交给accetptor,读写事件交给handler;在linux中是使用了epoll,通过epoll_ctl注册事件及事件处理函数,通过epoll_wait监听和分发相关事件;
redis 6.0以前版本因为处理命令都是单线程,所以reactor模型中分发器和处理器其实都是一个线程完成;redis 6.0后引入 io多线程模型,实则是可以看作单线程分发器、单线程acceptor、多线程handler;其中redis主线程同时扮演分发器+acceptor,其中多线程的handler仅负责读写数据,命令的执行还是依赖主线程进行
缓存常见问题
- 缓存雪崩:某一时刻,大量缓存同时失效;可以打散缓存失效时间
- 缓存穿透:恶意请求大量构造数据库中不存在数据请求,导致缓存失效,直接访问数据库;可以缓存空值/回写特殊值,使用布隆过滤器
- 缓存击穿:热点数据失效,大量请求同时请求;可以配合使用singleflight模式,保证同一key只有有一个线程访问到数据库,其他线程等到该线程回写缓存后再访问
缓存过期删除
redis缓存过期不是立刻删除,而是定期删除+惰性删除,因为需要实现过期立刻删除,代价太高,过期立刻删除可以通过定时器或者延时队列,如果是定时器,代表redis要为每个key设置一个定时器,key更新过期时间的话,还要重新更新对应定时器,当key数量越来越多的时候,消耗可想而知;按key过期时间组成延迟队列,在key多多时候开销也很大,而且还要考虑key过期时间调整,而且延迟队列还需要额外线程维护;
惰性删除:所有操作redis数据库的命令都会先判断一下key是否过期,过期则删除,不过期则不做任何处理
定期删除:redis 按一定频率定期循环遍历DB,如果这个db中没有设置有过期时间的key,就会直接跳过遍历下一个db,如果有,就会抽一批key,如果key过期,就直接删除,如果这一批key中,过期比例过低,就中断循环,遍历下一个db;如果执行事件超过阈值,则会中断该次定期循环;下一次遍历会从当前db的下一个继续遍历;可以通过调整dynamic-hz调整redis定期删除的频率;redis通过控制之下定期循环时间控制开销,达到在正常服务和清理过期key之间平衡
RDB 内存数据写入磁盘 主库 生成/加载 rdb会忽略过期key;从库在加载rdb则会全部加载进来,因为加载完后,能从主库收到删除的指令
AOF 对数据操作的命令写入磁盘;对应定期或者惰性删除的key,都会记录一条DEL命令 AOF 重写情况下,则会忽略已经过期的key
分布式
cap理论
- C(Consistency):一致性,分布式系统中所有节点同一时刻看到的数据是相同的
- A(available): 可用性,系统在任何时候都能够对客户端请求作出响应
- P(patition): 分区容错性,指系统在网络分区情况下仍可以继续工作,网络分区是指系统中节点之间网络连接出现问题,导致部分节点无法与其他节点通信,比如集群中多台机器,某台机器网络出现问题,但是不影响集群工作
三者最多同时满足两个,分布式系统中,网络无法100%可靠,因此基本都满足p,因此cp/ap类型 redis是ap类型,zookeeper cp类型(因为在选取主节点的时候服务不可用,当初阿里大量使用zookeeper作为注册中心曾出现过事故
拓展:base理论
base理论是对cap理论中的一致性和可用性的扩展,主要包括基本可用、软状态、最终一致性
- 基本可用,指分布式系统出现故障时,允许损失部分可用性,保证核心或者绝大部分功能可用;比如电商双十一,由于访问量巨大,可能会通过降级方式,比如推荐服务降级为按时间排序列表/退款服务先暂停,优先保证商品浏览和下单
- 软状态,指系统中数据存在中间状态,且不会影响系统的整体可用性,意味着系统数据可以在一段时间内处于不一致状态,比如消息队列系统中,消息可能一段时间内处于未处理状态,但系统最终会处理这些消息
- 最终一致性,指系统中所有数据副本在经过一段时间后,最终达到一致的状态
核心思想是即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当方式使系统达到最终一致性
负载均衡
- 轮询
- 加权轮询,可能出现连续几次命中权重大的机器
- 平滑加权轮询,current_weight=weight,选最大的,然后最大的重新赋值=current_weight-sum_weight,比如current_weight=a0,b0,c0,weight=a1,b2,c3,sum_weight=6,首轮选择c,因为current_weight=3+0=3,然后重新计算c=current_weight-sum_weight=-3,进行下一轮,选b,再重新上一次操作
- 随机,相比轮询,可控性更差
- 加权随机
- 哈希,取请求参数里的几个值做哈希,然后和节点数量取余(如果哈希算法不好,可能导致哈希冲突,某台机器请求过多,还有就是增加/减少机器数量时,取模基数变化了,需要对原先机器重新哈希,比较麻烦
- 一致性哈希,使用哈希环概念,比如初定2^32,需要先对机器做一次hash取2^32余,定下机器在哈希环上的位置,然后对进来请求也做一次hash 2^32余确定请求落在环上位置,顺时针的第一个服务器就是请求落到的机器,这样就就算增加/减少机器,不用对原先所有机器重新hash取余,只会影响新增节点及相邻节点请求
一致性哈希会有哈希环倾斜的问题,比如上面的说的三个服务器都落在哈希环上位置都比较集中,比如说都集中在右侧,按顺时针abc三个点,那因为c-〉a的顺时针有很多一个空余,那请求很大概率大部分会落在这个空余位置上,导致a服务器请求量比例过多,均衡效果下降;哈希环倾斜本质问题就是服务器数量较少,在真实服务器数量较少情况下,可以通过增加虚拟服务器节点解决,一个真实节点映射多个虚拟节点,比如上面说的abc,可以分别增加a1a2a3,b1b2b3,c1c2c3这种方式,通过hash取余落到hash环上,后续请求定位到虚拟节点,通过虚拟节点定位到真实节点方式解决时上诉问题;实际实现中,可以使用list存储升序有序的hash 取余后的值,hashmap存储 list中值到实际节点的映射,计算了请求hash取余后的值,到list中找到第一个比它大的值,再通过hashmap定位真实节点;如果确定读多写少,还可以用平衡二叉树等结构代替list存储节点值,提升请求匹配速度,相应的就会增加节点顺序的工作
限流
- 固定窗口,有边界问题(100/秒,1秒内,后0.1秒突增100,第二秒前0.1秒突增100个)
- 滑动窗口
- 漏桶算法,桶缓冲请求,出口恒定速率出请求
- 令牌桶,桶缓存令牌,有多少个令牌可以并发处理多少个请求
美团
mysql 直接update语句更新库存有啥问题,并发情况下
redis缓存 扩容过程
算法题 给定一个单链表的头指针head和两个整数left和right,保证left小于等于right。请你将链表中从位置left到位置right的节点进行反转,并返回反转后的链表。
富途
假设定义了一张数据表,用于任务认领
CREATE TABLE audit_task (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID,作为任务id',
staff_id bigint(20) unsigned NOT NULL DEFAULT 0 COMMENT '负责处理该任务的员工staff_id,staff_id为0表示任务无人认领 ',
status int(10) unsigned NOT NULL DEFAULT 0 COMMENT '任务状态0: 待认领,1:已认领,2:审核完毕 ',
PRIMARY KEY (id)
) ENGINE=InnoDB;
某员工登录系统,认领1个任务的处理逻辑如下:
开始事务(begin);
select id from audit_task where status=0 order by id asc limit 1;
if(select到一个id)
{
update audit_task set status=1, staff_id=该员工的id where id = select到的id;
}
提交事务(commit);
问:
事务隔离级别是可重复读(Repeatable Read),上述认领任务的处理逻辑有没有问题?为什么?如何解决?
你需要设计一个文件系统,该文件系统能够创建新路径并绑定一个值。
其中每个路径均以分隔符 / 开始,每个分隔符后必须存在若干小写字母。
例如,/futu 和 /futu/moomoo 是有效路径,而空字符串和 / 则不是。
你需要实现以下方法:
- bool createPath(string path, int value):判断 path 是否能够被创建,如果能则创建并绑定对应 value,同时返回 true,否则返回 false。
- int get(string path):如果路径 path 存在,返回与 path 相关联的值,否则返回 -1。
注意:
路径不可以被重复创建以修改绑定值
创建路径的前提为前置路径均已被提前创建,例如初始路径为 /futu 可以被创建,但是/futu/moomoo 则不行,因为我们必须先创建 /futu。
输入1:
createPath("/", 0)
createPath("/a", 1)
createPath("/a/b", 2)
get("/")
get("/a")
get("/a/b")
输出1:
false
true
true
-1
1
2
输入2:
createPath("/a", 1)
createPath("/a", 2)
createPath("/a/b/c", 3)
get("/a")
get("/a/b")
输出2:
true
false
false
1
-1
pdd
python 代码执行过程
- 编译型语言(c),通过对源文件进行编译生成机器码,然后运行生成的机器码
- 解释型语言(ruby),没有编译过程,在运行过程中通过解释器逐行解释,进行运行
python个人理解上准确来说是先编译后解释的语言(先编译成字节码,然后解释器运行时jit即时编译成机器码)
- 词法分析/解析:python解释器通过对py源码进行词法分析和解析将py源码转化为ast抽象语法树(这个过程会判断是否符合python语法,不符合会抛出syntaxError异常
- 编译:python解释器会将ast语法树编译为字节码也就是pyCodeObject,如果是一些重用模块也就是被import的一些模块,还会被持久化保存为pyc文件
- 运行:python 虚拟机运行编译后的字节码,通过即时编译JIT将字节码进一步编译成机器码进行运行
python print 执行过程
涉及了io操作,因此会涉及用户态到内核态到转换
经过上面 代码执行流程 词法分析/解析->编译生成字节码—>python虚拟机运行,在解析过程中会检查print入参转是否字符串,不是则转化;在运行字节码指令生成的机器码时,因为涉及了io操作,会调用到操作系统的write函数将数据写入标准输出(一般是控制台
python 内存管理
- 内存池管理小块内存分配和释放,减少用户态和内核态切换也就是减少调用系统malloc/free函数
- 垃圾回收,主要通过引用计数回收,以及标记清除避免循环引用+分代回收等方式减少内存碎片等
- 垃圾回收算法中,主要有标记清除,分代(0,1,2代,每一代回收算法有所不同),复制(可使用一半),标记整理(整理完再删除)
拓展 golang内存管理
- 内存分配,小对象mcache分配,大对象mheap分配;小对象分配涉及size class 和span class转换,以及mcahe,mcentral到mheap逐级申请过程
- 垃圾回收,标记清除结合三色标记和混合屏障解决stop the world问题
redis实现消息队列
- list结构Lpush+BRpop,不支持可重复消费,分组消费等,没有消息确认
- zetset 不支持阻塞消费,分组/重复消费
- pubsub发布订阅,支持消息多播,但是不支持消息持久化
- 5.0 stream类型,支持分组消费,消息多播,消息持久化,消息消费确认等(pending Entre List /xack)
分布式锁
单体服务可以通过线程锁方式解决多线程并发问题,但是多机器部署情况下,每个机器内部都有自己的线程池,因此在高并发情况下,需要通过分布式锁解决并发问题
常规分布式锁实现
setnx配合lua脚本原子性操作
- 获取锁的时候通过lua脚本结合setnx+expire操作设置锁key及唯一id值,lua脚本是避免了setnx和expire两个操作过程中redis发生宕机导致锁永远没法释放,其中唯一可以是uuid,复杂点的也可以是雪花算法生成的唯一id
- 释放锁的时候通过lua脚本先获取key对应的value值,判断是否和获取锁生成的时候的一致来判断是否当下分布式锁是否因为超时/redis主从切换导致被其他线程获取,释放了其他线程的锁
当然上面的只是分布式锁的简单实现,不支持同个线程重复获取同一把锁,或者线程获取锁失败时无法重试,以及可能出现业务操作时间过长的时候导致锁出新非预期的超时释放问题,可以通过以下方式解决
可重入问题解决实现
可重入,使用hash结构记录重入次数,因为hash结构redis中不支持setnx,需要lua脚本保证原子性
可重试问题解决实现
如果需要支持获取锁方法等待时间内获取,可以通过循环+redis的发布订阅减少cpu消耗,具体做法是在锁释放的时候发布一个信息到对应channel,获取对应锁失败的时候订阅该channl,在给定时间内,当channl有对应释放信息量的时候进行重试
可续约问题解决实现
避免锁的因为线程阻塞非预期超时释放,可以增加一个看门狗线程任务,每隔1/3超时时间进行重新expire对应key的操作;在正常释放锁的时候需要取消掉相关定时任务;分布式锁+定时任务关系可以同hashmap常量存储
redis主从延迟问题
还有个问题时在redis主从集群下,分布式锁在主从切换过程可能有延迟问题,切换延迟过程中,从节点可能还没有分布式锁信息,其他线程这时可以获取成功
可以通过联锁方式解决,就是在获取锁的时候,多个实例节点都写入锁信息才算是获取成功,等于是原先的主从节点都看作独立节点,必须所有独立节点都写入锁信息,当然为了高可用,才算成功,这些实例节点也可以分别再做主从复制,也就是多主节点,在所有主节点上都写入分布式锁才算成功;这样安全性更高,但是运维成本和实现成本也比较高
java redisson分布式锁流程
- 可重入:利用hash结构代替string存储锁的value值+重入次数
- 可重试:循环加上pubsub发布订阅的方式,对于同一把锁的释放时发布一个信号量到对应channel,锁获取失败的时候订阅该channe,在给定超时时间内,当有相关信号量时进行重试获取锁操作,减少cpu消耗
- 超时续约:避免锁的因为线程阻塞非预期超时释放,可以增加一个看门狗线程任务,每隔1/3超时时间进行重新expire对应key的操作;在正常释放锁的时候需要取消掉相关定时任务;分布式锁+定时任务关系可以同hashmap常量存储
- 主从延迟锁失效:使用mulitlock 联锁机制,多个独立的redis节点,必须所有节点都获取重入锁,才算成功