Star's Blog

Keep learning, Keep improving


  • 首页

  • 分类

  • 关于

  • 标签

  • 归档

  • 搜索

Java NIO面试题剖析

发表于 2019-08-07 | 分类于 网络

首先我们分别画图来看看,BIO、NIO、AIO,分别是什么?

BIO

传统的网络通讯模型,就是BIO,同步阻塞IO

它其实就是服务端创建一个ServerSocket, 然后就是客户端用一个Socket去连接服务端的那个ServerSocket, ServerSocket接收到了一个的连接请求就创建一个Socket和一个线程去跟那个Socket进行通讯。

接着客户端和服务端就进行阻塞式的通信,客户端发送一个请求,服务端Socket进行处理后返回响应。

在响应返回前,客户端那边就阻塞等待,上门事情也做不了。

这种方式的缺点:每次一个客户端接入,都需要在服务端创建一个线程来服务这个客户端

这样大量客户端来的时候,就会造成服务端的线程数量可能达到了几千甚至几万,这样就可能会造成服务端过载过高,最后崩溃死掉。

BIO模型图

Acceptor

传统的IO模型的网络服务的设计模式中有俩种比较经典的设计模式:一个是多线程, 一种是依靠线程池来进行处理。

如果是基于多线程的模式来的话,就是这样的模式,这种也是Acceptor线程模型。

NIO

NIO是一种同步非阻塞IO, 基于Reactor模型来实现的。

其实相当于就是一个线程处理大量的客户端的请求,通过一个线程轮询大量的channel,每次就获取一批有事件的channel,然后对每个请求启动一个线程处理即可。

这里的核心就是非阻塞,就那个selector一个线程就可以不停轮询channel,所有客户端请求都不会阻塞,直接就会进来,大不了就是等待一下排着队而已。

这里面优化BIO的核心就是,一个客户端并不是时时刻刻都有数据进行交互,没有必要死耗着一个线程不放,所以客户端选择了让线程歇一歇,只有客户端有相应的操作的时候才发起通知,创建一个线程来处理请求。

NIO:模型图

Reactor模型

AIO

AIO:异步非阻塞IO,基于Proactor模型实现。

每个连接发送过来的请求,都会绑定一个Buffer,然后通知操作系统去完成异步的读,这个时间你就可以去做其他的事情

等到操作系统完成读之后,就会调用你的接口,给你操作系统异步读完的数据。这个时候你就可以拿到数据进行处理,将数据往回写

在往回写的过程,同样是给操作系统一个Buffer,让操作系统去完成写,写完了来通知你。

这俩个过程都有buffer存在,数据都是通过buffer来完成读写。

这里面的主要的区别在于将数据写入的缓冲区后,就不去管它,剩下的去交给操作系统去完成。

操作系统写回数据也是一样,写到Buffer里面,写完后通知客户端来进行读取数据。

AIO:模型图

阅读全文 »

用户表分库分表方案

发表于 2019-07-21 | 分类于 系统架构

再次抛出笔者的观点,在能满足业务场景的情况下,单表>分区>单库分表>分库分表,推荐优先级从左到右逐渐降低。

本篇文章主要讲用户表(或者类似这种业务属性的表)的分表方案,至于订单表,流水表等,本文的方案可能不是很合适,可以参考另一篇文章《分库分表技术演进&最佳实践-修订篇》。

我们首先来看一下分表时主要需要做的事情:

  1. 选定分片键:既然是用户表那分片键非用户ID莫属;
  2. 修改代码:以sharding-jdbc这种client模式的中间件为例,主要是引入依赖,然后新增一些配置。业务代码并不怎么需要改动。
  3. 存量数据迁移;
  4. 业务发展超过容量评估后需要开发和运维介入扩容;

做过分库分表的都知道,第3步最麻烦,而且非常不好验证迁前后数据一致性(目前业界主流的迁移方案是存量数据迁移+利用binlog进行增量数据同步,待两边的数据持平后,将业务代码中的开关切到分表模式)。

第4步同样麻烦,业务增长完全超过当初分表设计的容量评估是很常见的事情,这也成为业务高速发展的一个隐患。而且互联网类型的业务都希望能做到7x24小时不停服务,这样就给扩容带来了更大的挑战。笔者看过比较好的方案就是58沈剑提出的成倍扩容方案。如下图所示,假设现在已经有2张表:tb_user_1,tb_user_2。且有两个库是主备关系,并且分表算法是hash(user_id)%2:

现在要扩容到4张表,做法是将两个库的主从关系切断。然后slave晋升为master,这样就有两个主库:master-1,master-2。新的分表算法是:

  • 库选择算法为:hash(user_id)%4的结果为1或者2,就选master-1库,hash(user_id)%4的结果为3或者0,就选master-2库;
  • 表的选择算法为:hash(user_id)%2的结果为1则选tb_user_1表,hash(user_id)%2的结果为0则选tb_user_2表。

如此以来,两个库中总计4张表,都冗余了1倍的数据:master-1中tb_user_1冗余了3、7、11…,master-1中tb_user_2冗余了4、8、12…,master-2中tb_user_1冗余了1、5、9…,master-2中tb_user_2冗余了2、6、10…。将这些冗余数据删掉后,库、表、数据示意图如下所示:

即使这样方案,还是避免不了分表时的存量数据迁移,以及分表后业务发展到一定时期后的繁琐扩容。那么有没有一种很好的方案,能够一劳永逸,分表时不需要存量数据迁移,用户量无论如何增长,扩容时都不需要迁移存量数据,只需要新增一个数据库示例,修改一下配置即可。软件开发行业,一个方案能撑过3~5年就是一个很优秀的方案,我们现在YY的是整个生命周期内都不用改动的完美的方案。没错,我们在寻找银弹。

这个方案笔者在两个地方都接触到了:

  1. 某V厂面试时,部门老大提出的方案;
  2. 和美团大牛普架讨论了解到的CAT存储方案;

说明:CAT是美团点评开源的APM,目前在Github上的star已经破万(Github地址:https://github.com/dianping/cat),比skywalking和pinpoint还快,如果你正在选型APM,而且能接受代码侵入,那么CAT是一个不错的选择。

CAT存储方案是按照写入时间顺序存储,假设每小时写入量是千万级别,那么分表就按照小时维度。也就是说,2019年7月18号10点数据写入到表tb_catdata_2019071810中,2019年7月18号12点数据写入到表tb_catdata_2019071812中,2019年7月20号14点数据写入到表tb_catdata_2019072014中。这样做的优点如下:

  1. 历史数据不用迁移;
  2. 扩容非常简单;

缺点如下:

  1. 读写热点集中,所有写操作全部打在最新的表上。

有没有发现,这个方案的优点就是我们需要的。BINGO,要的就是这样的方案。那么对应到用户表上来具体的分表方案非常类似:按照range切分。需要说明的是,这个方案的前提是用户ID一定要趋势递增,最好严格递增。笔者给出3种用户ID递增的方案:

  • 自增ID

假设存量数据用户表的id最大值是960W,那么分表算法是这样的,表序号只需要根据user_id/10000000就能得到:

  1. 用户ID在范围[1, 10000000)中分到tb_user_0中(需要将tb_user重命名为tb_user_0);
  2. 用户ID在范围[10000000, 20000000)中分到tb_user_1中;
  3. 用户ID在范围[20000000, 30000000)中分到tb_user_2中;
  4. 用户ID在范围[30000000, 40000000)中分到tb_user_3中;
  5. 以此类推。

如果你的tb_user本来就有自增主键,那这种方案就比较好。但是需要注意几点,由于用户ID是自增的,所以这个ID不能通过HTTP暴露出去,否则可以通过新注册一个用户后,就能得到你的真实用户数,这是比较危险的。其次,存量数据在单表中可以通过自增ID生成,但是当切换分表后,用户ID如果还是用自增生成,需要注意在创建新表时设置AUTO_INCREMENT,例如创建表tb_user_2时,设置AUTO_INCREMENT=10000000,DDL如下:

1
2
3
4
5
6
7
8
CREATE TABLE if not exists `tb_user_2` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY,
`username` varchar(16) NOT NULL COMMENT '用户名',
`remark` varchar(16) NOT NULL COMMENT '备注'
) ENGINE=InnoDB AUTO_INCREMENT=10000000;

- 这样的话,当新增用户时,用户ID就会从10000000开始,而不会与之前的用户ID冲突
insert into tb_user_2 values(null, 'afei', 'afei');
阅读全文 »

分布式锁:Redis、Zookeeper

发表于 2019-07-21 | 分类于 中间件

为什么用分布式锁?

在讨论这个问题之前,我们先来看一个业务场景:

系统A是一个电商系统,目前是一台机器部署,系统中有一个用户下订单的接口,但是用户下订单之前一定要去检查一下库存,确保库存足够了才会给用户下单。

由于系统有一定的并发,所以会预先将商品的库存保存在redis中,用户下单的时候会更新redis的库存。

此时系统架构如下:

但是这样一来会产生一个问题:假如某个时刻,redis里面的某个商品库存为1,此时两个请求同时到来,其中一个请求执行到上图的第3步,更新数据库的库存为0,但是第4步还没有执行。

而另外一个请求执行到了第2步,发现库存还是1,就继续执行第3步。

这样的结果,是导致卖出了2个商品,然而其实库存只有1个。

很明显不对啊!这就是典型的库存超卖问题

此时,我们很容易想到解决方案:用锁把2、3、4步锁住,让他们执行完之后,另一个线程才能进来执行第2步。

按照上面的图,在执行第2步时,使用Java提供的synchronized或者ReentrantLock来锁住,然后在第4步执行完之后才释放锁。

这样一来,2、3、4 这3个步骤就被“锁”住了,多个线程之间只能串行化执行。

但是好景不长,整个系统的并发飙升,一台机器扛不住了。现在要增加一台机器,如下图:

增加机器之后,系统变成上图所示,我的天!

假设此时两个用户的请求同时到来,但是落在了不同的机器上,那么这两个请求是可以同时执行了,还是会出现库存超卖的问题。

为什么呢?因为上图中的两个A系统,运行在两个不同的JVM里面,他们加的锁只对属于自己JVM里面的线程有效,对于其他JVM的线程是无效的。

因此,这里的问题是:Java提供的原生锁机制在多机部署场景下失效了

这是因为两台机器加的锁不是同一个锁(两个锁在不同的JVM里面)。

那么,我们只要保证两台机器加的锁是同一个锁,问题不就解决了吗?

此时,就该分布式锁隆重登场了,分布式锁的思路是:

在整个系统提供一个全局、唯一的获取锁的“东西”,然后每个系统在需要加锁时,都去问这个“东西”拿到一把锁,这样不同的系统拿到的就可以认为是同一把锁。

至于这个“东西”,可以是Redis、Zookeeper,也可以是数据库。

文字描述不太直观,我们来看下图:

通过上面的分析,我们知道了库存超卖场景在分布式部署系统的情况下使用Java原生的锁机制无法保证线程安全,所以我们需要用到分布式锁的方案。

那么,如何实现分布式锁呢?接着往下看!

阅读全文 »

大型网站的页面静态化

发表于 2019-07-20 | 分类于 系统架构

前言

我们小伙伴们在访问淘宝、网易等大型网站时有没有考虑到,网站首页、商品详情页以及新闻详情页面是如何处理的?怎么能够支撑这么大流量的访问呢?

很多小伙伴们就会提出他们都采用了静态化的方案,这样用户请求直接获取静态数据html,就不需要访问数据库了,性能就会大大提高;而且提高网站SEO优化。

那今天就带着大家聊一下静态化。把之前工作场景中静态化方案遇到的问题,以及如何演变的,分享给小伙伴。

方案一:网页静态HTML化

这个方案是最早使用的方案,我们就拿CMS系统举例,类似网易的新闻网站

核心流程图:

上图的核心思想:

1)管理后台调用新闻服务创建文章成功后,发送消息到消息队列

2)静态服务监听消息,把文章静态化,也就是生成html文件

3)在静态服务器上面安装一个文件同步工具,此工具的功能可以做到只同步有变动的文件,即做增量同步(老顾用久没用了,忘了工具的名称)

4)通过同步工具把html文件同步到所有的web服务器上面

这样的话就达到了,用户访问一些变化不大的页面时,是直接访问的html文件,直接在web服务器那边直接返回,不需要在访问数据库了,系统吞吐量比较高。

这个方案的问题:

1、网页布局样式僵化,无法修改

如果产品经理觉得新闻详情页面的布局要调整一下,现在的不够美观,或者加个其他模块,那就坑爹了,我们需要把所有的已经静态html化的文章全部重新静态化。这个是不现实的,因为像网易这么大的体量,新闻量是很大的,会被搞死。

2、页面会出现暂时间不一致

会出现用户刚刚再看最新的新闻,刷新一下又不存在了。这个是因为同步工具在同步到web服务器是要有时间的,同步到web服务器A上面了,但web服务器B还没有来得及同步。用户在访问的时候通过nginx进行负载均衡,随机把请求分配给web服务器的导致的。当然可以调整nginx负载均衡策略去解决。

3、Html文件太多,无法维护

这个是很明显的问题,html文件会越来越多,对存储空间要求很大,而且每台web服务器都一样,浪费磁盘空间;将来迁移维护也会带来很大的麻烦。

4、同步工具的不稳定

因为文件一旦多之后,同步工具稳定性就出现了问题

这个方案应该是比较传统的(不推荐)

方案二:伪静态化

什么是伪静态?

举个例子:我们一般访问一个文章,一般的链接地址为:http://www.xxx.com/news?id=1代表请求id为1的文章。**不过这种链接方式对SEO不是太友好(SEO对网站来说太重要了)**;所以一般进行改造:http://www.xxx.com/news/1.html 这样看上去就是个静态页面。一般我们可以采用nginx对url进行rewrite。小伙伴如何有兴趣可以自行了解,比较简单。

之所以是伪静态其实也是需要动态处理的。

针对方案一上面问题,方案进一步的演化,如下图

此方案的核心思想

1)管理后台调用新闻服务创建文章成功后,发送消息到消息队列

2)缓存服务监听消息,把文章内容缓存到缓存服务器上面

3)用户发起请求,web服务器根据id,直接查询缓存服务器

4)获取数据返回给用户

此方案就解决了方案一的一个大问题,就是html文件多的问题,因为不需要生成html,而且用缓存的方式,解决不需要访问数据库,提升系统吞吐量。

不过此方案的问题:

1、网页布局样式维护成本比较高,因为此方案照样是把所有的内容放到了缓存中,如果需要修改布局,需要重新设置缓存。

2、分布式缓存压力比较大,一旦缓存故障就导致所有请求会查询数据库,导致系统崩溃

还有个小问题,就是实时数据处理,就是页面中如价格,库存需要到后台读取的。当然小伙伴也许就会说,也可以处理啊,用户把商品内容请求到后,然后在用浏览器发送异步的ajax请求获得商品数量就好了啊。这样就是无形的增加了一次请求。(此问题可以忽略)

此方案类似很多公司都在使用,如:同程旅游等

阅读全文 »

消息中间件的面试四连炮

发表于 2019-07-20 | 分类于 中间件

概述

大家平时也有用到一些消息中间件(MQ),但是对其理解可能仅停留在会使用API能实现生产消息、消费消息就完事了。

对MQ更加深入的问题,可能很多人没怎么思考过。

比如,你跳槽面试时,如果面试官看到你简历上写了,熟练掌握消息中间件,那么很可能给你发起如下 4 个面试连环炮!

  • 为什么要使用MQ?
  • 使用了MQ之后有什么优缺点?
  • 怎么保证MQ消息不丢失?
  • 怎么保证MQ的高可用性?

本文将通过一些场景,配合着通俗易懂的语言和多张手绘彩图,讨论一下这些问题。

为什么要使用MQ?

相信大家也听过这样的一句话:好的架构不是设计出来的,是演进出来的。

这句话在引入MQ的场景同样适用,使用MQ必定有其道理,是用来解决实际问题的。而不是看见别人用了,我也用着玩儿一下。

其实使用MQ的场景有挺多的,但是比较核心的有3个:

异步、解耦、削峰填谷

异步

我们通过实际案例说明:假设A系统接收一个请求,需要在自己本地写库执行SQL,然后需要调用BCD三个系统的接口。

假设自己本地写库要3ms,调用BCD三个系统分别要300ms、450ms、200ms。

那么最终请求总延时是3 + 300 + 450 + 200 = 953ms,接近1s,可能用户会感觉太慢了。

此时整个系统大概是这样的:

但是一旦使用了MQ之后,系统A只需要发送3条消息到MQ中的3个消息队列,然后就返回给用户了。

假设发送消息到MQ中耗时20ms,那么用户感知到这个接口的耗时仅仅是20 + 3 = 23ms,用户几乎无感知,倍儿爽!

此时整个系统结构大概是这样的:

可以看到,通过MQ的异步功能,可以大大提高接口的性能。

解耦

假设A系统在用户发生某个操作的时候,需要把用户提交的数据同时推送到B、C两个系统的时候。

这个时候负责A系统的哥们想:没事啊,B、C两个系统给我提供一个Http接口或者RPC接口,我把数据推送过去不就完事了吗。负责A系统的哥们美滋滋。

如下图所示:

一切看起来很美好,但是随着业务快速迭代,这个时候系统D也想要这个数据。那既然这样,A系统的开发同学就改咯,在发送数据给BC的同时加上一个D。

但是,越到后面越发现,麻烦来了。。。

整个系统好像不止这个数据要发送给BCD、还有第二、第三个数据要发送给BCD。甚至有时候又加入了E、F等等系统,他们也要这个数据。

并且有时候可能B系统突然又不要这个数据了,A系统该来改去,A系统的开发哥们头皮发麻。

更复杂的场景是,数据通过接口传给其他系统有时候还要考虑重试、超时等一些异常情况,真是头发都白了呀。。。

来看下图,体会一下这无助的现场:

这个时候,就该我们的MQ粉墨登场了!

这种情况下使用MQ来解耦是在合适不过了,因为负责A系统的哥们只需要把消息扔到MQ就行了,其他系统按需来订阅消息就好了。

就算某个系统不需要这个数据了,也不会需要A系统改动代码。

看看加入MQ解耦的下图,是不是清爽了很多!

削峰填谷

举个例子,比如我们的订单系统,在下单的时候就会往数据库写数据。但是数据库只能支撑每秒1000左右的并发写入,并发量再高就容易宕机。

低峰期的时候并发也就100多个,但是在高峰期时候,并发量会突然激增到5000以上,这个时候数据库肯定死了。

如下图,来感受一下数据库被打死的绝望:

阅读全文 »
1…456…20
Morning Star

Morning Star

100 日志
14 分类
37 标签
GitHub
© 2021 Morning Star
由 Hexo 强力驱动
|
主题 — NexT.Pisces v5.1.4