数据库平滑扩容方案
[toc]
每一个项目都是由小项目发展而来,从最初的一台数据库,到后面的几千上万台数据库,这发展的过程,都要涉及到一个技术问题:当数据量太大的时候,如何进行扩容?
现有一个站点,用户数据库有2个,网站用户数据通过ID取模,分别存在两台用户数据库中,随着数据增大,两台数据库已经不够用了,现在需要增加数据库进行扩容,应该如何进行扩容?
[toc]
每一个项目都是由小项目发展而来,从最初的一台数据库,到后面的几千上万台数据库,这发展的过程,都要涉及到一个技术问题:当数据量太大的时候,如何进行扩容?
现有一个站点,用户数据库有2个,网站用户数据通过ID取模,分别存在两台用户数据库中,随着数据增大,两台数据库已经不够用了,现在需要增加数据库进行扩容,应该如何进行扩容?
[toc]
缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,造成缓存穿透。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。
解决方案:
有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层数据库的查询压力。
另外也有一个更为简单粗暴的方法,如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),仍然把这个空结果进行缓存,但它的过期时间会很短,比如30s。
[toc]
TCP连接的断开有两种方式:
java中,调用Socket#close()可以关闭Socket,该方法类似Unix网络编程中的close方法,将Socket的 读写 都关闭,已经排队等待发送的数据会被尝试发送,最后(默认)发送FIN。考虑一个典型的网络事务,A向B发送数据,A发送完毕后close(),FIN发送出去;B一直read直到返回了-1,也通过close()发送FIN,4次挥手,连接关闭,一切都很和谐。
那什么时候会用RST而非FIN关闭连接呢?
Socket#setSoLinger(true,0),则close时会发送RST;close()方法关闭了Socket,虽然TCP规定半关闭状态下B仍然可以接收数据,但close动作关闭了该socket上的任何数据操作,如果此时A继续write,B将返回RST,A的该次write无法立即通知应用层(因为write仅把数据写入发送缓冲区),只会把状态保存在tcp协议栈内,下次write时才会抛出SocketException。tcp是以流动的方式传输数据,传输的最小单位为一个报文段(segment)。
tcp Header中有个Options标识位,常见的标识为mss(Maximum Segment Size最大消息长度) 指的是,连接层每次传输的数据有个最大限制MTU(Maximum Transmission Unit),一般是1500比特,超过这个量要分成多个报文段,mss则是这个最大限制减去TCP的header,光是要传输的数据的大小,一般为1460比特。换算成字节,也就是180多字节。
MSS = MTU - header
tcp为提高性能,发送端会将需要发送的数据发送到缓冲区,等待缓冲区满了之后,再将缓冲中的数据发送到接收方。同理,接收方也有缓冲区这样的机制,来接收数据。
发生TCP粘包、拆包主要是由于下面一些原因:
应用程序写入的数据大于套接字缓冲区大小,这将会发生拆包。
应用程序写入数据小于套接字缓冲区大小,网卡将应用多次写入的数据发送到网络上,这将会发生粘包。
进行mss(最大报文长度)大小的TCP分段,当TCP报文长度-TCP头部长度>mss的时候将发生拆包。
接收方法不及时读取套接字缓冲区数据,这将发生粘包。
……
既然知道了tcp是无界的数据流,且协议本身无法避免粘包,拆包的发生,那我们只能在应用层数据协议上,加以控制。通常在制定传输数据时,可以使用如下方法:
1、使用带消息头的协议、消息头存储消息开始标识及消息长度信息,服务端获取消息头的时候解析出消息长度,然后向后读取该长度的内容。
2、设置定长消息,服务端每次读取既定长度的内容作为一条完整消息。
3、设置消息边界,服务端从网络流中按消息编辑分离出消息内容。
=====================================================================
问题:
TCP是以段为单位进行数据包的发送的。
(1)在建立TCP连接的同时,也可以确定发送数据包的单位,称之为“最大消息长度”:MSS。最理想的情况是,最大消息长度MSS正好是IP层中不被分片处理的最大数据长度。
(2)TCP在传送大量数据的时候,是以“段=MSS的大小”将数据进行分割发送的,进行重发时也是以MSS为单位的。
(3)最大消息长度——MSS是在三次握手的时候,在两端主机之间被计算得出的。两端主机在发出“建立TCP连接请求的SYN包”时,会在SYN包的TCP首部中写入MSS选项,告诉对方自己所能够适应的MSS的大小,然后发送端主机会在两者之间选择一个较小的MSS值投入使用。
TCP为什么引入接受缓存这个数据结构?
如果没有接受缓存的话,或者说只有一个缓存的话,为了保证接受的数据是按顺序传输的,所以如果位于x序号之后的序号分组先到达目的主机的运输层的话必然丢弃,这样的话将在重传上花费很大的开销,所以一般如果有过大的序号达到接收端,那么会按照序号缓存起来等待之前的序号分许到达,然后一并交付到应用进程。
TCP 粘包/拆包的原因及解决方法
TCP是以流的方式来处理数据,一个完整的包可能会被TCP拆分成多个包进行发送,也可能把小的封装成一个大的数据包发送。
TCP粘包/分包的原因:
应用程序写入的字节大小大于套接字发送缓冲区的大小,会发生拆包现象,而应用程序写入数据小于套接字缓冲区大小,网卡将应用多次写入的数据发送到网络上,这将会发生粘包现象;
进行MSS大小的TCP分段,当TCP报文长度-TCP头部长度>MSS的时候将发生拆包
以太网帧的payload(净荷)大于MTU(1500字节)进行ip分片。
解决方法
消息定长:FixedLengthFrameDecoder类
包尾增加特殊字符分割:行分隔符类:LineBasedFrameDecoder或自定义分隔符类 :DelimiterBasedFrameDecoder
将消息分为消息头和消息体:LengthFieldBasedFrameDecoder类。分为有头部的拆包与粘包、长度字段在前且有头部的拆包与粘包、多扩展头部的拆包与粘包。
[toc]
TCP是可靠传输,可靠的核心是收到包后回复一个ack来告诉对方收到了。但如果只是单纯的发送一个确认,代价会比较高(20字节的ip首部,20字节的tcp首部),最好能附带响应数据一起发送给对方。
Delayed Ack是指收到包后不立即ack,而是等一小会(比如40毫秒)看看,如果这40毫秒以内正好有一个包发给client,那么这个ack包就跟着发过去,而不需要增加任何大小,这样节省了资源。当然如果超过这个时间还没有包发给client,那么这个ack也要发给client了(要不client以为丢包了,又要重发)。
假如这个时候ack包还在等待延迟发送的时候,又收到了client的一个包,那么这个时候server有两个ack包要回复,那么os会把这两个ack包合起来立即回复一个ack包给client,告诉client前两个包都收到了。
也就是Delayed Ack开启的情况下:ack包有顺风车就搭;如果凑两个ack包自己包个车也立即发车;再如果等了40毫秒以上也没顺风车,那么自己打个车也发车。
[toc]
随着业务规模的不断扩大,需要选择合适的方案去应对数据规模的增长,以应对逐渐增长的访问压力和数据量。
关于数据库的扩展主要包括:业务拆分、主从复制,数据库分库与分表。这篇文章主要讲述数据库分库与分表。
(1)业务拆分
业务起步初始,为了加快应用上线和快速迭代,很多应用都采用集中式的架构。随着业务系统的扩大,系统变得越来越复杂,越来越难以维护,开发效率变得越来越低,并且对资源的消耗也变得越来越大,通过硬件提高系统性能的方式带来的成本也越来越高。
因此,在选型初期,一个优良的架构设计是后期系统进行扩展的重要保障。例如:电商平台,包含了用户、商品、评价、订单等几大模块,最简单的做法就是在一个数据库中分别创建users、shops、comment、order四张表。
但是,随着业务规模的增大,访问量的增大,我们不得不对业务进行拆分。每一个模块都使用单独的数据库来进行存储,不同的业务访问不同的数据库,将原本对一个数据库的依赖拆分为对4个数据库的依赖,这样的话就变成了4个数据库同时承担压力,系统的吞吐量自然就提高了。
(2)主从复制
MySQL主从复制的原理:数据复制的实际就是Slave从Master获取Binary log文件,然后再本地镜像的执行日志中记录的操作。由于主从复制的过程是异步的,因此Slave和Master之间的数据有可能存在延迟的现象,此时只能保证数据最终的一致性。
(3)数据库分库与分表
每台机器都有自身的物理上限,所以当应用已经能触及或远远超出单台机器的某个上限的时候,惟有寻找别的机器的帮助或者继续升级硬件,但常见的方案还是通过添加更多的机器来共同承担压力。
还得考虑当业务逻辑不断增长,机器能不能通过线性增长就能满足需求?因此,使用数据库的分库分表,能够立竿见影的提升系统的性能。
问题描述
JAVA的client和server,使用socket通信。server使用NIO。