Canal准备

准备

对于自建 MySQL , 需要先开启 Binlog 写入功能,配置 binlog-format 为 ROW 模式,my.cnf 中配置如下

1
2
3
4
[mysqld]
log-bin=mysql-bin # 开启 binlog
binlog-format=ROW # 选择 ROW 模式
server_id=1

配置 MySQL replaction 需要定义,不要和 canal 的 slaveId 重复

注意:针对阿里云 RDS for MySQL , 默认打开了 binlog , 并且账号默认具有 binlog dump 权限 , 不需要任何权限或者 binlog 设置,可以直接跳过这一步
授权 canal 链接 MySQL 账号具有作为 MySQL slave 的权限, 如果已有账户可直接 grant

1
2
3
4
CREATE USER canal IDENTIFIED BY 'canal';  
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';
-- GRANT ALL PRIVILEGES ON *.* TO 'canal'@'%' ;
FLUSH PRIVILEGES;

(十)可靠事件通知

可靠事件通知

可靠事件通知模式的设计理念比较容易理解,即是主服务完成后将结果通过事件(常常是消息队列)传递给从服务,从服务在接受到消息后进行消费,完成业务,从而达到主服务与从服务间的消息一致性。首先能想到的也是最简单的就是同步事件通知,业务处理与消息发送同步执行,实现逻辑见下方代码及时序图。

同步事件

伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
public void trans() {  
try {
// 1. 操作数据库
bool result = dao.update(data);// 操作数据库失败,会抛出异常
// 2. 如果数据库操作成功则发送消息
if(result){
mq.send(data);// 如果方法执行失败,会抛出异常
}
} catch (Exception e) {
roolback();// 如果发生异常,就回滚
}
}

理想化场景:

image

上面的逻辑看上去天衣无缝,如果数据库操作失败则直接退出,不发送消息;如果发送消息失败,则数据库回滚;如果数据库操作成功且消息发送成功,则业务成功,消息发送给下游消费。然后仔细思考后,同步消息通知其实有==两点==不足的地方。

不足之处

  • 第一点:
    在微服务的架构下,有可能出现网络IO问题或者服务器宕机的问题,如果这些问题出现在时序图的第7步,使得消息投递后无法正常通知主服务(网络问题),或无法继续提交事务(宕机),那么主服务将会认为消息投递失败,会滚主服务业务,然而实际上消息已经被从服务消费,那么就会造成主服务和从服务的数据不一致。具体场景可见下面两张时序图。

网络异常:

image

服务器宕机:

image

  • 第二点 :事件服务(在这里就是消息服务)与业务过于耦合,如果消息服务不可用,会导致业务不可用。应该将事件服务与业务解耦,独立出来异步执行,或者在业务执行后先尝试发送一次消息,如果消息发送失败,则降级为异步发送。

异步事件

本地事件服务

为了解决前面描述的同步事件的问题,异步事件通知模式被发展了出来,既业务服务和事件服务解耦,事件异步进行,由单独的事件服务保证事件的可靠投递。

image

异步事件通知-本地事件服务

当业务执行时,在同一个本地事务中将事件写入本地事件表,同时投递该事件,如果事件投递成功,则将该事件从事件表中删除。如果投递失败,则使用事件服务定时地异步统一处理投递失败的事件,进行重新投递,直到事件被正确投递,并将事件从事件表中删除。这种方式最大可能地保证了事件投递的实效性,并且当第一次投递失败后,也能使用异步事件服务保证事件至少被投递一次。

然而,这种使用本地事件服务保证可靠事件通知的方式也有它的不足之处,那便是业务仍旧与事件服务有一定耦合(第一次同步投递时),更为严重的是,本地事务需要负责额外的事件表的操作,为数据库带来了压力,在高并发的场景,由于每一个业务操作就要产生相应的事件表操作,几乎将数据库的可用吞吐量砍了一半,这无疑是无法接受的。正是因为这样的原因,可靠事件通知模式进一步地发展-外部事件服务出现在了人们的眼中。

外部事件服务

外部事件服务在本地事件服务的基础上更进了一步,将事件服务独立出主业务服务,主业务服务不在对事件服务有任何强依赖。

image

异步事件通知-外部事件服务

业务服务在提交前,向事件服务发送事件,事件服务只记录事件,并不发送。业务服务在提交或回滚后通知事件服务,事件服务发送事件或者删除事件。不用担心业务系统在提交或者会滚后宕机而无法发送确认事件给事件服务,因为事件服务会定时获取所有仍未发送的事件并且向业务系统查询,根据业务系统的返回来决定发送或者删除该事件。

外部事件虽然能够将业务系统和事件系统解耦,但是也带来了额外的工作量:外部事件服务比起本地事件服务来说多了两次网络通信开销(提交前、提交/回滚后),同时也需要业务系统提供单独的查询接口给事件系统用来判断未发送事件的状态。

流程梳理
  1. 首先事务发起方先往 MQ 发送一条预读消息,这条消息与普通消息的区别在于他只对 MQ 可见不会向下传播。
  2. MQ接受到消息后,先进行持久化,则存储中会新增一条状态为待发送的消息,接着给事务发起方返回处理完成的 ACK;事务发起方收到处理完成的 ACK 之后开始执行本地事务。
  3. 发起方会根据本地事务的执行状态来决定这个预读消息是应该继续往前还是回滚。另外 MQ 也应该支持自己反查来解决异常情况,如果发起方本地事务已经执行完毕发送消息到MQ,但是消息因为网络原因丢失,那么怎么解决。所以这个反查机制很重要。
  4. 本地事务执行成功以后,MQ 也接收到成功通知,接着将消息状态更新为可发送,然后将消息推送给下游的消费者,这个时候消费者就可以去处理自己的本地事务 。

注意点:由于MQ通常都会保证消息能够投递成功,因此,如果业务没有及时返回ACK结果,那么就有可能造成MQ的重复消息投递问题。因此,对于消息最终一致性的方案,消息的消费者必须要对消息的消费支持幂等,不能造成同一条消息的重复消费的情况。

可靠事件通知注意事项

可靠事件模式需要注意的有两点,1. 事件的正确发送;2. 事件的重复消费。

通过异步消息服务可以确保事件的正确发送,然而事件是有可能重复发送的,那么就需要消费端保证同一条事件不会重复被消费,简而言之就是保证事件消费的 ==幂等性==

如果事件本身是具备幂等性的状态型事件,如订单状态的通知(已下单、已支付、已发货等),则需要判断事件的顺序。一般通过时间戳来判断,既消费过了新的消息后,当接受到老的消息直接丢弃不予消费。如果无法提供全局时间戳,则应考虑使用全局统一的序列号。

对于不具备幂等性的事件,一般是动作行为事件,如扣款100,存款200,则应该将事件id及事件结果持久化,在消费事件前查询事件id,若已经消费则直接返回执行结果;若是新消息,则执行,并存储执行结果。

nacos单机模式支持mysql

单机模式支持mysql

在0.7版本之前,在单机模式时nacos使用嵌入式数据库实现数据的存储,不方便观察数据存储的基本情况。0.7版本增加了支持mysql数据源能力,具体的操作步骤:

  1. 安装数据库,版本要求:5.6.5+
  2. 初始化mysql数据库,数据库初始化文件:nacos-mysql.sql
  3. 修改conf/application.properties文件,增加支持mysql数据源配置(目前只支持mysql),添加mysql数据源的url、用户名和密码。
1
2
3
4
5
6
spring.datasource.platform=mysql

db.num=1
db.url.0=jdbc:mysql://11.162.196.16:3306/nacos_devtest?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
db.user=nacos_devtest
db.password=youdontknow

缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级

缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级

参考

缓存雪崩

缓存雪崩我们可以简单的理解为:由于原有缓存失效,新缓存未到期间(例如:我们设置缓存时采用了相同的过期时间,在同一时刻出现大面积的缓存过期),所有原本应该访问缓存的请求都去查询数据库了,而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕机。从而形成一系列连锁反应,造成整个系统崩溃。

缓存正常从Redis中获取,示意图如下:

image

缓存失效瞬间示意图如下:

image

解决方案:

(1)碰到这种情况,一般并发量不是特别多的时候,使用最多的解决方案是加锁排队,伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public Object getProductListNew(){
int cacheTime = 30;
String cacheKey = "product_list";
String localKey = cacheKey;

String cacheValue = CacheHelper.get(cacheKey);
if(cacheValue != null){
return cacheValue;
} else {
sychronized(localKey){
if(cacheValue != null){
return cacheValue;
} else {
cacheValue = getProductListFromDB();
CacheHelper.add(cacheKey, cacheValue, cacheTime);
}

}
return cacheValue;
}

}

加锁排队只是为了减轻数据库的压力,并没有提高系统吞吐量。假设在高并发下,缓存重建期间key是锁着的,这是过来1000个请求999个都在阻塞的。同样会导致用户等待超时,这是个治标不治本的方法!

注意:加锁排队的解决方式分布式环境的并发问题,有可能还要解决分布式锁的问题;线程还会被阻塞,用户体验很差!因此,在真正的高并发场景下很少使用!

(2)给每一个缓存数据增加相应的缓存标记,记录缓存的是否失效,如果缓存标记失效,则更新数据缓存,实例伪代码如下:

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
public Object getProductListNew(){
int cacheTime = 30;
String cacheKey = "product_list";

//缓存标记
String cacheSign = cacheKey + "_sign";

String sign = CacheHelper.get(cacheSign);

String cacheValue = CacheHelper.get(cacheKey);

if(sign != null){
return cacheValue;
} else {
//过期
CacheHelper.add(cacheSign, 1, cacheTime);
ThreadPool.QueueUserWorkItem((arg -> {
cacheValue = getProductListFromDB();
CacheHelper.add(cacheKey, cacheValue, cacheTime * 2);
}));

return cacheValue;
}

}

解释说明:

1、缓存标记:记录缓存数据是否过期,如果过期会触发通知另外的线程在后台去更新实际key的缓存;

2、缓存数据:它的过期时间比缓存标记的时间延长1倍,例:标记缓存时间30分钟,数据缓存设置为60分钟。 这样,当缓存标记key过期后,实际缓存还能把旧数据返回给调用端,直到另外的线程在后台更新完成后,才会返回新缓存。

关于缓存崩溃的解决方法,这里提出了三种方案:使用锁或队列、设置过期标志更新缓存、为key设置不同的缓存失效时间,还有一各被称为“二级缓存”的解决方法,有兴趣的读者可以自行研究。

缓存穿透

缓存穿透是指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)。这样请求就绕过缓存直接查数据库,这也是经常提的缓存命中率问题。

缓存穿透解决方案:

(1)采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。

(2)如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。通过这个直接设置的默认值存放到缓存,这样第二次到缓存中获取就有值了,而不会继续访问数据库,这种办法最简单粗暴! 把空结果也给缓存起来,这样下次同样的请求就可以直接返回空了,即可以避免当查询的值为空时引起的缓存穿透。同时也可以单独设置个缓存区域存储空值,对要查询的key进行预先校验,然后再放行给后面的正常缓存处理逻辑。

缓存预热

缓存预热就是系统上线后,提前将相关的缓存数据直接加载到缓存系统。避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!

缓存预热解决方案:

  • (1)直接写个缓存刷新页面,上线时手工操作下;

  • (2)数据量不大,可以在项目启动的时候自动进行加载;

  • (3)定时刷新缓存;

缓存更新

除了缓存服务器自带的缓存失效策略之外(Redis默认的有6中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:

(1)定时去清理过期的缓存;

(2)当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。

两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,大家可以根据自己的应用场景来权衡。

缓存降级

当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。

降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。

在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案:

(1)一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;

(2)警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;

(3)错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;

(4)严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。

windows 搭建consul

windows 搭建consul

windows 搭建consul

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

cmd 命令窗口执行:consul agent -dev

consul 自带 UI 界面,打开网址:http://localhost:8500 ,可以看到当前注册的服务界面



创建服务
sc create consul binPath= "C:\consul1.9.0\consul_1.9.0_windows_amd64\consul.exe agent -server -ui -bootstrap -client 0.0.0.0 -data-dir="C:\consul1.9.0\data-dir" -bind 172.16.129.139"


consul.exe agent -server -ui -bootstrap -client 0.0.0.0 -data-dir="C:\consul1.9.0\data-dir" -bind 172.16.xx.139

tomcat最大线程数的设置

tomcat最大线程数的设置

Tomcat的server.xml中连接器设置如下

1.Tomcat配置

1
2
3
4
5
<Connector port="8080"     
maxThreads="150" minSpareThreads="25" maxSpareThreads="75"
enableLookups="false" redirectPort="8443" acceptCount="100"
debug="0" connectionTimeout="20000"
disableUploadTimeout="true" />

2.如何加大tomcat连接数

在tomcat配置文件server.xml中的配置中,和连接数相关的参数有:

  • minProcessors:最小空闲连接线程数,用于提高系统处理性能,默认值为10
  • maxProcessors:最大连接线程数,即:并发处理的最大请求数,默认值为75
  • acceptCount:允许的最大连接数,应大于等于maxProcessors,默认值为100
  • enableLookups:是否反查域名,取值为:true或false。为了提高处理能力,应设置为false
  • connectionTimeout:网络连接超时,单位:毫秒。设置为0表示永不超时,这样设置有隐患的。通常可设置为30000毫秒。

其中和最大连接数相关的参数为maxProcessors和acceptCount。如果要加大并发连接数,应同时加大这两个参数。
web server允许的最大连接数还受制于操作系统的内核参数设置,通常Windows是2000个左右,Linux是1000个左右。tomcat5中的配置示例:

1
2
3
4
5
<Connector port="8080"
maxThreads="150" minSpareThreads="25" maxSpareThreads="75"
enableLookups="false" redirectPort="8443" acceptCount="100"
debug="0" connectionTimeout="20000"
disableUploadTimeout="true" />

对于其他端口的侦听配置,以此类推。

3.tomcat中如何禁止列目录下的文件

在{tomcat_home}/conf/web.xml中,把listings参数设置成false即可,如下:

1
2
3
4
<init-param>  
<param-name>listings</param-name>
<param-value>false</param-value>
</init-param>

4.如何加大tomcat可以使用的内存

tomcat默认可以使用的内存为128MB,在较大型的应用项目中,这点内存是不够的,需要调大。
Unix下,在文件{tomcat_home}/bin/catalina.sh的前面,增加如下设置:
JAVA_OPTS=’-Xms【初始化内存大小】 -Xmx【可以使用的最大内存】’
需要把这个两个参数值调大。例如:
JAVA_OPTS=’-Xms256m -Xmx512m’
表示初始化内存为256MB,可以使用的最大内存为512MB

nacos 部署方式

nacos 部署方式

nacos you should know

Nacos支持三种部署模式

  • 单机模式 - 用于测试和单机试用。
  • 集群模式 - 用于生产环境,确保高可用。
  • 多集群模式 - 用于多数据中心场景。

单机模式下运行Nacos

Standalone means it is non-cluster Mode. *

  1. Linux/Unix/Mac

    1
    sh startup.sh -m standalone
  2. Windows

    1
    cmd startup.cmd -m standalone

单机模式支持mysql
在0.7版本之前,在单机模式时nacos使用嵌入式数据库实现数据的存储,不方便观察数据存储的基本情况。0.7版本增加了支持mysql数据源能力,具体的操作步骤:

  1. 安装数据库,版本要求:5.6.5+
  2. 初始化mysql数据库,数据库初始化文件:nacos-mysql.sql
  3. 修改conf/application.properties文件,增加支持mysql数据源配置(目前只支持mysql),添加mysql数据源的url、用户名和密码。
1
2
3
4
5
6
spring.datasource.platform=mysql

db.num=1
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
db.user=nacos_config
db.password=youdontknow

集群Nacos

集群Nacos

工作模式:

因此开源的时候推荐用户把所有服务列表放到一个vip下面,然后挂到一个域名下面

http://ip1:port/openAPI 直连ip模式,机器挂则需要修改ip才可以使用。

http://VIP:port/openAPI 挂载VIP模式,直连vip即可,下面挂server真实ip,可读性不好。

http://nacos.com:port/openAPI 域名 + VIP模式,可读性好,而且换ip方便,推荐模式

工作模式

环境说明:

  1. 64 bit OS Linux/Unix/Mac,推荐使用Linux系统。
  2. 64 bit JDK 1.8+;下载.配置。
  3. Maven 3.2.x+;下载.配置。
  4. 3个或3个以上Nacos节点才能构成集群。

下载

使用源码编译:

1
2
3
4
unzip nacos-source.zip
cd nacos/
mvn -Prelease-nacos clean install -U
cd nacos/distribution/target/nacos-server-1.3.0/nacos/bin

直接使用压缩包:

1
2
3
4
5
unzip nacos-server-1.3.0.zip
or
tar -xvf nacos-server-1.3.0.tar.gz

cd nacos/bin

配置集群配置文件

在nacos的解压目录nacos/的conf目录下,有配置文件cluster.conf,请每行配置成ip:port。(请配置3个或3个以上节点)

1
2
3
ip1:8848
ip2:8848
ip3:8848

确定数据源

使用内置数据源

无需进行任何配置

使用外置数据源

生产使用建议至少主备模式,或者采用高可用数据库。

初始化 MySQL 数据库

在nacos的解压目录nacos/的conf目录下,存在sql文件 nacos-mysql.sql

修改application.properties 配置

application.properties配置文件

启动服务器

Linux/Unix/Mac
Stand-alone mode

1
sh startup.sh -m standalone

集群模式 + 使用内置数据源

1
sh startup.sh -p embedded

集群模式 + 使用外置数据源

1
sh startup.sh

服务注册发现 和 配置管理

服务注册

1
curl -X PUT 'http://127.0.0.1:8848/nacos/v1/ns/instance?serviceName=nacos.naming.serviceName&ip=20.18.7.10&port=8080'

服务发现

1
curl -X GET 'http://127.0.0.1:8848/nacos/v1/ns/instances?serviceName=nacos.naming.serviceName'

发布配置

1
curl -X POST "http://127.0.0.1:8848/nacos/v1/cs/configs?dataId=nacos.cfg.dataId&group=test&content=helloWorld"

获取配置

1
curl -X GET "http://127.0.0.1:8848/nacos/v1/cs/configs?dataId=nacos.cfg.dataId&group=test"

关闭服务器

Linux/Unix/Mac

1
sh shutdown.sh

git ssh http 协议切换

git ssh http 协议切换

1
2
3
4
5
6
7
8
9
10
11
12
13
# 先看一下远端地址是否自己想要的
git remote -v
# 不是就移除
git remote remove origin
# 添加新的
git remote add origin http://172.16.5.77/HTDATA/CHD-Energy-Project.git
# 再看看
git remote -v
# 合个代码试试
git fetch origin
git rebase origin/master
# 推个代码试试
git push origin mingbai-dev

事务的隔离级别

事务的隔离级别

为何需要设置数据库隔离级别?

在数据库操作中,在并发的情况下可能出现如下问题:

  • 更新丢失(Lost Update)
    1
    不论后面的数据库更新是否提交,都有可能让前面的更新丢失。

如果多个线程操作,基于同一个查询结构对表中的记录进行修改,那么后修改的记录将会覆盖前面修改的记录,前面的修改就丢失掉了,这就叫做更新丢失。这是因为系统没有执行任何的锁操作,因此并发事务并没有被隔离开来。

第 1 类丢失更新:事务A撤销时,把已经提交的事务B的更新数据覆盖了。

时间 取款事务A 取款事务B
T1 开始事务
T2 开始事务
T3 查询余额为1000元
T4 查询余额为1000元
T5 汇入100元,修改余额为1100元
T6 提交事务
T7 取款100元, 修改余额为900元
T8 撤销事务
T9 余额回复为1000元(丢失更新)

第 2 类丢失更新:事务A覆盖事务B已经提交的数据,造成事务B所做的操作丢失。

时间 取款事务A 取款事务B
T1 开始事务
T2 开始事务
T3 查询余额为1000元
T4 查询余额为1000元
T5 取款100元, 修改余额为900元
T6 提交事务
T7 汇入100元,修改余额为1100元
T8 提交事务
T9 余额回复为1100元(丢失更新)

解决方法:对行加锁,只允许并发一个更新事务。

  • 脏读(Dirty Read)

    脏读(Dirty Read):A事务读取B事务尚未提交的数据并在此基础上操作,而B事务执行回滚,那么A读取到的数据就是脏数据。

1
读到了别的事务回滚前的脏数据
时间 取款事务A 取款事务B
T1 开始事务
T2 开始事务
T3 查询余额为1000元
T4 取出500元,余额为500元
T5 查询余额为500元 (脏读)
T6 撤销事务,余额恢复为1000元
T7 汇入100元,修改余额600元
T8 提交事务

解决办法:如果在第一个事务提交前,任何其他事务不可读取其修改过的值,则可以避免该问题。

  • 不可重复读(Non-repeatable Reads)

    一个事务对同一行数据重复读取两次,但是却得到了不同的结果。事务T1读取某一数据后,事务T2对其做了修改,当事务T1再次读该数据时得到与前一次不同的值。

1
一个事务中两次读取的数据的内容不一致 (一般基于行而言)
1
2
3
4
事务A首先读取了一条数据,然后执行逻辑的时候,事务B将这条数据改变了,
然后事务A再次读取的时候,发现数据不匹配了,就是不可重复读

同一数据,在一个事务的两次读取之间存在另一更新、删除的事务。
时间 取款事务A 取款事务B
T1 开始事务
T2 开始事务
T3 查询余额为1000元
T4 查询余额为1000元
T5 取出100元,修改余额为900元
T6 提交事务
T7 查询余额为900元(不可重复读)

解决办法:如果只有在修改事务完全提交之后才可以读取数据,则可以避免该问题。

  • 幻读

    指两次执行同一条 select语句会出现不同的结果,第二次读会增加一数据行,并没有说这两次执行是在同一个事务中。一般情况下,幻象读应该正是我们所需要的。但有时候却不是,如果打开的游标,在对游标进行操作时,并不希望新增的记录加到游标命中的数据集中来。隔离级别为游标稳定性的,可以阻止幻象读。例如:目前工资为1000的员工有10人。那么事务1中读取所有工资为1000的员工,得到了10条记录;这时事务2向员工表插入了一条员工记录,工资也为1000;那么事务1再次读取所有工资为1000的员工共读取到了11条记录。

    1
    一个事务中两次读取的数据的数量不一致 (一般基于表而言)
1
2
3
4
5
事务A首先根据条件索引得到N条数据,
然后事务B改变了这N条数据之外的M条或者增添了M条符合事务A搜索条件的数据,
导致事务A再次搜索发现有N+M条数据了,就产生了幻读。

同一数据,在一个事务中的两次(读取)写入之间存在另一插入的事务。
时间 统计金额事务A 转账事务B
T1 开始事务
T2 开始事务
T3 统计总存款为10000元
T4 新增一个存款账户转入100元
T5 提交事务
T7 再次统计总存款为10100元

不可重复读和幻读比较:

两者有些相似,但是前者针对的是update或delete,后者针对的insert。

隔离级别包括哪些?

数据库事务的隔离级别有4个,由低到高依次为

  • Read uncommitted (未提交读 ==》 读未提交)

如果一个事务已经开始写数据,则另外一个事务则不允许同时进行写操作,但允许其他事务读此行数据。该隔离级别可以通过 排他写锁 实现。这样就避免了更新丢失,却可能出现脏读。也就是说事务B读取到了事务A未提交的数据。

1
2
通俗来说就是,事务A开始写,事务B不能写,只能读。可能出现脏读。
(读到未提交的数据)
  • Read committed (提交读 ==》 读已提交)

读取数据的事务允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问该行。该隔离级别避免了脏读,但是却可能出现不可重复读。事务A事先读取了数据,事务B紧接了更新了数据,并提交了事务,而事务A再次读取该数据时,数据已经发生了改变。

1
2
3
通俗来讲就是,读数据都可以,一旦有事务开始写,会禁止其他事务访问该行。
避免脏读,可能出现不可重复读。
(同一事务读取一行数据,两次结果不一致,后面一次读取时,数据已经发生改变)
  • Repeatable read (可重复读)

可重复读是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,即使第二个事务对数据进行修改,第一个事务两次读到的的数据是一样的。这样就发生了在一个事务内两次读到的数据是一样的,因此称为是可重复读。读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务。这样避免了不可重复读取和脏读,但是有时可能出现幻象读。(读取数据的事务)这可以通过“共享读锁”和“排他写锁”实现。

1
2
3
4
5
6
7
8
9
10
11
12
在一个事务内,多次读同一数据。在事务未提交前,另一事务也在访问同一数据,
在第一个事务两次读之间,即使第二个事务修改数据,第一个数据的两次读到的数据仍然是一致的。

避免了不可重复读和脏读。

读事务,禁止写(允许读事务)。
写事务,禁止任何其他事务。

可能出现幻读 eg:
1. 先更新某一列为从‘1’更新为‘2
2. 插入一行,该里值仍未‘1
3. 再次读取仍读到列值为‘1’的数据
  • Serializable (串行化)

提供严格的事务隔离。它要求事务序列化执行,事务只能一个接着一个地执行,但不能并发执行。如果仅仅通过“行级锁”是无法实现事务序列化的,必须通过其他机制保证新插入的数据不会被刚执行查询操作的事务访问到。序列化是最高的事务隔离级别,同时代价也花费最高,性能很低,一般很少使用,在该级别下,事务顺序执行,不仅可以避免脏读、不可重复读,还避免了幻像读。

隔离级别 脏读 不可重复读 幻读 第一类丢失更新 第二类丢失更新
读未提交 允许 允许 允许 不允许 允许
读已提交 不允许 允许 允许 不允许 允许
可重复读 不允许 不允许 允许 不允许 不允许
串行化 不允许 不允许 不允许 不允许 不允许