简栈

拥抱AI,持续成长

粘包拆包问题是处于网络比较底层的问题,在数据链路层、网络层以及传输层都有可能发生。我们日常的网络应用开发大都在传输层进行,由于UDP有消息保护边界,不会发生粘包拆包问题,因此粘包拆包问题只发生在TCP协议中。

什么是粘包、拆包?

假设客户端向服务端连续发送了两个数据包,用packet1和packet2来表示,那么服务端收到的数据可以分为三种,现列举如下:

**第一种情况:**接收端正常收到两个数据包,即没有发生拆包和粘包的现象,此种情况不在本文的讨论范围内。

img

**第二种情况:**接收端只收到一个数据包,由于TCP是不会出现丢包的,所以这一个数据包中包含了发送端发送的两个数据包的信息,这种现象即为粘包。这种情况由于接收端不知道这两个数据包的界限,所以对于接收端来说很难处理。

img

**第三种情况:**这种情况有两种表现形式,如下图。接收端收到了两个数据包,但是这两个数据包要么是不完整的,要么就是多出来一块,这种情况即发生了拆包和粘包。这两种情况如果不加特殊处理,对于接收端同样是不好处理的。

img

img

阅读全文 »

在这之前我只记住了StringBuilder不是线程安全的,StringBuffer是线程安全的这个结论,至于StringBuilder为什么不安全从来没有去想过。

分析

在分析设个问题之前我们要知道StringBuilder和StringBuffer的内部实现跟String类一样,都是通过一个char数组存储字符串的,不同的是String类里面的char数组是final修饰的,是不可变的,而StringBuilder和StringBuffer的char数组是可变的。

首先通过一段代码去看一下多线程操作StringBuilder对象会出现什么问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class StringBuilderDemo {

public static void main(String[] args) throws InterruptedException {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < 10; i++){
new Thread(new Runnable( {
@Override
public void run() {
for (int j = 0; j < 1000; j++){
stringBuilder.append("a");
}
}
}).start();
}

Thread.sleep(100);
System.out.println(stringBuilder.length());
}

}

我们能看到这段代码创建了10个线程,每个线程循环1000次往StringBuilder对象里面append字符。正常情况下代码应该输出10000,但是实际运行会输出什么呢?

http://static.cyblogs.com/16cc76d348d29915.png

我们看到输出了“9326”,小于预期的10000,并且还抛出了一个ArrayIndexOutOfBoundsException异常(异常不是必现)。

1、为什么输出值跟预期值不一样

我们先看一下StringBuilder的两个成员变量(这两个成员变量实际上是定义在AbstractStringBuilder里面的,StringBuilder和StringBuffer都继承了AbstractStringBuilder)

阅读全文 »

概述

本方法(invokeBeanFactoryPostProcessors)会实例化和调用所有 BeanFactoryPostProcessor(包括其子类BeanDefinitionRegistryPostProcessor)。

BeanFactoryPostProcessor 接口是 Spring 初始化 BeanFactory 时对外暴露的扩展点,Spring IoC 容器允许 BeanFactoryPostProcessor 在容器实例化任何 bean 之前读取 bean 的定义,并可以修改它。

BeanDefinitionRegistryPostProcessor 继承自 BeanFactoryPostProcessor,比 BeanFactoryPostProcessor 具有更高的优先级,主要用来在常规的 BeanFactoryPostProcessor 检测开始之前注册其他 bean 定义。特别是,你可以通过 BeanDefinitionRegistryPostProcessor 来注册一些常规的 BeanFactoryPostProcessor,因为此时所有常规的 BeanFactoryPostProcessor 都还没开始被处理。

项目中的实战

BeanDefinitionRegistryPostProcessor初始化Bean的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 想根据配置文件来动态的生成我们的Bean对象
public class MultiOssScannerConfigurer implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware {
@Setter
private MultiOssProperties multiOssProperties;

@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {
String beanSuffixName = StringUtils.capitalize(OssConstants.BEAN_SUFFIX_NAME);
multiOssProperties.getClients().forEach((productCode, ossProperties) -> {
AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(OssClient.class,
() -> OssClientUtils.buildOssClient(ossProperties))
.getRawBeanDefinition();
beanDefinition.setInitMethodName("init");
beanDefinition.setDestroyMethodName("shutDown");
beanDefinitionRegistry.registerBeanDefinition(productCode + beanSuffixName, beanDefinition);
});
}
}

BeanFactoryPostProcessor初始化的时候do something

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class KeplerBeanFactoryPostInitializer implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
Map<String, AviatorFunction> aviatorFunctionMap = beanFactory.getBeansOfType(AviatorFunction.class);
if (aviatorFunctionMap.size() > 0) {
log.info("初始化自定义RuleLoader...");
aviatorFunctionMap.forEach((k, v) -> {
log.info("加载Rule:{}", k);
AviatorEvaluator.addFunction(v);
});
}
RuleLoader.initRule();
}
}

跟进源代码

阅读全文 »

本文主要是分析Spring bean的循环依赖,以及Spring的解决方式。 通过这种解决方式,我们可以应用在我们实际开发项目中。

什么是循环依赖?
怎么检测循环依赖
Spring怎么解决循环依赖
Spring对于循环依赖无法解决的场景
Spring解决循环依赖的方式我们能够学到什么?

什么是循环依赖?

循环依赖其实就是循环引用,也就是两个或则两个以上的bean互相持有对方,最终形成闭环。比如A依赖于B,B依赖于C,C又依赖于A。如下图:

http://static.cyblogs.com/20170912082357749.jpg

注意,这里不是函数的循环调用,是对象的相互依赖关系。循环调用其实就是一个死循环,除非有终结条件。

Spring中循环依赖场景有:

(1)构造器的循环依赖
(2)field属性的循环依赖。

怎么检测是否存在循环依赖

检测循环依赖相对比较容易,Bean在创建的时候可以给该Bean打标,如果递归调用回来发现正在创建中的话,即说明了循环依赖了。

Spring怎么解决循环依赖

阅读全文 »

跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。

跳跃表支持平均O(logN)、最坏O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点。在大部分情况下,跳跃表的效率可以和平衡树相媲美,并且因为跳跃表的实现比平衡树要来得更为简单,所以有不少程序都使用跳跃表来代替平衡树。

Redis使用跳跃表作为有序集合键的底层实现之一,如果一个有序集合包含的元素数量比较多,又或者有序集合中元素的成员(member)是比较长的字符串时,Redis就会使用跳跃表来作为有序集合键的底层实现。

以下是个典型的跳跃表例子

http://static.cyblogs.com/skiplist.png

从图中可以看到, 跳跃表主要由以下部分构成:

  • 表头(head):负责维护跳跃表的节点指针。
  • 跳跃表节点:保存着元素值,以及多个层。
  • 层:保存着指向其他元素的指针。高层的指针越过的元素数量大于等于低层的指针,为了提高查找的效率,程序总是从高层先开始访问,然后随着元素值范围的缩小,慢慢降低层次。
  • 表尾:全部由 NULL 组成,表示跳跃表的末尾。

因为跳跃表的定义可以在任何一本算法或数据结构的书中找到, 所以本章不介绍跳跃表的具体实现方式或者具体的算法, 而只介绍跳跃表在 Redis 的应用、核心数据结构和 API 。

跳跃表的实现

为了满足自身的功能需要, Redis 基于 William Pugh 论文中描述的跳跃表进行了以下修改:

阅读全文 »

Redis 通过 MULTI 、 DISCARD 、 EXEC 和 WATCH 四个命令来实现事务功能, 本章首先讨论使用 MULTI 、 DISCARD 和 EXEC 三个命令实现的一般事务, 然后再来讨论带有 WATCH 的事务的实现。

因为事务的安全性也非常重要, 所以本章最后通过常见的 ACID 性质对 Redis 事务的安全性进行了说明。

事务

事务提供了一种“将多个命令打包, 然后一次性、按顺序地执行”的机制, 并且事务在执行的期间不会主动中断 —— 服务器在执行完事务中的所有命令之后, 才会继续处理其他客户端的其他命令。

以下是一个事务的例子, 它先以 MULTI 开始一个事务, 然后将多个命令入队到事务中, 最后由 EXEC 命令触发事务, 一并执行事务中的所有命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
redis> MULTI
OK

redis> SET book-name "Mastering C++ in 21 days"
QUEUED

redis> GET book-name
QUEUED

redis> SADD tag "C++" "Programming" "Mastering Series"
QUEUED

redis> SMEMBERS tag
QUEUED

redis> EXEC
1) OK
2) "Mastering C++ in 21 days"
3) (integer) 3
4) 1) "Mastering Series"
2) "C++"
3) "Programming"

一个事务从开始到执行会经历以下三个阶段:

  1. 开始事务。
  2. 命令入队。
  3. 执行事务。

下文将分别介绍事务的这三个阶段。

开始事务

阅读全文 »

一、Redis-Sentinel(哨兵)

1、介绍

Redis-Sentinel是redis官方推荐的高可用性解决方案,
当用redis作master-slave的高可用时,如果master本身宕机,redis本身或者客户端都没有实现主从切换的功能。

而redis-sentinel就是一个独立运行的进程,用于监控多个master-slave集群,
自动发现master宕机,进行自动切换slave > master。

sentinel主要功能如下:

  1. 不时的监控redis是否良好运行,如果节点不可达就会对节点进行下线标识
  2. 如果被标识的是主节点,sentinel就会和其他的sentinel节点“协商”,如果其他节点也认为主节点不可达,
   就会选举一个sentinel节点来完成自动故障转移
  3. 在master-slave进行切换后,master_redis.conf、slave_redis.conf和sentinel.conf的内容都会发生改变,
   即master_redis.conf中会多一行slaveof的配置,sentinel.conf的监控目标会随之调换

2、工作原理

每个Sentinel以每秒钟一次的频率向它所知的Master,Slave以及其他 Sentinel 实例发送一个 PING 命令

如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 则这个实例会被 Sentinel 标记为主观下线。

如果一个Master被标记为主观下线,则正在监视这个Master的所有 Sentinel 要以每秒一次的频率确认Master的确进入了主观下线状态。

阅读全文 »

实现Reactor模型可分为以下三种:

  • 单线程模型
  • 单Reactor多线程模型
  • 主从Reactor多线程模型。

单线程模型

Reactor单线程模型,指的是所有的IO操作都在同一个线程上面完成,线程的职责如下:

  • 作为NIO服务端,接收客户端的TCP连接;

  • 作为NIO客户端,向服务端发起TCP连接;

  • 读取通信对端的请求或者应答消息;

  • 向通信对端发送消息请求或者应答消息。

由于Reactor模式使用的是异步非阻塞IO,所有的IO操作都不会导致阻塞,理论上一个线程可以独立处理所有IO相关的操作。从架构层面看,一个NIO线程确实可以完成其承担的职责。例如,通过Acceptor接收客户端的TCP连接请求消息,链路建立成功之后,通过Dispatch将对应的ByteBuffer派发到指定的Handler上进行消息解码。用户线程可以通过消息编码通过NIO线程将消息发送给客户端。

Server端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class Reactor1 {
public static void main(String[] args) throws IOException {
Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.socket().bind(new InetSocketAddress(1234));
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (selector.select() > 0) {
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (key.isAcceptable()) {
ServerSocketChannel acceptServerSocketChannel = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = acceptServerSocketChannel.accept();
socketChannel.configureBlocking(false);
System.out.println("accept from "+socketChannel.socket().getInetAddress().toString());
// LOGGER.info("Accept request from {}", socketChannel.getRemoteAddress());
socketChannel.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable() && key.isValid()) {
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int count = socketChannel.read(buffer);
if (count <= 0) {
socketChannel.close();
key.cancel();
System.out.println("Received invalide data, close the connection");
//LOGGER.info("Received invalide data, close the connection");
continue;
}
System.out.println("Received message"+new String(buffer.array()));
//LOGGER.info("Received message {}", new String(buffer.array()));
}
keys.remove(key);
}
}
}
}
Client
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Client1 {

public static void main(String[] args) throws IOException, InterruptedException {
SocketChannel socketChannel;
socketChannel = SocketChannel.open();
//socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("localhost", 1234));
Date now = new Date();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");//可以方便地修改日期格式
String str = dateFormat.format( now );
byte[] requst = str.getBytes();
ByteBuffer buffer = ByteBuffer.allocate(requst.length);
buffer.put(requst);
buffer.flip();
try {
while (buffer.hasRemaining()) {
socketChannel.write(buffer);
}
}catch (IOException e) {
e.printStackTrace();
}
socketChannel.close();
}
}
阅读全文 »

MySQL是现在最流行的关系型数据库(RDB)的选择,创建一个应用时,无论是用户数据还是订单数据,使用关系型数据库存储是最可靠稳定的选择,借助RDB提供的可靠性、事务等功能,为应用提供完善的支持。MySQL是开源软件,可以免费使用,MySQL在发展多年后越来越成熟,成为大部分公司的数据库首选。MySQL采用插件式的存储引擎架构,5.5版本后默认使用InnoDB存储引擎。

MySQL架构

MySQL从概念上可以分为四层:

  • 第一层是接入层: 不同语言的客户端通过mysql的协议与mysql服务器进行连接通信,接入层进行权限验证、连接池管理、线程管理等。
  • 第二层是服务层: 包括sql解析器、sql优化器、数据缓冲、缓存等。
  • 第三层是存储引擎层: mysql中存储引擎是基于表的。
  • 第四层是系统文件层: 保存数据、索引、日志等。
    http://static.cyblogs.com/mysql-arch.png

MVCC

MVCC是Multi Version Concurrency Control的简称,代表多版本并发控制。为什么需要MVCC,还要从数据库事务的ACID特性说起。
相信很多朋友都了解ACID,它们分别代表了

  • Atomicity(原子性)
  • Consistency(一致性)
  • Isolation(隔离性)
  • Durability(持久性)

原子性表示一个事务的操作结果要么全部执行要么全部不执行。

一致性表示事务总是从一个一致的状态转换到另一个一致的状态。

隔离性表示一个事务的修改结果在什么时间能够被其他事务看到,SQL1992规范中对隔离性定义了不同的隔离级别,分为读未提交(READ UNCOMMITED),事务能够看到其他事务没有提及的修改,当另一个事务又回滚了修改后的情况又被称为脏读dirty read。
读已提交(READ COMMITTED),事务能够看到其他事务提交后的修改,这时会出现一个事务内两次读取数据可能因为其他事务提交的修改导致不一致的情况,称为不可重复读。 可重复读(REPEATABLE READ),在两次读取时读取到的数据的状态是一致的,和序列化(SERIALIZABLE)可重复读中可能出现第二次读读到第一次没有读到的数据,也就是被其他事务插入的数据,这种情况称为幻读phantom read,序列化级别中不能出现幻读。
隔离级别依次增强,但是导致的问题是并发能力的减弱。各种数据库厂商会对各个隔离级别进行实现。和Java中的多线程问题相同,数据库通常使用锁来实现隔离性。
最原生的锁,锁住一个资源后会禁止其他任何线程访问同一个资源。但是很多应用的一个特点都是读多写少的场景,很多数据的读取次数远大于修改的次数,而读取数据间互相排斥显得不是很必要。所以就使用了一种读写锁的方法,读锁和读锁之间不互斥,而写锁和写锁、读锁都互斥。这样就很大提升了系统的并发能力。之后人们发现并发读还是不够,又提出了能不能让读写之间也不冲突的方法,就是读取数据时通过一种类似快照的方式将数据保存下来,这样读锁就和写锁不冲突了,不同的事务session会看到自己特定版本的数据。当然快照是一种概念模型,不同的数据库可能用不同的方式来实现这种功能。

阅读全文 »

Mongodb分片概括

  • 分片在多台服务器上分布数据的方法, Mongodb使用分片来支持具有非常大的数据集和高吞吐量的操作的部署
  • 具有大数据集和高吞吐量应用程序的数据库系统,可以挑战单台服务器的容量。
    例如,高查询率可以耗尽服务器的cpu容量,工作集大小大于系统的RAM强制磁盘驱动器的I/O容量,
  • 有两种方法来解决系统增长:垂直和水平缩放。
    • 垂直缩放 涉及增加的单个服务器的容量,例如使用更强大的CPU,加入更多的RAM,或增加的存储空间量。可用技术中的限制可能限制单个机器对于给定工作负载足够强大。此外,基于云的提供商具有基于可用硬件配置的硬上限。因此,对于垂直缩放存在实际的最大值。
    • 包括将系统数据和负载在多个服务器,添加额外的服务器,需要增加容量。虽然单个机器的总速度或容量可能不高,但是每个机器处理整个工作负载的子集,潜在地提供比单个高速大容量服务器更好的效率。扩展部署的容量仅需要根据需要添加额外的服务器,这可以是比单个机器的高端硬件低的总体成本。权衡是基础设施的复杂性和部署的维护。
  • Mongodb的支持水平扩展,分片。
分片目的

对于单台数据库服务器,庞大的数据量及高吞吐量的应用程序对它而言无疑是个巨大的挑战。频繁的CRUD操作能够耗尽服务器的CPU资源,快速的数据增长也会让硬盘存储无能为力,最终内存无法满足数据需要导致大量的I/O,主机负载严重。为了解决这种问题,对于数据库系统一般有两种方法:垂直扩展和分片(水平扩展)。

【垂直扩展】:添加更多的CPU和存储资源来增加系统性能。这种方式缺点是:拥有大量CPU和RAM资源的高端机器比普通PC机器昂贵得太多,而且单点故障会影响整个系统的服务。

【分片】:相反地,分片将大的数据集分配到多台主机上,每个分片是一个独立的数据库,这些分片整体上构成一个完整的逻辑数据库。分片减少了每台服务器上的数据操作量,随着集群的增长,每台分片处理越来越少的数据,结果,增加了系统整体服务能力。另外,分片还减少了每台服务器需要存储的数据量。

MongoDB中的分片

MongoDB通过配置分片集群来支持分片,一个分片集群包括以下几个组件:分片,查询路由,配置服务器

  • **分片:**用来存储数据,为了提供系统可用性和数据一致性,一个生产环境的分片集群,通常每个分片是一个副本集。
  • 查询路由:指客户端应用访问每个分片的路径。
  • 配置服务器:存储集群的元数据,这些数据包含了集群数据集到各分片的映射关系。查询路由就是通过这些元数据到特定的分片上执行指定的数据操作。(从v3.2开始,配置服务器也可以作为副本集,但是必须使用WiredTiger存储引擎,反对使用3个镜像实例作为配置服务器)
数据划分
阅读全文 »
0%