Redis的I/O多路复用
在Redis6.0之前,它的网络I/O模型是单线程的,那么它是怎么处理多个客户端的连接的呢?这里就涉及到Redis的I/O模型,基于多路复用的高性能 I/O 模型。
我个人对它的理解如下:
它的模型需要用到操作系统的epoll机制,也就是说需要内核来监控Redis建立的多个Socket,这也就是为什么Windows操作系统发挥不了Redis的最大性能,因为它只支持Select模式,不支持epoll模式。
Redis的一个线程只需要来处理客户端的请求,为它建立一个Socket。epoll机制允许内核一次监听多个socket,每当这些socket变得可读或者可写,就通知Redis的线程,然后Redis的线程会为对应的Socket注册对应的事件,之后将他们放入事件处理队列。这是事件在出队后,会调用对应事件绑定的函数,来处理对应的事件。
比如说,现在有两个客户端,这两个客户端都发起了请求,分别对应accept事件和read事件。Redis 分别对这两个事件注册 accept 和 get 回调函数。当 Linux 内核监听到有连接请求或读数据请求时,就会触发 Ac ...
如何提高服务调用速度
假如电商系统的 QPS 已经达到了每秒 2 万次,在做了服务化拆分之后,由于我们把业务逻辑都拆分到了单独部署的服务中,那么假设你在完成一次完整的请求时需要调用 4~5 次服务,计算下来,RPC 服务需要承载大概每秒 10 万次的请求。
所以RPC框架的选型要:
1、选择合适的网络模型,有针对性地调整网络参数优化网络传输性能;
2、选择合适的序列化方式,以提升封包、解包的性能。
什么是RPCRPC指的是通过网络调用另一台计算机上部署服务的技术。而 RPC 框架就封装了网络调用的细节,让你像调用本地服务一样调用远程部署的服务。
在引入RPC框架后会出现的问题假设一个电商系统的商品详情页面需要商品数据、评论数据还有店铺数据。如果是一体化的项目,只需要从商品库、评论库和店铺库获取数据就可以了,不考虑缓存的情况下有三次网络请求。
但是如果独立出商品服务、评论服务和店铺服务之后,那么就需要分别调用这三个服务,而这三个服务又会分别调用各自的数据库,这就是六次网络请求。
想要优化RPC调用,那么就要了解RPC调用的步骤都有哪些:
客户端首先会将调用的类名、方法名、参数名、参数值等信息,序列化成二进 ...
路由器结构
路由器结构一个路由器的体系结构如下图:
输入端口作用:
1、接收数据链路层的数据。
2、查询转发表,决定数据的输出端口。到达的分组通过路由器的交换结构转发到输出端口。
交换结构:交换结构将路由器的输入端口连接到它的输岀端口。这种交换结构完全包含在路由器之中,即它是一个网络路由器中的网络!
输出端口:输出端口存储从交换结构接收的分组,并通过执行必要的链路层和物理层功能在输出链路上传输这些分组。
路由选择处理器:路由选择处理器执行控制平面功能。
在传统的路由器中,它执行路由选择协议,维护路由选择表与关联链路状态信息,并为该路由器计算转发表。
在SDN路由器中,路由选择处理器(在其他活动中)负责与远程控制器通信,目的是接收由远程控制器计算的转发表项,并在该路由器的输入端口安装这些表项。路由选择处理器还执行网络管理功能。
以上仅仅是每个部分大致功能的描述,后续会有更为详细的介绍。
参考《计算机网络自顶向下》
微服务后系统要如何改造
微服务化后,一个项目的架构图如下所示:
微服务拆分的原则单体化的项目就像一个大的蜘蛛网,不同模块交织在一起,调用比较复杂,一个问题出bug可能会导致连锁的问题。所以要对架构进行拆分。而进行拆分则需要遵循以下原则:
1、做到单一服务内部功能的高内聚和低耦合也就是说每个服务只完成自己职责之内的任务,对于不是自己职责的功能交给其它服务来完成。
2、需要关注服务拆分的粒度,先粗略拆分再逐渐细化拆分初期可以把服务粒度拆得粗一些,后面随着团队对于业务和微服务理解的加深,再考虑把服务粒度细化。
3、拆分的过程,要尽量避免影响产品的日常功能迭代要一边做产品功能迭代,一边完成服务化拆分。拆分只能在现有一体化系统的基础上不断剥离业务独立部署,剥离的顺序你可以参考以下几点:
优先剥离比较独立的边界服务(比如短信服务、地理位置服务),从非核心的服务出发减少拆分对现有业务的影响
当两个服务存在依赖关系时优先拆分被依赖的服务。比如内容服务依赖于用户服务获取用户的基本信息,那么如果先把内容服务拆分出来,内容服务就会依赖于一体化架构中的用户模块,这样还是无法保证内容服务的快速部署能力。
4、服务接口的定 ...
网络层概述
转发和路由选择:数据平面和控制平面转发:当一个分组到达某路由器的一条输入链路时,该路由器必须将该分组移动到适当的输出链路。如果分组来自一个已知的恶意注意,那么它会被路由器阻挡。
路由选择:当分组从发送方流向接收方时,网络层必须决定这些分组所采用的路由或路径。计算这些路径的算法被称为路由选择算法。
每台路由器中都有一个转发表。路由器检査到达分组首部的一个或多个字段值,这些值对应存储在转发表项中的值,指出了该分组将被转发的路由器的输出链路接口。
说白了,就是通过这个路由表,找到应该将数据发送到哪一个出口。
网络服务模型网络层能提供的某些可能的服务:
确保交付。该服务确保分组将最终到达目的地。
具有时延上界的确保交付。该服务不仅确保分组的交付,而且在特定的主机到主机时延上界内(例如在100ms内)交付。
有序分组交付。该服务确保分组以它们发送的顺序到达目的地。
确保最小带宽。这种网络层服务模仿在发送和接收主机之间一条特定比特率(例如1 Mbps)的传输链路的行为。只要发送主机以低于特定比特率的速率传输比特(作为分组的组成部分),则所有分组最终会交付到目的主机。
安全性。网络层能够在源加 ...
如何实现一个完备的缓存
读写锁1、允许多个线程同时读共享变量;
2、只允许一个线程写共享变量;
3、如果一个写线程正在执行写操作,此时禁止读线程读共享变量。
实现一个缓存我们声明一个Cache类,然后里面用HashMap来实现存储,但是它并不是线程安全的,我们采用ReadWriteLock来保证线程安全。
12345678910111213141516171819202122232425262728class Cache<K,V> { final Map<K, V> m = new HashMap<>(); final ReadWriteLock rwl = new ReentrantReadWriteLock(); // 读锁 final Lock r = rwl.readLock(); // 写锁 final Lock w = rwl.writeLock(); // 读缓存 V get(K key) { r.lock(); try { ret ...
数据该如何迁移
数据迁移需要满足以下几点:
迁移的过程中,要保证新数据可以写入
迁移后,新旧数据库数据要一致
迁移过程中可以回滚
下面给出几个具体的迁移方案。
“双写”方案1、将新的库配置为源库的从库用来同步数据
2、改造业务代码,在数据写入的时候不仅要写入旧库也要写入新库。同时要保证在写入新库失败的数据被单独记录,以便后续添加。
3、校验数据了,这里只抽取部分数据。
4、将流量切换到新库,最好采用灰度的方式,即先切10%的流量过去,然后50%,慢慢加到100。
5、如果有问题,要将流量切回之前的库。
6、如果没有问题,将双写改为只写新库。
级联同步方案1、先将新库配置为旧库的从库,用作数据同步
2、再将一个备库配置为新库的从库,用作数据的备份
3、等到三个库的写入一致后,将数据库的读流量切换到新库
4、然后暂停应用的写入,将业务的写入流量切换到新库
但是这里的一个缺点是,切换时要暂停应用的使用,所以要选择低峰期来执行。
数据迁移时如何预热缓存上述的两种方案也可以在迁移缓存时使用,但是需要注意,直接在新的服务上加一个空的缓存,有可能会导致数据库宕机,所以,缓存迁移的重点是保持缓存的热度。 ...
如何实现一个限流器
信号量模型信号量模型可以简单概括为:一个计数器,一个等待队列,三个方法。
在信号量模型里,计数器和等待队列对外是透明的,所以只能通过信号量模型提供的三个方法来访问它们,这三个方法分别是:init()、down() 和 up()。
init():设置计数器的初始值。
down():计数器的值减 1;如果此时计数器的值小于 0,则当前线程将被阻塞,否则当前线程可以继续执行。
up():计数器的值加 1;如果此时计数器的值小于或者等于 0,则唤醒等待队列中的一个线程,并将其从等待队列中移除。
这里的 init()、down() 和 up() 三个方法都是原子性的,并且这个原子性是由信号量模型的实现方保证的。
123456789101112131415161718192021222324class Semaphore { // 计数器 int count; // 等待队列 Queue queue; // 初始化操作 Semaphore(int c) { this.count = c; } void down() { ...
Lock和Condition
Java当中,Lock和Condition一起使用的时候,可以实现多条件的并发,一个最简单的例子就是阻塞队列,它的实现需要两个条件变量,一个是队列不为空,另一个是队列不能满。
一个简单的例子如下:
12345678910111213141516171819202122232425262728293031323334353637public class BlockedQueue<T>{ final Lock lock = new ReentrantLock(); // 条件变量:队列不满 final Condition notFull = lock.newCondition(); // 条件变量:队列不空 final Condition notEmpty = lock.newCondition(); // 入队 void enq(T x) { lock.lock(); try { while (队列已满) { // 等待队列不满,这里就是阻塞线程 notFull.awai ...
如何保证消息仅被消费一次
首先,如果要确保每个消息只被消费一次,那么就要确保每一个消息都正常到达了消费端,即不能出现消息丢失。
以下三个地方会造成消息丢失:
1、消息从生产者写入到消息队列的过程;
2、消息在消息队列中的存储场景;
3、消息被消费者消费的过程。
1、在消息生产的过程中丢失消息生产者一般是独立部署的应用程序,而消息队列一般也是独立部署。那么生产者的消息发往消息队列需要通过网络,这就有可能丢失。这里的一个比较好的解决办法就是重试,即重新发送消息。
但是重试则会导致消息重复,比如第一条消息因为在网络中拥堵,导致超过时延,生产设判断消息丢失,重新发送消息。但是过段时间重新发送的消息和之前的消息都到达了消息队列,那么这条消息就重复了。
2、在消息队列中丢失消息拿Kafka来说,消息一般是存储在本地磁盘,而为了减少刷盘次数,消息会先写入Page Cache(操作系统提供的缓存)中,然后找合适时间刷盘。
这样设计,好处在于减少I/O,但是如果在未刷盘时,服务器掉电,就会导致在Page Cache中的数据丢失。
这里的解决办法有,调整刷盘时机,即过一段时间,或者一定量消息强行刷盘,但是会影响性能。另一 ...