0%

想必在linux上写过程序的同学都有分析进程占用多少内存的经历,或者被问到这样的问题——你的程序在运行时占用了多少内存(物理内存)?通常我们可以通过top命令查看进程占用了多少内存。这里我们可以看到VIRT、RES和SHR三个重要的指标,他们分别代表什么意思呢?这是本文需要跟大家一起探讨的问题。当然如果更加深入一点,你可能会问进程所占用的那些物理内存都用在了哪些地方?这时候top命令可能不能给到你你所想要的答案了,不过我们可以分析proc文件系统提供的smaps文件,这个文件详尽地列出了当前进程所占用物理内存的使用情况。

本文将分为三个部分。

第一部分简要阐述虚拟内存和驻留内存这两个重要的概念;

第二部分解释top命令中VIRT、RES以及SHR三个参数的实际参考意义;

最后一部分向大家介绍一下smaps文件的格式,通过分析smaps文件我们可以详细了解进程物理内存的使用情况,比如mmap文件占用了多少空间、动态内存开辟消耗了多少空间、函数调用栈消耗了多少空间等等。

关于内存的两个概念

要理解top命令关于内存使用情况的输出,我们必须首先搞清楚虚拟内存(Virtual Memory)和驻留内存(Resident Memory)两个概念。

虚拟内存

   首先需要强调的是虚拟内存不同于物理内存,虽然两者都包含内存字眼但是它们属于两个不同层面的概念。进程占用虚拟内存空间大并非意味着程序的物理内存也一定占用很大。虚拟内存是操作系统内核为了对进程地址空间进行管理(process address space management)而精心设计的一个逻辑意义上的内存空间概念。我们程序中的指针其实都是这个虚拟内存空间中的地址。比如我们在写完一段C++程序之后都需要采用g++进行编译,这时候编译器采用的地址其实就是虚拟内存空间的地址。因为这时候程序还没有运行,何谈物理内存空间地址?凡是程序运行过程中可能需要用到的指令或者数据都必须在虚拟内存空间中。既然说虚拟内存是一个逻辑意义上(假象的)的内存空间,为了能够让程序在物理机器上运行,那么必须有一套机制可以让这些假象的虚拟内存空间映射到物理内存空间(实实在在的RAM内存条上的空间)。这其实就是操作系统中页映射表(page table)所做的事情了。内核会为系统中每一个进程维护一份相互独立的页映射表。。页映射表的基本原理是将程序运行过程中需要访问的一段虚拟内存空间通过页映射表映射到一段物理内存空间上,这样CPU访问对应虚拟内存地址的时候就可以通过这种查找页映射表的机制访问物理内存上的某个对应的地址。“页(page)”是虚拟内存空间向物理内存空间映射的基本单元。

下图1演示了虚拟内存空间和物理内存空间的相互关系,它们通过Page Table关联起来。其中虚拟内存空间中着色的部分分别被映射到物理内存空间对应相同着色的部分。而虚拟内存空间中灰色的部分表示在物理内存空间中没有与之对应的部分,也就是说灰色部分没有被映射到物理内存空间中。这么做也是本着“按需映射”的指导思想,因为虚拟内存空间很大,可能其中很多部分在一次程序运行过程中根本不需要访问,所以也就没有必要将虚拟内存空间中的这些部分映射到物理内存空间上。

到这里为止已经基本阐述了什么是虚拟内存了。总结一下就是,虚拟内存是一个假象的内存空间,在程序运行过程中虚拟内存空间中需要被访问的部分会被映射到物理内存空间中。虚拟内存空间大只能表示程序运行过程中可访问的空间比较大,不代表物理内存空间占用也大。

图1. 虚拟内存空间到物理内存空间映射

1
图1. 虚拟内存空间到物理内存空间映射

Copy

驻留内存

  驻留内存,顾名思义是指那些被映射到进程虚拟内存空间的物理内存。上图1中,在系统物理内存空间中被着色的部分都是驻留内存。比如,A1、A2、A3和A4是进程A的驻留内存;B1、B2和B3是进程B的驻留内存。进程的驻留内存就是进程实实在在占用的物理内存。一般我们所讲的进程占用了多少内存,其实就是说的占用了多少驻留内存而不是多少虚拟内存。因为虚拟内存大并不意味着占用的物理内存大。

  关于虚拟内存和驻留内存这两个概念我们说到这里。下面一部分我们来看看top命令中VIRT、RES和SHR分别代表什么意思。

top命令中VIRT、RES和SHR的含义

搞清楚了虚拟内存的概念之后解释VIRT的含义就很简单了。VIRT表示的是进程虚拟内存空间大小。对应到图1中的进程A来说就是A1、A2、A3、A4以及灰色部分所有空间的总和。也就是说VIRT包含了在已经映射到物理内存空间的部分和尚未映射到物理内存空间的部分总和。

  RES的含义是指进程虚拟内存空间中已经映射到物理内存空间的那部分的大小。对应到图1中的进程A来说就是A1、A2、A3以及A4几个部分空间的总和。所以说,看进程在运行过程中占用了多少内存应该看RES的值而不是VIRT的值。

  最后来看看SHR所表示的含义。SHR是share(共享)的缩写,它表示的是进程占用的共享内存大小。在上图1中我们看到进程A虚拟内存空间中的A4和进程B虚拟内存空间中的B3都映射到了物理内存空间的A4/B3部分。咋一看很奇怪。为什么会出现这样的情况呢?其实我们写的程序会依赖于很多外部的动态库(.so),比如libc.so、libld.so等等。这些动态库在内存中仅仅会保存/映射一份,如果某个进程运行时需要这个动态库,那么动态加载器会将这块内存映射到对应进程的虚拟内存空间中。多个进展之间通过共享内存的方式相互通信也会出现这样的情况。这么一来,就会出现不同进程的虚拟内存空间会映射到相同的物理内存空间。这部分物理内存空间其实是被多个进程所共享的,所以我们将他们称为共享内存,用SHR来表示。某个进程占用的内存除了和别的进程共享的内存之外就是自己的独占内存了。所以要计算进程独占内存的大小只要用RES的值减去SHR值即可。

进程的smaps文件

  通过top命令我们已经能看出进程的虚拟空间大小(VIRT)、占用的物理内存(RES)以及和其他进程共享的内存(SHR)。但是仅此而已,如果我想知道如下问题:

进程的虚拟内存空间的分布情况,比如heap占用了多少空间、文件映射(mmap)占用了多少空间、stack占用了多少空间?
进程是否有被交换到swap空间的内存,如果有,被交换出去的大小?
mmap方式打开的数据文件有多少页在内存中是脏页(dirty page)没有被写回到磁盘的?
mmap方式打开的数据文件当前有多少页面已经在内存中,有多少页面还在磁盘中没有加载到page cahe中?
等等
  以上这些问题都无法通过top命令给出答案,但是有时候这些问题正是我们在对程序进行性能瓶颈分析和优化时所需要回答的问题。所幸的是,世界上解决问题的方法总比问题本身要多得多。linux通过proc文件系统为每个进程都提供了一个smaps文件,通过分析该文件我们就可以一一回答以上提出的问题。

  在smaps文件中,每一条记录(如下图2所示)表示进程虚拟内存空间中一块连续的区域。其中第一行从左到右依次表示地址范围、权限标识、映射文件偏移、设备号、inode、文件路径。详细解释可以参见understanding-linux-proc-id-maps。

  接下来8个字段的含义分别如下:

  • Size:表示该映射区域在虚拟内存空间中的大小。
  • Rss:表示该映射区域当前在物理内存中占用了多少空间      
  • Shared_Clean:和其他进程共享的未被改写的page的大小
  • Shared_Dirty: 和其他进程共享的被改写的page的大小
  • Private_Clean:未被改写的私有页面的大小。
  • Private_Dirty: 已被改写的私有页面的大小。
  • Swap:表示非mmap内存(也叫anonymous memory,比如malloc动态分配出来的内存)由于物理内存不足被swap到交换空间的大小。
  • Pss:该虚拟内存区域平摊计算后使用的物理内存大小(有些内存会和其他进程共享,例如mmap进来的)。比如该区域所映射的物理内存部分同时也被另一个进程映射了,且该部分物理内存的大小为1000KB,那么该进程分摊其中一半的内存,即Pss=500KB。

screenshot

1
图2. smaps文件中的一条记录

Copy

  有了smap如此详细关于虚拟内存空间到物理内存空间的映射信息,相信大家已经能够通过分析该文件回答上面提出的4个问题。

  最后希望所有读者能够通过阅读本文对进程的虚拟内存和物理内存有一个更加清晰认识,并能更加准确理解top命令关于内存的输出,最后可以通过smaps文件更进一步分析进程使用内存的情况。

作者:無名
链接:https://www.orchome.com/298


本文整理自

仅做个人学习总结所用,遵循CC 4.0 BY-SA版权协议,如有侵权请联系删除!


Python是也是一个面对对象编程(Object-Oriented Programming)的语言,面对对象编程是一种设计思想,意味着我们把对象作为程序的基本单元,而每个对象包含了自己的属性和方法。面向对象编程主要有以下特点:

  1. 封装(Encapsulation):对外部世界隐藏对象的工作细节。
  2. 继承(Inheritance):继承使子类具有父类的各种属性和方法,而不需要编写相同的代码。
  3. 多态(Polymorphism):为不同的数据类型的实体提供统一的接口。

使用OOP有以下的优点:

  1. 提高软件开发的生产效率
  2. 使软件的可维护性更好
  3. 提高软件的质量

在 Python 中,元组、列表和字典等数据类型是对象,函数也是对象。那么,我们能创建自己的对象吗?Of Course!跟其他 OOP 语言类似,我们使用(class)来自定义对象。

类和实例(Class, Instance)

每个类都有自己的属性(attribute)和方法(method),比如一个人的身高、体重和年龄,这些都是属性,而吃饭、说话和洗澡都是方法。(要注意:在class外部定语的可执行函数叫做function,类内部的函数叫做方法method)

类的定义以class为开头,类名的首字母推荐要大写,冒号之后换行缩进紧跟着属性和方法的定义,属性无非就是一个变量的定义,而方法的定义和函数的定义是一样的,也是以def开头。请看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
# class
class Person:
# attribute fields
name = 'William'
age = 45
# method
def greet(self):
print("Hi, my name is " + self.name)
# Create an Object
p1 = Person()
# Call the method
p1.greet()

类的定义是一个具体实例(instance)的设计蓝图,在创建实例的时候,我们只要调用类名,然后加括号就可以了。在greet方法中,我们使用了特殊参数self,它永远指向创建的实例本身,所以self.name就会指向当前被创建实例的name属性。p1.greet()是方法调用的示范,我们只要在实例名后加上句号(.)紧跟着方法名,即可调用实例的方法。

我们也可以在创建好实例之后,对它的属性和方法进行修改:

1
2
3
4
5
6
7
8
# Modify Object Properties
p1.age = 40

# Delete Object Properties
del p1.age

# Delete Objects
del p1

__init__ 是 Python 中的特殊方法(special method),它用于初始化对象。它是一个实例被创建时最先被调用的函数,并且每次创建实例,它的init都会被调用,而且它的第一个参数永远是 self,指向创建的实例本身。(init是initial的简写,顾名思义就是用来初始化的)

1
2
3
4
5
6
7
class Person:
def __init__(self):
self.name = 'Alice'
def greet(self):
print("Hi, my name is " + self.name)
p1 = Person()
p1.greet()

我们也可以在init方法中添加其他参数,这样我们的的初始化能更加灵活和方便,同时在创建实例的时候,需要传入与init方法匹配的参数:

1
2
3
4
5
6
7
class Person:
def __init__(self, init_name):
self.name = init_name
def greet(self):
print("Hi, my name is " + self.name)
p1 = Person("David")
p1.greet()

继承和多态(Inheritance,Polymorphism)

继承

在面向对象编程中,当我们已经创建了一个类,而又想再创建一个与之相似的类,比如添加几个方法,或者修改原来的方法,这时我们不必从头开始,可以从原来的类派生出一个新的类,我们把原来的类称为父类或基类,而派生出的类称为子类,子类继承了父类的所有数据和方法。

让我们看一个简单的例子,首先我们定义一个 Animal 类:

1
2
3
4
5
class Animal():
def __init__(self, name):
self.name = name
def greet(self):
print('Hello, I am %s.' % self.name)

现在,我们想创建一个 Dog 类,比如:

1
2
3
4
5
class Dog():
def __init__(self, name):
self.name = name
def greet(self):
print('WangWang.., I am %s. ' % self.name)

可以看到,Dog 类和 Animal 类几乎是一样的,只是 greet 方法不一样,我们完全没必要创建一个新的类,可以直接创建子类(child class)来继承父类Animal:

1
2
3
class Dog(Animal):
def greet(self):
print('WangWang.., I am %s. ' % self.name)

Dog 类是从 Animal 类继承而来的,Dog 类自动获得了 Animal 类的所有数据和方法,而且还可以对从父类继承来的方法进行修改,调用的方式是一样的:

1
2
3
4
animal = Animal('animal')
animal.greet()
dog = Dog('dog')
dog.greet()

我们也可以在Dog 类中添加新的方法:

1
2
3
4
5
6
7
8
class Dog(Animal):
def greet(self):
print('WangWang.., I am %s. ' % self.name)
def run(self):
print('I am running!')

dog = Dog('dog')
dog.greet()

多态

多态的概念其实不难理解,它是指对不同类型的参数进行相同的操作,根据对象(或类)类型的不同而表现出不同的行为。继承可以拿到父类的所有数据和方法,子类可以重写父类的方法,也可以新增自己特有的方法。有了继承,才有了多态,这样才能实现为不同的数据类型的实体提供统一的接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Animal():
def __init__(self, name):
self.name = name
def greet(self):
print(f'Hello, I am {self.name}.')

class Dog(Animal):
def greet(self):
print(f'WangWang.., I am {self.name}.')

class Cat(Animal):
def greet(self):
print(f'MiaoMiao.., I am {self.name}')

def hello(animal):
animal.greet()

可以看到,catdog 是两个不同的对象,对它们调用 greet 方法,它们会自动调用实际类型的 greet 方法,作出不同的响应:

1
2
3
4
dog = Dog('dog')
hello(dog)
cat = Cat('cat');
hello(cat)

Iterators

在某些情况下,我们希望实例对象可被用于for...in循环,这时我们需要在类中定义__iter____next__方法。其中,__iter()__方法返回迭代器对象本身__next()__方法返回容器的下一个元素,在没有后续元素时会抛出StopIteration异常。(Python 的 for 循环实质上是先通过内置函数 iter() 获得一个迭代器,然后再不断调用 next() 函数实现的。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Fib():
def __init__(self):
self.a, self.b = 0, 1
def __iter__(self):
return self
def __next__(self):
self.a, self.b = self.b, self.a + self.b
return self.a

fib = Fib()
for i in fib:
if i > 10:
break
print(i)# 1, 1, 2, 3, 5, 8

访问限制 underscore

在某些情况下,我们希望限制用户访问对象的属性或方法,也就是希望它是私有的,对外隐蔽。比如,对于上面的例子,我们希望 name 属性在外部不能被访问,我们可以在属性或方法的名称前面加上两个下划线,即 __,以下是对之前例子的改动:

1
2
3
4
5
6
7
8
class Animal():
def __init__(self, name):
self.__name = name
def greet(self):
print(f'Hello, I am self.__name.')

animal = Animal('a1')
animal.__name # error

需要注意的是,在 Python 中,以双下划线开头,并且以双下划线结尾(即 __xxx__)的变量是特殊变量,特殊变量是可以直接访问的。所以,不要用 __name__ 这样的变量名。另外,如果变量名前面只有一个下划线_,表示此变量不要随便访问,虽然它可以直接被访问。

模块调用

有时候一个模块中放不了许多的类,那么我们就要把一些类放入其他的模块中,当我们需要那些类的时候,只需要调用对应模块即可。创建一个模块很简单,只要把代码保存在一个文件中,然后加上后缀.py即可。你可以任意命名文件名,但是必须以.py为后缀。以下我们在animal.py文件中定义了多个不同的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# animal.py
class Animal():
def __init__(self, name):
self.name = name
def greet(self):
print(f'Hello, I am {self.name}.')

class Dog(Animal):
def greet(self):
print(f'WangWang.., I am {self.name}.')

class Cat(Animal):
def greet(self):
print(f'MiaoMiao.., I am {self.name}')

如果要在其他文件中调用单个类的不同方法,使用import语句即可:

1
2
3
from animal import Animal
animal = Animal('animal')
animal.greet()

调用多个类:

1
2
3
4
5
from animal import Dog, Cat
dog = Dog('duoduo')
dog.greet()
cat = Cat('Kitty')
cat.greet()

其他调用方法:

1
2
3
4
5
6
7
8
9
10
11
# importing an entire module
import animal
cat = animal.Cat('Kitty')

# import all classes from a model
from animal import *
cat = Cat('Kitty')

# Using Aliases
from animal import Cat as C
cat = C('Kitty')

Python标准库(Python Standard Library)

现在我们已经对函数和类有基础的认识了,我们来聊聊如何使用别人编写好的库,Python有自己的标准库,我们下载Python的时候,它们已经被默认安装了,其中有很多现成的模块供我们调用,我们试一下用random模块做做一些随机数操作:

1
2
3
4
5
6
7
8
from random import randint, choice
# Generate a random number between 1 and 6
print(randint(1, 6))

players = ['alice', 'david', 'charles', 'michael']
# choose a randomly chosen element
random_pick = choice(players)
print(random_pick)

PIP包管理器

有的时候,我们想要使用别人编写好的模块,我们可以通过包管理器下载别人的包(package),包是由很多module组成的,来实现某种功能。库(library)是抽象概念,也可以是各种模块组成。而Python中最流行的包管理器就是pip。pip3的安装请大家自行搜索,网上有很多的教程,以下我会使用一个和图像处理相关的模块。

PIL(Python Image Library)是Python中的标准图像处理库。PIL功能强大,而且API非常简单易用。由于PIL仅支持到Python 2.7,加上年久失修,于是一群志愿者在PIL的基础上创建了兼容的版本,名字叫Pillow,支持最新Python 3.x,又加入了许多新特性,因此,我们可以直接安装使用Pillow。以下是安装pillow的命令行:

1
pip3 install pillow

然后我们可以创建一个Python,使用PIL的模块,写出一段可以把照片弄模糊的代码:

1
2
3
4
5
6
7
from PIL import Image, ImageFilter

# Open an image
im = Image.open('test.jpg')
# Use bluring filter
im2 = im.filter(ImageFilter.BLUR)
im2.save('blur.jpg', 'jpeg')

包管理器让我们可以直接使用前人的轮子,简化我们的开发过程。

类的编写规范 Styling Classes

类的名字最好使用骆驼命名法(CamelCase),也就是让每个单词的第一个字母大写,不使用下划线分割单词。实例和模块的名字最好都使用Snake case,也就是所有字母都小写,然后使用下划线分割。当然这不是强制的,但这是工业界比较合理的命名规范,大家可以参照一下Google的Python代码规范:http://google.github.io/styleguide/pyguide.html

您可以使用空行来组织代码,但不要过度使用它们。在一个类中,您可以在方法之间使用一个空行,而在一个模块中,您可以使用两个空行来分隔类。

如果需要从标准库和编写的模块中导入模块,请首先将标准库模块的调用语句写在最前面。然后添加一个空行,再调用自己编写的模块。在具有多个import语句的程序中,此约定使查看程序中使用的不同模块的来源更加容易。


本文整理自

Python类和模块(Class, Module)

仅做个人学习总结所用,遵循CC 4.0 BY-SA版权协议,如有侵权请联系删除!


在sql的使用中,我们总是碰到需要删除重复数据的情况,但是又不能全部删除完,必须要保留至少一个重复的数据。重复的记录根据两个字段a2,a3判断(实际使用中可以拓展为多个)

eg:表A

a1 a2 a3
1 1 1
2 1 2
3 2 2
4 2 2
5 3 3
6 2 2

在上述的表中第三行和第四行重复,我们要选择一行删除,流程如下:

  1. 选择重复的行:
1
2
3
select *,count(*) 
from A group by a2,a3
having count(*)>1;

结果如下:

a1 a2 a3 count(*)
3 2 2 3
  1. 使用in来找到我们想要的ID
1
2
3
4
5
6
7
SELECT *
FROM A
WHERE (a2,a3) IN
(SELECT A.`a2`,A.`a3`
FROM A
GROUP BY A.`a2`,A.`a3`
HAVING COUNT(*)>1)

得到的结果如下:

|a1|a2|a3|
| — | — |
|3|2|2|
|4|2|2|
|6|2|2|
那么后面就很好办了:

3.选出要删除的值:

1
2
3
4
5
6
7
8
9
10
11
12
SELECT * 
FROM A
WHERE (a2, a3) IN
(SELECT `a2`,`a3`
FROM A
GROUP BY A.`a2`,A.`a3`
HAVING COUNT(*) > 1)
AND a1 NOT IN
(SELECT MIN(a1)
FROM A
GROUP BY A.`a2`,A.`a3`
HAVING COUNT(*) > 1) ;

结果是保留a1最小的值,其他选项全部选出,
请注意此时并不是将Select 改为delete就可以了,如果你直接这样子改的话,会报如下错误:

You can’t specify target table ‘A’ for update in FROM clause

该错误提示你,不能先select出同一表中的某些值,再update这个表(在同一语句中)。所以要稍微修改一下。

  1. 删除值
    sql语句如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//创建中间表
CREATE TABLE F(a1 INTEGER,a2 INTEGER,a3 INTEGER);
//将要删除的数据插入中间表
INSERT INTO F (
SELECT *
FROM A
WHERE (a2, a3) IN (SELECT `a2`,`a3`
FROM A GROUP BY A.`a2`,A.`a3`
HAVING COUNT(*) > 1)
AND a1 NOT IN
(SELECT MIN(a1) FROM A
GROUP BY A.`a2`,A.`a3`
HAVING COUNT(*) > 1)) ;
//删除中间表
DELETE FROM A WHERE a1 IN (SELECT a1 FROM F);
SELECT *FROM A;

结果如下:

|a1|a2|a3|
|—|—|
|1|1|1|
|2|1|2|
|3|2|2|
|5|3|3|

完毕

注:如果说不用保留一行数据的话那么就简单多了,只需要一个很简单的sql语句:

1
DELETE FROM A WHERE (a2,a3) IN (SELECT a2,a3 FROM A GROUP BY a2,a3 HAVING COUNT(*)>1)

本文整理自

SQL删除重复数据,只保留一行

仅做个人学习总结所用,遵循CC 4.0 BY-SA版权协议,如有侵权请联系删除!


方法1: 直接使用数据库提供的SQL语句

  • 语句样式: MySQL中,可用如下方法: SELECT * FROM 表名称 LIMIT M,N
  • 适应场景: 适用于数据量较少的情况(元组百/千级)
  • 原因/缺点: 全表扫描,速度会很慢 且 有的数据库结果集返回不稳定(如某次返回1,2,3,另外的一次返回2,1,3). Limit限制的是从结果集的M位置处取出N条输出,其余抛弃.

方法2: 建立主键或唯一索引, 利用索引(假设每页10条)

  • 语句样式: MySQL中,可用如下方法: SELECT * FROM 表名称 WHERE id_pk > (pageNum*10) LIMIT M
  • 适应场景: 适用于数据量多的情况(元组数上万)
  • 原因: 索引扫描,速度会很快. 有朋友提出: 因为数据查询出来并不是按照pk_id排序的,所以会有漏掉数据的情况,只能方法3

方法3: 基于索引再排序

  • 语句样式: MySQL中,可用如下方法: SELECT * FROM 表名称 WHERE id_pk > (pageNum*10) ORDER BY id_pk ASC LIMIT M
  • 适应场景: 适用于数据量多的情况(元组数上万). 最好ORDER BY后的列对象是主键或唯一所以,使得ORDERBY操作能利用索引被消除但结果集是稳定的(稳定的含义,参见方法1)
  • 原因: 索引扫描,速度会很快. 但MySQL的排序操作,只有ASC没有DESC(DESC是假的,未来会做真正的DESC,期待…).

方法4: 基于索引使用prepare

第一个问号表示pageNum,第二个?表示每页元组数

  • 语句样式: MySQL中,可用如下方法: PREPARE stmt_name FROM SELECT * FROM 表名称 WHERE id_pk > (?* ?) ORDER BY id_pk ASC LIMIT M
  • 适应场景: 大数据量
  • 原因: 索引扫描,速度会很快. prepare语句又比一般的查询语句快一点。

方法5: 利用MySQL支持ORDER操作可以利用索引快速定位部分元组,避免全表扫描

比如: 读第1000到1019行元组(pk是主键/唯一键).

1
SELECT * FROM your_table WHERE pk>=1000 ORDER BY pk ASC LIMIT 0,20

方法6: 利用子查询/连接+索引快速定位元组的位置,然后再读取元组.

比如(id是主键/唯一键,蓝色字体时变量)

利用子查询示例:

1
2
3
SELECT * FROM your_table WHERE id <=
(SELECT id FROM your_table ORDER BY id desc LIMIT ($page-1)*$pagesize ORDER BY id desc
LIMIT $pagesize

利用连接示例:

1
2
3
SELECT * FROM your_table AS t1
JOIN (SELECT id FROM your_table ORDER BY id desc LIMIT ($page-1)*$pagesize AS t2
WHERE t1.id <= t2.id ORDER BY t1.id desc LIMIT $pagesize;

mysql大数据量使用limit分页,随着页码的增大,查询效率越低下。

测试实验

1. 直接用limit start, count分页语句, 也是我程序中用的方法:
1
select * from product limit start, count

当起始页较小时,查询没有性能问题,我们分别看下从10, 100, 1000, 10000开始分页的执行时间(每页取20条)。

如下:

1
2
3
4
select * from product limit 10, 20   0.016
select * from product limit 100, 20 0.016
select * from product limit 1000, 20 0.047
select * from product limit 10000, 20 0.094

我们已经看出随着起始记录的增加,时间也随着增大, 这说明分页语句limit跟起始页码是有很大关系的,那么我们把起始记录改为40w看下(也就是记录的一般左右)

1
select * from product limit 400000, 20   3.229

再看我们取最后一页记录的时间

1
select * from product limit 866613, 20   37.44

像这种分页最大的页码页显然这种时间是无法忍受的。

从中我们也能总结出两件事情:

  1. limit语句的查询时间与起始记录的位置成正比
  2. mysql的limit语句是很方便,但是对记录很多的表并不适合直接使用。
2. 对limit分页问题的性能优化方法

利用表的覆盖索引来加速分页查询

我们都知道,利用了索引查询的语句中如果只包含了那个索引列(覆盖索引),那么这种情况会查询很快。

因为利用索引查找有优化算法,且数据就在查询索引上面,不用再去找相关的数据地址了,这样节省了很多时间。另外Mysql中也有相关的索引缓存,在并发高的时候利用缓存就效果更好了。

在我们的例子中,我们知道id字段是主键,自然就包含了默认的主键索引。现在让我们看看利用覆盖索引的查询效果如何。

这次我们之间查询最后一页的数据(利用覆盖索引,只包含id列),如下:

1
select id from product limit 866613, 20 0.2

相对于查询了所有列的37.44秒,提升了大概100多倍的速度

那么如果我们也要查询所有列,有两种方法,一种是id>=的形式,另一种就是利用join,看下实际情况:

1
SELECT * FROM product WHERE ID > =(select id from product limit 866613, 1) limit 20

查询时间为0.2秒!

另一种写法

1
SELECT * FROM product a JOIN (select id from product limit 866613, 20) b ON a.ID = b.id

查询时间也很短!

3. 复合索引优化方法

MySql 性能到底能有多高?MySql 这个数据库绝对是适合dba级的高手去玩的,一般做一点1万篇新闻的小型系统怎么写都可以,用xx框架可以实现快速开发。可是数据量到了10万,百万至千万,他的性能还能那么高吗?一点小小的失误,可能造成整个系统的改写,甚至更本系统无法正常运行!好了,不那么多废话了。

用事实说话,看例子:

数据表 collect ( id, title ,info ,vtype) 就这4个字段,其中 title 用定长,info 用text, id 是逐渐,vtype是tinyint,vtype是索引。这是一个基本的新闻系统的简单模型。现在往里面填充数据,填充10万篇新闻。最后collect 为 10万条记录,数据库表占用硬1.6G。

OK ,看下面这条sql语句:

1
select id,title from collect limit 1000,10;

很快;基本上0.01秒就OK,再看下面的

1
select id,title from collect limit 90000,10;

从9万条开始分页,结果?

8-9秒完成,my god 哪出问题了?其实要优化这条数据,网上找得到答案。看下面一条语句:

1
select id from collect order by id limit 90000,10;

很快,0.04秒就OK。为什么?因为用了id主键做索引当然快。网上的改法是:

1
select id,title from collect where id>=(select id from collect order by id limit 90000,1) limit 10;

这就是用了id做索引的结果。可是问题复杂那么一点点,就完了。看下面的语句

select id from collect where vtype=1 order by id limit 90000,10; 很慢,用了8-9秒!

到了这里我相信很多人会和我一样,有崩溃感觉!vtype 做了索引了啊?怎么会慢呢?vtype做了索引是不错,你直接

1
select id from collect where vtype=1 limit 1000,10;

是很快的,基本上0.05秒,可是提高90倍,从9万开始,那就是0.05*90=4.5秒的速度了。和测试结果8-9秒到了一个数量级。

从这里开始有人提出了分表的思路,这个和dis #cuz 论坛是一样的思路。思路如下:

建一个索引表:t (id,title,vtype) 并设置成定长,然后做分页,分页出结果再到 collect 里面去找info 。是否可行呢?实验下就知道了。

10万条记录到 t(id,title,vtype) 里,数据表大小20M左右。用

1
select id from t where vtype=1 order by id limit 90000,10;

很快了。基本上0.1-0.2秒可以跑完。为什么会这样呢?我猜想是因为collect 数据太多,所以分页要跑很长的路。limit 完全和数据表的大小有关的。其实这样做还是全表扫描,只是因为数据量小,只有10万才快。OK, 来个疯狂的实验,加到100万条,测试性能。加了10倍的数据,马上t表就到了200多M,而且是定长。还是刚才的查询语句,时间是0.1-0.2秒完成!分表性能没问题?

错!因为我们的limit还是9万,所以快。给个大的,90万开始

1
select id from t where vtype=1 order by id limit 900000,10;

看看结果,时间是1-2秒!why ?

分表了时间还是这么长,非常之郁闷!有人说定长会提高limit的性能,开始我也以为,因为一条记录的长度是固定的,mysql 应该可以算出90万的位置才对啊?可是我们高估了mysql 的智能,他不是商务数据库,事实证明定长和非定长对limit影响不大?怪不得有人说discuz到了100万条记录就会很慢,我相信这是真的,这个和数据库设计有关!

难道MySQL 无法突破100万的限制吗???到了100万的分页就真的到了极限?

答案是:NO 为什么突破不了100万是因为不会设计mysql造成的。下面介绍非分表法,来个疯狂的测试!一张表搞定100万记录,并且10G 数据库,如何快速分页!

好了,我们的测试又回到 collect表,开始测试结论是:

30万数据,用分表法可行,超过30万他的速度会慢道你无法忍受!当然如果用分表+我这种方法,那是绝对完美的。但是用了我这种方法后,不用分表也可以完美解决!

答案就是:复合索引!有一次设计mysql索引的时候,无意中发现索引名字可以任取,可以选择几个字段进来,这有什么用呢?

开始的

1
select id from collect order by id limit 90000,10;

这么快就是因为走了索引,可是如果加了where 就不走索引了。抱着试试看的想法加了 search(vtype,id) 这样的索引。

然后测试

1
select id from collect where vtype=1 limit 90000,10;

非常快!0.04秒完成!

再测试:

1
select id ,title from collect where vtype=1 limit 90000,10;

非常遗憾,8-9秒,没走search索引!

再测试:search(id,vtype),还是select id 这个语句,也非常遗憾,0.5秒。

综上:如果对于有where 条件,又想走索引用limit的,必须设计一个索引,将where 放第一位,limit用到的主键放第2位,而且只能select 主键!

完美解决了分页问题了。可以快速返回id就有希望优化limit , 按这样的逻辑,百万级的limit 应该在0.0x秒就可以分完。看来mysql 语句的优化和索引时非常重要的!


本文整理自

MySQL 百万级数据量分页查询方法及其优化

仅做个人学习总结所用,遵循CC 4.0 BY-SA版权协议,如有侵权请联系删除!


今天来带大家研究一下Linux内存管理。对于精通 CURD 的业务同学,内存管理好像离我们很远,但这个知识点虽然冷门(估计很多人学完根本就没机会用上)但绝对是基础中的基础,这就像武侠中的内功修炼,学完之后看不到立竿见影的效果,但对你日后的开发工作是大有裨益的,因为你站的更高了。

前提约定:本文讨论技术内容前提,操作系统环境都是 x86架构的 32 位 Linux系统。

虚拟地址

即使是现代操作系统中,内存依然是计算机中很宝贵的资源,看看你电脑几个T固态硬盘,再看看内存大小就知道了。为了充分利用和管理系统内存资源,Linux采用虚拟内存管理技术,利用虚拟内存技术让每个进程都有4GB 互不干涉的虚拟地址空间。

进程初始化分配和操作的都是基于这个「虚拟地址」,只有当进程需要实际访问内存资源的时候才会建立虚拟地址和物理地址的映射,调入物理内存页。

打个不是很恰当的比方。这个原理其实和现在的某某网盘一样,假如你的网盘空间是1TB,真以为就一口气给了你这么大空间吗?那还是太年轻,都是在你往里面放东西的时候才给你分配空间,你放多少就分多少实际空间给你,但你和你朋友看起来就像大家都拥有1TB空间一样。

虚拟地址的好处

  • 避免用户直接访问物理内存地址,防止一些破坏性操作,保护操作系统
  • 每个进程都被分配了4GB的虚拟内存,用户程序可使用比实际物理内存更大的地址空间

4GB 的进程虚拟地址空间被分成两部分:「用户空间」和「内核空间」

用户空间内核空间

物理地址

上面章节我们已经知道不管是用户空间还是内核空间,使用的地址都是虚拟地址,当需进程要实际访问内存的时候,会由内核的「请求分页机制」产生「缺页异常」调入物理内存页。

把虚拟地址转换成内存的物理地址,这中间涉及利用MMU 内存管理单元(Memory Management Unit ) 对虚拟地址分段和分页(段页式)地址转换,关于分段和分页的具体流程,这里不再赘述,可以参考任何一本计算机组成原理教材描述。

段页式内存管理地址转换

Linux 内核会将物理内存分为3个管理区,分别是:

ZONE_DMA

DMA内存区域。包含0MB~16MB之间的内存页框,可以由老式基于ISA的设备通过DMA使用,直接映射到内核的地址空间。

ZONE_NORMAL

普通内存区域。包含16MB~896MB之间的内存页框,常规页框,直接映射到内核的地址空间。

ZONE_HIGHMEM

高端内存区域。包含896MB以上的内存页框,不进行直接映射,可以通过永久映射和临时映射进行这部分内存页框的访问。

物理内存区划分

用户空间

用户进程能访问的是「用户空间」,每个进程都有自己独立的用户空间,虚拟地址范围从从 0x000000000xBFFFFFFF 总容量3G 。

用户进程通常只能访问用户空间的虚拟地址,只有在执行内陷操作或系统调用时才能访问内核空间。

进程与内存

进程(执行的程序)占用的用户空间按照「 访问属性一致的地址空间存放在一起 」的原则,划分成 5个不同的内存区域。 访问属性指的是“可读、可写、可执行等 。

  • 代码段

    代码段是用来存放可执行文件的操作指令,可执行程序在内存中的镜像。代码段需要防止在运行时被非法修改,所以只准许读取操作,它是不可写的。

  • 数据段

    数据段用来存放可执行文件中已初始化全局变量,换句话说就是存放程序静态分配的变量和全局变量。

  • BSS段

    BSS段包含了程序中未初始化的全局变量,在内存中 bss 段全部置零。

  • heap

    堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)

  • stack

    栈是用户存放程序临时创建的局部变量,也就是函数中定义的变量(但不包括 static 声明的变量,static意味着在数据段中存放变量)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进先出特点,所以栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。

上述几种内存区域中数据段、BSS 段、堆通常是被连续存储在内存中,在位置上是连续的,而代码段和栈往往会被独立存放。堆和栈两个区域在 i386 体系结构中栈向下扩展、堆向上扩展,相对而生。

程序内存区域分段

你也可以再linux下用size 命令查看编译后程序的各个内存区域大小:

1
2
3
[lemon ~]# size /usr/local/sbin/sshd
text data bss dec hex filename
1924532 12412 426896 2363840 2411c0 /usr/local/sbin/sshd

内核空间

x86 32 位系统里,Linux 内核地址空间是指虚拟地址从 0xC0000000 开始到 0xFFFFFFFF 为止的高端内存地址空间,总计 1G 的容量, 包括了内核镜像、物理页面表、驱动程序等运行在内核空间 。

内核空间细分区域.

直接映射区

直接映射区 Direct Memory Region:从内核空间起始地址开始,最大896M的内核空间地址区间,为直接内存映射区。

直接映射区的896MB的「线性地址」直接与「物理地址」的前896MB进行映射,也就是说线性地址和分配的物理地址都是连续的。内核地址空间的线性地址0xC0000001所对应的物理地址为0x00000001,它们之间相差一个偏移量PAGE_OFFSET = 0xC0000000

该区域的线性地址和物理地址存在线性转换关系「线性地址 = PAGE_OFFSET + 物理地址」也可以用 virt_to_phys()函数将内核虚拟空间中的线性地址转化为物理地址。

高端内存线性地址空间

内核空间线性地址从 896M 到 1G 的区间,容量 128MB 的地址区间是高端内存线性地址空间,为什么叫高端内存线性地址空间?下面给你解释一下:

前面已经说过,内核空间的总大小 1GB,从内核空间起始地址开始的 896MB 的线性地址可以直接映射到物理地址大小为 896MB 的地址区间。退一万步,即使内核空间的1GB线性地址都映射到物理地址,那也最多只能寻址 1GB 大小的物理内存地址范围。

请问你现在你家的内存条多大?快醒醒都 0202 年了,一般 PC 的内存都大于 1GB 了吧!

img

所以,内核空间拿出了最后的 128M 地址区间,划分成下面三个高端内存映射区,以达到对整个物理地址范围的寻址。而在 64 位的系统上就不存在这样的问题了,因为可用的线性地址空间远大于可安装的内存。

动态内存映射区

vmalloc Region 该区域由内核函数vmalloc来分配,特点是:线性空间连续,但是对应的物理地址空间不一定连续。 vmalloc 分配的线性地址所对应的物理页可能处于低端内存,也可能处于高端内存。

永久内存映射区

Persistent Kernel Mapping Region 该区域可访问高端内存。访问方法是使用 alloc_page (_GFP_HIGHMEM) 分配高端内存页或者使用kmap函数将分配到的高端内存映射到该区域。

固定映射区

Fixing kernel Mapping Region 该区域和 4G 的顶端只有 4k 的隔离带,其每个地址项都服务于特定的用途,如 ACPI_BASE 等。

在这里插入图片描述

回顾一下

上面讲的有点多,先别着急进入下一节,在这之前我们再来回顾一下上面所讲的内容。如果认真看完上面的章节,我这里再画了一张图,现在你的脑海中应该有这样一个内存管理的全局图。

内核空间用户空间全图

内存数据结构

要让内核管理系统中的虚拟内存,必然要从中抽象出内存管理数据结构,内存管理操作如「分配、释放等」都基于这些数据结构操作,这里列举两个管理虚拟内存区域的数据结构。

用户空间内存数据结构

在前面「进程与内存」章节我们提到,Linux进程可以划分为 5 个不同的内存区域,分别是:代码段、数据段、BSS、堆、栈,内核管理这些区域的方式是,将这些内存区域抽象成vm_area_struct的内存管理对象。

vm_area_struct是描述进程地址空间的基本管理单元,一个进程往往需要多个vm_area_struct来描述它的用户空间虚拟地址,需要使用「链表」和「红黑树」来组织各个vm_area_struct

链表用于需要遍历全部节点的时候用,而红黑树适用于在地址空间中定位特定内存区域。内核为了内存区域上的各种不同操作都能获得高性能,所以同时使用了这两种数据结构。

用户空间进程的地址管理模型:

wm_arem_struct

内核空间动态分配内存数据结构

在内核空间章节我们提到过「动态内存映射区」,该区域由内核函数vmalloc来分配,特点是:线性空间连续,但是对应的物理地址空间不一定连续。 vmalloc 分配的线性地址所对应的物理页可能处于低端内存,也可能处于高端内存。

vmalloc 分配的地址则限于vmalloc_startvmalloc_end之间。每一块vmalloc分配的内核虚拟内存都对应一个vm_struct结构体,不同的内核空间虚拟地址之间有4k大小的防越界空闲区间隔区。与用户空间的虚拟地址特性一样,这些虚拟地址与物理内存没有简单的映射关系,必须通过内核页表才可转换为物理地址或物理页,它们有可能尚未被映射,当发生缺页时才真正分配物理页面。

动态内存映射

总结一下

Linux内存管理是一个非常复杂的系统,本文所述只是冰山一角,从宏观角度给你展现内存管理的全貌,但一般来说,这些知识在你和面试官聊天的时候还是够用的,当然我也希望大家能够通过读书了解更深层次的原理。


本文整理自

面试问了解Linux内存管理吗?10张图给你安排的明明白白!

仅做个人学习总结所用,遵循CC 4.0 BY-SA版权协议,如有侵权请联系删除!


不知道从什么时候开始,网上流传着这么一个说法:

MySQL的WHERE子句中包含 IS NULL、IS NOT NULL、!= 这些条件时便不能使用索引查询,只能使用全表扫描。

这种说法愈演愈烈,甚至被很多同学奉为真理。咱啥话也不说,举个例子。假如我们有个表s1,结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CREATE TABLE s1 (
id INT NOT NULL AUTO_INCREMENT,
key1 VARCHAR(100),
key2 VARCHAR(100),
key3 VARCHAR(100),
key_part1 VARCHAR(100),
key_part2 VARCHAR(100),
key_part3 VARCHAR(100),
common_field VARCHAR(100),
PRIMARY KEY (id),
KEY idx_key1 (key1),
KEY idx_key2 (key2),
KEY idx_key3 (key3),
KEY idx_key_part(key_part1, key_part2, key_part3)
) Engine=InnoDB CHARSET=utf8;

这个表里有10000条记录:

1
2
3
4
5
6
7
mysql> SELECT COUNT(*) FROM s1;
+----------+
| COUNT(*) |
+----------+
| 10000 |
+----------+
1 row in set (0.00 sec)

下边我们直接贴几个图:

image_1dfqmch3p1f881eqmvb29gk1tom6e.png-40.7kB

image_1dfqmbf5616fb1g0b1trv13elsst61.png-40.7kB

image_1dfqmarklhku131o18rs15281min5k.png-40.2kB

上边几个查询语句的WHERE子句中用了IS NULLIS NOT NULL!=这些条件,但是从它们的执行计划中可以看出来,这些语句都采用了相应的二级索引执行查询,而不是使用所谓的全表扫描,谣言不攻自破。当然,戳破这些谣言并不是本文的目的,本文来更细致的分析一下这些查询到底是怎么执行的。

NULL值是怎么在记录中存储的

在MySQL中,每一条记录都有它固定的格式,我们以InnoDB存储引擎的Compact行格式为例,来看一下NULL值是怎样存储的。在Compact行格式下,一条记录是由下边这几个部分构成的:

image_1dfqmp377ebqgqf15e1tuv1qri6r.png-72.8kB

为了故事的顺利发展,我们新建一个称之为record_format_demo的表:

1
2
3
4
5
6
CREATE TABLE record_format_demo (
c1 VARCHAR(10),
c2 VARCHAR(10) NOT NULL,
c3 CHAR(10),
c4 VARCHAR(10)
) CHARSET=ascii ROW_FORMAT=COMPACT;

因为我们的重点是NULL值是如何存储在记录中的,所以重点唠叨一下行格式的NULL值列表部分,其他的部分可以到小册中查看。存储NULL值的过程如下:

  1. 首先统计表中允许存储NULL的列有哪些。

    我们前边说过,主键列、被NOT NULL修饰的列都是不可以存储NULL值的,所以在统计的时候不会把这些列算进去。比方说表record_format_demo的3个列c1c3c4都是允许存储NULL值的,而c2列是被NOT NULL修饰,不允许存储NULL值。

  2. 如果表中没有允许存储NULL的列,则NULL值列表也不存在了,否则将每个允许存储NULL的列对应一个二进制位,二进制位按照列的顺序逆序排列,二进制位表示的意义如下:

    • 二进制位的值为1时,代表该列的值为NULL
    • 二进制位的值为0时,代表该列的值不为NULL

    因为表record_format_demo有3个值允许为NULL的列,所以这3个列和二进制位的对应关系就是这样:

image_1dfqn3dt810cpog1l4710q637q78.png-19.3kB

再一次强调,二进制位按照列的顺序逆序排列,所以第一个列c1和最后一个二进制位对应。

  1. 设计InnoDB的大叔规定NULL值列表必须用整数个字节的位表示,如果使用的二进制位个数不是整数个字节,则在字节的高位补0。

    record_format_demo只有3个值允许为NULL的列,对应3个二进制位,不足一个字节,所以在字节的高位补0,效果就是这样:

image_1dfqn48071s0i104314m31isi1ks97l.png-37.7kB

以此类推,如果一个表中有9个允许为NULL,那这个记录的NULL值列表部分就需要2个字节来表示了。

假设我们现在向record_format_demo表中插入一条记录:

1
2
INSERT INTO record_format_demo(c1, c2, c3, c4)
VALUES('eeee', 'fff', NULL, NULL);

这条记录的c1c3c4这3个列中c3c4的值都为NULL,所以这3个列对应的二进制位的情况就是:

image_1dfqng28g7df1l68r4737p3a882.png-38.6kB

所以这记录的NULL值列表用十六进制表示就是:0x06

键值为NULL的记录是怎么在B+树中存放的

对于InnoDB存储引擎来说,记录都是存储在页面中的(一个页面默认是16KB大小),这些页面可以作为B+树的节点而组成一个索引,类似这种样子(只是用下边的图举个B+树的例子而已,跟我们上边列举的表没关系):

image_1dfqnp86e76v16h31l7qk21v458f.png-296kB

聚簇索引和二级索引都对应着像上图一样的B+树(也就是说有多少个索引就有多少棵对应的B+树),不过:

  • 对于聚簇索引索引来说,页面中的记录是按照主键值进行排序的;而对于二级索引来说,页面中的记录是按照给定的索引列的值进行排序的。
  • 对于聚簇索引来说,B+树每一层节点(页面)都是按照页中记录的主键值大小进行排序的;而对于二级索引来说,B+树每一层节点(页面)都是按照页中记录的给定的索引列的值进行排序的。
  • 对于聚簇索引来说,B+树叶子节点对应的页面中存储的是完整的用户记录(就是一条记录中包含我们定义的所有列值,还包含一些InnoDB自己添加的一些隐藏列);而对于二级索引来说,B+树叶子节点对应的页面中存储的只是索引列的值 + 主键值

按规定,一条记录的主键值不允许存储NULL值,所以下边语句中的WHERE子句结果肯定为FALSE

1
SELECT * FROM tbl_name WHERE primary_key IS NULL;

像这样的语句优化器自己就能判定出WHERE子句必定为NULL,所以压根儿不会去执行它,不信我们看(Extra信息提示WHERE子句压根儿不成立):

image_1dfqofhth2941mtorq72f1nqf8s.png-35.5kB

对于二级索引来说,索引列的值可能为NULL。那对于索引列值为NULL的二级索引记录来说,它们被放在B+树的哪里呢?答案是:放在B+树的最左边。比方说我们有如下查询语句:

1
SELECT * FROM s1 WHERE key1 IS NULL;

那它的查询示意图就如下所示:

image_1dfqqjqnahm6176uta91j7j1q8ram.png-52.9kB

从图中可以看出,对于s1表的二级索引idx_key1来说,值为NULL的二级索引记录都被放在了B+树的最左边,这是因为设计InnoDB的大叔有这样的规定:

We define the SQL null to be the smallest possible value of a field.

也就是说他们把SQL中的NULL值认为是列中最小的值。

在通过二级索引idx_key1对应的B+树快速定位到叶子节点中符合条件的最左边的那条记录后,也就是本例中id值为521的那条记录之后,就可以顺着每条记录都有的next_record属性沿着由记录组成的单向链表去获取记录了,直到某条记录的key1列不为NULL。

小贴士: 通过B+树快速定位到叶子节点的记录的过程是靠一个所谓的页目录(Page Directory)做到的,不过这不是本文的重点,大家可以到小册中翻看,都有详细解释。

使不使用索引的依据到底是什么?

那既然IS NULLIS NOT NULL!=这些条件都可能使用到索引,那到底什么时候索引,什么时候采用全表扫描呢?

答案很简单:成本。当然,关于如何定量的计算使用某个索引执行查询的成本比较复杂,我们在小册中花了很大的篇幅来唠叨了。不过因为篇幅有限,我们在这里只准备定性的分析一下。对于使用二级索引进行查询来说,成本组成主要有两个方面:

  • 读取二级索引记录的成本
  • 将二级索引记录执行回表操作,也就是到聚簇索引中找到完整的用户记录的操作所付出的成本。

很显然,要扫描的二级索引记录条数越多,那么需要执行的回表操作的次数也就越多,达到了某个比例时,使用二级索引执行查询的成本也就超过了全表扫描的成本(举一个极端的例子,比方说要扫描的全部的二级索引记录,那就要对每条记录执行一遍回表操作,自然不如直接扫描聚簇索引来的快)。

所以MySQL优化器在真正执行查询之前,对于每个可能使用到的索引来说,都会预先计算一下需要扫描的二级索引记录的数量,比方说对于下边这个查询:

1
SELECT * FROM s1 WHERE key1 IS NULL;

优化器会分析出此查询只需要查找key1值为NULL的记录,然后访问一下二级索引idx_key1,看一下值为NULL的记录有多少(如果符合条件的二级索引记录数量较少,那么统计结果是精确的,如果太多的话,会采用一定的手段计算一个模糊的值,当然算法也比较麻烦,我们就不展开说了,小册里有说),这种在查询真正执行前优化器就率先访问索引来计算需要扫描的索引记录数量的方式称之为index dive。当然,对于某些查询,比方说WHERE子句中有IN条件,并且IN条件中包含许多参数的话,比方说这样:

1
SELECT * FROM s1 WHERE key1 IN ('a', 'b', 'c', ... , 'zzzzzzz');

这样的话需要统计的key1值所在的区间就太多了,这样就不能采用index dive的方式去真正的访问二级索引idx_key1,而是需要采用之前在背地里产生的一些统计数据去估算匹配的二级索引记录有多少条(很显然根据统计数据去估算记录条数比index dive的方式精确性差了很多)。

反正不论采用index dive还是依据统计数据估算,最终要得到一个需要扫描的二级索引记录条数,如果这个条数占整个记录条数的比例特别大,那么就趋向于使用全表扫描执行查询,否则趋向于使用这个索引执行查询。

理解了这个也就好理解为什么在WHERE子句中出现IS NULLIS NOT NULL!=这些条件仍然可以使用索引,本质上都是优化器去计算一下对应的二级索引数量占所有记录数量的比值而已。

不信谣,不传谣

大家可以看到,MySQL中决定使不使用某个索引执行查询的依据很简单:就是成本够不够小。而不是是否在WHERE子句中用了IS NULLIS NOT NULL!=这些条件。大家以后也多多辟谣吧,没那么复杂,只是一个成本而已。


本文整理自

MySQL中IS NULL、IS NOT NULL、!=不能用索引?胡扯!

仅做个人学习总结所用,遵循CC 4.0 BY-SA版权协议,如有侵权请联系删除!


Java应用上线前,常常需要估算所需的内存,从而设置正确的内存选项参数。正确计算Java对象所占内存从而估算应用的整体所占内存,就显得很有必要。那么,如何计算Java对象所占的内存呢?

1.Java对象的内存布局

计算Java对象所占内存,首先需要了解Java对象的内存布局。一个Java对象在内存中可以分为三部分:对象头、实例数据和对齐填充。关于对象头的详细介绍可查看这篇文章;实例数据即Java的成员字段,包括基本类型和对象引用;对齐填充并不必须存在,只用作占位对齐字节。一个对象的内存布局示意如下:

1
2
3
4
5
|---------------------------|-----------------|---------|
| Object Header | Instance Data | Padding |
|-----------|---------------|-----------------|---------|
| Mark Word | Klass Pointer | field1|filed2| | Padding |
|-----------|---------------|-----------------|---------|

需要注意以下几点:

  1. 对象默认以8字节对齐,即对象所占空间必须是8的整数倍。默认对齐字节数可以使用选项-XX:ObjectAlignmentInBytes=num设置,最小值为8,最大值为256。
  2. 为了避免空间浪费,实例数据会进行重排序,排序的优先级为: long = double > int = float > char = short > byte > boolean > object reference。
  3. 继承体系里不同类的字段不会混合在一起,父类成员字段分配之后才会分配子类,每个类里的字段遵循第2条规则。
  4. 继承体系里不同类间需要8字节对齐。
  5. 在继承体系中,父类层次中有至少4字节的空闲而子类含有4字节及其以下的字段,将按优先级:int = float > char = short > byte > boolean > object reference填充这4字节。对象头部如果有剩余也会使用该规则填充

2.内存布局实例研究

为了方便的研究对象所占的内存,建议使用官方提供的jol工具,如果使用Maven,只需加入如下依赖:

1
2
3
4
5
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>

然后便可以愉快的查看内存布局了:

1
2
3
4
public static void main(String[] args) {
System.out.println(ClassLayout.parseClass(Object.class).toPrintable());
System.out.println(ClassLayout.parseInstance(new Integer(1)).toPrintable());
}

上述代码第一行的输出如下(JDK8 64 bit):

1
2
3
4
5
6
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 12 (object header) N/A
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

由于目前的计算机基本为64位架构,所以忽略32位JVM,只对64位进行讨论。由于JDK8以后默认开启-XX:CompressedOops选项,所以上述为开启指针压缩的结果。

2.1 int VS Integer

Java中,一个int占4个字节,那么Integer对象占多少字节呢?Integer对象中只有一个value字段用于存储实际的整数。不开启指针压缩时,其布局为:

1
2
3
4
5
|-------------------------------------------|----------------|-----------------|
| Object Header | Instance Data | Padding |
|-------------------|-----------------------|----------------|-----------------|
| Mark Word(8 byte) | Klass Pointer(8 byte) | value(4 byte) | Padding(4 byte) |
|-------------------|-----------------------|----------------|-----------------|

开启指针压缩时,内存布局为:

1
2
3
4
5
|-------------------------------------------|----------------|
| Object Header | Instance Data |
|-------------------|-----------------------|----------------|
| Mark Word(8 byte) | Klass Pointer(4 byte) | value(4 byte) |
|-------------------|-----------------------|----------------|

可知如果不开启指针压缩,一个Integer对象需要占用24字节,就算开启指针压缩也需要占用16字节,是int的四倍多。Integer的内存占用超出想象,由此在Java中产生了许多优化方案。考虑Java集合,其中的对象泛型不支持基本数据类型,而只能使用IntegerLong等包装器类,这样将会耗费过多的内存。为了节约内存,一些开源工具支持基本类型的容器,比如:Koloboke

2.2 字段重排序

为了更高效的使用内存,实例数据字段将会重排序。排序的优先级为: long = double > int = float > char = short > byte > boolean > object reference。如下所示的类:

1
2
3
4
5
6
7
class FieldTest{
byte a;
int c;
boolean d;
long e;
Object f;
}

将会重排序为(开启CompressedOops选项):

1
2
3
4
5
6
7
OFFSET  SIZE               TYPE DESCRIPTION            
16 8 long FieldTest.e
24 4 int FieldTest.c
28 1 byte FieldTest.a
29 1 boolean FieldTest.d
30 2 (alignment/padding gap)
32 8 java.lang.Object FieldTest.f

2.3 继承体系的布局

继承体系中,类间不混排,而是独立分隔开,但每个类中的字段遵循前述的优先级。如下的类:

1
2
3
4
5
6
7
8
9
class Father {
int a;
int b;
long c;
}

class Child extends Father {
long d;
}

重排序的结果为:

1
2
3
4
5
OFFSET  SIZE   TYPE DESCRIPTION    
16 8 long Father.c
24 4 int Father.a
28 4 int Father.b
32 8 long Child.d

不开启指针压缩时,如果继承体系中的类字段没有占满8字节,将补齐字节对齐8字节。如下的类:

1
2
3
4
5
6
7
8
9
10
class Father {
long a;
byte b;
byte c;
}

class Child extends Father {
long d;
byte e;
}

重排序的结果为:

1
2
3
4
5
6
7
8
OFFSET  SIZE   TYPE DESCRIPTION            
16 8 long Father.a
24 1 byte Father.b
25 1 byte Father.c
26 6 (alignment/padding gap)
32 8 long Child.d
40 1 byte Child.e
41 7 (alignment/padding gap)

开启指针压缩时,情况稍有不同:如果父类层次中有至少4字节的空闲,则子类中如果含有4字节及其以下的字段,将按优先级:int = float > char = short > byte > boolean > object reference填充。开启指针压缩时,由于对象头只有12字节,剩余的4字节也将按这样的规则填充。如下的类:

1
2
3
4
5
6
7
8
9
10
class Father {
byte b;
long a;
byte c;
}

class Child extends Father {
byte e;
long d;
}

重排序的结果为:

1
2
3
4
5
6
7
8
OFFSET  SIZE   TYPE DESCRIPTION                             
12 1 byte Father.b
13 1 byte Father.c
14 2 (alignment/padding gap)
16 8 long Father.a
24 8 long Child.d
32 1 byte Child.e
33 7 (alignment)

2.4 非静态内部类

非静态内部类隐含一个指向外部类对象的引用,如下的类:

1
2
3
4
5
6
7
8
class Outer {
int a;
Inner i;

class Inner{
int b;
}
}

其中Inner的字段排序结果为:

1
2
3
4
5
OFFSET  SIZE    TYPE DESCRIPTION      
0 12 (object header)
12 4 int Inner.b
16 4 Outer Inner.this$0
20 4 (loss due to the next object alignment)

可见其中的隐含引用,这也就是能使用Inner.this引用外部对象的原因。

3.估算应用所占内存

明白了这些,那么估算应用所占内存便成为可能。一个应用使用如下的数据结构存储数据:

1
HashMap<Integer, String> cache = new HashMap<Integer, String>();

该应用有约100万数据,其中每个String的长度约为50,该应用大约占用多少内存呢?假设该应用运行在64位JVM上,开启CompressedOops选项。

由前述分析知:一个Integer占用16字节。那么长度为50的字符串占用多少字节呢?String的数据结构如下:

1
2
3
4
public final class String {
private int hash; // Default to 1
private final char value[];
}

其中含有一个int值和字符数组,而数组的对象头中含有一个4字节的长度字段,故对象头部为16字节。该String对象的内存布局示意如下:

img

字符串内存布局示意

可知,长度为50的字符串占用24+120=144字节的空间。
估计HashMap大小的关键是估算Entry的大小,它的数据结构如下:

1
2
3
4
5
6
static class Entry<K,V> implements Map.Entry<K,V> {
final K key; // 引用
V value; // 引用
Entry<K,V> next; // 引用
int hash;
}

可知一个Entry所占内存为:12B对象头+16B实例数据+4B对齐填充,共占用32字节。由于含有100万条数据,故将创建100万个Entry。由此可估算所占内存为:100万Integer、100万String和100万Entry,忽略HashMap的其他小额占用,最终占用内存:

1
(16 + 144 + 32) * 1 000 000 ≈ 192 MB

使用visual vm工具进行监控的实际数据如下:

img

应用所占内存示意

附相关资料:

Java对象结构及大小计算
Java Object Memory Structure
Java数据对齐讨论
jdk jol工具


本文整理自

如何正确计算Java对象所占内存?

仅做个人学习总结所用,遵循CC 4.0 BY-SA版权协议,如有侵权请联系删除!


对象头

Java虚拟机中每个Java对象都有一个对象头,对象头由标记字段和类型指针构成。其中标记字段用以存储Java虚拟机有关对象的运行数据,如哈希码、GC信息及锁信息,而指针类型指向该对象的类。

压缩指针

在64位的虚拟机中,对象头的标记字段占64位,而类型指针又占64位。也就是说一个对象额外占用的字节就是16个字节。以Integer对象为例,它仅有一个int类型的私有字段,占4个字节。因此,每个Integer的额外开销至少400%,这也就是Java为什么要引入基本数据类型的原因之一。为了减少内存开销,64位Java虚拟机引入了压缩指针概念(对应虚拟机选项 -XX:+UseCompressedOops,默认开启),将堆中原本64位的Java对象指针压缩成32位的。
这样一来,对象头的类型指针也会被压缩成32位,使得对象头大小从16字节降低为12字节。压缩指针不仅可以作用对象头的类型指针,还可以作用引用类型的字段,引用类型的数组。

压缩指针原理

默认情况下,Java虚拟机中对象的起始地址需要对齐至8的倍数(这个概念我们称之为内存对齐(对应虚拟机选项 -XX:ObjectAlignmentInBytes,默认值为 8)。如果一个对象用不到8N字节,那么空白的那部分空间就白白浪费掉了。这些浪费掉的空间我们称之为对象之间的填充。默认情况下,Java虚拟机中32位的指针可以寻址到2的35次方,也就是32GB的内存空间(超过32位会关闭压缩指针)。在对压缩指针解引用时,我们需要将其左移3位,再加上一个固定的偏移量,便可以寻址到32GB地址空间伪64位指针了。

此外,我们可以配置刚刚提到的内存对齐选项(-XX:ObjectAlignmentInBytes)来进一步提升内存寻址范围。但是,这也可能增加对象填充,导致压缩指针没有打到节省空间效果。

关闭压缩指针

就算关闭了压缩指针,Java虚拟机也会进行内存对齐。内存对齐不仅在于对象和对象之间,也存在于对象的各个字段之间。比如说,Java虚拟机中的long字段、double字段,以及非压缩指针状态下的引用字段为8的倍数。

内存对齐原因

内存对齐的一个原因是让字段出现在同一CPU的缓存中。如果字段不对齐,那么就有可能出现跨缓存行的字段。也就是说,该字段的读取的读取可能需要跨两个缓存行,而改字段的存储也可能同时污染两个缓存行。这种情况对程序的执行效率是不利的。


本文整理自

压缩指针

仅做个人学习总结所用,遵循CC 4.0 BY-SA版权协议,如有侵权请联系删除!


1个partition只能被同组的一个consumer消费,同组的consumer则起到均衡效果

消费者多于partition

topic: test 只有一个partition
创建一个topic——test,

1
bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic test

在g2组中启动两个consumer,

1
2
1. bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test --from-beginning --consumer.config config/consumer_g2.properties
2. bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test --from-beginning --consumer.config config/consumer_g2.properties

消费者数量为2大于partition数量1,此时partition和消费者进程对应关系如下:

1
bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group g2
1
2
3
TOPIC PARTITION CURRENT-OFFSET LOG-END-OFFSET LAG CONSUMER-ID HOST CLIENT-ID
test 0 9 9 0 consumer-1-4a2a4aa8-32f4-4904-9c16-1c0bdf7128a2 /127.0.0.1 consumer-1
- - - - - consumer-1-fd7b120f-fd21-4e07-8c23-87b71c1ee8a5 /127.0.0.1 consumer-1

消费者consumer-1-fd7b120f-fd21-4e07-8c23-87b71c1ee8a5无对应的partition。
用图表示为

img

生产者消费者对应关系1.jpg

如上图,向test发送消息:1,2, 3,4,5,6,7,8,9
只有C1能接收到消息,C2则不能接收到消息,即同一个partition内的消息只能被同一个组中的一个consumer消费。当消费者数量多于partition的数量时,多余的消费者空闲。
也就是说如果只有一个partition你在同一组启动多少个consumer都没用,partition的数量决定了此topic在同一组中被可被均衡的程度,例如partition=4,则可在同一组中被最多4个consumer均衡消费。

消费者少于和等于partition

topic:test2包含3个partition

1
bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 3 --topic test2

开始时,在g3组中启动2个consumer,

1
2
1.bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test2 --from-beginning --consumer.config config/consumer_g3.properties
2.bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test2 --from-beginning --consumer.config config/consumer_g3.properties

则对应关系如下:

1
2
3
4
TOPIC PARTITION CURRENT-OFFSET LOG-END-OFFSET LAG CONSUMER-ID HOST CLIENT-ID
test2 0 8 8 0 consumer-1-8b872ef7-a2f0-4bd3-b2a8-7b26e4d8ab2c /127.0.0.1 consumer-1
test2 1 7 7 0 consumer-1-8b872ef7-a2f0-4bd3-b2a8-7b26e4d8ab2c /127.0.0.1 consumer-1
test2 2 8 8 0 consumer-1-f362847d-1094-4895-ad8b-1e1f1c88936c /127.0.0.1 consumer-1

其中,consumer-1-8b872ef7-a2f0-4bd3-b2a8-7b26e4d8ab2c对应了2个partition
用图表示为:

img

生产者消费者对应关系2.jpg

消费者数量2小于partition的数量3,此时,向test2发送消息1,2,3,4,5,6,7,8,9
C1接收到1,3,4,6,7,9
C2接收到2,5,8
此时P1、P2对对应C1,即多个partition对应一个消费者,C1接收到消息量是C2的两倍
然后,在g3组中再启动一个消费者,使得消费者数量为3等于topic2中partition的数量

1
3.bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test2 --from-beginning --consumer.config config/consumer_g3.properties

对应关系如下:

1
2
3
4
TOPIC PARTITION CURRENT-OFFSET LOG-END-OFFSET LAG CONSUMER-ID HOST CLIENT-ID
test2 0 8 8 0 consumer-1-8b872ef7-a2f0-4bd3-b2a8-7b26e4d8ab2c /127.0.0.1 consumer-1
test2 1 7 7 0 consumer-1-ab472ed5-de11-4e56-863a-67bf3a3cc36a /127.0.0.1 consumer-1
test2 2 8 8 0 consumer-1-f362847d-1094-4895-ad8b-1e1f1c88936c /127.0.0.1 consumer-1

此时,partition和消费者是一对一关系,向test2发送消息1,2,3,4,5,6,7,8,9
C1接收到了:2,5,8
C2接收到了:3,6,9
C3接收到了:1,4,7
C1,C2,C3均分了test2的所有消息,即消息在同一个组之间的消费者之间均分了!

多个消费者组

启动g4组,仅包含一个消费者C1,消费topic2的消息,此时消费端有两个消费者组

1
bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test2 --from-beginning --consumer.config config/consumer_g4.properties --delete-consumer-offsets

g4组的C1的对应了test2的所有partition:

1
bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group g4
1
2
3
4
TOPIC PARTITION CURRENT-OFFSET LOG-END-OFFSET LAG CONSUMER-ID HOST CLIENT-ID
test2 0 36 36 0 consumer-1-befc9234-260d-4ad3-b283-b67a2bf446ca /127.0.0.1 consumer-1
test2 1 35 35 0 consumer-1-befc9234-260d-4ad3-b283-b67a2bf446ca /127.0.0.1 consumer-1
test2 2 36 36 0 consumer-1-befc9234-260d-4ad3-b283-b67a2bf446ca /127.0.0.1 consumer-1

用图表示为

img

生产者消费者对应关系3.jpg

如上图,向test2发送消息1,2,3,4,5,6,7,8,9
那么g3组各个消费者及g4组的消费者接收到的消息是怎样地呢?欢迎思考!!
答案:
消息被g3组的消费者均分,g4组的消费者在接收到了所有的消息。
g3组:
C1接收到了:2,5,8
C2接收到了:3,6,9
C3接收到了:1,4,7
g4组:
C1接收到了:1,2,3,4,5,6,7,8,9
启动多个组,则会使同一个消息被消费多次


本文整理自

kafka中partition和消费者对应关系

仅做个人学习总结所用,遵循CC 4.0 BY-SA版权协议,如有侵权请联系删除!


前言

Spring在TransactionDefinition接口中规定了7种类型的事务传播行为。事务传播行为是Spring框架独有的事务增强特性,他不属于的事务实际提供方数据库行为。这是Spring为我们提供的强大的工具箱,使用事务传播行可以为我们的开发工作提供许多便利。但是人们对他的误解也颇多,你一定也听过“service方法事务最好不要嵌套”的传言。要想正确的使用工具首先需要了解工具。本文对七种事务传播行为做详细介绍,内容主要代码示例的方式呈现。

基础概念

1. 什么是事务传播行为?

事务传播行为用来描述由某一个事务传播行为修饰的方法被嵌套进另一个方法的时事务如何传播。

用伪代码说明:

1
2
3
4
5
6
7
8
9
public void methodA(){
methodB();
//doSomething
}

@Transaction(Propagation=XXX)
public void methodB(){
//doSomething
}

代码中methodA()方法嵌套调用了methodB()方法,methodB()的事务传播行为由@Transaction(Propagation=XXX)设置决定。这里需要注意的是methodA()并没有开启事务,某一个事务传播行为修饰的方法并不是必须要在开启事务的外围方法中调用。

2. Spring中七种事务传播行为

事务传播行为类型 说明
PROPAGATION_REQUIRED 如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。
PROPAGATION_SUPPORTS 支持当前事务,如果当前没有事务,就以非事务方式执行。
PROPAGATION_MANDATORY 使用当前的事务,如果当前没有事务,就抛出异常。
PROPAGATION_REQUIRES_NEW 新建事务,如果当前存在事务,把当前事务挂起。
PROPAGATION_NOT_SUPPORTED 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
PROPAGATION_NEVER 以非事务方式执行,如果当前存在事务,则抛出异常。
PROPAGATION_NESTED 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。

定义非常简单,也很好理解,下面我们就进入代码测试部分,验证我们的理解是否正确。

代码验证

文中代码以传统三层结构中两层呈现,即Service和Dao层,由Spring负责依赖注入和注解式事务管理,DAO层由Mybatis实现,你也可以使用任何喜欢的方式,例如,Hibernate,JPA,JDBCTemplate等。数据库使用的是MySQL数据库,你也可以使用任何支持事务的数据库,并不会影响验证结果。

首先我们在数据库中创建两张表:

user1

1
2
3
4
5
6
CREATE TABLE `user1` (
`id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
`name` VARCHAR(45) NOT NULL DEFAULT '',
PRIMARY KEY(`id`)
)
ENGINE = InnoDB;

user2

1
2
3
4
5
6
CREATE TABLE `user2` (
`id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
`name` VARCHAR(45) NOT NULL DEFAULT '',
PRIMARY KEY(`id`)
)
ENGINE = InnoDB;

然后编写相应的Bean和DAO层代码:

User1

1
2
3
4
5
public class User1 {
private Integer id;
private String name;
//get和set方法省略...
}

User2

1
2
3
4
5
public class User2 {
private Integer id;
private String name;
//get和set方法省略...
}

User1Mapper

1
2
3
4
5
public interface User1Mapper {
int insert(User1 record);
User1 selectByPrimaryKey(Integer id);
//其他方法省略...
}

User2Mapper

1
2
3
4
5
public interface User2Mapper {
int insert(User2 record);
User2 selectByPrimaryKey(Integer id);
//其他方法省略...
}

最后也是具体验证的代码由service层实现,下面我们分情况列举。

1.PROPAGATION_REQUIRED

我们为User1Service和User2Service相应方法加上Propagation.REQUIRED属性。

User1Service方法:

1
2
3
4
5
6
7
8
9
@Service
public class User1ServiceImpl implements User1Service {
//省略其他...
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void addRequired(User1 user){
user1Mapper.insert(user);
}
}

User2Service方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Service
public class User2ServiceImpl implements User2Service {
//省略其他...
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void addRequired(User2 user){
user2Mapper.insert(user);
}
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void addRequiredException(User2 user){
user2Mapper.insert(user);
throw new RuntimeException();
}

}

1.1 场景一

此场景外围方法没有开启事务。

验证方法1:

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public void notransaction_exception_required_required(){
User1 user1=new User1();
user1.setName("张三");
user1Service.addRequired(user1);

User2 user2=new User2();
user2.setName("李四");
user2Service.addRequired(user2);

throw new RuntimeException();
}

验证方法2:

1
2
3
4
5
6
7
8
9
10
@Override
public void notransaction_required_required_exception(){
User1 user1=new User1();
user1.setName("张三");
user1Service.addRequired(user1);

User2 user2=new User2();
user2.setName("李四");
user2Service.addRequiredException(user2);
}

分别执行验证方法,结果:

验证方法序号 数据库结果 结果分析
1 “张三”、“李四”均插入。 外围方法未开启事务,插入“张三”、“李四”方法在自己的事务中独立运行,外围方法异常不影响内部插入“张三”、“李四”方法独立的事务。
2 “张三”插入,“李四”未插入。 外围方法没有事务,插入“张三”、“李四”方法都在自己的事务中独立运行,所以插入“李四”方法抛出异常只会回滚插入“李四”方法,插入“张三”方法不受影响。

结论:通过这两个方法我们证明了在外围方法未开启事务的情况下Propagation.REQUIRED修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。

1.2 场景二

外围方法开启事务,这个是使用率比较高的场景。

验证方法1:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void transaction_exception_required_required(){
User1 user1=new User1();
user1.setName("张三");
user1Service.addRequired(user1);

User2 user2=new User2();
user2.setName("李四");
user2Service.addRequired(user2);

throw new RuntimeException();
}

验证方法2:

1
2
3
4
5
6
7
8
9
10
11
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void transaction_required_required_exception(){
User1 user1=new User1();
user1.setName("张三");
user1Service.addRequired(user1);

User2 user2=new User2();
user2.setName("李四");
user2Service.addRequiredException(user2);
}

验证方法3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Transactional
@Override
public void transaction_required_required_exception_try(){
User1 user1=new User1();
user1.setName("张三");
user1Service.addRequired(user1);

User2 user2=new User2();
user2.setName("李四");
try {
user2Service.addRequiredException(user2);
} catch (Exception e) {
System.out.println("方法回滚");
}
}

分别执行验证方法,结果:

验证方法序号 数据库结果 结果分析
1 “张三”、“李四”均未插入。 外围方法开启事务,内部方法加入外围方法事务,外围方法回滚,内部方法也要回滚。
2 “张三”、“李四”均未插入。 外围方法开启事务,内部方法加入外围方法事务,内部方法抛出异常回滚,外围方法感知异常致使整体事务回滚。
3 “张三”、“李四”均未插入。 外围方法开启事务,内部方法加入外围方法事务,内部方法抛出异常回滚,即使方法被catch不被外围方法感知,整个事务依然回滚。

结论:以上试验结果我们证明在外围方法开启事务的情况下Propagation.REQUIRED修饰的内部方法会加入到外围方法的事务中,所有Propagation.REQUIRED修饰的内部方法和外围方法均属于同一事务,只要一个方法回滚,整个事务均回滚。

2.PROPAGATION_REQUIRES_NEW

我们为User1Service和User2Service相应方法加上Propagation.REQUIRES_NEW属性。
User1Service方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Service
public class User1ServiceImpl implements User1Service {
//省略其他...
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void addRequiresNew(User1 user){
user1Mapper.insert(user);
}
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void addRequired(User1 user){
user1Mapper.insert(user);
}
}

User2Service方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Service
public class User2ServiceImpl implements User2Service {
//省略其他...
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void addRequiresNew(User2 user){
user2Mapper.insert(user);
}

@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void addRequiresNewException(User2 user){
user2Mapper.insert(user);
throw new RuntimeException();
}
}

2.1 场景一

外围方法没有开启事务。

验证方法1:

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public void notransaction_exception_requiresNew_requiresNew(){
User1 user1=new User1();
user1.setName("张三");
user1Service.addRequiresNew(user1);

User2 user2=new User2();
user2.setName("李四");
user2Service.addRequiresNew(user2);
throw new RuntimeException();

}

验证方法2:

1
2
3
4
5
6
7
8
9
10
@Override
public void notransaction_requiresNew_requiresNew_exception(){
User1 user1=new User1();
user1.setName("张三");
user1Service.addRequiresNew(user1);

User2 user2=new User2();
user2.setName("李四");
user2Service.addRequiresNewException(user2);
}

分别执行验证方法,结果:

验证方法序号 数据库结果 结果分析
1 “张三”插入,“李四”插入。 外围方法没有事务,插入“张三”、“李四”方法都在自己的事务中独立运行,外围方法抛出异常回滚不会影响内部方法。
2 “张三”插入,“李四”未插入 外围方法没有开启事务,插入“张三”方法和插入“李四”方法分别开启自己的事务,插入“李四”方法抛出异常回滚,其他事务不受影响。

结论:通过这两个方法我们证明了在外围方法未开启事务的情况下Propagation.REQUIRES_NEW修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。

2.2 场景二

外围方法开启事务。

验证方法1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void transaction_exception_required_requiresNew_requiresNew(){
User1 user1=new User1();
user1.setName("张三");
user1Service.addRequired(user1);

User2 user2=new User2();
user2.setName("李四");
user2Service.addRequiresNew(user2);

User2 user3=new User2();
user3.setName("王五");
user2Service.addRequiresNew(user3);
throw new RuntimeException();
}

验证方法2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void transaction_required_requiresNew_requiresNew_exception(){
User1 user1=new User1();
user1.setName("张三");
user1Service.addRequired(user1);

User2 user2=new User2();
user2.setName("李四");
user2Service.addRequiresNew(user2);

User2 user3=new User2();
user3.setName("王五");
user2Service.addRequiresNewException(user3);
}

验证方法3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void transaction_required_requiresNew_requiresNew_exception_try(){
User1 user1=new User1();
user1.setName("张三");
user1Service.addRequired(user1);

User2 user2=new User2();
user2.setName("李四");
user2Service.addRequiresNew(user2);
User2 user3=new User2();
user3.setName("王五");
try {
user2Service.addRequiresNewException(user3);
} catch (Exception e) {
System.out.println("回滚");
}
}

分别执行验证方法,结果:

验证方法序号 数据库结果 结果分析
1 “张三”未插入,“李四”插入,“王五”插入。 外围方法开启事务,插入“张三”方法和外围方法一个事务,插入“李四”方法、插入“王五”方法分别在独立的新建事务中,外围方法抛出异常只回滚和外围方法同一事务的方法,故插入“张三”的方法回滚。
2 “张三”未插入,“李四”插入,“王五”未插入。 外围方法开启事务,插入“张三”方法和外围方法一个事务,插入“李四”方法、插入“王五”方法分别在独立的新建事务中。插入“王五”方法抛出异常,首先插入 “王五”方法的事务被回滚,异常继续抛出被外围方法感知,外围方法事务亦被回滚,故插入“张三”方法也被回滚。
3 “张三”插入,“李四”插入,“王五”未插入。 外围方法开启事务,插入“张三”方法和外围方法一个事务,插入“李四”方法、插入“王五”方法分别在独立的新建事务中。插入“王五”方法抛出异常,首先插入“王五”方法的事务被回滚,异常被catch不会被外围方法感知,外围方法事务不回滚,故插入“张三”方法插入成功。

结论:在外围方法开启事务的情况下Propagation.REQUIRES_NEW修饰的内部方法依然会单独开启独立事务,且与外部方法事务也独立,内部方法之间、内部方法和外部方法事务均相互独立,互不干扰。

3.PROPAGATION_NESTED

我们为User1Service和User2Service相应方法加上Propagation.NESTED属性。
User1Service方法:

1
2
3
4
5
6
7
8
9
@Service
public class User1ServiceImpl implements User1Service {
//省略其他...
@Override
@Transactional(propagation = Propagation.NESTED)
public void addNested(User1 user){
user1Mapper.insert(user);
}
}

User2Service方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Service
public class User2ServiceImpl implements User2Service {
//省略其他...
@Override
@Transactional(propagation = Propagation.NESTED)
public void addNested(User2 user){
user2Mapper.insert(user);
}

@Override
@Transactional(propagation = Propagation.NESTED)
public void addNestedException(User2 user){
user2Mapper.insert(user);
throw new RuntimeException();
}
}

3.1 场景一

此场景外围方法没有开启事务。

验证方法1:

1
2
3
4
5
6
7
8
9
10
11
@Override
public void notransaction_exception_nested_nested(){
User1 user1=new User1();
user1.setName("张三");
user1Service.addNested(user1);

User2 user2=new User2();
user2.setName("李四");
user2Service.addNested(user2);
throw new RuntimeException();
}

验证方法2:

1
2
3
4
5
6
7
8
9
10
@Override
public void notransaction_nested_nested_exception(){
User1 user1=new User1();
user1.setName("张三");
user1Service.addNested(user1);

User2 user2=new User2();
user2.setName("李四");
user2Service.addNestedException(user2);
}

分别执行验证方法,结果:

验证方法序号 数据库结果 结果分析
1 “张三”、“李四”均插入。 外围方法未开启事务,插入“张三”、“李四”方法在自己的事务中独立运行,外围方法异常不影响内部插入“张三”、“李四”方法独立的事务。
2 “张三”插入,“李四”未插入。 外围方法没有事务,插入“张三”、“李四”方法都在自己的事务中独立运行,所以插入“李四”方法抛出异常只会回滚插入“李四”方法,插入“张三”方法不受影响。

结论:通过这两个方法我们证明了在外围方法未开启事务的情况下Propagation.NESTEDPropagation.REQUIRED作用相同,修饰的内部方法都会新开启自己的事务,且开启的事务相互独立,互不干扰。

3.2 场景二

外围方法开启事务。

验证方法1:

1
2
3
4
5
6
7
8
9
10
11
12
@Transactional
@Override
public void transaction_exception_nested_nested(){
User1 user1=new User1();
user1.setName("张三");
user1Service.addNested(user1);

User2 user2=new User2();
user2.setName("李四");
user2Service.addNested(user2);
throw new RuntimeException();
}

验证方法2:

1
2
3
4
5
6
7
8
9
10
11
@Transactional
@Override
public void transaction_nested_nested_exception(){
User1 user1=new User1();
user1.setName("张三");
user1Service.addNested(user1);

User2 user2=new User2();
user2.setName("李四");
user2Service.addNestedException(user2);
}

验证方法3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Transactional
@Override
public void transaction_nested_nested_exception_try(){
User1 user1=new User1();
user1.setName("张三");
user1Service.addNested(user1);

User2 user2=new User2();
user2.setName("李四");
try {
user2Service.addNestedException(user2);
} catch (Exception e) {
System.out.println("方法回滚");
}
}

分别执行验证方法,结果:

验证方法序号 数据库结果 结果分析
1 “张三”、“李四”均未插入。 外围方法开启事务,内部事务为外围事务的子事务,外围方法回滚,内部方法也要回滚。
2 “张三”、“李四”均未插入。 外围方法开启事务,内部事务为外围事务的子事务,内部方法抛出异常回滚,且外围方法感知异常致使整体事务回滚。
3 “张三”插入、“李四”未插入。 外围方法开启事务,内部事务为外围事务的子事务,插入“李四”内部方法抛出异常,可以单独对子事务回滚。

结论:以上试验结果我们证明在外围方法开启事务的情况下Propagation.NESTED修饰的内部方法属于外部事务的子事务,外围主事务回滚,子事务一定回滚,而内部子事务可以单独回滚而不影响外围主事务和其他子事务

4. REQUIRED,REQUIRES_NEW,NESTED异同

由“1.2 场景二”和“3.2 场景二”对比,我们可知:
NESTED和REQUIRED修饰的内部方法都属于外围方法事务,如果外围方法抛出异常,这两种方法的事务都会被回滚。但是REQUIRED是加入外围方法事务,所以和外围事务同属于一个事务,一旦REQUIRED事务抛出异常被回滚,外围方法事务也将被回滚。而NESTED是外围方法的子事务,有单独的保存点,所以NESTED方法抛出异常被回滚,不会影响到外围方法的事务。

由“2.2 场景二”和“3.2 场景二”对比,我们可知:
NESTED和REQUIRES_NEW都可以做到内部方法事务回滚而不影响外围方法事务。但是因为NESTED是嵌套事务,所以外围方法回滚之后,作为外围方法事务的子事务也会被回滚。而REQUIRES_NEW是通过开启新的事务实现的,内部事务和外围事务是两个事务,外围事务回滚不会影响内部事务。

5. 其他事务传播行为

鉴于文章篇幅问题,其他事务传播行为的测试就不在此一一描述了,感兴趣的读者可以去源码中自己寻找相应测试代码和结果解释。传送门:https://github.com/TmTse/tran…

模拟用例

介绍了这么多事务传播行为,我们在实际工作中如何应用呢?下面我来举一个示例:

假设我们有一个注册的方法,方法中调用添加积分的方法,如果我们希望添加积分不会影响注册流程(即添加积分执行失败回滚不能使注册方法也回滚),我们会这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Service
public class UserServiceImpl implements UserService {

@Transactional
public void register(User user){

try {
membershipPointService.addPoint(Point point);
} catch (Exception e) {
//省略...
}
//省略...
}
//省略...
}

我们还规定注册失败要影响addPoint()方法(注册方法回滚添加积分方法也需要回滚),那么addPoint()方法就需要这样实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Service
public class MembershipPointServiceImpl implements MembershipPointService{

@Transactional(propagation = Propagation.NESTED)
public void addPoint(Point point){

try {
recordService.addRecord(Record record);
} catch (Exception e) {
//省略...
}
//省略...
}
//省略...
}

我们注意到了在addPoint()中还调用了addRecord()方法,这个方法用来记录日志。他的实现如下:

1
2
3
4
5
6
7
8
9
10
11
@Service
public class RecordServiceImpl implements RecordService{

@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void addRecord(Record record){


//省略...
}
//省略...
}

我们注意到addRecord()方法中propagation = Propagation.NOT_SUPPORTED,因为对于日志无所谓精确,可以多一条也可以少一条,所以addRecord()方法本身和外围addPoint()方法抛出异常都不会使addRecord()方法回滚,并且addRecord()方法抛出异常也不会影响外围addPoint()方法的执行。

通过这个例子相信大家对事务传播行为的使用有了更加直观的认识,通过各种属性的组合确实能让我们的业务实现更加灵活多样。


本文整理自

Spring事务传播行为详解

仅做个人学习总结所用,遵循CC 4.0 BY-SA版权协议,如有侵权请联系删除!