0%

分布式锁

在分布式环境中,为了保证业务数据的正常访问,防止出现重复请求的问题,会使用分布式锁来阻拦后续请求。具体伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
public void doSomething(String userId){
User user=getUser(userId);
if(user==null){
user.setUserName("xxxxx");
user.setUserId(userId);
insert(user);
return;
}
update(user);
}

上面的代码很简单,查询db中有没有对应的user数据,如果有的话,执行更新操作,如果没有则插入。

我们知道,上面的代码是线程不安全的,在多线程的环境中,就会出现问题。为了能够保证数据的正确性,在单机环境下,我们可以使用synchronized的方法,来保证线程安全,具体修改:

1
2
3
4
5
6
7
8
9
10
11
public synchronized void doSomething(String userId){
User user=getUser(userId);
if(user==null){
user.setUserName("xxxxx");
user.setUserId(userId);
insert(user);
return;
}
update(user);
}

在单机器的环境下,能够解决线程安全的问题,那在分布式环境下呢? 这个时候需要用到分布式锁.

分布式锁需要借助其他组件来实现,常用的有rediszookeeper。下面我们就用redis的实现,来说明下问题,分布式锁具体的实现方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public  void doSomething(String userId){
String lock=RedisUtils.get("xxxx"+userId);
if(StringUtils.isNotEmpty(lock)){//说明当前userId已经被锁定
return;
}
RedisUtils.set("xxxx"+userId,userId,1000);//锁定10s
User user=getUser(userId);
if(user==null){
insert(user);
RedisUtils.delete("xxxx"+userId);
return;
}
update(user);
RedisUtils.delete("xxxx"+userId);

}

上面的代码解决了在分布式环境中的并发的问题。但同样需要考虑一个问题,如果insert操作和update操作异常了,分布式锁不会释放,后续的请求还会被拦截。

阅读全文 »

树的定义

为了保证数据的能够有效的查询,可以使用顺序结构。为了保证数据的插入效率,我们可以使用链型结构。但在某些场合,我们需要同时兼顾查询效率和插入的效率,应该怎么做?

树(Tree)型结构是一类常用的高效的非线性的数据结构,兼顾了顺序表的查询效率和链表的插入效率。例如我们电脑中的目录结构,采用的就是一种树形结构关系。 树的具体结构形状如下图:

树结构

关于树有以下几个定义:

度:每个节点拥有的叶子的个数称之为度。A节点的度是3
树的度:是指节点的最大值,当前树的度是4。
根节点:树的开始节点,A节点是根节点
叶子节点:没有子节点的节点,K、L、F节点是叶子节点

为什么树能够保持较高的查询和插入效率,对比顺序结构, 顺序结构的查询效率的时间复杂度为O(1),插入的效率为O(n),链式结构正好相反。

如果我们把数据结构由线形结构转成树形结构的话,查询和遍历节点的数量一定是小于等于n,所以树的效率一般是优于线性结构。

树的存储形式

  1. 双亲表示法

    双亲表示法是指用顺序结构来表示数,每个节点设置一个变量,来指示双亲节点所在的位置,如下:

    树结构

  2. 孩子表示法

    每个节点可能有多个子节点,可以用使用多级链表来分别表示。具体如下:

    树结构

  3. 孩子兄弟表示法

    又称为二叉树表示法,是以二叉链表的方式进行存储。表示如下:

    树结构

阅读全文 »

协程(Coroutine)又称为微线程,我们知道线程是CPU的执行的最小单位,线程执行的最小代码单位是方法。

比如在执行的时候,一个线程从程序的入口调用Main方法,Main调用A方法,A方法又调用B方法,整个函数的执行完成的顺序是B->A->Main。这个调用的顺序是明确的,是通过压栈和出栈的方式确定的。

而协程不同, Main调用B,在调用B的过程中可以中断,Main函数继续执行一会,Main再中断,B继续再执行一会, 继续执行的代码是上次中断的地方。

用伪代码表示两个方法:

funcA(){
     funcB();
     print 4;
     print 5;
     print 6;
}
funcB(){
     print 1;
     print 2;
     print 3;
}

如果是用正常的单线程线程来执行的时候,打印结果是123456,如果采用协程,打印结果就有可能是142536.

协程的执行的结果有点和多线程类似,但本质与多线程不同,线程有上下文切换,存在变量的拷贝,而协程只是轻量级的方法中断,所以切换效率是高于线程。

协程所有的变量都是共享内存,访问不需要加锁,使用时只需简单的判断,不存在线程不安全问题。

在Java中,还不支持协程的机制,所以用C#来演示下协程的过程。

     static void Main(string[] args)
     {
          System.Console.WriteLine("执行方法:Main");
          IEnumerable<int> intList = Xc.GetList();
          foreach (int i in intList)
          {
               System.Console.WriteLine("协程1:执行");
               Console.WriteLine("协程1:获得返回的结果是:" + i);
          }
     }

     class Xc
     {
          public static IEnumerable<int> GetList()
          {
               System.Console.WriteLine("执行方法:GetList");
               for (int i = 0; i < 10; i++)
               {
                    yield return i;
                    System.Console.WriteLine("协程2: 执行");
                    System.Console.WriteLine("协程2:doSomething");
                    Thread.Sleep(1000);
               }
          }
     }
阅读全文 »

什么是C10K问题

随着互联网的普及,web的访问呈几何倍数的增长,我们知道一个请求和响应的过程的背后是连接的互换数据,最初的服务器都是基于进程/线程模型的,新到来一个TCP连接,就需要分配1个进程(或者线程)。

而进程和线程又是系统昂贵的资源,一台机器创建的线程数量和进程数量是有限的,不可能无限制的创建。

C10K的核心问题就是即使在硬件资源都满足的情况先,系统也难以承载有10000个客户端连接请求

造成C10K问题的原因

造成C10K问题的本质其实是操作系统的问题,对于传统的阻塞I/O处理方式,当线程或进程创建的足够多时,即使服务器硬件条件满足,也会导致系统的卡顿和崩溃。所以要解决C10K的问题,就应该尽可能的降低CPU的开销和进程的创建。

怎么解决C10K的问题

上面分析了C10K的本质,所以解决办法就围绕着,降低进程的开销,比如让一个进程能够管理多个连接。而如何是一个进程管理多个连接呢?因为在服务端无法知道到底是哪个端口会发来数据。我个人理解具体方法有下面几种:

  1. 同步轮询(select

    方法很简单,直接挨个检查处理各个连接,当所有连接都有数据的时候,方法没有问题,如果有一个连接没有数据,那整个流程就阻塞在哪里,端口就无法进行获得。如果一个进程出来的过多,也会带来性能问题。

  2. 智能跳过轮询 (poll

    这个方法在上面的方法又改进了一步,在读取前先判断当前句柄是否已经是ready状态,如果不是则跳过。

  1. 轮询标记有数据,然后再轮询(epoll

    既然逐个排查所有文件句柄状态效率不高,可以先标记哪些句柄有变化,然后再读取变化的数据。

阅读全文 »

使用socket通信原理实现简单的http协议,代码很简单(动态类访问和NIO晚点实现,有点懒。。):

  ServerSocket serverSocket;

    public Tomcat(int port) {
        try {
            serverSocket = new ServerSocket(port);
            System.out.println("服务端启动成功,端口是:" + port);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void start() {
        while (true) {
            try {
                Socket accept = serverSocket.accept();
                InputStream inputStream = accept.getInputStream();
                byte[] buff = new byte[1024];
                int len = inputStream.read(buff);
                if (len > 0) {
                    String msg = new String(buff, 0, len);
                    System.out.println("接收到数据:" + msg);
                }
                OutputStream outputStream = accept.getOutputStream();
                StringBuilder resp = new StringBuilder();
                resp.append("HTTP/1.1\n");
                resp.append("Content-type:text/html\n\n");
                resp.append("<h1>Hello Tomcat!</h1>");
                byte[] bytes = resp.toString().getBytes();
                outputStream.write(bytes);
                outputStream.flush();
                outputStream.close();
            } catch (Exception ex) {

            }
        }
    }

    public static void main(String[] args) {
        Tomcat tomcat = new Tomcat(8080);
        tomcat.start();
    }

启动后访问:http://localhost:8080/, 返回结果如下:

手写简单tomcat服务器

阅读全文 »

Java访问mysql的时候,需要用到jdbc驱动,传统连接方式为:

1
2
3
4
5
6
7
8
9
10
11
try {
Driver mysqlDriver = (Driver) Class.forName("com.mysql.jdbc.Driver").newInstance();
DriverManager.registerDriver(mysqlDriver);
Connection connection = DriverManager.getConnection("jdbc:mysql://192.168.0.***:3306/rzframe?useSSL=false&serverTimezone=UTC", "root", "*******");
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery("select * from rz_user");//查询
connection.close();
} catch (Exception e) {
e.printStackTrace();
}

我们对上面的代码做一个简单的性能测试,代码如下:

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
public static void main(String[] args) {
long start = System.currentTimeMillis();
CountDownLatch countDownLatch = new CountDownLatch(100);
for (int i = 0; i < 1000; i++) {
try {
CountDownLatch finalCountDownLatch = countDownLatch;
Thread thread = new Thread(() -> {
try {
doJDBC();
} catch (Exception ex) {

} finally {
finalCountDownLatch.countDown();
}
});
thread.start();
if (i != 0 && i % 100 == 0) {
countDownLatch.await();
System.out.println(i);
countDownLatch = new CountDownLatch(100);
}
} catch (Exception ex) {

}
}
long end = System.currentTimeMillis();
System.out.println("耗时:" + (end - start));

}

上面代码用了100个线程分批次去完成查询的动作,在我的机器上运行时间45s左右。

从上面的代码可以看出问题,Connection对象每一次都是重新创建,查询完成后,直接是调用close方法,如果不释放,会报连接数过多的异常。 如果查询多次,那浪费在创建Connection的时间就会很多,我们知道在程序优化的手段中,有一个池化可以很好的解决这个问题。

池化的概念就是先创建多个对方存在在一个容器中,当时候的时候可以直接拿出来时候,用完后再进行归还。 跟着这个思想,我们来创建自己的连接池。

编写思路

  1. 创建一个线程安全的容器(由于是多线程访问),队列或者是list,因为Connection的对象并不是有序的,所以可以使用list容器

  2. 对Connection的对象进行封装,增加一个isBusy变量,每次读取的时候就可以选出空闲的Connection对象

  3. 如果取的时候,没有可用的Connection对象,则可以再自动创建对象,可以自动扩容,直到扩容到允许的最大值。

封装的Connection类:

阅读全文 »

原来一直使用mysql,没有自己真正的搭建,搭建也只是本地的连接,现在手上有两台电脑,想搭建一个主从的架构,在配置mysql的环节费了一点时间,其实都是很小的问题,今天记录下:

  1. mysql 安装和启动

    1
    2
    3
    4
    5
    6
    mysql --install 

    mysql --initialize

    net start mysql

    安装完之后,发现忘了记密码,可以到 mysql-8.0.16-winx64\data中找到.err文件,里面有初始密码 ,本地用localhost连上之后可以自己修改密码。

    mysql安装和连接踩坑记录

mysql安装和连接踩坑记录

  1. 远程连接

    mysql 需要远程连接需要权限,错误的信息如下:

    1
    2
    错误号码1130
    Host 'xxxxxxxxx' is not allowed to connect to this MySQL server

    执行下面的代码可以解决:

    1
    2
    use mysql;
    update user set host = '%' where user = 'root'
  2. 如果碰到无法更新,因为mysql 更新使用安全模式,需要关闭.

    报错信息如下:

    1
    2
    Error Code: 1175. You are using safe update mode and you tried to      update a table without a WHERE that uses a KEY column.  To disable safe mode, toggle the option in Preferences -> SQL Editor and reconnect.     

    执行以下sql:

1
SET SQL_SAFE_UPDATES = 0;   
  1. 连接远程的mysql时报错,原因时mysql8.0的加密方法变了。

    1
    2
    3
    错误号码2058

    Plugin caching_sha2_password could not be loaded: �Ҳ���ָ����ģ�顣
    1
    2
    3
    4
    ALTER USER 'root'@'localhost' IDENTIFIED BY 'password' PASSWORD EXPIRE NEVER; #修改加密规则 
    ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'password'; #更新一下用户的密码 
    FLUSH PRIVILEGES; #刷新权限

阅读全文 »

想要了解Java的垃圾回收过程首先要理解Java堆的内存模型,简单表示如下:

Java内存结构

从上面的图片可以看出,Java的堆内存可以简单的分为,新生代和老年代,在新生代中有分为三个区域,1个Eden区和2个Survivor区(分别叫from和to),默认比例为8:1

了解了垃圾回收的内存模型,下面就可以看下垃圾回收的过程。

  1. 创建一个新对象,判断是否大于或等于大对象的阈值(JVM用-XX:PretenureSizeThreshold来定义),如果判断为大对象 ,直接进入老年代。

  2. 如果不属于大对象,则优先在新生代Edge区分配内存,如果能够存放,则直接分配内存,对象创建结束,不存在垃圾回收过程

  3. 当Eden没有足够的空间进行分配的时候,虚拟机开始在新生代进行垃圾回收(MinorGC)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14

    新生代采用复制算法进行回收垃圾,过程如下:

    1. 在MinorGC前会对老年区的剩余空间进行统计,如果剩余区域的和大于新生代的所有对象之和,则开始进行一次`Minor GC`

    2. `Minor GC`采用复制算法进行垃圾回收,具体过程是把Eden区域的存活得对象拷贝到From区域,所有From区域的对象的年龄+1。From中的对象根据对象的年代决定是进入To,还是进入老年代。进入老年代的参数默认是年龄是15,可以通过`-XX:MaxTenuringThreshold`参数设置。

    3. 清空Eden和from区域,把To和from空间进行交换,保证To区域始终未空区域。

    4. 如果老年代剩余的内存空间小于新生代所有的对象,虚拟机会查看是否允许担保失败.

    5. 如果允许,则虚拟机会继续检查可用空间是否大于历次晋升到老年代的平均水平,如果条件成立,则尝试进行一次`MinorGC` ,显然这样回收是`有风险的`, 如果晋升的对象空间大于老年代的剩余空间,则会触发一次`Full GC`

    6. 虚拟机会查看不允许允许担保失败, 则会直接触发`Full GC`
  4. From空间对象晋升为老年代的时候,为了适应更多内存情况,JVM会检查在Survivor空间中相同年龄所有对象的大小综合大于Survivor空间的一半,则年龄大于或等于这个年龄的对象可以直接进入老年代。
  5. 老年代空间不足时、手工调用System.GC()会触发Full GC
阅读全文 »

Java内存模型

Java把内存大概分为:方法区,虚拟机栈,本地方法栈,堆和程序计数器5个区。

程序计数器 是一块比较小的内存空间,可以看成是一个线程的运行的指示器,是一个线程私有的内存。由于JVM的运行时通过线程抢占CPU的时间片实现的(在任何一个时刻,一个CPU只能执行一个指令),如果存在线程的上下文切换,每个线程在被移除时间片的时候都需要记录执行的位置。如果当前正在执行的是Native方法,则不需要计数器来标记。

虚拟机栈也是线程私有的内存,Java代码在运行的时候,会对每一个方法创建一个栈帧,用于存储局部变量,操作数据,动态链接等数据。

本地方法栈 和虚拟机栈作用类似,本地方法栈是为了Java调用Native方法服务。

Java堆 是Java常用的引用对象的真实存储的区域,也是修改和访问最为频繁的区域。

Java的内存模型分类大致如下:

Java内存结构

垃圾回收算法


阅读全文 »

Java代码的运行过程

Java字节码

栈帧的概念

栈帧(Stack Frame) 是用于虚拟机执行时方法调用和方法执行时的数据结构,它是虚拟栈数据区的组成元素。每一个方法从调用到方法返回都对应着一个栈帧入栈出栈的过程。

每一个栈帧在编译程序代码的时候所需要多大的局部变量表,多深的操作数栈都已经决定了 一次一个栈帧需要多少内存,不会受到程序运行期变量数据的影响,仅仅取决于具体的虚拟机实现。

一个线程中方法调用可能很长,很多方法都处于执行状态。

Java字节码栈帧的结构

从上面的图可以看出,栈帧的组成主要分为4个部分:

局部变量表

顾名思义,用来存储局部变量和方法的内存空间,第一位是当前类的指针this,后面的是当前函数的参数和方法内的变量。比如一个函数有两个参数x,y,第一位是this指针,第二个是x,第三个是y
局部变量占用的空间大小在Java程序被编译成Class文件时就已经确定了。

阅读全文 »