liuhao163.github.io

杂七杂八


  • Home

  • Categories

  • Tags

  • Archives

  • Sitemap

查看GC的利器之一GCVIEWER

Posted on 2019-03-09 | Edited on 2022-09-21 | In java , jvm优化

Java的GC查看工具-GCVIEWER

gcviews安装和使用

源码安装

1
2
3
4
5
git clone https://github.com/chewiebug/GCViewer.git

mvn clean pacakge

cd target

JVM开启gclog

java的启动命令增加 -Xloggc:./gc.log参数下面是我的一个实例

1
java -jar -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:./gc.log xxx.jar

启动

指定上面的gc.log的目录
java -jar gcviewer-1.36-SNAPSHOT.jar ./gc.log
会看到如下的图表

Chart的说明

view能看到各GC的情况,右边是一些说明信息,summary查看具体的描述信息
avatar
EventDetail标识具体的GC的事件信息
avatar

2019学习计划

Posted on 2019-03-09 | Edited on 2022-09-21

本年计划

JAVA

  • java并发编程
  • spring的源码
  • nio的学习
    • netty的源
    • netty构建服务发现demo
    • netty聊天工具构建
  • 算法的学习

消息队列

  • rocketmq的源码,涉及到netty可以先看netty

NOSQL

  • REDIS的源码学习

本周计划

Netty聊天软件服务端demo

  • 协议的自定义(Encoder)
  • 客户端和服务端通信
  • 客户端注册和服务端注册
  • 阶段性文档的整理(完成了哪些工作,有哪些坑)

完成并发的学习和梳理

  • 完成HB原则和基础
  • 完成synchroized关键字

调研下限流和断路器

CountDownLatch

Posted on 2019-03-08 | Edited on 2022-09-21 | In java , 并发

CountDownLatch解读

  • 是基于AQS原理,共享锁的一种实现,构造函数里的count值代表计数器,会将count赋值给state。
  • CountDownLatch不可重入。
  • 通过await()来阻塞线程。countDown使计数器-1,当count为0时候所有await的线程会同时获取锁。

实现原理

CountDownLatch有一个内部类Sync,该类继承了AQS,await()方法和countDown方法都是调用Sync的tryAcquireShared和tryReleaseShared

共享锁获取锁-tryAcquireShared

CountDownLatch掉用await()实际上是调用sync的acquireSharedInterruptibly,最终会调用AQS中的doAcquireSharedInterruptiblyState,在该方法中调用doAcquireSharedInterruptibly,在改方法中,会调用CountDownLatch实现的tryAcquireShared方法如果state==0返回1,否则-1,其中代码和逻辑如下:

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
//将当前线程封装成SHARDED类型的节点,加入到链表中
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
//自旋来获取锁,获取前置节点,如果前置节点是头结点,尝试获取锁,将当前节点设置为头节点,开始传播为后置节点解锁
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
/**自旋解锁:唤醒头结点的线程并且开始根据链表顺序传播解锁。
* 因为是公平锁,所以无论现在在哪个线程都会根据链表顺序从head节点开始唤醒
**/
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}

//通独占锁,根据前置节点的情况判断是否应该休眠(前置节点是SINGAL,当前节点的线程会block,如果前置节点是cancel会跳过所有cancel的节点)
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}

private void setHeadAndPropagate(Node node, int propagate) {
//当前的头节点当中间变量同时设置头结点
Node h = head;
setHead(node);
/*
* Try to signal next queued node if:
* Propagation was indicated by caller,
* or was recorded (as h.waitStatus either before
* or after setHead) by a previous operation
* (note: this uses sign-check of waitStatus because
* PROPAGATE status may transition to SIGNAL.)
* and
* The next node is waiting in shared mode,
* or we don't know, because it appears null
*
* The conservatism in both of these checks may cause
* unnecessary wake-ups, but only when there are multiple
* racing acquires/releases, so most need signals now or soon
* anyway.
*/
//propagate>0(这里是state==0),或者头结点为空,或者头结点不是CANCELED状态,或者现在的头节点为空,或者头结点不是CANCELED状态
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;//把当前Node状态之后的节点Shared都唤醒
if (s == null || s.isShared())
doReleaseShared();
}
}

private void doReleaseShared() {
/*
* Ensure that a release propagates, even if there are other
* in-progress acquires/releases. This proceeds in the usual
* way of trying to unparkSuccessor of head if it needs
* signal. But if it does not, status is set to PROPAGATE to
* ensure that upon release, propagation continues.
* Additionally, we must loop in case a new node is added
* while we are doing this. Also, unlike other uses of
* unparkSuccessor, we need to know if CAS to reset status
* fails, if so rechecking.
*/
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
//自旋解锁,如果头结点的状态是SIGNAL,因为刚才将当前节点置为了Head,实际就是当前节点,同时一般ws也是SINGAL
if (ws == Node.SIGNAL) {
//cas成功后将头结点的线程解锁
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
//刚才的setHead()方法已经将Thread=null所以,如果修改成功了会跳出循环
continue; // loop on failed CAS
}

//上面修改成功了node是
if (h == head) // loop if head changed
break;
}
}

共享锁释放-tryAcquireShared

CountDownLatch调用countDown()时候回调用sync.releaseShared(1),实际是AQS的releaseShared(int args)方法,如果返回true即state==0,在该方法中会调用doReleaseShared(),获取锁和传播获取锁的流程

CountDownLatch中的Sync的tryReleaseShared:

1
2
3
4
5
6
7
8
9
10
11
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}

AQS中的releaseShared代码

1
2
3
4
5
6
7
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}

java的队列同步器AQS

Posted on 2019-03-08 | Edited on 2022-09-21 | In java , 并发

AbstractQueuedSynchronizer

是java.util.concurrent.locks中最重要的类。翻译过来是“抽象队列同步器”,我们根据字面来理解:

  • Abstract-它是抽象的,业务方根据自己的场景来实现tryAcqurie、tryRelease等方法。例如:CountDownLatch、CyclicBarrier、Semaphore、ReentrantLock等;
  • Queue-它维护了一个FIFO的队列,竞争锁的线程都会加入到这个对列中,排队竞争资源;
  • Synchronizer-它用来保证在并发情况下,线程安全。

优点:
使用方便:满足日常线程调度的需求,通过CountDownLatch、CyclicBarrier、Semaphore,ReentrantLock很轻松实现了共享锁、公平锁等功能。
性能:在jdk1.6之前synchronized关键字很重,性能很不好(1.6之后引入偏向锁、自旋锁、轻量级锁性能已经替身了许多)

主要逻辑

以ReentrantLock为例,来解释加锁和释放锁的逻辑

加锁

调用acquire方法,对业务进行加锁,acquire流程如下:

  1. 会调用tryAcquire(1)尝试获取锁,如果失败将当前线程加入到队列中;
  2. addWatier将当前的线程封装成EXCLUSIVE模式的Node加入到队列中,其中为了避免失败采用了自旋的方法;
  3. 之后通过自旋acquireQueued获取锁资源,然后根据之前记录的线程是否被中断过的状态,进行线程中断,acquireQueued逻辑如下;
    • 获取当前节点的前置节点,如果前置节点是头结点,说明自己可以开始竞争锁资源了,如果成功,将当前节点置为头节点返回;
    • 如果当前节点不是头结点,调用shouldParkAfterFailedAcquire判断该节点是否可以进入休眠状态,分支逻辑如下:
      • 前置节点如果的waitStatus如果是SIGNAL,说明后续节点可以休息返回ture;
      • 如果前置节点的waitStatus是CANCELLED状态(>0),通过循环跳过当前节点前置所有的CANCELLED节点;(之后结合释放锁来看)
      • 否则将前置节点的状态CAS改为SIGNAL,acquireQueued下一次自旋,节点就会返回true;
  4. 如果返回允许挂起后调用parkAndCheckInterrupt(),通过LockSupport.lock()方法挂起当前线程;
  5. 获取锁资源后,判断线程是否被中断过调用:selfInterrupt()方法来中断线程。

涉及的代码如下

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
/**
独占锁的获取锁方法
调用tryAcquire获取锁,如果失败将当前的节点和state加入到队列中,开始竞争/排队获取锁。
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

//将当前线程封装成node(mode是独占模式)插入到队列末尾
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// 快速插入将node插入到队列的末尾,只插入一次,失败后通过自旋重复插入直到成功
Node pred = tail;
if (pred != null) {
node.prev = pred;
//compareAndSetTail实现上是通过,修改tail字段的值
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//失败后用自旋开始插入
enq(node);
return node;
}

//自旋插入队列尾
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) {
//如果tail为空。初始化队列(tail=head=new Node),然后下次循环将node插入到到tail后面,这时候 new Node()->node
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}


final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
//自旋找到头结点,获取锁
for (;;) {
//获取前置节点
final Node p = node.predecessor();
//如果前置节点是头节点,tryAcquire获取锁
if (p == head && tryAcquire(arg)) {
//成功后将当前节点设置为头节点,interrupted=false
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}

//如果失败判断状态,并且挂起当前线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}

//判断是否该挂起线程
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//前置节点的类型
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
//如果前置节点状态为SIGNAL,当前节点可以挂起
return true;
if (ws > 0) {//代码为什么不ws==Node.CANCELLED???
//循环链表,跳过所有前置节点watiStatus是CANCELLED的节点,直到第一个不是CANCELLED的节点。
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//等待状态是是Propagated状态,CAS改为SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}

//
private final boolean parkAndCheckInterrupt() {
//具体实现见下面LockSupport.park详解
LockSupport.park(this);
return Thread.interrupted();
}

释放锁

调用acquire方法,对业务进行加锁,acquire流程如下:

调用release方法多锁进行释放,其中逻辑如下,ps:ReentrantLock的公平锁模式会判断是否有前置节点,如果有才会修改state

  1. 通过调用tryRelease(1)释放锁,成功后取到头head链表的head节点(一般head节点持有的线程都是自己);
  2. 判断head节点的状态,不为空,同时waitStatus!=0,因为在acqureQueued如果有后置节点已经将他改为SIGNAL状态,所以如果为空说明没有后置节点;
  3. 调用unparkSuccessor(h),唤醒后置节点,具体逻辑如下:
    • 获取头节点位置,见头节点位置将头节点waitStatus置为0;
    • 找到后置节点,如果后置节点为空或者是CANCELLED,从尾部开始遍历链表,找到离头节点最近的一个不是CANCELLED状态的节点
    • 通过LockSupport.unpark(s.thread)唤醒线程开始竞争锁;
  4. 这时候这个被唤醒的线程回到acquireQueued的自旋中,走到“p == head && tryAcquire(arg)”这个分支中,即时这时候p!=head,走入shouldParkAfterFailedAcquire方法中,由于这个节点到head之前的节点都赢是CANCELLED对象(见上报从尾部开始遍历链表),所以跳过所有CANCELLED节点,下次必然会p==head,获取锁。

涉及的代码如下

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
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}

//唤醒后置节点
private void unparkSuccessor(Node node) {
//获取头节点状态,置为0防止重复唤醒
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);

//找到后置节点,如果后置节点为空,或者状态是CANCELLED,从尾部开始遍历链表,找到离头节点最近的一个不是CANCELLED状态的节点
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
//唤醒节点
if (s != null)
LockSupport.unpark(s.thread);
}

总结

AQS利用队列实现了在并发情况下,多线程之间的调度问题;并且合理利用了自旋和挂起笔名了线程之间上下文切换的损耗,和无效的竞争。

其他相关知识

LockSupport对线程阻塞和唤醒(park/unpark)

相比wait和notify的优势

  1. 不用先用synchronized获取同步锁。
  2. LockSupport是基于线程对象的,而wait和notify是基于锁对象的,所以不会遇到遇到“Thread.suspend 和 Thread.resume所可能引发的死锁”问题

源码如下:

1
2
3
4
5
6
7
8
9
public static void park(Object blocker) {
//获取当前线程,并且设置parkBlocker对象,jstack查看时候就可以知道线程是在哪个对象上阻塞,这里是AQS的实现类
Thread t = Thread.currentThread();
setBlocker(t, blocker);
//阻塞线程,绝对时间,并且无限制的阻塞
UNSAFE.park(false, 0L);
//阻塞停止,将parkBlocker置为空。
setBlocker(t, null);
}

AQS中FIFO队列介绍,内部类Node

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static final class Node {
/** Marker to indicate a node is waiting in shared mode */
static final Node SHARED = new Node();
/** Marker to indicate a node is waiting in exclusive mode */
static final Node EXCLUSIVE = null;

/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED = 1;
/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL = -1;
/** waitStatus value to indicate thread is waiting on condition */
static final int CONDITION = -2;
static final int PROPAGATE = -3;

volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;

}

网络协议-UDP

Posted on 2019-03-06 | Edited on 2022-09-21 | In 计算机基础 , 网络

UDP协议的定义

UDP 是User Datagram Protocol的简称, 中文名是用户数据报协议,一种无连接的协议,不保证数据是否传输到,不保证数据包的顺序。特点是速度较快

UDP的特点

结合TCP来看UDP的特点

  1. TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接
  2. TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付
  3. TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流;UDP是面向报文的
  4. UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等)
  5. 每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信
  6. TCP首部开销20字节;UDP的首部开销小,只有8个字节
  7. TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道

支持并发的定时器+数据归档解决查询效率

Posted on 2019-03-06 | Edited on 2022-09-21 | In 经验积累 , 工具

项目背景

客户的数据量徒增,每天1QWPV,1个月1张表的话大约这一个客户有30亿数据,页面上一些接口的响应变慢。

原因是:后端数据存在了ES中,ES特点是模糊查询很开,但是但是如果用到了一些叫复杂的聚合函数需要进行索引扫描,所以效率了堪忧。

优化方案

  • 每天,用定时器将客户网站数据归档,目前是采用mysql保存。
  • 前端查询采用缓存数据+当天数据的方式呈现给客户,当日的数据很小所以可以满足性能上的要求。
  • 考虑到将来会有很多客户,可能一个定时器执行效率慢,所以定时器要支持水平扩展。

具体执行

  1. 将数据库中所有的网站ID放到redis的zset中,为什么用zset?有序-支持分页,排重;
  2. 利用setNx做锁, 从zset取出一条数据锁住:setNx的key是:prefix:statslock:id:yyyyMMdd, ttl:1天;
    • 因为采用的是RedisTemplate不支持setNx和expire排异常轻快无法解锁,所以锁是按天锁住数据
  3. ES查询昨天的数据,归档
  4. 前端查询ES当钱数据+归档数据返回,测试从10sec请求变为了600ms

示例代码如下:

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
public void doWafDataArchiveByDate(LocalDateTime localDateTime) {
String dateFormater = localDateTime.format(YYYYMMDD);

//分页
Long total = statsRedis.domainSize();
Long pageNo = (total - 1L) / ZSET_PAGE_SIZE + 1;
Long curPage = 1L;

do {
//从redis获取数据
Long startIdx = (curPage - 1L) * ZSET_PAGE_SIZE;
Long endIdx = startIdx + ZSET_PAGE_SIZE - 1L;
Set<String> set = statsRedis.queryRange(startIdx, endIdx);

curPage++;
for (String domainUuid : set) {
try {
//锁1天
if (taskLock.lock(domainUuid, dateFormater)) {
try {
//do logic
} catch (Exception e) {
e.printStackTrace();
}
} else {
}
} finally {
taskLock.unlock(domainUuid,dateFormater);
}
}
} while (curPage <= pageNo);
}

遗留的坑以及方案

待补充

网络协议-TCP

Posted on 2019-03-03 | Edited on 2022-09-21 | In 计算机基础 , 网络

TCP/IP的三次握手

流程

  1. 第一次握手:client端建立链接视乎。client端发送包(SYN=1,seq=x),这时候客户端进入SYN_SENT状态
  2. 第二次握手:server端收到SYN包,需要确认client端的seq(ack=x+1)。server端发送SYN+ACK包(SYN=1,ACK=1,seq=y,ack=x+1),这时候server端进入SYN_RECV状态
  3. 第三次握手:client端手server端SYN+ACK包,需要确认server端的seq(ack=y+1)。client端发送ACK包(ACK=1,ack=y+1),这时候client和server都是ESTABLISHED

至此完成链接,开始传输数据

目的

建立安全的双工的通信通道,保证双方都能发送、收到数据。

TCP/IP的四次挥手

四处挥手流程

  1. 第一次挥手:client端发出断开链接请求。client发送包(FIN=1,seq=x),client端状态为FIN_WAIT_1
  2. 第二次挥手:server端收到FIN包,需要确认client端的seq(ack=x+1)。server端发送ACK包(ACK=1,seq=y,ack=x+1) ,server端状态为CLOSE_WAIT,client端收到后状态FIN_WAIT_2
  3. 第三次挥手:server端不会立刻关闭链接,会继续发送完到client端的数据,server端发送FIN包(FIN=1,seq=z,ack=x+1),server端状态为LAST_ACK,client端状态为FIN_WAIT_2
  4. 第四次挥手:client端发送ACK包确认server端的FIN(ack=z+1),client端发送ACK包(ACK=1,ack=z+1,seq=h),server端状态为CLOSED,client端状态为TIME_WAIT状态。

至此4次挥手完成,客户端进入TIME_WAIT–2MSL–>CLOSED。
主要:如果client发送了FIN,服务端没有发送ACK直接发送了FIN,这时候客户端会变成CLOSEING。(比如FIN包由于网络原因丢失)

四次挥手目的

实现双工链接关闭的安全性。防止客户端的端口重用后,还收到上次服务端的请求

TIME_WAIT和CLOSE_WAIT状态

TIME_WAIT状态:对于复杂的网络状态,TCP 的实现提出了多种应对措施,TIME_WAIT 状态的提出就是为了应对其中一种异常状况。为了理解 TIME_WAIT 状态的必要性,我们先来假设没有这么一种状态会导致的问题。暂以 A、B 来代指 TCP 连接的两端,A 为主动关闭的一端。

  • 四次挥手中,A 发 FIN, B 响应 ACK,B 再发 FIN,A 响应 ACK 实现连接的关闭。而如果 A 响应的 ACK 包丢失,B 会以为 A 没有收到自己的关闭请求,然后会重试向 A 再发 FIN 包。如果没有 TIME_WAIT 状态,A 不再保存这个连接的信息,收到一个不存在的连接的包,A 会响应 RST 包,导致 B 端异常响应。此时, TIME_WAIT 是为了保证全双工的 TCP 连接正常终止。
  • 我们还知道,TCP 下的 IP 层协议是无法保证包传输的先后顺序的。如果双方挥手之后,一个网络四元组(src/dst ip/port)被回收,而此时网络中还有一个迟到的数据包没有被 B 接收,A 应用程序又立刻使用了同样的四元组再创建了一个新的连接后,这个迟到的数据包才到达 B,那么这个数据包就会让 B 以为是 A 刚发过来的。此时, TIME_WAIT 的存在是为了保证网络中迷失的数据包正常过期。

    由以上两个原因,TIME_WAIT 状态的存在是非常有意义的。

为什么客户端要等待2MSL才能关闭

协议规定主动关闭一方,进入FIN_WAIT_2->TIME_WAIT,必须等待2MSL(MSL为最大报文段生存时间,LWIP为1分钟,windows为2分钟)时间然后才进入CLOSED,删除TCP控制块。在2MSL等待时间内迟到的报文段将被抛弃。

  • 报文段有生存时间,当连接关闭时,有可能收到迟到的报文段。这时,若立马就建立新的连接(同一端口),那么新的连接就会接收迟到的报文,误以为是发给自己的。
  • 另一个原因是可靠的实现全双工连接的终止:由于网络原因可能服务端没有收到ACK包,所有会在一段时间内重新发送FIN包和ACK包。

不用等待2MSL的方法:使能SO_REUSEPORT(允许重用本地地址),可以通过调用setsockopt函数来使能。

Mysql自增ID用完了怎么办

Posted on 2019-03-02 | Edited on 2022-09-21 | In Mysql , Mysql基础

表自增ID

  1. 如果主键是auto_incrment,如果达到了最大值,不会在增长,这时候写入会报主键冲突
  2. 如果没指定,会用dict_sys.row_id的值,作为主键,长度6字节,如果达到最大值会从0开始循环写入

所以,推荐这只auto_incrment,毕竟报主键冲突比覆盖以前的数据要好的多。

XID

mysql对应事物的ID,Mysql的内存中会维护一个golbal_query_id的变量,改变量在内存中重启会清0,每次执行语句会吧当前值赋值给Query_id,并且+1,
如果当前语句是事务中的一个语句那么会把这个Xid赋值给事物ID

Mysql在重启后由于会清零,同时会有一个新的binlog所以一个binlog中xid是不冲突的。但是如果执行时间过长,理论上也可能会冲突吗,因为XID到达最大值后会从0开始分配,只不过这个值太大了2E64-1,所以冲突只存在在理论中。

Innodb trxid

区别于XID,XID是Server层的trxId是innodb层的

innodb内部维护了个max_trx_id,每次申请时候会获取当前的max_trx_id,然后max_trx_id+1;

inndob的事务可见性核心思想是:每次更新都更新了数据的trx_id,当一个事务读到数据的时候判断一致性视图和当前trx_id的关系

对于正在执行的事物可以通过:information_schema.innodb_trx表中看到当前事务的trx_id;

注意trx_id的分配规则:

  1. 只读事物不分配trx_id,把当前事物的trx_id变量的指针地址转成int+2e48
    1. 这么做的好处是只读事物中,指针地址不会变,同一个只读事务在innodb_locks和innodb_trx查出来的trx_id是一致的;
    2. 不同的事务由于指针地址会变,查出来的trx_id一定是不同的
    3. 加上2e48也是能区别于读写事物
  2. 同时trx_id可能看到不只加了1:

    1. 因为update和delete操作也会+1,(标记删除,放到purge队列中)
    2. innodb的后台操作比如表的索引信息统计等都会+1

    只读事务不分配trx_id的好处

  3. 减少事务视图中活跃事务数,innodb在创建一致性视图时候值需要copy读写事物的trx_id就好了

  4. 可以减少trx_id的申请次数

    trx_id是被持久化不会清0的所以当到达最大值后,会从0开始,这时候就可能会出现脏读。因为后续的数据trx_id变小了,变成可见的了,但是因为这个值是2e48-1所以这个错误也只存在于理论上。(50万TPS,要跑17.8年)

thread_id

mysql有个thread_id_counter每次分配完后+1,如果到达最大值后会从0开始,这时候会看到俩个一样的thread_id

不过mysql在分配时候有一个逻辑来处理这种情况

thread_ids是个唯一数组,添加失败后会一直循环(待验证)

1
2
3
do {
new_id= thread_id_counter++;
} while (!thread_ids.insert_unique(new_id).second);

要不要使用分区表

Posted on 2019-03-01 | Edited on 2022-09-21 | In Mysql , Mysql架构

mysql的分区表

分区表是什么?他有什么好处?有什么坏处?为什么不建议生产环境上使用分区表?

测试的脚本如下

1
2
3
4
5
6
7
8
9
10
11
CREATE TABLE `t` (
`ftime` datetime NOT NULL,
`c` int(11) DEFAULT NULL,
KEY (`ftime`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
PARTITION BY RANGE (YEAR(ftime))
(PARTITION p_2017 VALUES LESS THAN (2017) ENGINE = InnoDB,
PARTITION p_2018 VALUES LESS THAN (2018) ENGINE = InnoDB,
PARTITION p_2019 VALUES LESS THAN (2019) ENGINE = InnoDB,
PARTITION p_others VALUES LESS THAN MAXVALUE ENGINE = InnoDB);
insert into t values('2017-4-1',1),('2018-4-1',1);

可以看到表空间下,有一个frm文件和4个ibd文件说明

  • server层这是一个表
  • innodb的引擎层这是4个表

分区表引擎层行为

对于innodb来说

因为mysql的间隙锁和行锁是在引擎层做的,所以同一张表,分区表和普通的表的加锁规则可能会有所不同。还是用咱们例子对于分区表加锁的流程如下
avator

而如果一个普通表他的加锁规则如下
avator

原因是2018-02-1这条数据是在另一个分区,所以SessionA的间隙锁范围没设计到,儿2017-12-1这条数据和SessionA在同一分区所以被block住

对于myisam来说

由于myisam是表锁,但是这个由于引擎上分区表被分成了四张表所以,会造成下面的情况,破坏了引擎原生锁的规则
avator

其他的坑

对于mysisam表采用的是通用分区策略,第一次访问分区表,会把表分区都走一遍,所以如果一个分区很大比如超过1024个,而我们的句柄数只有1024,这时候很可能会报错
对于innodb没有这个问题,因为采用的是本地分区策略,在后续版本Myisam已经不支持分区表了(5.7 deprecated,8.0正式弃用)

分区表对于Server层来说

因为server层对于分区表看成是一张表,所以在MDL的时候不同分区的DDL会被block主。如图:
avator

分区表应用场景

对于业务来说只看到一张表,所以很简洁,并且可以随时添加新的分区,以及按照历史分区清理表数据

总结

由于分区表的特性会破坏引擎层的锁的定义,所以不建议在生产环境使用,建议采用手工分区的方式

grant后一定要flush privileges

Posted on 2019-03-01 | Edited on 2022-09-21 | In Mysql , Mysql运维

创建用户

1
2
3
create user 'ua'@'%' identified by 'pa';

revoke all privileges on *.* from 'ua'@'%';

这条命令有俩个含义

  1. 磁盘中,数据库mysql.user表插入一个用户
  2. 内存中同步用户。

这时候是不需要flush privileges的。只有当我们手动修改一张表时候为了让内存和数据库表同步才需要执行该命令。

添加权限的方式具体参加Mysql手册

1…192021…23

Liu hao

励志当好厨子的程序员

229 posts
54 categories
81 tags
RSS
GitHub E-Mail
© 2018 – 2023 Liu hao
Powered by Hexo v3.9.0
|
Theme – NexT.Pisces v7.0.0