开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Docker部署SpringBoot项目

发表于 2021-07-26
  1. 将spring boot项目打包成一个jar包

docker.PNG
2. 预先在服务器上创建一个存放jar包的文件夹

捕获.PNG

  1. 利用xshell将打包好的jar包发送到服务器上

注意:需要先在xshell上新建一个会话,输入命令rz -y.选择要上传的文件
4. 在我们第二步创建的文件夹下 创建一个dockerfile文件.docker的内容如下图所示:

捕获.PNG

注意:dockerfile文件里面的内容根据自己的情况而定

  1. 查看hellowDocker文件夹中包含的内容

捕获.PNG

6.制作镜像

捕获.PNG

7.查看镜像有没有制作成功

捕获.PNG

8.启动容器

捕获.PNG

注意:
-d参数是让容器后台运行
-p 是做端口映射,将服务器中的8087端口映射到容器中的8087(项目中端口配置的是8087)端口

9.在浏览器中进行测试,查看是否成功(服务器ip:端口号/项目中的方法路径)

捕获.PNG

至此已成功完成.

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spring Boot资源文件问题总结(Spring Boo

发表于 2021-07-26

文件系统是我们开发过程中常常会接触的问题。那么在Spring Boot框架中,文件的访问又是什么样的呢?今天在此做一个总结。

1,file和classpath

存放在电脑上实际位置的文件,在Spring Boot中用file:开头表示。例如:

  • file:a.txt 当前目录下的a.txt文件。当前路径在开发环境下一般为Maven项目的目录下(与pom.xml同目录下),在打包为jar文件后当前路径即为运行jar文件时的运行路径。
  • file:D:\a.txt 表示绝对路径,在此不多赘述。

而在jar文件内部中,我们一般把文件路径称为classpath,所以读取内部的文件就是从classpath内读取,classpath指定的文件不能解析成File对象,但是可以解析成InputStream。例如:

  • classpath:/a.txt jar包根目录下的a.txt。classpath以/开头表示绝对路径,即为jar包根目录。

2,Spring Boot的静态资源访问

我们都知道Spring Boot工程文件夹中的src/main/resources是用于存放资源的地方。默认时Spring Boot打包之后静态资源位置如下:

  • classpath:/static
  • classpath:/public
  • classpath:/resources
  • classpath:/META-INF/resources

在Spring Boot中classpath的根目录就对应工程文件夹下的src/main/resources。

可以先看这个例子:

在工程文件夹下src/main/resources/static下放入图片qiqi.png:

image.png

运行,访问127.0.0.1:8080/qiqi.png,效果如下:

image.png

这个例子可见外部访问的资源路径和Spring Boot工程中资源文件路径的一一对应关系。即外部访问时的“根目录”即对应着上述的四个静态资源位置(classpath)。

还可以新建一个Controller类,写如下方法:

1
2
3
4
java复制代码@GetMapping("/pic")
public String showPic() {
return "/qiqi.png";
}

运行,访问127.0.0.1:8080/pic,效果同上。

在这个Controller方法中,上面@GetMapping是路由路径,return的是对应的资源路径。

其实这个默认的资源路径是可以修改的。

我们需要知道在配置文件application.properties中可以加入下列两个配置项:

1
2
arduino复制代码spring.mvc.static-path-pattern
spring.web.resources.static-locations

我们来逐一进行讲解。

(1) spring.mvc.static-path-pattern - 指定资源访问路径

这个spring.mvc.static-path-pattern代表的是应该以什么样的路径来访问静态资源,也就是只有静态资源满足什么样的匹配条件,Spring Boot才会处理静态资源请求。说白了就是资源的外部访问路径。根据上述例子我们知道了这个配置默认值为/**。假设在上述工程配置文件中加入:

1
properties复制代码spring.mvc.static-path-pattern=/resources/**

那么再访问我们那个图片就要访问网址:127.0.0.1:8080/resources/qiqi.png

image.png

好了,我们如果现在想使用Controller类的@GetMapping进行路由的话,如果还是像上面那么写:

1
2
3
4
java复制代码@GetMapping("/pic")
public String showPic() {
return "/qiqi.png";
}

访问127.0.0.1:8080/pic,你会发现:

image.png

为什么这时就不行了呢?

这是因为我们改变了spring.mvc.static-path-pattern配置的值,那么我们对应的Controller类方法中的返回值,也要对应改变。

之前spring.mvc.static-path-pattern没有配置那默认就是/**,那访问/qiqi.png就可以找到图片。现在这个配置改为/resources/**,那很显然要访问/resources/qiqi.png了,再访问/qiqi.png当然访问不到了!因此对应的Controller类方法中也要做出对应修改。

上述配置spring.mvc.static-path-pattern为/resources/**,那么我们修改Controller方法如下:

1
2
3
4
java复制代码@GetMapping("/pic")
public String showPic() {
return "/resources/qiqi.png";
}

可见返回值改成了/resources/qiqi.png。

image.png

可见,Controller类中的方法的返回值,并非是资源文件的实际的相对路径,而是对应的资源的外部访问路径。这一点也是我和我身边许多朋友容易混淆的一点。

(2) spring.web.resources.static-locations - 指定静态资源查找路径

再者,spring.web.resources.static-locations用于指定静态资源文件的查找路径,查找文件是会依赖于配置的先后顺序依次进行。根据上述例子可见这个值默认是:

1
typescript复制代码classpath:/static,classpath:/public,classpath:/resources,classpath:/META-INF/resources

其实上面提到了Spring Boot默认资源文件位置,实质上就是这个配置的值。

假设在上述工程中配置文件写入:

1
properties复制代码spring.web.resources.static-locations=classpath:/myRes

那么我们要将qiqi.png放在项目文件夹的src\main\resources\myRes文件夹中,才能访问:

image.png

配置此项后,默认值将失效!

还可以使用磁盘路径例如:

1
properties复制代码spring.web.resources.static-locations=file:res

即指定资源文件在项目文件夹中的res目录中(即打包后运行jar文件时的运行路径下的res文件夹中)。也可以使用绝对路径。

image.png

image.png

通俗地讲,spring.mvc.static-path-pattern配置指定了我们外部访问的路径,而访问这个外部路径时就会去spring.web.resources.static-locations配置的路径中找对应的资源。

(3) 集成Spring Security之后导致上述配置失效

今天在维护一个项目的时候发现:即使是正确配置上述的静态资源配置,访问静态资源时一直报404,我也很纳闷:之前好好的啊!怎么就不行了呢?经查阅各种资料发现:若配置了拦截器,则会导致上述配置失效。这个项目使用了Spring Security,可能是因为其中的拦截器配置导致这个资源配置失效了。

之前一个项目配置了Swagger之后也出现了这个配置失效的问题,我想应该是同一个原因导致。

经参考官方文档之后,发现还有一个方式可以配置静态资源访问路径和对应位置。我们新建一个配置类,重写WebMvcConfigurer中的addResourceHandlers方法即可。

因此,在一些外部依赖自带拦截器的情况下,就很有可能覆盖我们上述资源路径配置,导致我们上述资源配置失效。因此这个时候,我们就不能通过写上述配置文件的方式配置静态资源访问了!就要通过写配置类的方式。

我们先来看一个例子,我这里项目目录结构如下:

image.png

然后我们新建一个软件包config,在里面写配置类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码package com.example.resourcetest.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
* 自定义MVC配置器
*/
@Configuration
public class MyWebMvcConfig implements WebMvcConfigurer {

/**
* 重写资源路径配置
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/image/**").addResourceLocations("file:res/image/");
}

}

可见我们只需要调用addResourceHandlers方法参数registry的方法也可以实现资源路径配置,配置效果和上面是一样的。

其中两个方法的意义如下:

  • addResourceHandler 等同于上述配置spring.mvc.static-path-pattern,代表的是应该以什么样的路径来访问静态资源
  • addResourceLocations 等同于上述配置spring.web.resources.static-locations,用于指定静态资源文件的查找路径

同样地,配置此项后,默认的资源搜索路径将失效!

可见我们要先调用addResourceHandler再调用addResourceLocations,两者是一一对应的,上述代码意思就是:外部访问路径/image/xxx时,就会去当前路径下res/image/目录下找xxx。

现在,访问127.0.0.1:8080/image/gz-12.png,可见访问成功:

image.png

可见通过配置类的方式,我们仍然可以实现上述在配置文件中实现的效果。不过这里需要注意的是,和配置文件中不同,指定静态文件查找路径时,若路径是个目录则必须以/结尾!否则也会出现404的情况。

当然,在addResourceHandler和addResourceLocations方法中都可以添加多个路径,例如:

1
java复制代码registry.addResourceHandler("/image/**").addResourceLocations("file:res/image/", "classpath:/static/");

也就是说外部访问/image/xxx时,会去当前目录下res/image/和类路径/static/中去寻找xxx,上面也讲了类路径classpath了,这里对应的也是一样的。

还可以这样:

1
java复制代码registry.addResourceHandler("/image/**", "/img/**").addResourceLocations("file:res/image/");

也就是写了多个外部访问路径,表示访问/image/xxx和/img/xxx时,都会到当前路径下的res/image/下去查找xxx。

当然,事实上配置类配置的方式也会更加高级,上述配置文件中我们只能配置一个外部访问路径,对应其它多个实际资源查找路径。而在配置类中,我们可以定义多个外部访问路径,对应不同的资源查找路径,例如我将代码改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码package com.example.resourcetest.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
* 自定义MVC配置器
*/
@Configuration
public class MyWebMvcConfig implements WebMvcConfigurer {

/**
* 重写资源路径配置
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 访问/image/xxx对应去res/image查找资源
registry.addResourceHandler("/image/**").addResourceLocations("file:res/image/");
// 访问/web/xxx对应去res/web下查找资源
registry.addResourceHandler("/web/**").addResourceLocations("file:res/web/");
}

}

这时,我访问http://127.0.0.1:8080/image/gz-12.png可以访问到res/image中的图片,然后访问http://127.0.0.1:8080/web/test.html可以访问到res/web中的网页。

可见,配置类提供了更加灵活的配置方式,还能够解决我们配置被其它依赖覆盖的问题。

需要注意的是,通常一个项目只能有一个类实现WebMvcConfigurer,否则会造成覆盖产生问题。除了这里配置静态资源之外,之前做用户登录的拦截器也要实现这个接口中的方法,这些方法是可以写在一个配置类中的,毕竟实现的是一个接口。

然后同样地,如果是要自定义Controller路由资源,也要和上述配置文件中的一样注意return的访问路径问题。例如我要自定义res/image中图片访问路径,由于配置了addResourceHandler("/image/**"),那对应的Controller方法如下:

1
2
3
4
java复制代码@GetMapping("/rabbit-halloween")
public String image() {
return "/image/gz-12.png";
}

然后访问http://127.0.0.1:8080/rabbit-halloween也可以访问到图片。

所以说如果发现配置文件配置资源路径不起作用,我们就可以删掉配置文件中的相关配置,通过编写配置类的方式来实现资源路径自定义,包括更加灵活的情况下例如需要多个访问路径对应各自不同的资源文件查找路径,也需要用到配置类方式。

3,Spring Boot的配置文件位置指定

我们也知道Spring Boot的配置文件默认是位于classpath:/application.properties,默认会被打包进jar文件。

其实我们也可以修改这个配置文件的位置。

在我们的Spring主类上加入如下注解:

1
java复制代码@PropertySource(value={"自定义配置文件路径"})

value表示配置文件位置,也可以填多个:

1
java复制代码@PropertySource(value={"配置1路径", "配置2路径"})

此处以我的主类全部代码为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.PropertySource;

@SpringBootApplication
@PropertySource(value={"file:self.properties"})
public class DemoApplication {

public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}

}

即定义配置文件为项目文件夹下的self.properties文件(打包后运行jar文件时的运行目录下的self.properties文件)。

4,多环境的配置文件外置方案

我们知道,默认情况下,我们可以在src/main/resources文件夹下创建多个配置文件以对应多个不同环境下的配置,方便灵活切换,例如开发-生产环境下,我们一共有三个配置文件:

1
2
3
matlab复制代码application.properties
application-dev.properties
application-prod.properties

然后在主配置文件application.properties里面配置一个配置项,即可一键切换配置环境:

1
2
properties复制代码# 指定当前使用开发环境配置文件
spring.profiles.active=dev

我们默认的配置文件名是application,因此多环境的情况下,配置文件命名如下:

1
matlab复制代码application-环境配置名.properties

主配置文件就是application.properties,在里面配置:

1
properties复制代码spring.profiles.active=环境配置名

运行时即可使用指定环境的配置文件。

这个时候想配置文件外置,如果按照上述第3部分的方法来,发现就不行了。那么多环境配置的情况下,配置文件如何外置呢?

我们需要先知道,其实Spring Boot会默认在这四个位置扫描配置文件:

1
2
3
4
makefile复制代码file:./config/
file:./
classpath:/config/
classpath:/

我们可以指定spring.config.location属性,来实现自定义Spring Boot的配置文件扫描路径。spring.config.location属性不仅可以设定扫描指定的配置文件,还可以指定扫描指定文件夹。

在我们的main方法中最开头,使用System.setProperty方法即可设定,下面给几个例子:

1
2
3
4
5
6
7
8
java复制代码// 扫描项目文件夹(jar运行目录)中Resources/config目录中所有的配置文件
System.setProperty("spring.config.location", "file:Resources/config/");

// 扫描项目文件夹(jar运行目录)中Resources/config/app.properties文件
System.setProperty("spring.config.location", "file:Resources/config/app.properties");

// 扫描项目文件夹(jar运行目录)中Resources/config目录和Resources/config2目录中所有的配置文件
System.setProperty("spring.config.location", "file:Resources/config/, file:Resources/config2/");

可见spring.config.location属性比较灵活,既可以设定文件还可以指定文件夹,注意指定的如果是文件夹,路径最后一定要以/结尾。指定多个文件或者文件夹时路径中间以英文逗号分隔。

好了,知道了spring.config.location属性,我们就知道多环境配置文件外置的方法了。例如我想把所有配置文件application.properties、application-dev.properties和application-prod.properties放到项目文件夹下的Resources/config目录下,那么我的完整主类代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码package com.gitee.swsk33.test;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class TestApplication {

public static void main(String[] args) {
// 设定配置文件扫描目录
System.setProperty("spring.config.location", "file:Resources/config/");
SpringApplication.run(TestApplication.class, args);
}

}

可见很简单,只在主方法main中最头部写了System.setProperty("spring.config.location", "file:Resources/config/");这一行代码即可完成配置。

然后我的三个配置文件放在项目文件夹下的Resources/config目录下:

image.png

再在主配置文件里面配置:

1
properties复制代码spring.profiles.active=dev

运行,即可使用开发环境的配置文件application-dev.properties了。

5,配置文件改名

我们也知道,Spring Boot配置文件默认名字是application.properties,Spring Boot默认情况下也是通过搜寻这个名字的文件找到配置文件的。

如果说想改配置文件的名字怎么做呢?其实除了上述直接指定配置文件路径以外,还可以修改属性spring.config.name来实现,也是使用System.setProperty方法来修改。例如在main方法最前面写上:

1
java复制代码System.setProperty("spring.config.name", "config");

那么Spring Boot就会去搜寻名为config.properties的文件作为配置文件。

因此可见spring.config.name的默认值为application,这个值不需要写扩展名,扩展名会在Spring Boot中自动适配。

上面修改了spring.config.name属性为config,那么如果说是多环境配置,我们的其余环境的配置文件也要跟着改为如下:

1
2
arduino复制代码config-dev.properties
config-prod.properties

6,总结

Spring Boot的资源文件访问和我们普通Java程序可能有所不同,大家一定要注意资源文件配置,以及配置文件的加载。

本文参考的官方文档:

  • Spring Boot静态资源访问
  • @PropertySource标识的使用
  • Spring Boot外置配置文件

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Redis只能做缓存?太out了!

发表于 2021-07-26

⚠️本文为掘金社区首发签约文章,未获授权禁止转载

大多数数据库,由于经常和磁盘打交道,在高并发场景下,响应会非常的慢。为了解决这种速度差异,大多数系统都习惯性的加入一个缓存层,来加速数据的读取。redis由于它优秀的处理能力和丰富的数据结构,已经成为了事实上的分布式缓存标准。

但是,如果你以为redis只能做缓存的话,那就太小看它了。

redis丰富的数据结构,使得它的业务使用场景非常广泛,加上rdb的持久化特性,它甚至能够被当作落地的数据库使用。在这种情况下,redis能够撑起大多数互联网公司,尤其是社交、游戏、直播类公司的半壁江山。

1. Redis能够胜任存储工作

redis提供了非常丰富的集群模式:主从、哨兵、cluster,满足服务高可用的需求。同时,redis提供了两种持久化方式:aof和rdb,常用的是rdb。

通过bgsave指令,主进程会fork出新的进程,回写磁盘。bgsave相当于做了一个快照,由于它并没有WAL日志和checkpoint机制,是无法做到实时备份的。如果机器突然断电,那就很容易丢失数据。

幸运的是,redis是内存型的数据库,主丛同步的速度是非常快的。如果你的集群维护的好,内存分配的合理,那么除非机房断电,否则redis的SLA,会一直保持在非常高的水平。

img

听起来不是绝对可靠啊,有丢失数据的可能!这在一般CRUD的业务中,是无法忍受的。但为什么redis能够满足大多数互联网公司的需求?这也是由业务属性所决定的。

在决定最大限度拥抱redis之前,你需要确认你的业务是否有以下特点:

除了核心业务,是否大多数业务对于数据的可靠性要求较低,丢失一两条数据是可以忍受的?

  1. 面对的是C端用户,可根据用户ID快速定位到一类数据,数据集合普遍较小?无大量范围查询需求?
  2. 是否能忍受内存型数据的成本需求?
  3. 是否业务几乎不需要事务操作?

很幸运的是,这类业务需求特别的多。比如常见的社交,游戏、直播、运营类业务,都是可以完全依赖Redis的。

2. Reids应用场景

Redis具有松散的文档结构,丰富的数据类型,能够适应千变万化的scheme变更需求,接下来我将介绍Redis除缓存外的大量的应用场景。

img

2.1 基本用户数据存储

在传统的数据库设计中,用户表是非常难以设计的,变更的时候会伤筋动骨。使用Redis的hash结构,可以实现松散的数据模型设计。某些不固定,验证型的功能属性,可以以JSON接口直接存储在hash的value中。使用hash结构,可以采用HGET和HMGET等指令,只获取自己所需要的数据,在使用上也是非常便捷的。

1
2
3
4
5
6
7
sql复制代码>HSET user:199929 sex m
>HSET user:199929 age 22
>HGETALL user:199929
1) "sex"
2) "m"
3) "age"
4) "22"

这种非统计型的、读多写少的场景,是非常适合使用KV结构进行存储的。Redis的hash结构提供了非常丰富的指令,某个属性也可以使用HINCRBY进行递增递减,非常的方便。

2.2 实现计数器

上面稍微提了一下HINCRBY指令,而对于Redis的Key本身来说,也有INCRBY指令,实现某个值的递增递减。

比如以下场景:统计某个帖子的点赞数;存放某个话题的关注数;存放某个标签的粉丝数;存储一个大体的评论数;某个帖子热度;红点消息数;点赞、喜欢、收藏数等。

1
2
3
4
sql复制代码> INCRBY feed:e3kk38j4kl:like 1
> INCRBY feed:e3kk38j4kl:like 1
> GET feed:e3kk38j4kl:like
"2"

像微博这样容易出现热点的业务,传统的数据库,肯定是撑不住的,就要借助于内存数据库。由于Redis的速度非常快,就不用再采用传统DB非常慢的count操作,所有这种递增操作都是毫秒级别的,而且效果都是实时的。

2.3 排行榜

排行榜能提高参与者的积极性,所以这项业务非常常见,它本质上是一个topn的问题。

Redis中有一个叫做zset的数据结构,使用跳表实现的有序列表,可以很容易实现排行榜一类的问题。当存入zset中的数据,达到千万甚至是亿的级别,依然能够保持非常高的并发读写,且拥有非常棒的平均响应时间(5ms以内)。

使用zadd 可以添加新的记录,我们会使用排行相关的分数,作为记录的score值,然后使用zrevrange指令即可获取实时的排行榜数据,而zrevrank则可以非常容易的获取用户的实时排名。

1
2
3
4
5
6
7
8
9
10
11
12
python复制代码>ZADD sorted:xjjdog:2021-07  55 dog0
>ZADD sorted:xjjdog:2021-07 89 dog1
>ZADD sorted:xjjdog:2021-07 32 dog2
>ZCARD sorted:xjjdog:2021-07
>3
> ZREVRANGE sorted:xjjdog:2021-07 0 -10 WITHSCORES # top10排行榜
1) "dog1"
2) "89"
3) "dog0"
4) "55"
5) "dog2"
6) "32"

2.4 好友关系

set结构,是一个没有重复数据的集合,你可以将某个用户的关注列表、粉丝列表、双向关注列表、黑名单、点赞列表等,使用独立的zset进行存储。

使用ZADD、ZRANK等,将用户的黑名单使用ZADD添加,ZRANK使用返回的sorce值判断是否存在黑名单中。使用sinter指令,可以获取A和B的共同好友。

除了好友关系,有着明确黑名单、白名单业务场景的数据,都可以使用set结构进行存储。这种业务场景还有很多,比如某个用户上传的通讯录,计算通讯录的好友关系等等。

在实际使用中,使用zset存储这类关系的更多一些。zset同set一样,都不允许有重复值,但zset多了一个score字段,我们可以存储一个时间戳,用来标明关系建立所发生的时间,有更明确的业务含义。

2.5 统计活跃用户数

类似统计每天的活跃用户、用户签到、用户在线状态,这种零散的需求,实在是太多了。如果为每一个用户存储一个bool变量,那占用的空间就太多了。这种情况下,我们可以使用bitmap结构,来节省大量的存储空间。

1
2
3
4
5
6
7
8
9
10
shell复制代码>SETBIT online:2021-07-23 3876520333 1
>SETBIT online:2021-07-24 3876520333 1
>GETBIT online:2021-07-23 3876520333
1
>BITOP AND active online:2021-07-23 online:2021-07-24
>GETBIT active 3876520333
1
>DEBUG OBJECT online:2021-07-23
Value at:0x7fdfde438bf0 refcount:1 encoding:raw serializedlength:5506446 lru:16410558 lru_seconds_idle:5
(0.96s)

注意,如果你的id很大,你需要先进行一次预处理,否则它会占用非常多的内存。

bitmap包含一串连续的2进制数字,使用1bit来表示真假问题。在bitmap上,可以使用and、or、xor等位操作(bitop)。

2.6 分布式锁

Redis的分布式锁,是一种轻量级的解决方案。虽然它的可靠性比不上Zookeeper之类的系统,但Redis分布式锁有着极高的吞吐量。

一个最简陋的加锁动作,可以使用redis带nx和px参数的set指令去完成。下面是一小段简单的分布式样例代码。

1
2
3
4
5
6
7
8
9
10
11
12
typescript复制代码public String lock(String key, int timeOutSecond) {
for (; ; ) {
String stamp = String.valueOf(System.nanoTime());
boolean exist = redisTemplate.opsForValue().setIfAbsent(key, stamp, timeOutSecond, TimeUnit.SECONDS);
if (exist) {
return stamp;
}
}
}
public void unlock(String key, String stamp) {
redisTemplate.execute(script, Arrays.asList(key), stamp);
}

删除操作的lua为。

1
2
3
4
5
6
7
sql复制代码local stamp = ARGV[1]
local key = KEYS[1]
local current = redis.call("GET",key)
if stamp == current then
redis.call("DEL",key)
return "OK"
end

redisson的RedLock,是使用最普遍的分布式锁解决方案,有读写锁的差别,并处理了多redis实例情况下的异常问题。

2.7 分布式限流

使用计数器去实现简单的限流,在Redis中是非常方便的,只需要使用incr配合expire指令即可。

1
2
vbnet复制代码 incr key
expire key 1

这种简单的实现,通常来说不会有问题,但在流量比较大的情况下,在时间跨度上会有流量突然飙升的风险。根本原因,就是这种时间切分方式太固定了,没有类似滑动窗口这种平滑的过度方案。

同样是redisson的RRateLimiter,实现了与guava中类似的分布式限流工具类,使用非常便捷。下面是一个简短的例子:

1
2
3
4
5
6
7
ini复制代码 RRateLimiter limiter = redisson.getRateLimiter("xjjdogLimiter");
// 只需要初始化一次
// 每2秒钟5个许可
limiter.trySetRate(RateType.OVERALL, 5, 2, RateIntervalUnit.SECONDS);

// 没有可用的许可,将一直阻塞
limiter.acquire(3);

2.8 消息队列

redis可以实现简单的队列。在生产者端,使用LPUSH加入到某个列表中;在消费端,不断的使用RPOP指令取出这些数据,或者使用阻塞的BRPOP指令获取数据,适合小规模的抢购需求。

Redis还有PUB/SUB模式,不过pubsub更适合做消息广播之类的业务。

在Redis5.0中,增加了stream类型的数据结构。它比较类似于Kafka,有主题和消费组的概念,可以实现多播以及持久化,已经能满足大多数业务需求了。

2..9 LBS应用

早早在Redis3.2版本,就推出了GEO功能。通过GEOADD指令追加lat、lng经纬数据,可以实现坐标之间的距离计算、包含关系计算、附近的人等功能。

关于GEO功能,最强大的开源方案是基于PostgreSQL的PostGIS,但对于一般规模的GEO服务,redis已经足够用了。

2.10 更多扩展应用场景

要看redis能干什么,就不得不提以下java的客户端类库redisson。redisson包含丰富的分布式数据结构,全部是基于redis进行设计的。

redisson提供了比如Set、 SetMultimap、 ScoredSortedSet、 SortedSet, Map、 ConcurrentMap、 List、 ListMultimap、 Queue、BlockingQueue等非常多的数据结构,使得基于redis的编程更加的方便。在github上,可以看到有上百个这样的数据结构:github.com/redisson/re…

对于某个语言来说,基本的数组、链表、集合等api,配合起来能够完成大部分业务的开发。Redis也不例外,它拥有这些基本的api操作能力,同样能够组合成分布式的、线程安全的高并发应用。

由于Redis是基于内存的,所以它的速度非常快,我们也会把它当作一个中间数据的存储地去使用。比如一些公用的配置,放到redis中进行分享,它就充当了一个配置中心的作用;比如把JWT的令牌存放到Redis中,就可以突破JWT的一些限制,做到安全登出。

3. 一站式Redis面临的挑战

redis的数据结构丰富,一般不会在功能性上造成困扰。但随着请求量的增加,SLA要求的提高,我们势必会对Redis进行一些改造和定制性开发。

3.1 高可用挑战

redis提供了主从、哨兵、cluster等三种集群模式,其中cluster模式为目前大多数公司所采用的方式。

但是,redis的cluster模式,有不少的硬伤。redis cluster采用虚拟槽的概念,把所有的key映射到 0~16383个整数槽内,属于无中心化的架构。但它的维护成本较高,slave也不能够参与读取操作。

它的主要问题,在于一些批量操作的限制。由于key被hash到多台机器上,所以mget、hmset、sunion等操作就非常的不友好,经常发生性能问题。

redis的主从模式是最简单的模式,但无法做到自动failover,通常在主从切换后,还需要修改业务代码,这是不能忍受的。即使加上haproxy这样的负载均衡组件,复杂性也是非常高的。

哨兵模式在主从数量比较多的时候,能够显著的体现它的价值。一个哨兵集群,能够监控成百上千个集群,但是哨兵集群本身的维护是比较困难的。幸运的是,redis的文本协议非常简单,在netty中,甚至直接提供了redis的codec。自研一套哨兵系统,加强它的功能,是可行的。

3.2 冷热数据分离

redis的特点是,不管什么数据,都一股脑地搞到内存里做计算,这对于有时间序列概念,有冷热数据之分的业务,造成了非常大的成本考验。为什么大多数开发者喜欢把数据存放在MySQL中,而不是Redis中?除了事务性要求以外,很大原因是历史数据的问题。

通常,这种冷热数据的切换,是由中间件完成的。我们上面也谈到了,Redis是一个文本协议,非常简单。做一个中间件,或者做一个协议兼容的Redis模拟存储,是比较容易的。

比如我们Redis中,只保留最近一年的活跃用户。一个好几年不活跃的用户,突然间访问了系统,这时候我们获取数据的时候,就需要中间件进行转换,从容量更大,速度更慢的存储中查找。

这个时候,Redis的作用,更像是一个热库,更像是一个传统cache层做的事情,发生在业务已经上规模的时候。但是注意,直到此时,我们的业务层代码,一直都是操作的redis的api。它们使用这众多的函数指令,并不关心数据到底是真正存储在redis中,还是在ssdb中。

3.3 功能性需求

redis还能玩很多花样。举个例子,全文搜索。很多人都会首选es,但redis生态就提供了一个模块:RediSearch,可以做查询,可以做filter。

但我们通常还会有更多的需求,比如统计类、搜索类、运营效果分析等。这类需求与大数据相关,即使是传统的DB也不能胜任。这时候,我们当然要把redis中的数据,导入到其他平台进行计算啦。

如果你选择的是redis数据库,那么dba打交道的,就是rdb,而不是binlog。有很多的rdb解析工具(比如redis-rdb-tools),能够定期把rdb解析成记录,导入到hadoop等其他平台。

此时,rdb成为所有团队的中枢,成为基本的数据交换格式。导入到其他db后的业务,该怎么玩怎么玩,完全不会因为业务系统选用了redis就无法运转。

4. 总结

大多数业务系统,跑在redis上,这是很多一直使用MySQL做业务系统的同学所不能想象的。看完了上面的介绍,相信你能够对redis能够实现的存储功能有个大体的了解。打开你的社交app、游戏app、视频app,看一下它们的功能,能够涵盖多少呢?

我这里要强调的是,某些数据,并不是一定要落地到RDBMS才算安全,它们并不是一个强需求。

那既然redis这么厉害,为什么还要有mysql、tidb这样的存储呢?关键还在于业务属性上。

如果一个业务系统,每次交互的数据,都是一个非常大的结果集,并涉及到非常复杂的统计、过滤工作,那么RDBMS是必须的;但如果一个系统,能够通过某个标识,快速定位到一类数据,这一类数据在可以预见的未来,是有限的,那就非常适合Redis存储。

一个电商系统,选用redis做存储就是作死,但一个社交系统就快活的多。在合适的场景选用合适的工具,才是我们应该做的。

但是一个系统,能否在产品验证期,就能快速的响应变化,快速开发上线,才是成功的关键。这也是使用redis做数据库,所能够带来的最大好处。千万别被那概率极低的丢数据场景,给吓怕了。比起产品成功,你的系统即使是牢如钢铁,也一文不值。

⚠️本文为掘金社区首发签约文章,未获授权禁止转载

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spring Boot中的Mybatis分页插件-pageh

发表于 2021-07-26

很多时候我们写DAO层接口会写一个查询所有记录的方法,但是在数据量非常大的时候,查询所有记录会巨慢无比,这时我们就需要用到分页查询。pagehelper就是个很好的分页插件。

1,配置

pagehelper的项目地址:地址

我们只需在Maven中加入如下依赖即可:

1
2
3
4
5
xml复制代码<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.6</version>
</dependency>

2,执行分页查询

首先这里先写好DAO和Mapper XML的查询全部部分:

Mapper XML的select节点:

1
2
3
xml复制代码<select id="getAll" resultMap="userResultMap">
select * from `user`
</select>

DAO:

1
2
3
4
java复制代码/**
* 获取全部用户
*/
List<User> getAll();

User类表示一个用户的POJO类。

然后在Service中写一个查询指定页的用户的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@Autowired
private UserDAO userDAO;

/**
* 查询用户
*
* @param pageNum 当前页码
* @param pageSize 一页的记录数
*/
public Page<User> getUserList(int currentPage, int pageSize) {
Page<User> userPage = PageHelper.startPage(currentPage, pageSize).doSelectPage(() -> userDAO.getAll());
return userPage;
}

使用PageHelper类即可很轻松的进行分页查询了!可见PageHelper的startPage方法用于指定要获取的当前页码和一页的记录数,startPage这个方法就设定了分页查询的基本参数,返回了个PageMethod对象,然后在此基础上,执行PageMethod对象的doSelectPage方法,这个方法中通过lambda语句执行我们Mybatis的查询全部方法,这样内部就自动完成了分页逻辑,并返回了指定页码的数据记录。

最后查询的结果是个Page对象,这个对象有如下方法:

  • getPageNum 获取当前的页码
  • getPages 获取总页数
  • getTotal 获取总记录数
  • getResult 获取当前页的数据记录,为一个List集合

所以在Controller中我们就可以通过调用Service取得结果的Page对象后,再调用getResult方法获取这一页的数据:

1
2
java复制代码//查询第一页数据,每一页15条记录
userService.getUserList(1, 15).getResult();

上述startPage方法第一个参数写0和1是一样的,都表示获取第一页。

3,优化分页模型

实际业务中通常会自己封装一个类表示我们分页查询后的结果:

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
java复制代码import com.github.pagehelper.Page;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.io.Serializable;
import java.util.List;

/**
* 分页查询结果模型,存放整个结果分页基本信息和当前页信息以及内容
*/
@Setter
@Getter
@NoArgsConstructor
public class Paging<T> implements Serializable {

/**
* 当前页
*/
private int currentPageNum;

/**
* 总页数
*/
private int totalPageNum;

/**
* 每页记录数量
*/
private int pageSize;

/**
* 总记录数
*/
private long totalCount;

/**
* 当前页记录集合
*/
List<T> dataCurrentPage;

/**
* 传入Page结果对象构造Pageing对象
*
* @param pageResult 分页查询结果Page对象
*/
public Paging(Page<T> pageResult) {
this.currentPageNum = pageResult.getPageNum();
this.totalPageNum = pageResult.getPages();
this.totalCount = pageResult.getTotal();
this.pageSize = pageResult.getPageSize();
this.dataCurrentPage = pageResult.getResult();
}

}

这里封装了一个Paging类,表示我们查询的分页结果类。然后改造上述Service的方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@Autowired
private UserDAO userDAO;

/**
* 查询用户
*
* @param pageNum 当前页码
* @param pageSize 一页的记录数
*/
public Paging<User> getUserList(int currentPage, int pageSize) {
Page<User> userPage = PageHelper.startPage(currentPage, pageSize).doSelectPage(() -> userDAO.getAll());
Paging<User> userPaging = new Paging<User>(userPage);
return userPaging;
}

让Service处理我们得到的分页结果Page对象中所需要的属性,并放入我们自己的分页模型Paging并返回,这样更加方便。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

退钱?欧洲杯史上首个区块链奖杯诞生!一文带你简单了解什么是区

发表于 2021-07-26

说起足球不得不的提起这位退钱哥了!

image.png

在这里插入图片描述
2019年我老表获得首届欧洲国家联赛得分王奖杯!你看我老表笑的多开心!

在这里插入图片描述

最近举办的欧洲杯中,我们也可以看到区块链的身影哦(虽然是广告

  1. 欧洲杯与区块链

6月10日,欧足联(UEFA) 在官方网站宣布,蚂蚁集团旗下蚂蚁链成为2020欧洲杯全球合作伙伴。欧足联与蚂蚁链签署了一项为期五年的合作协议,表示将共同探索应用区块链等技术加速足球产业数字化转型,为全球球迷提供更好更丰富的观赛体验。

在这里插入图片描述

得分王奖杯的设计师周一然介绍
“ 足球是全世界通用的语言,而区块链也是一种建立人与人之间信任的‘共识语言’,两者十分契合。”
在这里插入图片描述

  1. 区块链简介

2.1 概念

狭义来说

区块链是一种按照时间顺序将数据区块以顺序相连的方式组合成的一种链式数据结构,并以密码学方式保证的不可篡改和不可伪造的分布式账本。

广义来说

区块链技术是利用块链式数据结构来验证和存储数据、利用分布式节点共识算法来生成和更新数据、利用密码学的方式保证数据传输和访问的安全性利用由自动化脚本代码组成的智能合约来编程和操作数据的一种全新的分布式基础架构与计算范式

2.2 基础技术

2.2.1 哈希运算

区块链账本数据主要通过父区块哈希值组成链式结构来保证不可篡改性。

哈希算法(散列算法)

主要功能:把任意长度的输入通过一定的计算,生成一个固定长度的字符串,输出的字符串称为该输入的哈希值。

例子:
在百度随便找一个在线Hash算法
在这里插入图片描述
这里有很多种哈希算法!我们就用SHA-256为例子!
在这里插入图片描述
Go语言代码实现:
MD5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
go复制代码package main
import(
"fmt"
"crypto/md5"
"encoding/hex"
)
func main(){
//方法一
data:=[]byte("hello world")
s:=fmt.Sprintf("%x",md5.Sum(data))
fmt.Println(s)
//方法二
h:=md5.New()
h.Write(data)
s= hex.EncodeToString(h.Sum(nil))
fmt.Println(s)
}

SHA-256

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
go复制代码package main
import(
"fmt"
"github.com/nebulasio/go-nebulas/crypto/hash"
"encoding/hex"
"crypto/sha256"
)

func main(){
//方法一 一个方法直接输出
data:=[]byte("hello world")
hash:=hash.Sha256(data)
fmt.Println(hex.EncodeToString(hash))
sha256.New()
//方法二 按照步骤一步一步输出
h:= sha256.New() //创建sha256算法
h.Write(data) //用sha256算法对参数a进行加密,得到8个变量
hashTemp := h.Sum(nil)//将8个变量相加得到最终hash
fmt.Println(hex.EncodeToString(hashTemp))
}

特点:

  • 正向快速
    给定数据,可以在极短的时间内快速得到哈希值。
  • 输入敏感
    输入的信息如果发生了微小的变化,重新生成的哈希值与原来的哈希值也会有天壤之别。
  • 逆向困难
    无法在短时间内根据哈希值计算出原始的输入信息,这个特性也是哈希算法安全的基础,也因此是现代密码学的重要组成。
  • 强抗碰撞性
    不同的输入很难可以产生相同的哈希输出。

另外:

  • 通过哈希构建的区块链的链式结构,实现了防篡改。
  • 通过哈希构建的默克尔树,实现内容改变的快速检测。

2.2.2 数字签名

数字签名在信息安全,包括身份认证、数据完整性、不可否认性以及匿名性有着重要应用,是吸纳带密码学的重要分支。签名隶属于公钥密码学。

签名过程:
发送方用自己的私钥对发送信息进行所谓的加密运算,得到一个hash值,该hash值就是签名。使用时需要将签名和信息发给接收方。接受者用发送方公开的公钥和接收到的信息对签名及逆行验证,通过认证,说明接受到的信息是完整的,准确的,否则说明消息来源不对。

数字签名具体过程:

  1. 发送方A对原始数据通过哈希算法计算数字摘要,使用非对称密钥对中的私钥对数字摘要进行加密,这个加密后的数据就是数字签名。
  2. 数字签名与A的原始数据一起发送给验证签名的任何一方。

验证数字签名的流程:

  1. 首先,签名的验证方,一定要持有发送方A的非兑成密钥对的公钥
  2. 在接收到数字签名与A的原始数据后,首先使用公钥,对数字签名进行解密,得到原始摘要值。
  3. 然后对A的原始数据通过同样的哈希算法计算摘要值,进而比对解密得到的摘要值与重新计算得到的摘要值是否相同,如果相同,则签名通过。

在这里插入图片描述

2.2.3 P2P网络

peer to peer,简称p2p,对等网络。处于p2p中的网络中的所有节点地位都是相等的,网络不依赖一个中心。

2.2.4 共识算法

概念

由于点对点网络下存在较高的网络时延,各个节点所观察到的事务先后顺序不可能完全一致。因此区块链系统需要设置一种机制对在差不多时间内发生的事务的先后顺序达成一致。这种对一个时间窗口内的事务的先后顺序达成共识的算法被称为共识机制。

  • 共识算法:节点依据共识规矩达成共识的计算算法
  • 共识规则:每个区块链里面都有经过精心设计的规则性协议,这些协议通过共识算法来保证共识规则得以正确执行。

相当于是生活中的法律

主要有以下这几种共识算法:

  • 工作量证明PoW:比特币BTC、以太坊ETH、以太坊经典ETC
  • 权益证明PoS:ADA艾达币、Peercoin点点币
  • 授权工作证明DPoS:EOS、Asch、Steem
  • 拜占庭容错算BFT:实用拜占庭容错PBFT、派生BFT
  • RAFT算法:ETCD

几种算法的对比

PoW PoS DPoS Raft PBFT
场景 公链 公链、联盟链 公链、联盟链 联盟链 联盟链
去中心化 完全 完全 完全 半中性化 半中性化
记账节点 全网 全网 选出若干代表 选出一个Leader 动态决定
响应时间 10分钟 1分钟 3秒左右 秒级 秒级
存储效率 全账本 全账本 全账本 全账本 全账本+部分账本
吞吐量 约7TPS 约15TPS 约300TPS或更高 几千甚至上万 约10000TPS或更高
容错 50% 50% 50% 50% 33%

2.2.5 智能合约

定义:

简单来说,智能合约是一种在满足一定条件时,就自动执行的计算机程序。
例如自动售货机可以看作是一个智能合约的系统。客户需要选择商品并完成

特点:

  1. 计算机程序的if-then语句
  2. 条约达成时自动执行
  3. 数字化的合同
  4. 计算系统自动执行条款

特征:

  • 数据透明:区块链上的数据对参与方是公开透明的,数据处理也是公开透明的。
  • 不可篡改:区块链本身的所有数据不可篡改,区块链上的智能合约代码以及运行产生的数据输出也是不可篡改的。
  • 永久运行:支撑区块链网络的节点往往达到数百甚至上千,部分节点的失效并不会导致智能合约的停止,其可靠性理论上接近于永久运行。

构成:

  • 参与方:参与数字资产交易的人或是组织。
  • 资产:具备一定价值而作为交易标得地事物,可以是具体的可以是抽象的。
  • 交易:参与方对资产进行查询,转移等操作的过程。
  • 账本:记录资产的归属及其交易事实的数据库。

在这里插入图片描述

2.3 特点

2.3.1 透明可信

  1. 人人记账保证人人获取完整信息,从而实现信息透明。
  2. 节点间决策过程共同参与,共识保证可信性

2.3.2 防篡改可追溯

防篡改:交易一旦在全网范围内经过验证并添加至区块链,就很难被修改或者抹除。

可追溯:是指区块链上发生的任意一笔交易都是完整记录的,我们可以针对某一状态在区块链上追查与其相关的全部历史交易。

2.3.3 隐私安全保障

由于区块链系统中的任意节点都包含了完整的区块校验逻辑,所以任意节点都不需要依赖其他节点完成区块链中交易的确认过程,也就是无需额外地信任其他节点。

2.3.4 系统高可靠

  1. 每个节点对等地维护一个账本并参与整个系统地共识,一个节点出现故障,整个系统也能够正常运行
  2. 区块链系统支持拜占庭容错。传统地分布式系统虽然也具有高可靠特性,但是通常只能容忍系统内的节点发生崩溃现象,或者出现网络分区的问题,而系统一旦被攻克,或者说修改了节点的信息处理逻辑,则整个系统都将无法正常工作。
  1. 简单代码实现区块链

区块链的区块所需要的字段

字节 字段 说明
4 版本 区块版本号,表示本区块遵守的验证规则
32 父区块头哈希值 前一区块的Merkle树根的哈希值,同样采取SHA256计算
32 Merkle根 该区块中交易的Merkle树根的哈希值,同样采用SHA256计算
4 时间戳 该区块产生的近似时间,精确到秒的UNIX时间戳,必须严格大于前11各区块的时间的中值,同时全节点也会拒接那些超过自己两个小时的时间戳的区块
4 难度目标 该区块工作量证明算法的难度目标,已经使用特定算法编码
4 Nonce 未来找到满足难度目标所设定的随机数,为了解决32为随机数在算力飞升的情况下不够用的问题,规定时间戳和coinbase交易信息均改变,以此扩展nonce的位数

**注意:**区块不存储hash值,节点接受区块后独立计算并存储在本地。

3.1 区块相关:

  1. 定义一个区块的结构Block

区块头:6个字段
区块体:字符串表示data

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
go复制代码/*
1.定义一个区块的结构Block
a.区块头:6个字段
b.区块体:字符串表示data
*/
//区块
type Block struct {
Version int64 //版本
PerBlockHash []byte //前一个区块的hash值
Hash []byte //当前区块的hash值,是为了简化代码
MerKelRoot []byte //梅克尔根
TimeStamp int64 //时间抽
Bits int64 //难度值
Nonce int64 //随机值

//区块体
Data []byte //交易信息
}
  1. 提供一个创建区块的方法
    NewBlock(参数)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
go复制代码提供一个创建区块的方法
NewBlock(参数)
*/
func NewBlock(data string ,prevBlockHash []byte) *Block {
var block Block
block = Block{
Version: 2,
PerBlockHash: prevBlockHash,
//Hash: []byte{}, //区块不存储hash值,节点接受区块后独立计算并存储在本地。
MerKelRoot: []byte{},
TimeStamp: time.Now().Unix(),
Bits: targetBits,
Nonce: 0,
Data: []byte(data),
}
// block.SetHash() //填充Hash
PoW:= NewProofOfWork(&block)
nonce , hash :=PoW.Run()
block.Nonce=nonce
block.Hash=hash
return &block
}

另外别忘了创世块

1
2
3
go复制代码func NewGensisBlock() *Block{
return NewBlock("Genesis Block!",[]byte{})
}
  1. 定义一个工作量证明的结构ProofOfWork
    block | 目标值
1
2
3
4
5
6
7
8
9
go复制代码/*
3. 定义一个工作量证明的结构ProofOfWork
block
目标值
*/
type ProofOfWork struct {
block *Block
target *big.Int //目标值
}
  1. 提供一个创造PoW的方法

NewProofOfWork(参数)

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
go复制代码/*
4. 提供一个创造PoW的方法
NewProofOfWork(参数)
*/
const targetBits = 24
func NewProofOfWork(block *Block) *ProofOfWork { //工作量证明
target := big.NewInt(1) //000....001
target.Lsh(target,uint(256-targetBits)) //将1向左移动 //ox00000010000000..00
pow:=ProofOfWork{
block: block,
target: target,
}
return &pow
}

func (pow *ProofOfWork) PrepareData(nonce int64) []byte {
// 源码里面是要传二维切片 func Join(s [][]byte, sep []byte) []byte
block := pow.block
tmp :=[][]byte{
IntToByte(block.Version),
block.PerBlockHash,
block.MerKelRoot,
IntToByte(block.TimeStamp),
IntToByte(block.Bits),
IntToByte(block.Nonce),
}
data:=bytes.Join(tmp,[]byte{}) //之后再计算hash
return data
}
  1. 提供一个计算哈希值的方法

Run()

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
go复制代码/*
5. 提供一个计算哈希值的方法
Run()
*/

func (pow *ProofOfWork)Run() (int64,[]byte) {
//1.凭借数据
//2.哈希值转成big.Int类型
var hash [32]byte
var nonce int64 = 0
var hashInt big.Int
fmt.Println("开始挖矿了!")
fmt.Printf("难度 target hash : %x\n" ,pow.target.Bytes())
for nonce < math.MaxInt64 {
data:=pow.PrepareData(nonce)
hash = sha256.Sum256(data)
// Cmp compares x and y and returns:
//
// -1 if x < y
// 0 if x == y
// +1 if x > y
//
hashInt.SetBytes(hash[:])
if hashInt.Cmp(pow.target) == -1 {
fmt.Printf("Found ,nonce :%d ,hash :%x \n",nonce,hash)
}else {
//fmt.Printf("Not Found ,current nonce :%d ,hash :%x \n",nonce,hash)
nonce++
}
}
return nonce,hash[:]
}
  1. 提供一个校验函数

IsValid()

1
2
3
4
5
6
7
8
9
10
11
12
13
go复制代码/*
6. 提供一个校验函数
IsValid()
*/

func (pow *ProofOfWork)IsValid() bool{
var hashInt big.Int
data := pow.PrepareData(pow.block.Nonce)
hash:=sha256.Sum256(data)
hashInt.SetBytes(hash[:])
return hashInt.Cmp(pow.target) == -1 //如果是-1就是找到了就是

}

3.2 区块链相关

  1. 定义一个区块链结构BlockChain

​ Block数组

1
2
3
4
5
6
7
go复制代码/*
1. 定义一个区块链结构BlockChain
​ Block数组
*/
type BlockChain struct {
blocks []*Block
}
  1. 提供一个创建BlockChain()的方法

​ NewBlockChain()

1
2
3
4
5
6
7
8
go复制代码/*
2. 提供一个创建BlockChain()的方法
​ NewBlockChain()
*/
func NewBlockChain() *BlockChain {
block := NewGensisBlock()
return &BlockChain{blocks:[]*Block{block}} //创建只有一个元素的区块链,初始化
}
  1. 提供一个添加区块的方法

​ AddBlock(参数)

1
2
3
4
5
6
7
8
9
go复制代码/*
3. 提供一个添加区块的方法
​ AddBlock(参数)
*/
func (bc *BlockChain)AddBlock(data string) {
PerBlockHash := bc.blocks[len(bc.blocks)-1].Hash //这一个区块的哈希是前一块的哈希值
block := NewBlock(data,PerBlockHash)
bc.blocks = append(bc.blocks,block)
}

一个简单的区块链就实现了!
在这里插入图片描述

最后

小生凡一,期待你的关注。
在这里插入图片描述

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

MyBatis从前世到今生一网打尽(全网最全,建议收藏) 一

发表于 2021-07-26

「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!」

一、框架概述

1.1 软件开发常用结构

1.1.1、三层架构

三层架构

​ 三层架构包含的三层:

  1. 界面层(User Interface layer)
  2. 业务逻辑层(Business Logic Layer)
  3. 数据访问层(Data access layer)

​ 三层架构分别的职责是:

  1. 界面层(表示层,视图层):主要功能是接受用户的数据,显示请求的处理结果。使用 web 页面和
    用户交互,手机 app 也就是表示层的,用户在 app 中操作,业务逻辑在服务器端处理。
  2. 业务逻辑层:接收表示传递过来的数据,检查数据,计算业务逻辑,调用数据访问层获取数据。
  3. 数据访问层:与数据库打交道。主要实现对数据的增、删、改、查。将存储在数据库中的数据提交
    给业务层,同时将业务层处理的数据保存到数据库。

他们处理请求的交互过程是:用户——> 界面层——>业务逻辑层——>数据访问层——>DB 数据库

1.1.2、为什么要使用三层架构

  1. 结构清晰,耦合度低,各层分工明确。
  2. 可维护性高,可拓展性高。
  3. 有利于标准化。
  4. 开发人员可以只关注整个结构中的其中某一层的功能实现。
  5. 有利于各层逻辑的复用。

1.2、框架初探究

1.2.1、什么是框架

​ 框架(Framework)是整个或部分系统的可重用设计,表现为一组抽象构件及构件实例间交互的方法;另一种认为,框架是可被应用开发者定制的应用骨架、模板。
​ 简单的说,框架其实是半成品软件,就是一组组件,供你使用完成你自己的系统。从另一个角度来说框架一个舞台,你在舞台上做表演。在框架基础上加入你要完成的功能。
​ 框架安全的,可复用的,不断升级的软件。

1.2.1、框架解决的问题

​ 框架要解决的最重要的一个问题是技术整合,在 J2EE 的 框架中,有着各种各样的技术,不同的应用,系统使用不同的技术解决问题。需要从 J2EE 中选择不同的技术,而技术自身的复杂性,有导致更大的风险。

​ 企业在开发软件项目时,主要目的是解决业务问题。 即要求企业负责技术本身,又要求解决业务问题。这是大多数企业不能完成的。框架把相关的技术融合在一起,企业开发可以集中在业务领域方面。

1.3、常用框架

1.3.1、Mybatis

1
2
markdown复制代码	MyBatis 是一个优秀的基于 java 的持久层框架,内部封装了 jdbc,开发者只需要关注 sql 语句本身,而不需要处理加载驱动、创建连接、创建 statement、关闭连接,资源等繁杂的过程。
MyBatis 通过 xml 或注解两种方式将要执行的各种 sql 语句配置起来,并通过 java 对象和 sql 的动态参数进行映射生成最终执行的 sql 语句,最后由 mybatis 框架执行 sql 并将结果映射为 java对象并返回。

1.3.2、Spring

1
2
markdown复制代码	Spring 框架为了解决软件开发的复杂性而创建的。Spring 使用的是基本的 JavaBean 来完成以前非常复杂的企业级开发。Spring 解决了业务对象,功能模块之间的耦合,不仅在 javase,web 中使用,大部分 Java 应用都可以从 Spring 中受益。
Spring 是一个轻量级控制反转(IoC)和面向切面(AOP)的容器。

1.3.3、SpringMVC

1
markdown复制代码	Spring MVC 属于 SpringFrameWork 3.0 版本加入的一个模块,为 Spring 框架提供了构建 Web应用程序的能力。现在可以 Spring 框架提供的 SpringMVC 模块实现 web 应用开发,在 web 项目中可以无缝使用 Spring 和 Spring MVC 框架

二、Mybatis简介

2.1、传统的JDBC

2.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
java复制代码public void findStudent() {
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
try {
//注册 mysql 驱动
Class.forName("com.mysql.jdbc.Driver");
//连接数据的基本信息 url ,username,password
String url = "jdbc:mysql://localhost:3306/springdb";
String username = "root";
String password = "123456";
//创建连接对象
conn = DriverManager.getConnection(url, username, password);
//保存查询结果
List<Student> stuList = new ArrayList<>();
//创建 Statement, 用来执行 sql 语句
stmt = conn.createStatement();
//执行查询,创建记录集,
rs = stmt.executeQuery("select * from student");
while (rs.next()) {
Student stu = new Student();
stu.setId(rs.getInt("id"));
stu.setName(rs.getString("name"));
stu.setAge(rs.getInt("age"));
//从数据库取出数据转为 Student 对象,封装到 List 集合
stuList.add(stu);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
//关闭资源
if (rs != null) ;
{
rs.close();
}
if (stmt != null) {
stmt.close();
}
if (conn != null) {
conn.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

2.1.2、存在的问题

​ 我们在使用JDBC进行实际开发过程中存在的问题也是很明显的:

  1. 代码比较多,开发效率低。
  2. 需要关注 Connection ,Statement, ResultSet 对象创建和销毁。
  3. 对 ResultSet 查询的结果,需要自己封装为 List。
  4. 重复的代码比较多。
  5. 业务代码和数据库的操作混在一起,不利于现代的开发习惯。

2.2、MyBatis 历史

​ MyBatis 是 Apache 的一个开源项目 iBatis, 2010 年 6 月这个项目由 Apache Software
Foundation 迁移到了 Google Code,随着开发团队转投 Google Code 旗下, iBatis3.x
正式更名为 MyBatis ,代码于 2013 年 11 月迁移到 Github。

​ iBatis 一词来源于“internet”和“abatis”的组合,是一个基于 Java 的持久层框架。 iBatis
提供的持久层框架包括 SQL Maps 和 Data Access Objects(DAO)。

2.3、MyBatis 简介

  1. MyBatis 是支持定制化 SQL、存储过程以及高级映射的优秀的持久层框架。
  2. MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集
  3. MyBatis 可以使用简单的XML 或注解用于配置和原始映射,将接口和JavaOld Java Objects,普通的 Java 。对象)映射成数据库中的记录。

2.4、现有持久化技术的对比

2.4.1、JDBC

  • SQL 夹在 Java 代码块里,耦合度高导致硬编码内伤。
  • 维护不易且实际开发需求中 sql 是有变化,频繁修改的情况多见。

2.4.2、Hibernate 和 JPA。

  • 长难复杂 SQL,对于 Hibernate 而言处理也不容易。
  • 内部自动生产的 SQL,不容易做特殊优化。
  • 基于全映射的全自动框架,大量字段的 POJO 进行部分映射时比较困难。导致数据库性能下降。

2.4.3、MyBatis

  • 对开发人员而言,核心 sql 还是需要自己优化。
  • sql 和 java 编码分开,功能边界清晰,一个专注业务、一个专注数据。

2.5、MyBatis解决的问题

  1. 减轻使用 JDBC 的复杂性,不用编写重复的创建 Connetion , Statement 。
  2. 不用编写关闭资源代码。
  3. 直接使用 java 对象,表示结果数据。让开发者专注 SQL 的处理。 其他分心的工作由 MyBatis 代劳。

2.6、下载 MyBatis

下载网址:github.com/mybatis/myb…

image-20201003131136245

image-20201003131210894

image-20201003131309299

三、入门MyBatis

3.1、开发环境的准备

3.1.1、导入 jar包

​ 一般的我们要导入三个jar包。

1
2
3
markdown复制代码myBatis-3.4.1.jar
mysql-connector-java-5.1.37-bin.jar
log4j.jar

3.1.2、导入 log4j 的配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
xml复制代码<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">

<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">

<appender name="STDOUT" class="org.apache.log4j.ConsoleAppender">
<param name="Encoding" value="UTF-8" />
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%-5p %d{MM-dd HH:mm:ss,SSS} %m (%F:%L) \n" />
</layout>
</appender>
<logger name="java.sql">
<level value="debug" />
</logger>
<logger name="org.apache.ibatis">
<level value="info" />
</logger>
<root>
<level value="debug" />
<appender-ref ref="STDOUT" />
</root>
</log4j:configuration>

3.1.3、创建测试表

1
2
3
4
5
6
7
8
9
10
mysql复制代码-- 创建库
CREATE DATABASE test_mybatis;
-- 使用库
USE test_mybatis;
-- 创建表
CREATE TABLE user(
id INT(11) PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50),
pwd VARCHAR(50),
);

3.1.4、创建 javaBean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码package com.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
* @author Xiao_Lin
* @date 2021/1/5 11:33
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private Integer id;
private String username;
private String pwd;
}

3.1.5、创建 MyBatis 的全局配置文件

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
xml复制代码<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!--配置 mybatis 环境-->
<environments default="mysql">
<!--id:数据源的名称-->
<environment id="mysql">
<!--配置事务类型:使用 JDBC 事务(使用 Connection 的提交和回滚)-->
<transactionManager type="JDBC"/>
<!--数据源 dataSource:创建数据库 Connection 对象
type: POOLED 使用数据库的连接池
-->
<dataSource type="POOLED">
<!--连接数据库的四个要素-->
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql:///javaweb?characterEncoding=utf-8&amp;useSSL=false"/>
<property name="username" value="root"/>
<property name="password" value="1101121833"/>
</dataSource>
</environment>
</environments>
<mappers>
<!--告诉 mybatis 要执行的 sql 语句的位置,写的路径是字节码输出路径-->
<mapper resource="com/dao/StudentDao.xml"/>
</mappers>
</configuration>

3.1.6、编写UserDao接口

1
2
3
4
java复制代码public interface UserDao {
/*查询所有数据*/
List<User> selectAll();
}

3.1.7、创建 Mybatis 的 sql 映射文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
xml复制代码<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<!--
用来存储SQL语句的
namespace属性里面必须写 用来书写当前的mapper是对哪个DAO接口的实现
他的意思是表示该配置文件的唯一标识,意味着不同的XxxMapper.xml文件的namespace的值时不同的
-->

<!--
select标签表示查询操作,
id属性表示在mapper配置文件中是唯一标识,一般使用方法名作为其值。
paraType属性表示传入参数的类型,可省略不写,底层使用了反射,根据传入参数得到对象的类型 标签体中编写sql语句,#{变量},#表示占位符,和jdbc的?一样。
如果传入的参数是简单类型(包括String),那么该类型可以任意写
如果传入的参数是对象类型,那么变量的名称必须使用对象对应的类中的属性
resultType: 查询语句的返回结果数据类型,使用全限定类名
-->

<mapper namespace="com.mapper.UserDao">
<select id="selectAll" resultType="com.domain.User">
select * from user
</select>
</mapper>

3.1.8、配日志

​ mybatis.xml 文件加入日志配置,可以在控制台输出执行的 sql 语句和参数。

1
2
3
java复制代码<settings>
<setting name="logImpl" value="STDOUT_LOGGING" />
</settings>

3.1.9、测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码public class test {
/*
* mybatis 入门
*/
@Test
public void testStart() throws IOException {
//1.mybatis 主配置文件
String config = "mybatis-config.xml";
//2.读取配置文件
InputStream in = Resources.getResourceAsStream(config);
//3.创建 SqlSessionFactory 对象,目的是获取 SqlSession
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
//4.获取 SqlSession,SqlSession 能执行 sql 语句
SqlSession session = factory.openSession();
//5.执行 SqlSession 的 selectList()
List<User> users = session.selectList("com.dao.UserDao.selectAll");
//6.循环输出查询结果
studentList.forEach( u -> System.out.println(u));
//7.关闭 SqlSession,释放资源
session.close();
}
}

3.2、CRUD操作

3.2.1、insert

接口中新增方法

1
java复制代码int insertUser(User user);

xml中新增sql语句

1
2
3
xml复制代码<insert id="insertUser">
insert into user(id,username,pwd) values(#{id},#{username},#{pwd})
</insert>

新增测试方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码@Test
public void testInsert() throws IOException {
//1.mybatis 主配置文件
String config = "mybatis-config.xml";
//2.读取配置文件
InputStream in = Resources.getResourceAsStream(config);
//3.创建 SqlSessionFactory 对象
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
//4.获取 SqlSession
SqlSession session = factory.openSession();
//5.创建保存数据的对象
User user = new User();
user.setId(1005);
user.setUsername("张三");
user.setPwd("123456");
//6.执行插入 insert
int rows = session.insert("com.dao.UserDao.insertUser",student);
//7.提交事务
session.commit();
System.out.println("增加记录的行数:"+rows);
//8.关闭 SqlSession
session.close();
}

3.2.2、update

UserDao 接口中增加方法

1
java复制代码int updateUser(User user);

UserDao.xml 增加 sql 语句

1
2
3
mysql复制代码<update id="updateUser">
update user set username = #{username} where id = #{id}
</update>

新增测试方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码@Test
public void testUpdate() throws IOException {
//1.mybatis 主配置文件
String config = "mybatis-config.xml";
//2.读取配置文件
InputStream in = Resources.getResourceAsStream(config);
//3.创建 SqlSessionFactory 对象
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
//4.获取 SqlSession
SqlSession session = factory.openSession();
//5.创建保存数据的对象
User user = new User();
user.setId(1005);//要修改的 id
user.setUsername("李四"); //要修改的年龄值
//6.执行更新 update
int rows = session.update("com.dao.UserDao.updateUser",user);
//7.提交事务
session.commit();
System.out.println("修改记录的行数:"+rows);
//8.关闭 SqlSession
session.close();
}

3.2.3、delete

UsertDao 接口中增加方法

1
java复制代码int deleteUser(int id);

UserDao.xml 增加 sql 语句

1
2
3
java复制代码<delete id="deleteUser">
delete from user where id=#{id}
</delete>

增加测试方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码@Test
public void testUpdate() throws IOException {
//1.mybatis 主配置文件
String config = "mybatis-config.xml";
//2.读取配置文件
InputStream in = Resources.getResourceAsStream(config);
//3.创建 SqlSessionFactory 对象
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
//4.获取 SqlSession
SqlSession session = factory.openSession();
//5.删除的 id
int id = 1001;
//6.执行删除 delete
int rows = session.delete("com.dao.UserDao.deleteStudent",id);
//7.提交事务
session.commit();
System.out.println("修改记录的行数:"+rows);
//8.关闭 SqlSession
session.close();
}

3.2.2、完成两个绑定

  1. Mapper 接口与 Mapper 映射文件的绑定在 Mppper 映射文件中的标签中的 namespace 中必须指定 Mapper 接口的全类名,包名加类名。
  2. Mapper 映射文件中的增删改查标签的 id 必须指定成 Mapper 接口中的方法,必须相同否则无法通过代理实现绑定。

3.3、Mybatis对象分析

3.3.1、Resources类

​ Resources 类,顾名思义就是资源,用于读取资源文件。其有很多方法通过加载并解析资源文件,返

回不同类型的 IO 流对象。

3.3.2、SqlSessionFactoryBuilder 类

​ SqlSessionFactory 的 创 建 , 需 要 使 用 SqlSessionFactoryBuilder 对 象 的 build() 方 法 。 由 于
SqlSessionFactoryBuilder 对象在创建完工厂对象后,就完成了其历史使命,即可被销毁。所以,一般会将
该 SqlSessionFactoryBuilder 对象创建为一个方法内的局部对象,方法结束,对象销毁。

3.3.3、SqlSessionFactory类

​ SqlSessionFactory 接口对象是一个重量级对象(系统开销大的对象),是线程安全的,所以一个应用

只需要一个该对象即可。创建 SqlSession 需要使用 SqlSessionFactory 接口的的 openSession()方法。他有几个重载方法:

  1. openSession(true):创建一个有自动提交功能的 SqlSession
  2. openSession(false):创建一个非自动提交功能的 SqlSession,需手动提交
  3. openSession():同 openSession(false)

3.3.4、SqlSession 接口

​ SqlSession 接口对象用于执行持久化操作。一个 SqlSession 对应着一次数据库会话,一次会话以

SqlSession 对象的创建开始,以 SqlSession 对象的关闭结束。

​ SqlSession 接口对象是线程不安全的,所以每次数据库会话结束前,需要马上调用其 close()方法,将

其关闭。再次需要会话,再次创建。 SqlSession 在方法内部创建,使用完毕后关闭。

3.4、抽取工具类

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
java复制代码public class MyBatisUtil {
//定义 SqlSessionFactory
private static SqlSessionFactory factory = null;
static {
//使用 静态块 创建一次 SqlSessionFactory
try{
String config = "mybatis-config.xml";
//读取配置文件
InputStream in = Resources.getResourceAsStream(config);
//创建 SqlSessionFactory 对象
factory = new SqlSessionFactoryBuilder().build(in);
}catch (Exception e){
factory = null;
e.printStackTrace();
}
}
/* 获取 SqlSession 对象 */
public static SqlSession getSqlSession(){
SqlSession session = null;
if( factory != null){
session = factory.openSession();
}
return session;
}
}

四、MyBatis 全局配置文件

4.1、MyBatis 全局配置文件简介

​ The MyBatis configuration contains settings and properties that have a dramatic effecton how MyBatis behaves.

​ MyBatis 的配置文件包含了影响 MyBatis 行为甚深的设置(settings )和属性(properties)信息。

4.2、文件结构

1
2
3
4
5
6
7
8
9
10
11
12
13
markdown复制代码configuration 配置
properties 属性
settings 设置
typeAliases 类型命名
typeHandlers 类型处理器
objectFactory 对象工厂
plugins 插件
environments 环境
environment 环境变量
transactionManager 事务管理器
dataSource 数据源
databaseIdProvider 数据库厂商标识
mappers 映射器

4.2.1、properties 属性

​ properties属性是配置数据源相关属性,可外部配置且可动态替换的,既可以在典型的 Java 属性文件中配置,亦可通过 properties 元素的子元素来配置

1
2
3
4
5
6
7
8
9
xml复制代码<properties>
<!--驱动名(MySQL5 和MySQL8 不同)-->
<property name="driver" value="com.mysql.jdbc.Driver" />
<!--url名字(MySQL5 和MySQL8 不同)-->
<property name="url"
<value="jdbc:mysql://localhost:3306/javaweb" />
<property name="username" value="root" />
<property name="password" value="123456" />
</properties>

​ 然而 properties 的作用并不单单是这样,你可以创建一个资源文件,名为db.properties 的文件,将四个连接字符串的数据在资源文件中通过键值对(key=value)的方式放置,不要任何符号,一条占一行

1
2
3
4
properties复制代码jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/javaweb
jdbc.username=root
jdbc.password=123456

​ 引入方式是这样

1
2
3
4
5
6
7
8
xml复制代码<!--

properties: 引入外部的属性文件
resource: 从类路径下引入属性文件
url: 引入网络路径或者是磁盘路径下的属性文件

-->
<properties resource="db.properties" ></properties>

在 environment 元素的 dataSource 元素中为其动态设置

1
2
3
4
5
6
7
8
9
10
xml复制代码<environments default="mysql">
<environment id="mysql">
<transactionManager type="JDBC" />
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}" />
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
</dataSource>
</environment>

4.2.2、settings 属性

​ 这是 MyBatis 中极为重要的调整设置,它们会改变 MyBatis 的运行时行为。一般设置数据库的懒加载和缓存之类是否开启

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
xml复制代码<settings>
<setting name="cacheEnabled" value="true"/>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="multipleResultSetsEnabled" value="true"/>
<setting name="useColumnLabel" value="true"/>
<setting name="useGeneratedKeys" value="false"/>
<setting name="autoMappingBehavior" value="PARTIAL"/>
<setting name="autoMappingUnknownColumnBehavior" value="WARNING"/>
<setting name="defaultExecutorType" value="SIMPLE"/>
<setting name="defaultStatementTimeout" value="25"/>
<setting name="defaultFetchSize" value="100"/>
<setting name="safeRowBoundsEnabled" value="false"/>
<setting name="mapUnderscoreToCamelCase" value="false"/>
<setting name="localCacheScope" value="SESSION"/>
<setting name="jdbcTypeForNull" value="OTHER"/>
<setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
</settings>

4.2.3、typeAliases 别名

​ 类型别名是为 Java 类型设置一个短的名字,可以方便我们引用某个类,引用的时候不需要再写全路径名字

1
2
3
xml复制代码<typeAliases>
<typeAlias type="com.domain.User" alias="user"/>
</typeAliases>

​ 更简单的写法:类很多的情况下,可以批量设置别名这个包下的每一个类创建一个默认的别名,就是简
单类名小写

1
2
3
xml复制代码<typeAliases>
<package name="com.domain.User"/>
</typeAliases>

​ MyBatis 默认已经取好的别名,不需要我们人为去配置

image-20201003194922405

4.2.4、typeHandlers 类型处理器

​ 无论是 MyBatis 在预处理语句(PreparedStatement)中设置一个参数时,还是从结果集中取出一个值时, 都会用类型处理器将获取的值以合适的方式转换成 Java 类型,MyBatis 中默认提供的类型处理器

image-20201003195016491

注意

  1. 日期和时间的处理,JDK1.8 以前一直是个头疼的问题。我们通常使用 JSR310 规范领导者 Stephen Colebourne 创建的 Joda-Time 来操作。1.8 已经实现全部的 JSR310 规范了
  2. 日期时间处理上,我们可以使用 MyBatis 基于 JSR310(Date and Time API)编写的各种日期时间类型处理器。
  3. MyBatis3.4 以前的版本需要我们手动注册这些处理器,以后的版本都是自动注册的,如需注册,需要下载 mybatistypehandlers-jsr310,并通过如下方式注册

image-20201003195202535

4.2.5、plugins 插件机制

​ 插件是 MyBatis 提供的一个非常强大的机制,我们可以通过插件来修改 MyBatis 的一些核心行为。插件通过动态代理机制,可以介入四大对象的任何一个方法的执行,四大对象如下:

  1. Executor (update, query, flushStatements, commit, rollback, getTransaction, close,
    isClosed)
  2. ParameterHandler (getParameterObject, setParameters)
  3. ResultSetHandler (handleResultSets, handleOutputParameters)
  4. StatementHandler (prepare, parameterize, batch, update, query)

4.2.6、 environments 环境配置

​ MyBatis 可以配置多种环境,比如开发、测试和生产环境需要有不同的配置, 每种环境使用一个 environment 标签进行配置并指定唯一标识符,可以通过environments 标签中的default 属性指定一个环境的标识符来快速的切换环境

  • ​ environment:指定具体环境
  • ​ id:指定当前环境的唯一标识
  • ​ transactionManager、dataSource 都必须有
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
xml复制代码<environments default="mysql">
<environment id="mysql">
<transactionManager type="JDBC" />
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}" />
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
</dataSource>
</environment>
<environment id="oracle">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${orcl.driver}" />
<property name="url" value="${orcl.url}" />
<property name="username" value="${orcl.username}" />
<property name="password" value="${orcl.password}" />
</dataSource>
</environment>
</environments>

4.2.6.1、transactionManager

他有三种可选的类型:JDBC ,MANAGED ,自定义

  1. JDBC:使用了 JDBC 的提交和回滚设置,依赖于从数据源得到的连接来管理事务范围,JdbcTransactionFactory。
  2. MANAGED:不提交或回滚一个连接、让容器来管理事务的整个生命周期,比如JEE应用服务器上下文,ManagedTransactionFactory。
  3. 自定义:实现 TransactionFactory 接口,type=全类名/别名。

4.2.6.2、dataSource

他有四种可选的类型: UNPOOLED , POOLED . JNDI , 自定义

  1. UNPOOLED:不使用连接池, UnpooledDataSourceFactory。
  2. POOLED:使用连接池, PooledDataSourceFactory。
  3. JNDI: 在 EJB 或应用服务器这类容器中查找指定的数据源。
  4. 自定义:实现 DataSourceFactory 接口,定义数据源的获取方式。

4.2.6.3、总结

​ 际开发中我们使用 Spring 管理数据源,并进行事务控制的配置来覆盖上述配置

4.2.7、databaseIdProvider 数据库厂商标识

MyBatis 可以根据不同的数据库厂商执行不同的语句

1
2
3
4
xml复制代码<databaseIdProvider type="DB_VENDOR">
<property name="MySQL" value="mysql"/>
<property name="Oracle" value="oracle"/>
</databaseIdProvider>

Type: DB_VENDOR

​ 使用 MyBatis 提供的 VendorDatabaseIdProvider 解析数据库厂商标识。也可以实现 DatabaseIdProvider 接口来自定义.会通过 DatabaseMetaData#getDatabaseProductName() 返回的字符串进行设置。由于通常情况下这个字符串都非常长而且相同产品的不同版本会返回不同的值,所以最好通过设置属性别名来使其变短.

​ Property-name:数据库厂商标识

​ Property-value:为标识起一个别名,方便 SQL 语句使用 databaseId 属性引用

​ 配置了 databaseIdProvider 后,在 SQL 映射文件中的增删改查标签中使用 databaseId来指定数据库标识的别名

1
2
3
xml复制代码<select id="getEmployeeById" resultType="com.atguigu.mybatis.beans.Employee" databaseId="mysql">
select * from tbl_employee where id = #{id}
</select>

MyBatis 匹配规则如下:

  1. 如果没有配置 databaseIdProvider 标签,那么 databaseId=null
  2. 如果配置了 databaseIdProvider 标签,使用标签配置的 name 去匹配数据库信息,匹配上设置 databaseId=配置指定的值,否则依旧为 null
  3. 如果 databaseId 不为 null,他只会找到配置 databaseId 的 sql 语句
  4. MyBatis 会加载不带 databaseId 属性和带有匹配当前数据库 databaseId 属性的
    所有语句。如果同时找到带有 databaseId 和不带 databaseId 的相同语句,则后者
    会被舍弃。

4.2.8、mappers 映射器

用来在 mybatis 初始化的时候,告诉 mybatis 需要引入哪些 Mapper 映射文件

4.2.8.1、mapper 逐个注册 SQL 映射文件

  • resource : 引入类路径下的文件
  • url : 引入网络路径或者是磁盘路径下的文件
  • class : 引入 Mapper 接口.
1. 有 SQL 映射文件 , 要求 Mapper 接口与 SQL 映射文件同名同位置
2. 没有 SQL 映射文件 , 使用注解在接口的方法上写 SQL 语句.
1
2
3
4
5
xml复制代码<mappers>
<mapper resource="EmployeeMapper.xml" />
<mapper class="com.dao.EmployeeMapper"/>
<package name="com.dao.mybatis.dao"/>
</mappers>

4.2.8.2、使用批量注册

这种方式要求 SQL 映射文件名必须和接口名相同并且在同一目录下

1
2
3
xml复制代码<mappers>
<package name="com.dao"/>
</mappers>

五、MyBatis 映射文件

​ MyBatis 的真正强大在于它的映射语句,也是它的魔力所在。由于它的异常强大,映射器的 XML 文件就显得相对简单。如果拿它跟具有相同功能的 JDBC 代码进行对比,你会立即发现省掉了将近 95% 的代码。MyBatis 就是针对 SQL 构建的,并且比普通的方法做的更好。

​ SQL 映射文件有很少的几个顶级元素(按照它们应该被定义的顺序):

1
2
3
4
5
6
7
8
9
markdown复制代码cache – 给定命名空间的缓存配置。
cache-ref – 其他命名空间缓存配置的引用。
resultMap – 是最复杂也是最强大的元素,用来描述如何从数据库结果集中来加载对象。
parameterMap – 已废弃!
sql – 可被其他语句引用的可重用语句块。
insert – 映射插入语句
update – 映射更新语句
delete – 映射删除语句
select – 映射查询语

5.1 、Mybatis两种开发方式的比较

5.1.1、传统dao开发的弊端

​ 我们在前面入门的例子就是传统的dao操作,我们可以看到这个操作极为繁琐,不仅繁琐,而且dao实现类也没有干什么实质性的工作,它仅仅就是通过 SqlSession 的相关 API 定位到映射文件 mapper 中相应 id 的 SQL 语句,真正对 DB 进行操作的工作其实是由框架通过 mapper 中的 SQL 完成的。

5.1.2、现代dao开发好处

​ MyBatis 框架就抛开了 Dao 的实现类,直接定位到映射文件 mapper 中的相应 SQL 语句,对
DB 进行操作。这种对 Dao 的实现方式称为 Mapper 的动态代理方式。
​ Mapper 动态代理方式无需程序员实现 Dao 接口。接口是由 MyBatis 结合映射文件自动生成的动态代
理实现的。

5.2、Mybatis的动态代理

5.2.1、获取代理对象

​ 我们只需调用 SqlSession 的 getMapper()方法,即可获取指定接口的实现类对象。该方法的参数为指定 Dao接口类的 class 值。

1
2
3
4
5
6
java复制代码SqlSession session = factory.openSession();
UserDao dao = session.getMapper(UserDao.class);

//或者可以使用工具类
UserDao userDao =
MyBatisUtil.getSqlSession().getMapper(UserDao.class);

5.2.2、使用代理对象执行sql语句

1
2
3
4
5
java复制代码  @Test
public void TestUpdate(){
User user = new User(16,"赵六","110");
userDao.update(user);
}

5.2.3、动态代理原理

​ 这种手段我们称为动态代理,我们debug一下可以看到

image-20210105222445798

​ 点进MapperProxy 类定义:

image-20210105222552597

​ invoke()方法:

image-20210105222940680

​ 点进去execute方法,重点方法:

image-20210105223113283

5.3、主键生成方式和获取主键值

5.3.1、主键生成方式

  1. 支持主键自增,例如 MySQL 数据库
  2. 不支持主键自增,例如 Oracle 数据库

5.3.2、 获取主键值

1
ini复制代码若数据库支持自动生成主键的字段(比如 MySQL 和 SQL Server ),则可以设置`useGeneratedKeys=”true”`,然后再把 keyProperty 设置到目标属性上。
1
2
3
4
xml复制代码<insert id="insertEmployee"
insert id="insertEmployee"parameterType="com.atguigu.mybatis.beans.Employee" databaseId="mysql" useGeneratedKeys="true" keyProperty="id">
insert into tbl_employee(last_name,email,gender) values(#{lastName},#{email},#{gender})
</insert>

​ 而对于不支持自增型主键的数据库(例如 Oracle),则可以使用 selectKey 子元素:selectKey 元素将会首先运行,id 会被设置,然后插入语句会被调用

1
2
3
4
5
6
xml复制代码<insert id="insertEmployee" parameterType="com.atguigu.mybatis.beans.Employee" databaseId="oracle">
<selectKey order="BEFORE" keyProperty="id" resultType="integer">
select employee_seq.nextval from dual
</selectKey>
insert into orcl_employee(id,last_name,email,gender) values(#{id},#{lastName},#{email},#{gender})
</insert>

​ 或者这样写

1
2
3
4
5
6
xml复制代码<insert id="insertEmployee" parameterType="com.atguigu.mybatis.beans.Employee" databaseId="oracle">
<selectKey order="AFTER" keyProperty="id" resultType="integer">
select employee_seq.currval from dual
</selectKey>
insert into orcl_employee(id,last_name,email,gender) values(employee_seq.nextval,#{lastName},#{email},#{gender})
</insert>

5.4、参数传递

5.4.1、参数传递的方式

5.4.1.1、单个简单参数

​ 可以接受基本类型,对象类型。这种情况 MyBatis 可直接使用这个参数,不需要经过任何处理。

​ 在mapper.xml中用占位符 #{ 任意字符 }来表示这个参数,和方法的参数名无关。但是一般我们都会用方法的参数名来命名。

1
2
java复制代码//接口中的方法:
User selectById(int id);
1
2
3
4
5
6
7
xml复制代码<!--mapper文件-->
<select id="selectById" resultType="com.bjpowernode.domain.Student">
select id,username,pwd from user where id=#{abcd}
</select>
<!--
#{abcd} , abcd是自定义的变量名称,可以不和方法参数名相同,但是实际开发中一般是相同的。
-->

5.4.1.2、多个参数(贴注解)

​ 任意多个参数,都会被 MyBatis 重新包装成一个 Map 传入。Map 的 key 是 param1,param2,或者 0,1…,值就是参数的值。

​ 我们一般需要在方法形参前面加入**@Param(“自定义参数名”), mapper 文件使用#{自定义参数名}**来表示传入多个参数。

1
2
java复制代码//接口方法
List<User> selectUserByCondition(@Param("username") String username, @Param("pwd") int pwd);
1
2
3
4
xml复制代码<!--mapper文件-->
<select id="selectUserByCondition" resultType="com.domain.User">
select id,username,pwd from student where username = #{username} or pwd = #{pwd}
</select>

5.4.1.3、多个参数(封装成对象)

​ 当我们需要传递多个参数的时候,我们可以将这些对象直接封装成一个对象,我们就直接传入JavaBean对象即可,在占位符内写对象的属性。

5.4.1.4、Map

​ Map 集合可以存储多个值,使用 Map 向 mapper 文件一次传入多个参数。Map 集合使用 String 的 key,Object 类型的值存储参数。 mapper 文件使用 # { key } 引用参数值。

1
2
java复制代码//接口方法
List<User> selectMultiMap(Map<String,Object> map);
1
2
3
4
xml复制代码<!--mapper文件-->
<select id="selectMultiMap" resultType="com.domain.User">
select id,username,pwd from user where username=#{username} or pwd =#{pwd}
</select>

5.4.1.5、Collection/Array

​ 会被MyBatis 封装成一个map 传入, Collection 对应的key 是collection,Array 对应的key是 array. 如果确定是 List 集合,key 还可以是 list.

5.4.2、参数传递的源码分析

以命名参数为例:

1
2
java复制代码public User getUserByIdAndUsername
(@Param("id")Integer id, @Param("username")String username);

源码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码    public Object getNamedParams(Object[] args) {
final int paramCount = names.size();
if (args == null || paramCount == 0) {
return null;
} else if (!hasParamAnnotation && paramCount == 1) {
return args[names.firstKey()];
} else {
final Map<String, Object> param = new ParamMap<Object>();
int i = 0;
for (Map.Entry<Integer, String> entry : names.entrySet()) {
param.put(entry.getValue(), args[entry.getKey()]);
// add generic param names (param1, param2, ...)
final String genericParamName = GENERIC_NAME_PREFIX +
String.valueOf(i + 1);
// ensure not to overwrite parameter named with @Param
if (!names.containsValue(genericParamName)) {
param.put(genericParamName, args[entry.getKey()]);
}
i++;
}
return param;
}
}

5.4.3、参数处理

参数位置支持的属性

javaType、jdbcType、mode、numericScale、resultMap、typeHandler、jdbcTypeName、expression

使用实例

**实际上通常被设置的是:为可能为空的列名指定 jdbcType **

1
2
3
4
5
xml复制代码<select id="selectMultiObject" resultType="com.domain.User">
select id,username pwd from user
where username=#{username,javaType=string,jdbcType=VARCHAR}
or pwd =#{pwd,javaType=int,jdbcType=INTEGER}
</select>

5.4.4、参数的获取方式

5.4.4.1、#

​ #占位符:告诉 mybatis 使用实际的参数值代替。并使用 PrepareStatement 对象执行 sql 语句, #{…}代替sql 语句的?。这样做更安全,更迅速,通常也是首选做法.

1
2
3
4
xml复制代码<!--mapper文件-->
<select id="selectById" resultType="com.domain.User">
select id,username,pwd from user where id=#{id}
</select>
1
2
3
4
5
6
7
8
java复制代码//转为 MyBatis 的执行是:
String sql=” select id,username,pwd from user where id=?”;
PreparedStatement ps = conn.prepareStatement(sql);
ps.setInt(1,1005);

//解释:
where id=? 就是 where id=#{id}
ps.setInt(1,1005) , 1005 会替换掉 #{id}

5.4.4.2、$

$ 字符串替换:告诉 mybatis 使用所包含的“字符串”替换所在位置。使用∗∗Statement∗∗把sql语句和所包含的“字符串”替换所在位置。使用 **Statement** 把 sql 语句和所包含的“字符串”替换所在位置。使用∗∗Statement∗∗把sql语句和{}的内容连接起来。主要用在替换表名,列名,不同列排序等操作。

1
2
3
java复制代码//需求:使用不同列作为查询条件
//接口方法
User findByDiffField(@Param("col") String colunName,@Param("cval") Object value);
1
2
3
4
xml复制代码<!--mapper文件-->
<select id="findByDiffField" resultType="com.domain.User">
select * from user where ${col} = #{cval}
</select>

5.5、select 查询的几种情况

5.5.1、查询单行数据返回单个对象

1
java复制代码public User getUserById(Integer id );

5.5.2、查询多行数据返回对象的集合

1
java复制代码public List<User> getAllUser();

5.5.3、查询单行数据返回 Map 集合

1
java复制代码public Map<String,Object> getUserByIdReturnMap(Integer id );

5.5.4、 查询多行数据返回 Map 集合

1
2
java复制代码@MapKey("id") // 指定使用对象的哪个属性来充当 map 的 key
public Map<Integer,User> getAllUserReturnMap();

5.6、resultType 自动映射

​ 执行 sql 得到 ResultSet 转换的类型,使用类型的完全限定名或别名。 注意如果返回的是集合,那应该设置为集合包含的类型,而不是集合本身。resultType 和 resultMap,不能同时使用。

​ 接口方法返回是集合类型,需要指定集合中的类型,不是集合本身。

5.6.1、简单类型

1
2
java复制代码//接口方法
int countUser();
1
2
3
4
xml复制代码<!--mapper 文件-->
<select id="countUser" resultType="int">
select count(*) from user
</select>

5.6.2、对象类型

1
2
java复制代码//接口方法
List<User> selectUsers();
1
2
3
4
xml复制代码<!--mapper文件-->
<select id="selectUsers" resultType="com.domain.User">
select id,username,pwd from user
</select>

5.6.3、resultType的原理

​ 使用构造方法创建对象。调用 setXXX 给属性赋值。

sql 语句列 java 对象方法
id setId(rs.setInt(“id”))
username setUsername(rs.setString(“username”))
pwd setPwd(rs.setString(“pwd”))
  1. autoMappingBehavior 默认是 PARTIAL,开启自动映射的功能。唯一的要求是列名和javaBean 属性名一致
  2. 如果 autoMappingBehavior 设置为 null 则会取消自动映射
  3. 数据库字段命名规范,POJO 属性符合驼峰命名法,如 A_COLUMN aColumn,我们可以开启自动驼峰命名规则映射功能,mapUnderscoreToCamelCase=true

5.7、resultMap 自定义映射

​ resultMap 可以自定义 sql 的结果和 java 对象属性的映射关系。更灵活的把列值赋值给指定属性。
常用在列名和 java 对象属性名不一样的情况。

  1. id :用于完成主键值的映射。
  2. result :用于完成普通列的映射。
  3. association :一个复杂的类型关联,许多结果将包成这种类型。
  4. collection : 复杂类型的集。
属性 含义
property 映射到列结果的字段或属性,例如:”username”或”address.stree.number”
column 数据表的列名,通常和resultSet.getString(columnName)的返回值一致
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
xml复制代码<!--resultMap: resultMap 标签中的 id 属性值-->
<select id="getEmployeeById" resultMap="myEmp">
select id, last_name,email, gender from tbl_employee where id =#{id}
</select>

<!-- 创建 resultMap
id:自定义的唯一名称,在<select>使用
type:期望转为的 java 对象的全限定名称或别名
-->
<resultMap type="com.domain.Employee" id="myEmp">
<!-- 主键字段使用 id -->
<id column="id" property="id" />
<!--非主键字段使用 result-->
<result column="last_name" property="lastName"/>
<result column="email" property="email"/>
<result column="gender" property="gender"/>
</resultMap>

5.7.1、association

5.7.1.1、association

​ POJO 中的属性可能会是一个对象,我们可以使用联合查询,并以级 联属性的方式封装对象.使用 association 标签定义对象的封装规则

1
2
3
4
5
java复制代码@Data
public class Department {
private Integer id ;
private String departmentName ;
}
1
2
3
4
5
6
7
8
9
java复制代码@Data
public class Employee {
private Integer id ;
private String lastName;
private String email ;
private String gender ;
private Department dept ;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
xml复制代码<select id="getEmployeeAndDept" resultMap="myEmpAndDept" >
SELECT e.id eid, e.last_name, e.email,e.gender ,d.id did, d.dept_name
FROM tbl_employee e , tbl_dept d
WHERE e.d_id = d.id
AND e.id = #{id}
</select>
<resultMap type="com.domain.Employee" id="myEmpAndDept">
<id column="eid" property="id"/>
<result column="last_name" property="lastName"/>
<result column="email" property="email"/>
<result column="gender" property="gender"/>
<association property="dept" javaType="com.domain.Department">
<id column="did" property="id"/>
<result column="dept_name" property="departmentName"/>
</association>
</resultMap>

5.7.1.2、association 分步查询

​ 实际的开发中,对于每个实体类都应该有具体的增删改查方法,也就是 DAO 层, 因此对于查询员工信息并且将对应的部门信息也查询出来的需求,就可以通过分步的方式完成查询。

  1. 先通过员工的 id 查询员工信息
  2. 再通过查询出来的员工信息中的外键(部门 id)查询对应的部门信息.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
xml复制代码<select id="getEmployeeAndDeptStep" resultMap="myEmpAndDeptStep">
select id, last_name, email,gender,d_id from tbl_employee where id =#{id}
</select>
<resultMap type="com.atguigu.mybatis.beans.Employee" id="myEmpAndDeptStep">
<id column="id" property="id" />
<result column="last_name" property="lastName"/>
<result column="email" property="email"/>
<result column="gender" property="gender"/>
<association property="dept"
select="com.atguigu.mybatis.dao.DepartmentMapper.getDeptById"
column="d_id"
fetchType="eager">
</association>
</resultMap>

5.7.1.3、association 分步查询使用延迟加载

在分步查询的基础上,可以使用延迟加载来提升查询的效率,只需要在全局的Settings 中进行如下的配置

1
2
3
4
xml复制代码<!-- 开启延迟加载 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 设置加载的数据是按需还是全部 -->
<setting name="aggressiveLazyLoading" value="false"/>

5.7.2、collection

5.7.2.1、collection

​ POJO 中的属性可能会是一个集合对象,我们可以使用联合查询,并以级联属性的方式封
装对象.使用 collection 标签定义对象的封装规则

1
2
3
4
5
6
java复制代码@Data
public class Department {
private Integer id ;
private String departmentName ;
private List<Employee> emps ;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
xml复制代码<select id="getDeptAndEmpsById" resultMap="myDeptAndEmps">
SELECT d.id did, d.dept_name ,e.id eid ,e.last_name ,e.email,e.gender
FROM tbl_dept d
LEFT OUTER JOIN tbl_employee e
ON d.id = e.d_id
WHERE d.id = #{id}
</select>
<resultMap type="com.atguigu.mybatis.beans.Department" id="myDeptAndEmps">
<id column="did" property="id"/>
<result column="dept_name" property="departmentName"/>
<!--
property: 关联的属性名
ofType: 集合中元素的类型
-->
<collection property="emps" ofType="com.atguigu.mybatis.beans.Employee">
<id column="eid" property="id"/>
<result column="last_name" property="lastName"/>
<result column="email" property="email"/>
<result column="gender" property="gender"/>
</collection>
</resultMap>

5.7.2.2、collection 分步查询

​ 实际的开发中,对于每个实体类都应该有具体的增删改查方法,也就是 DAO 层, 因此对于查询部门信息并且将对应的所有的员工信息也查询出来的需求,就可以通过分步的方式完成查询。

  1. 先通过部门的 id 查询部门信息
  2. 再通过部门 id 作为员工的外键查询对应的部门信息.
1
2
3
4
5
6
7
8
xml复制代码<select id="getDeptAndEmpsByIdStep" resultMap="myDeptAndEmpsStep">
select id ,dept_name from tbl_dept where id = #{id}
</select>
<resultMap type="com.atguigu.mybatis.beans.Department" id="myDeptAndEmpsStep">
<id column="id" property="id"/>
<result column="dept_name" property="departmentName"/>
<collection property="emps" select="com.atguigu.mybatis.dao.EmployeeMapper.getEmpsByDid" column="id"></collection>
</resultMap>

5.7.3、 扩展: 分步查询多列值的传递

  1. 如果分步查询时,需要传递给调用的查询中多个参数,则需要将多个参数封装成Map 来进行传递,语法如下: {k1=v1, k2=v2….}。
  2. 在所调用的查询方,取值时就要参考 Map 的取值方式,需要严格的按照封装 map时所用的 key 来取值。

5.7.4、扩展: association 或 collection 的 fetchType 属性

  1. 在<association> 和<collection>标签中都可以设置 fetchType,指定本次查询是否要使用延迟加载。默认为 fetchType=”lazy” ,如果本次的查询不想使用延迟加载,则可设置为fetchType=”eager”。
  2. fetchType 可以灵活的设置查询是否需要使用延迟加载,而不需要因为某个查询不想使用延迟加载将全局的延迟加载设置关闭。

5.8、模糊查询like

​ 模糊查询的实现有两种方式,:

  1. 在 java 代码中给查询数据加上% 。
  2. mapper 文件中使用 like name "%" #{xxx} "%"

5.8.1、方式一

​ 来查询姓名有“力”的

1
2
java复制代码//接口方法
List<User> selectLikeFirst(String username);
1
2
3
4
xml复制代码<!--接口文件-->
<select id="selectLikeFirst" resultType="com.domain.User">
select id,username,pwd from user where username like #{username}
</select>

5.8.2、方式二

1
2
java复制代码//接口方法
List<User> selectLikeSecond(String username);
1
2
3
4
xml复制代码<!--mapper文件-->
<select id="selectLikeSecond" resultType="com.domain.User">
select id,username,pwd from user where username like "%" #{studentName} "%"
</select>

六、MyBatis 动态 SQL

6.1、MyBatis 动态 SQL 简介

​ 动态 SQL,通过 MyBatis 提供的各种标签对条件作出判断以实现动态拼接 SQL 语句。这里的条件判

断使用的表达式为 OGNL 表达式。常用的动态 SQL 标签有、、、等。

​ MyBatis 的动态 SQL 语句,与 JSTL 中的语句非常相似。动态 SQL,主要用于解决查询条件不确定的情况:在程序运行期间,根据用户提交的查询条件进行查询。提交的查询条件不同,执行的 SQL 语句不同。若将每种可能的情况均逐一列出,对所有条件进行排列组合,将会出现大量的 SQL 语句。此时,可使用动态 SQL 来解决这样的问题。

  1. 动态 SQL 是 MyBatis 强大特性之一。极大的简化我们拼装 SQL 的操作
  2. 动态 SQL 元素和使用 JSTL 或其他类似基于 XML 的文本处理器相似
  3. MyBatis 采用功能强大的基于 OGNL 的表达式来简化操作
  4. OGNL( Object Graph Navigation Language )对象图导航语言,这是一种强大的表达式语言,通过它可以非常方便的来操作对象属性。 类似于我们的 EL,SpEL 等

注意:xml 中特殊符号如”,>,<等这些都需要使用转义字符

6.2、if和 where

if

If 用于完成简单的判断.

Where

Where 用于解决 SQL 语句中 where 关键字以及条件中第一个 and 或者 or 的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
xml复制代码<select id="getEmpsByConditionIf" resultType="com.atguigu.mybatis.beans.Employee">
select id , last_name ,email , gender
from tbl_employee
<where>
<if test="id!=null">
and id = #{id}
</if>
<if test="lastName!=null &amp;&amp; lastName!=&quot;&quot;">
and last_name = #{lastName}
</if>
<if test="email!=null and email.trim()!=''">
and email = #{email}
</if>
<if test="&quot;m&quot;.equals(gender) or &quot;f&quot;.equals(gender)">
and gender = #{gender}
</if>
</where>
</select>

6.3、trim

Trim 可以在条件判断完的 SQL 语句前后 添加或者去掉指定的字符,常用方法如下:

​ prefix: 添加前缀
​ prefixOverrides: 去掉前缀
​ suffix: 添加后缀
​ suffixOverrides: 去掉后缀

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
xml复制代码<select id="getEmpsByConditionTrim" resultType="com.atguigu.mybatis.beans.Employee">
select id , last_name ,email , gender from tbl_employee
<trim prefix="where" suffixOverrides="and">
<if test="id!=null">
id = #{id} and
</if>
<if test="lastName!=null &amp;&amp; lastName!=&quot;&quot;">
last_name = #{lastName} and
</if>
<if test="email!=null and email.trim()!=''">
email = #{email} and
</if>
<if test="&quot;m&quot;.equals(gender) or &quot;f&quot;.equals(gender)">
gender = #{gender}
</if>
</trim>
</select>

6.4、set

set 主要是用于解决修改操作中 SQL 语句中可能多出逗号的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
xml复制代码<update id="updateEmpByConditionSet">
update tbl_employee
<set>
<if test="lastName!=null &amp;&amp; lastName!=&quot;&quot;">
last_name = #{lastName},
</if>
<if test="email!=null and email.trim()!=''">
email = #{email} ,
</if>
<if test="&quot;m&quot;.equals(gender) or &quot;f&quot;.equals(gender)">
gender = #{gender}
</if>
</set>
where id =#{id}
</update>

6.5、choose(when、otherwise)

choose 主要是用于分支判断,类似于 java 中的 switch case,只会满足所有分支中的一个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
xml复制代码<select id="getEmpsByConditionChoose" resultType="com.atguigu.mybatis.beans.Employee">
select id ,last_name, email,gender from tbl_employee
<where>
<choose>
<when test="id!=null">
id = #{id}
</when>
<when test="lastName!=null">
last_name = #{lastName}
</when>
<when test="email!=null">
email = #{email}
</when>
<otherwise>
gender = 'm'
</otherwise>
</choose>
</where>
</select>

6.6、foreach

foreach 主要用户循环迭代

​ collection: 要迭代的集合
​ item: 当前从集合中迭代出的元素
​ open: 开始字符
​ close:结束字符
​ separator: 元素与元素之间的分隔符
​ index:
​ 迭代的是 List 集合: index 表示的当前元素的下标
​ 迭代的 Map 集合: index 表示的当前元素的 key

1
2
3
4
5
6
xml复制代码<select id="getEmpsByConditionForeach" resultType="com.atguigu.mybatis.beans.Employee">
select id , last_name, email ,gender from tbl_employee where id in
<foreach collection="ids" item="curr_id" open="(" close=")" separator="," >
#{curr_id}
</foreach>
</select>

6.7、sql

sql 标签是用于抽取可重用的 sql 片段,将相同的,使用频繁的 SQL 片段抽取出来,单
独定义,方便多次引用.

抽取sql

1
2
3
xml复制代码<sql id="selectSQL">
select id , last_name, email ,gender from tbl_employee
</sql>

引用 SQL

1
xml复制代码<include refid="selectSQL"></include>

七、MyBatis 缓存机制

7.1、缓存的简介

  1. MyBatis 包含一个非常强大的查询缓存特性,它可以非常方便地配置和定制。缓存可以极大的提升查询效率
  2. MyBatis 系统中默认定义了两级缓存,一级缓存和二级缓存
  3. 默认情况下,只有一级缓存(SqlSession 级别的缓存,也称为本地缓存)开启。
  4. 二级缓存需要手动开启和配置,他是基于 namespace 级别的缓存
  5. 为了提高扩展性。MyBatis 定义了缓存接口 Cache。我们可以通过实现 Cache 接口来自定义二级缓存

7.2、 一级缓存的使用

一级缓存(local cache), 即本地缓存, 作用域默认为 sqlSession。当 Session flush 或close 后, 该 Session 中的所有 Cache 将被清空。本地缓存不能被关闭, 但可以调用 clearCache() 来清空本地缓存, 或者改变缓存的作用域.

7.2.1、一级缓存的工作机制

同一次会话期间只要查询过的数据都会保存在当前 SqlSession 的一个 Map 中
key: hashCode+查询的 SqlId+编写的 sql 查询语句+参数

7.2.2、一级缓存失效的几种情况

  1. 不同的 SqlSession 对应不同的一级缓存
  2. 同一个 SqlSession 但是查询条件不同
  3. 同一个 SqlSession 两次查询期间执行了任何一次增删改操作
  4. 同一个 SqlSession 两次查询期间手动清空了缓存

##7.3、二级缓存的使用

  1. 二级缓存(second level cache),全局作用域缓存, 二级缓存默认不开启,需要手动配置。
  2. MyBatis 提供二级缓存的接口以及实现,缓存实现要求 POJO 实现 Serializable 接口
  3. 二级缓存在 SqlSession 关闭或提交之后才会生效

二级缓存使用的步骤:

  1. 全局配置文件中开启二级缓存
  2. 需要使用二级缓存的映射文件处使用 cache 配置缓存
  3. 注意:POJO 需要实现 Serializable 接口

二级缓存相关的属性

  1. eviction=“FIFO”:缓存回收策略:
  2. LRU – 最近最少使用的:移除最长时间不被使用的对象。
  3. FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
  4. SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。
  5. WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。默认的是 LRU。
  6. flushInterval:刷新间隔,单位毫秒,默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新
  7. size:引用数目,正整数,代表缓存最多可以存储多少个对象,太大容易导致内存溢出
  8. readOnly:只读,true/false

true:只读缓存;会给所有调用者返回缓存对象的相同实例。因此这些对象不能被
修改。这提供了很重要的性能优势。

false:读写缓存;会返回缓存对象的拷贝(通过序列化)。这会慢一些,但是安全,
因此默认是 false。

7.4、缓存的相关属性设置

属性 含义
全局 setting 的 cacheEnable 配置二级缓存的开关,一级缓存一直是打开的
select 标签的 useCache 属性 配置这个 select 是否使用二级缓存。一级缓存一直是使用的
sql 标签的 flushCache 属性 增删改默认 flushCache=true。sql 执行以后,会同时清空一级和二级缓存。查询默认 flushCache=false。
sqlSession.clearCache(): 只是用来清除一级缓存

7.5、整合第三方缓存

为了提高扩展性。MyBatis 定义了缓存接口 Cache。我们可以通过实现 Cache 接口来自定义二级缓存,EhCache 是一个纯 Java 的进程内缓存框架,具有快速、精干等特点,是 Hibernate 中默认的 CacheProvider

整合 EhCache 缓存的步骤

  1. 导入 ehcache 包,以及整合包,日志包(maven也行)
    ehcache-core-2.6.8.jar、mybatis-ehcache-1.0.3.jar
    slf4j-api-1.6.1.jar、slf4j-log4j12-1.6.2.jar
  2. 编写 ehcache.xml 配置文件
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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../config/ehcache.xsd">
<!-- 磁盘保存路径 -->
<diskStore path="D:\atguigu\ehcache" />

<defaultCache
maxElementsInMemory="1000"
maxElementsOnDisk="10000000"
eternal="false"
overflowToDisk="true"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU">
</defaultCache>
</ehcache>

<!--
属性说明:
l diskStore:指定数据在磁盘中的存储位置。
l defaultCache:当借助CacheManager.add("demoCache")创建Cache时,EhCache
便会采用<defalutCache/>指定的的管理策略

以下属性是必须的:
l maxElementsInMemory - 在内存中缓存的element的最大数目
l maxElementsOnDisk - 在磁盘上缓存的element的最大数目,若是0表示无穷大
l eternal - 设定缓存的elements是否永远不过期。如果为true,则缓存的数据始
终有效,如果为false那么还要根据timeToIdleSeconds,timeToLiveSeconds判断
l overflowToDisk - 设定当内存缓存溢出的时候是否将过期的element缓存到磁
盘上

以下属性是可选的:
l timeToIdleSeconds - 当缓存在EhCache中的数据前后两次访问的时间超过
timeToIdleSeconds的属性取值时,这些数据便会删除,默认值是0,也就是可闲置
时间无穷大
l timeToLiveSeconds - 缓存element的有效生命期,默认是0.,也就是element存活
时间无穷大
diskSpoolBufferSizeMB 这个参数设置DiskStore(磁盘缓存)的缓存区大小.默认
是30MB.每个Cache都应该有自己的一个缓冲区.
l diskPersistent - 在VM重启的时候是否启用磁盘保存EhCache中的数据,默认是
false。
l diskExpiryThreadIntervalSeconds - 磁盘缓存的清理线程运行间隔,默认是120
秒。每个120s,相应的线程会进行一次EhCache中数据的清理工作
l memoryStoreEvictionPolicy - 当内存缓存达到最大,有新的element加入的时
候, 移除缓存中element的策略。默认是LRU(最近最少使用),可选的有LFU
(最不常使用)和FIFO(先进先出)
-->
  1. 配置 cache 标签
1
xml复制代码<cache type="org.mybatis.caches.ehcache.EhcacheCache"></cache>

八、MyBatis 逆向工程

8.1、逆向工程简介

MyBatis Generator: 简称 MBG,是一个专门为 MyBatis 框架使用者定制的代码生成器,可以快速的根据表生成对应的映射文件,接口,以及 bean 类。支持基本的增删改查,以及 QBC 风格的条件查询。但是表连接、存储过程等这些复杂 sql 的定义需要我们手工编写

官方文档地址
www.mybatis.org/generator/
官方工程地址
github.com/mybatis/gen…

8.2、逆向工程的配置

  1. 导入逆向工程的 jar 包:mybatis-generator-core-1.3.2.jar
  2. 编写 MBG 的配置文件(重要几处配置),可参考官方手册
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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>
<!--
targetRuntime: 执行生成的逆向工程的版本
MyBatis3Simple: 生成基本的CRUD
MyBatis3: 生成带条件的CRUD
-->
<context id="DB2Tables" targetRuntime="MyBatis3">
<jdbcConnection driverClass="com.mysql.jdbc.Driver"
connectionURL="jdbc:mysql://localhost:3306/mybatis_1129"
userId="root"
password="1234">
</jdbcConnection>
<!-- javaBean的生成策略-->
<javaModelGenerator targetPackage="com.atguigu.mybatis.beans"
targetProject=".\src">
<property name="enableSubPackages" value="true" />
<property name="trimStrings" value="true" />
</javaModelGenerator>
<!-- SQL映射文件的生成策略 -->
<sqlMapGenerator targetPackage="com.atguigu.mybatis.dao"
targetProject=".\conf">
<property name="enableSubPackages" value="true" />
</sqlMapGenerator>

<!-- Mapper接口的生成策略 -->
<javaClientGenerator type="XMLMAPPER"
targetPackage="com.atguigu.mybatis.dao" targetProject=".\src">
<property name="enableSubPackages" value="true" />
</javaClientGenerator>
<!-- 逆向分析的表 -->
<table tableName="tbl_dept" domainObjectName="Department"></table>
<table tableName="tbl_employee" domainObjectName="Employee"></table>
</context>
</generatorConfiguration>
  1. 编写java代码运行
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码    @Test
public void testMBG() throws Exception {
List<String> warnings = new ArrayList<String>();
boolean overwrite = true;
File configFile = new File("mbg.xml");
ConfigurationParser cp = new ConfigurationParser(warnings);
Configuration config = cp.parseConfiguration(configFile);
DefaultShellCallback callback = new DefaultShellCallback(overwrite);
MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config,
callback, warnings);
myBatisGenerator.generate(null);
}

8.3、逆向工程的使用

基本查询的测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码 @Test
public void testSelect() throws Exception {
SqlSessionFactory ssf = getSqlSessionFactory();
SqlSession session = ssf.openSession();
try {
EmployeeMapper mapper =
session.getMapper(EmployeeMapper.class);
List<Employee> emps = mapper.selectAll();
for (Employee employee : emps) {
System.out.println(employee);
}
} finally {
session.close();
}
}

带条件查询的测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码    @Test
public void testSelect() throws Exception {
SqlSessionFactory ssf = getSqlSessionFactory();
SqlSession session = ssf.openSession();
try {
EmployeeMapper mapper =
session.getMapper(EmployeeMapper.class);
//条件查询: 名字中带有'张' 并且 email中'j' 或者 did = 2
EmployeeExample example = new EmployeeExample();
Criteria criteria = example.createCriteria();
criteria.andLastNameLike("%张%");
criteria.andEmailLike("%j%");
//or
Criteria criteriaOr = example.createCriteria();
criteriaOr.andDIdEqualTo(2);
example.or(criteriaOr);
List<Employee> emps = mapper.selectByExample(example);
for (Employee employee : emps) {
System.out.println(employee);
}
} finally {
session.close();
}
}

#九、扩展-PageHelper 分页插件

9.1、PageHelper 分页插件简介

PageHelper 是 MyBatis 中非常方便的第三方分页插件,官方文档:https://github.com/pagehelper/Mybatis-PageHelper/blob/master/README_zh.md

9.2、PageHelper 的使用步骤

  1. 导入相关包 pagehelper-x.x.x.jar 和 jsqlparser-0.9.5.jar
  2. 在 MyBatis 全局配置文件中配置分页插件
1
2
3
xml复制代码<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor"></plugin>
</plugins>
  1. 使用 PageHelper 提供的方法进行分页
  2. 可以使用更强大的 PageInfo 封装返回结果

9.3、Page 对象的使用

在查询之前通过 PageHelper.startPage(页码,条数)设置分页信息,该方法返回 Page 对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码
@Test
public void testPageHelper() throws Exception{
SqlSessionFactory ssf = getSqlSessionFactory();
SqlSession session = ssf.openSession();
try {
EmployeeMapper mapper =
session.getMapper(EmployeeMapper.class);
//设置分页信息
Page<Object> page = PageHelper.startPage(9, 1);
List<Employee> emps = mapper.getAllEmps();
for (Employee employee : emps) {
System.out.println(employee);
}
System.out.println("=============获取分页相关的信息
=================");
System.out.println("当前页: " + page.getPageNum());
System.out.println("总页码: " + page.getPages());
System.out.println("总条数: " + page.getTotal());
System.out.println("每页显示的条数: " + page.getPageSize());
} finally {
session.close();
}
}

9.4、PageInfo 对象的使用

在查询完数据后,使用 PageInfo 对象封装查询结果,可以获取更详细的分页信息以及可以完成分页逻辑

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
java复制代码    @Test
public void testPageHelper1() throws Exception{
SqlSessionFactory ssf = getSqlSessionFactory();
SqlSession session = ssf.openSession();
try {
EmployeeMapper mapper =
session.getMapper(EmployeeMapper.class);
//设置分页信息
Page<Object> page = PageHelper.startPage(9, 1);
List<Employee> emps = mapper.getAllEmps();
//
PageInfo<Employee> info = new PageInfo<>(emps,5);
for (Employee employee : emps) {
System.out.println(employee);
}
System.out.println("=============获取详细分页相关的信息
=================");
System.out.println("当前页: " + info.getPageNum());
System.out.println("总页码: " + info.getPages());
System.out.println("总条数: " + info.getTotal());
System.out.println("每页显示的条数: " + info.getPageSize());
System.out.println("是否是第一页: " + info.isIsFirstPage());
System.out.println("是否是最后一页: " + info.isIsLastPage());
System.out.println("是否有上一页: " + info.isHasPreviousPage());
System.out.println("是否有下一页: " + info.isHasNextPage());
System.out.println("============分页逻辑===============");
int [] nums = info.getNavigatepageNums();
for (int i : nums) {
System.out.print(i +" " );
}
} finally {
session.close();
}

}

十、SSM 框架整合

10.1、整合注意事项

  1. 查看不同 MyBatis 版本整合 Spring 时使用的适配包;

image-20201010152502489

  1. 下载整合适配包:github.com/mybatis/spr…
  2. 官方整合示例,jpetstore:github.com/mybatis/jpe…

10.2、整合思路、步骤

搭建环境

创建一个动态的 WEB 工程,导入 SSM 需要使用的 jar 包,导入整合适配包,导入其他技术的一些支持包 连接池 数据库驱动 日志….

Spring + Springmvc

  1. 在web.xml 中配置: Springmvc 的前端控制器 实例化Spring 容器的监听器 ,字符编码过滤器 REST 过滤器
  2. 创建 Spring 的配置文件: applicationContext.xml:组件扫描、 连接池、 事务…..
  3. 创建 Springmvc 的配置文件: springmvc.xml : 组件扫描、 视图解析器

MyBatis

  1. 创建 MyBatis 的全局配置文件
  2. 编写实体类 Mapper 接口 Mapper 映射文件

Spring + MyBatis

  1. MyBatis 的 SqlSession 的创建 .
  2. MyBatis 的 Mapper 接口的代理实现类

测试

10.3、整合的配置

10.3.1、web.xml

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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5">
<!-- 字符编码过滤器 -->
<filter>
<filter-name>CharacterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- REST 过滤器 -->
<filter>
<filter-name>HiddenHttpMethodFilter</filter-name>
<filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>HiddenHttpMethodFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

<!-- 实例化SpringIOC容器的监听器 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>

<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<!-- Springmvc的前端控制器 -->
<servlet>
<servlet-name>springDispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springmvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springDispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>

10.3.2、Spring 配置

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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:mybatis-spring="http://mybatis.org/schema/mybatis-spring"
xsi:schemaLocation="http://mybatis.org/schema/mybatis-spring
http://mybatis.org/schema/mybatis-spring-1.2.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-4.0.xsd">

<!-- 组件扫描 -->
<context:component-scan base-package="com.atguigu.ssm">
<context:exclude-filter type="annotation"
expression="org.springframework.stereotype.Controller"/>
</context:component-scan>

<!-- 连接池 -->
<context:property-placeholder location="classpath:db.properties"/>
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${jdbc.driver}"></property>
<property name="jdbcUrl" value="${jdbc.url}"></property>
<property name="user" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>

</bean>

<!-- 事务 -->
<bean id="dataSourceTransactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>
</beans>

10.3.3 SpringMVC 配置

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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.0.xsd">
<!-- 组件扫描 -->
<context:component-scan base-package="com.atguigu.ssm" use-default-filters="false">
<context:include-filter type="annotation"
expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<!--视图解析器 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/"></property>
<property name="suffix" value=".jsp"></property>
</bean>

<mvc:default-servlet-handler/>
<mvc:annotation-driven/>
</beans>

10.3.4、MyBatis 配置

全局文件的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
xml复制代码<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- Spring 整合 MyBatis 后, MyBatis中配置数据源,事务等一些配置都可以
迁移到Spring的整合配置中。MyBatis配置文件中只需要配置与MyBatis相关
的即可。
-->
<!-- settings: 包含很多重要的设置项 -->
<settings>
<!-- 映射下划线到驼峰命名 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!-- 设置Mybatis对null值的默认处理 -->
<setting name="jdbcTypeForNull" value="NULL"/>
<!-- 开启延迟加载 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 设置加载的数据是按需还是全部 -->
<setting name="aggressiveLazyLoading" value="false"/>
<!-- 配置开启二级缓存 -->
<setting name="cacheEnabled" value="true"/>
</settings>
</configuration>

SQL 映射文件配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
xml复制代码<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.atguigu.ssm.mapper.EmployeeMapper">
<!-- public List<Employee> getAllEmps(); -->
<select id="getAllEmps" resultMap="myEmpsAndDept" >
select e.id eid, e.last_name,e.email,e.gender, d.id did, d.dept_name
from tbl_employee e ,tbl_dept d
where e.d_id = d.id
</select>
<resultMap type="com.atguigu.ssm.beans.Employee" id="myEmpsAndDept">
<id column="eid" property="id"/>
<result column="last_name" property="lastName"/>
<result column="email" property="email"/>
<result column="gender" property="gender"/>
<association property="dept" javaType="com.atguigu.ssm.beans.Department">
<id column="did" property="id"/>
<result column="dept_name" property="departmentName"/>
</association>
</resultMap>
</mapper>

Spring 整合 MyBatis 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
xml复制代码<!-- Spring 整合 Mybatis -->
<!--1. SqlSession -->
<bean class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- 指定数据源 -->
<property name="dataSource" ref="dataSource"></property>
<!-- MyBatis的配置文件 -->
<property name="configLocation"
value="classpath:mybatis-config.xml"></property>
<!-- MyBatis的SQL映射文件 -->
<property name="mapperLocations"
value="classpath:mybatis/mapper/*.xml"></property>
<property name="typeAliasesPackage"
value="com.atguigu.ssm.beans"></property>
</bean>
<!-- Mapper接口
MapperScannerConfigurer 为指定包下的Mapper接口批量生成代理实现类.bean
的默认id是接口名首字母小写.
-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.atguigu.ssm.mapper"></property>
</bean>
<!-- <mybatis-spring:scan base-package="com.atguigu.ssm.mapper"/> -->

测试

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

mysql进阶系列:mysql中MyISAM和InnoDB有

发表于 2021-07-26

本文测试mysql版本5.7+

本片文章详细列举下MyISAM和InnoDB的区别

一. 面试官:mysql中MyISAM和InnoDB有什么区别

1. 存储结构

MyISAM:每张表被存放在三个文件:frm-表格定义、MYD(MYData)-数据文件、MYI(MYIndex)-索引文件。

Innodb:所有的表都保存在同一个数据文件中(也可能是多个文件,或者是独立的表空间文件),InnoDB表的大小只受限于操作系统文件的大小,一般为2GB。

2. 存储空间

MyISAM:MyISAM可被压缩,存储空间较小。

Innodb:InnoDB的表需要更多的内存和存储,它会在主内存中建立其专用的缓冲池用于高速缓冲数据和索引。

3. 可移植性、备份及恢复

MyISAM:由于MyISAM的数据是以文件的形式存储,所以在跨平台的数据转移中会很方便。在备份和恢复时可单独针对某个表进行操作。

Innodb:免费的方案可以是拷贝数据文件、备份binlog,或者用mysqldump,在数据量达到几十G的时候就相对痛苦了。

4. 文件格式

MyISAM:数据和索引是分别存储的,数据(.MYD),索引(.MYI)。

Innodb:数据和索引是集中存储的(.ibd)。

5. 记录存储顺序

MyISAM:按记录插入顺序保存。

Innodb:按主键大小有序插入。

6. 外键

MyISAM:不支持。

Innodb:支持。

7. 事务

MyISAM:不支持。

Innodb:支持。

8. 锁支持

MyISAM:表级锁(锁粒度大并发能力弱)。

Innodb:行级锁、表级锁(锁粒度小并发能力高)。

9. SELECT查询

MyISAM更优。

10. INSERT、UPDATE、DELETE

InnoDB更优。

11. select count(*)

MyISAM:更快,因为MyISAM内部维护了一个计数器,可以直接调取(但是如果加了where条件就和InnoDB一样了)。

Innodb:不保存具体的行数,需要遍历整个表来计算。

12. 索引的实现方式

MyISAM:B+树索引,MyISAM是堆表 。

Innodb: B+树索引,Innodb是索引组织表。

13. 哈希索引

MyISAM:不支持。

Innodb:支持。(自定义哈希,不能手动创建)

14. 全文索引

MyISAM:支持。

Innodb:支持(5.6版本开始支持的)。

二. MyISAM的索引和InnoDB的索引有什么区别

  • InnoDB索引是聚簇索引,MyISAM索引是非聚簇索引。
  • InnoDB的主键索引的叶子节点存储的是行数据,因此主键索引非常高效。
  • MyISAM索引的叶子节点存储的是行数据地址,需要一次寻址的操作才能获取到数据。
  • InnoDB非主键索引的叶子节点存储的是主键和其他带索引的数据,因此查询是用索引覆盖会更高效。

InnoDB-主键索引

叶子节点存储的是具体的行数据

InnoDB-index-primary.png

InnoDB-非主键索引

非主键索引的叶子节点存储的是主键

InnoDB-index-notPrimary.png

MyISAM

叶子节点存储的是行数据的地址,需要一次寻址

MyISAM-index.png

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

CRUD搬砖两三年了,怎么阅读Spring源码?

发表于 2021-07-26

作者:小傅哥

博客:bugstack.cn

沉淀、分享、成长,让自己和他人都能有所收获!😄

👨‍💻连读同事写的代码都费劲,还读Spring? 咋的,Spring 很难读!

这个与我们码农朝夕相处的 Spring,就像睡在你身边的媳妇,你知道找她要吃、要喝、要零花钱、要买皮肤。但你不知道她的仓库共有多少存粮、也不知道她是买了理财还是存了银行。🍑开个玩笑,接下来我要正经了!


一、为什么Spring难读懂?

为什么 Spring 天天用,但要想去读一读源码,怎么就那么难!因为由Java和J2EE开发领域的专家 Rod Johnson 于 2002 年提出并随后创建的 Spring 框架,随着 JDK 版本和市场需要发展至今,至今它已经越来越大了!

当你阅读它的源码你会感觉:

  1. 怎么这代码跳来跳去的,根本不是像自己写代码一样那么单纯
  2. 为什么那么多的接口和接口继承,类A继承的类B还实现了类A实现的接口X
  3. 简单工厂、工厂方法、代理模式、观察者模式,怎么用了会有这样多的设计模式使用
  4. 又是资源加载、又是应用上下文、又是IOC、又是AOP、贯穿的还有 Bean 的声明周期,一片一片的代码从哪下手

怎样,这就是你在阅读 Spring 遇到的一些列问题吧?其实不止你甚至可以说只要是从事这个行业的码农,想读 Spring 源码都会有种不知道从哪下手的感觉。所以我想了个办法,既然 Spring 太大不好了解,那么我就尝试从一个小的 Spring 开始,手撸 实现一个 Spring 是不可以理解的更好,别说效果还真不错,在花了将近2个月的时间,实现一个简单版本的 Spring 后 现在对 Spring 的理解,有了很大的提升,也能读懂 Spring 的源码了。

二、分享手撸 Spring

通过这样手写简化版 Spring 框架,了解 Spring 核心原理。在手写的过程中会简化 Spring 源码,摘取整体框架中的核心逻辑,简化代码实现过程,保留核心功能,例如:IOC、AOP、Bean生命周期、上下文、作用域、资源处理等内容实现。

源码:github.com/fuzhengwei/…

1. 实现一个简单的Bean容器

凡是可以存放数据的具体数据结构实现,都可以称之为容器。例如:ArrayList、LinkedList、HashSet等,但在 Spring Bean 容器的场景下,我们需要一种可以用于存放和名称索引式的数据结构,所以选择 HashMap 是最合适不过的。

这里简单介绍一下 HashMap,HashMap 是一种基于扰动函数、负载因子、红黑树转换等技术内容,形成的拉链寻址的数据结构,它能让数据更加散列的分布在哈希桶以及碰撞时形成的链表和红黑树上。它的数据结构会尽可能最大限度的让整个数据读取的复杂度在 O(1) ~ O(Logn) ~O(n)之间,当然在极端情况下也会有 O(n) 链表查找数据较多的情况。不过我们经过10万数据的扰动函数再寻址验证测试,数据会均匀的散列在各个哈希桶索引上,所以 HashMap 非常适合用在 Spring Bean 的容器实现上。

另外一个简单的 Spring Bean 容器实现,还需 Bean 的定义、注册、获取三个基本步骤,简化设计如下;

  • 定义:BeanDefinition,可能这是你在查阅 Spring 源码时经常看到的一个类,例如它会包括 singleton、prototype、BeanClassName 等。但目前我们初步实现会更加简单的处理,只定义一个 Object 类型用于存放对象。
  • 注册:这个过程就相当于我们把数据存放到 HashMap 中,只不过现在 HashMap 存放的是定义了的 Bean 的对象信息。
  • 获取:最后就是获取对象,Bean 的名字就是key,Spring 容器初始化好 Bean 以后,就可以直接获取了。

2. 运用设计模式,实现 Bean 的定义、注册、获取

将 Spring Bean 容器完善起来,首先非常重要的一点是在 Bean 注册的时候只注册一个类信息,而不会直接把实例化信息注册到 Spring 容器中。那么就需要修改 BeanDefinition 中的属性 Object 为 Class,接下来在需要做的就是在获取 Bean 对象时需要处理 Bean 对象的实例化操作以及判断当前单例对象在容器中是否已经缓存起来了。整体设计如图 3-1

  • 首先我们需要定义 BeanFactory 这样一个 Bean 工厂,提供 Bean 的获取方法 getBean(String name),之后这个 Bean 工厂接口由抽象类 AbstractBeanFactory 实现。这样使用模板模式的设计方式,可以统一收口通用核心方法的调用逻辑和标准定义,也就很好的控制了后续的实现者不用关心调用逻辑,按照统一方式执行。那么类的继承者只需要关心具体方法的逻辑实现即可。
  • 那么在继承抽象类 AbstractBeanFactory 后的 AbstractAutowireCapableBeanFactory 就可以实现相应的抽象方法了,因为 AbstractAutowireCapableBeanFactory 本身也是一个抽象类,所以它只会实现属于自己的抽象方法,其他抽象方法由继承 AbstractAutowireCapableBeanFactory 的类实现。这里就体现了类实现过程中的各司其职,你只需要关心属于你的内容,不是你的内容,不要参与。
  • 另外这里还有块非常重要的知识点,就是关于单例 SingletonBeanRegistry 的接口定义实现,而 DefaultSingletonBeanRegistry 对接口实现后,会被抽象类 AbstractBeanFactory 继承。现在 AbstractBeanFactory 就是一个非常完整且强大的抽象类了,也能非常好的体现出它对模板模式的抽象定义。

3. 基于Cglib实现含构造函数的类实例化策略

填平这个坑的技术设计主要考虑两部分,一个是串流程从哪合理的把构造函数的入参信息传递到实例化操作里,另外一个是怎么去实例化含有构造函数的对象。

图 4-1

  • 参考 Spring Bean 容器源码的实现方式,在 BeanFactory 中添加 Object getBean(String name, Object... args) 接口,这样就可以在获取 Bean 时把构造函数的入参信息传递进去了。
  • 另外一个核心的内容是使用什么方式来创建含有构造函数的 Bean 对象呢?这里有两种方式可以选择,一个是基于 Java 本身自带的方法 DeclaredConstructor,另外一个是使用 Cglib 来动态创建 Bean 对象。Cglib 是基于字节码框架 ASM 实现,所以你也可以直接通过 ASM 操作指令码来创建对象

4. 为Bean对象注入属性和依赖Bean的功能实现

鉴于属性填充是在 Bean 使用 newInstance 或者 Cglib 创建后,开始补全属性信息,那么就可以在类 AbstractAutowireCapableBeanFactory 的 createBean 方法中添加补全属性方法。这部分大家在实习的过程中也可以对照Spring源码学习,这里的实现也是Spring的简化版,后续对照学习会更加易于理解

  • 属性填充要在类实例化创建之后,也就是需要在 AbstractAutowireCapableBeanFactory 的 createBean 方法中添加 applyPropertyValues 操作。
  • 由于我们需要在创建Bean时候填充属性操作,那么就需要在 bean 定义 BeanDefinition 类中,添加 PropertyValues 信息。
  • 另外是填充属性信息还包括了 Bean 的对象类型,也就是需要再定义一个 BeanReference,里面其实就是一个简单的 Bean 名称,在具体的实例化操作时进行递归创建和填充,与 Spring 源码实现一样。Spring 源码中 BeanReference 是一个接口

5. 设计与实现资源加载器,从Spring.xml解析和注册Bean对象

依照本章节的需求背景,我们需要在现有的 Spring 框架雏形中添加一个资源解析器,也就是能读取classpath、本地文件和云文件的配置内容。这些配置内容就是像使用 Spring 时配置的 Spring.xml 一样,里面会包括 Bean 对象的描述和属性信息。 在读取配置文件信息后,接下来就是对配置文件中的 Bean 描述信息解析后进行注册操作,把 Bean 对象注册到 Spring 容器中。整体设计结构如下图:

  • 资源加载器属于相对独立的部分,它位于 Spring 框架核心包下的IO实现内容,主要用于处理Class、本地和云环境中的文件信息。
  • 当资源可以加载后,接下来就是解析和注册 Bean 到 Spring 中的操作,这部分实现需要和 DefaultListableBeanFactory 核心类结合起来,因为你所有的解析后的注册动作,都会把 Bean 定义信息放入到这个类中。
  • 那么在实现的时候就设计好接口的实现层级关系,包括我们需要定义出 Bean 定义的读取接口 BeanDefinitionReader 以及做好对应的实现类,在实现类中完成对 Bean 对象的解析和注册。

6. 设计与实现资源加载器,从Spring.xml解析和注册Bean对象

为了能满足于在 Bean 对象从注册到实例化的过程中执行用户的自定义操作,就需要在 Bean 的定义和初始化过程中插入接口类,这个接口再有外部去实现自己需要的服务。那么在结合对 Spring 框架上下文的处理能力,就可以满足我们的目标需求了。整体设计结构如下图:

  • 满足于对 Bean 对象扩展的两个接口,其实也是 Spring 框架中非常具有重量级的两个接口:BeanFactoryPostProcess 和 BeanPostProcessor,也几乎是大家在使用 Spring 框架额外新增开发自己组建需求的两个必备接口。
  • BeanFactoryPostProcessor,是由 Spring 框架组建提供的容器扩展机制,允许在 Bean 对象注册后但未实例化之前,对 Bean 的定义信息 BeanDefinition 执行修改操作。
  • BeanPostProcessor,也是 Spring 提供的扩展机制,不过 BeanPostProcessor 是在 Bean 对象实例化之后修改 Bean 对象,也可以替换 Bean 对象。这部分与后面要实现的 AOP 有着密切的关系。
  • 同时如果只是添加这两个接口,不做任何包装,那么对于使用者来说还是非常麻烦的。我们希望于开发 Spring 的上下文操作类,把相应的 XML 加载 、注册、实例化以及新增的修改和扩展都融合进去,让 Spring 可以自动扫描到我们的新增服务,便于用户使用。

7. 实现应用上下文,自动识别、资源加载、扩展机制

可能面对像 Spring 这样庞大的框架,对外暴露的接口定义使用或者xml配置,完成的一系列扩展性操作,都让 Spring 框架看上去很神秘。其实对于这样在 Bean 容器初始化过程中额外添加的处理操作,无非就是预先执行了一个定义好的接口方法或者是反射调用类中xml中配置的方法,最终你只要按照接口定义实现,就会有 Spring 容器在处理的过程中进行调用而已。整体设计结构如下图:

  • 在 spring.xml 配置中添加 init-method、destroy-method 两个注解,在配置文件加载的过程中,把注解配置一并定义到 BeanDefinition 的属性当中。这样在 initializeBean 初始化操作的工程中,就可以通过反射的方式来调用配置在 Bean 定义属性当中的方法信息了。另外如果是接口实现的方式,那么直接可以通过 Bean 对象调用对应接口定义的方法即可,((InitializingBean) bean).afterPropertiesSet(),两种方式达到的效果是一样的。
  • 除了在初始化做的操作外,destroy-method 和 DisposableBean 接口的定义,都会在 Bean 对象初始化完成阶段,执行注册销毁方法的信息到 DefaultSingletonBeanRegistry 类中的 disposableBeans 属性里,这是为了后续统一进行操作。这里还有一段适配器的使用,因为反射调用和接口直接调用,是两种方式。所以需要使用适配器进行包装,下文代码讲解中参考 DisposableBeanAdapter 的具体实现

-关于销毁方法需要在虚拟机执行关闭之前进行操作,所以这里需要用到一个注册钩子的操作,如:Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("close!"))); 这段代码你可以执行测试,另外你可以使用手动调用 ApplicationContext.close 方法关闭容器。

8. 向虚拟机注册钩子,实现Bean对象的初始化和销毁方法

可能面对像 Spring 这样庞大的框架,对外暴露的接口定义使用或者xml配置,完成的一系列扩展性操作,都让 Spring 框架看上去很神秘。其实对于这样在 Bean 容器初始化过程中额外添加的处理操作,无非就是预先执行了一个定义好的接口方法或者是反射调用类中xml中配置的方法,最终你只要按照接口定义实现,就会有 Spring 容器在处理的过程中进行调用而已。整体设计结构如下图:

  • 在 spring.xml 配置中添加 init-method、destroy-method 两个注解,在配置文件加载的过程中,把注解配置一并定义到 BeanDefinition 的属性当中。这样在 initializeBean 初始化操作的工程中,就可以通过反射的方式来调用配置在 Bean 定义属性当中的方法信息了。另外如果是接口实现的方式,那么直接可以通过 Bean 对象调用对应接口定义的方法即可,((InitializingBean) bean).afterPropertiesSet(),两种方式达到的效果是一样的。
  • 除了在初始化做的操作外,destroy-method 和 DisposableBean 接口的定义,都会在 Bean 对象初始化完成阶段,执行注册销毁方法的信息到 DefaultSingletonBeanRegistry 类中的 disposableBeans 属性里,这是为了后续统一进行操作。这里还有一段适配器的使用,因为反射调用和接口直接调用,是两种方式。所以需要使用适配器进行包装,下文代码讲解中参考 DisposableBeanAdapter 的具体实现

-关于销毁方法需要在虚拟机执行关闭之前进行操作,所以这里需要用到一个注册钩子的操作,如:Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("close!"))); 这段代码你可以执行测试,另外你可以使用手动调用 ApplicationContext.close 方法关闭容器。

9. 定义标记类型Aware接口,实现感知容器对象

如果说我希望拿到 Spring 框架中一些提供的资源,那么首先需要考虑以一个什么方式去获取,之后你定义出来的获取方式,在 Spring 框架中该怎么去承接,实现了这两项内容,就可以扩展出你需要的一些属于 Spring 框架本身的能力了。

在关于 Bean 对象实例化阶段我们操作过一些额外定义、属性、初始化和销毁的操作,其实我们如果像获取 Spring 一些如 BeanFactory、ApplicationContext 时,也可以通过此类方式进行实现。那么我们需要定义一个标记性的接口,这个接口不需要有方法,它只起到标记作用就可以,而具体的功能由继承此接口的其他功能性接口定义具体方法,最终这个接口就可以通过 instanceof 进行判断和调用了。整体设计结构如下图:

  • 定义接口 Aware,在 Spring 框架中它是一种感知标记性接口,具体的子类定义和实现能感知容器中的相关对象。也就是通过这个桥梁,向具体的实现类中提供容器服务
  • 继承 Aware 的接口包括:BeanFactoryAware、BeanClassLoaderAware、BeanNameAware和ApplicationContextAware,当然在 Spring 源码中还有一些其他关于注解的,不过目前我们还是用不到。
  • 在具体的接口实现过程中你可以看到,一部分(BeanFactoryAware、BeanClassLoaderAware、BeanNameAware)在 factory 的 support 文件夹下,另外 ApplicationContextAware 是在 context 的 support 中,这是因为不同的内容获取需要在不同的包下提供。所以,在 AbstractApplicationContext 的具体实现中会用到向 beanFactory 添加 BeanPostProcessor 内容的 ApplicationContextAwareProcessor 操作,最后由 AbstractAutowireCapableBeanFactory 创建 createBean 时处理相应的调用操作。关于 applyBeanPostProcessorsBeforeInitialization 已经在前面章节中实现过,如果忘记可以往前翻翻

10. 关于Bean对象作用域以及FactoryBean的实现和使用

关于提供一个能让使用者定义复杂的 Bean 对象,功能点非常不错,意义也非常大,因为这样做了之后 Spring 的生态种子孵化箱就此提供了,谁家的框架都可以在此标准上完成自己服务的接入。

但这样的功能逻辑设计上并不复杂,因为整个 Spring 框架在开发的过程中就已经提供了各项扩展能力的接茬,你只需要在合适的位置提供一个接茬的处理接口调用和相应的功能逻辑实现即可,像这里的目标实现就是对外提供一个可以二次从 FactoryBean 的 getObject 方法中获取对象的功能即可,这样所有实现此接口的对象类,就可以扩充自己的对象功能了。MyBatis 就是实现了一个 MapperFactoryBean 类,在 getObject 方法中提供 SqlSession 对执行 CRUD 方法的操作 整体设计结构如下图:

  • 整个的实现过程包括了两部分,一个解决单例还是原型对象,另外一个处理 FactoryBean 类型对象创建过程中关于获取具体调用对象的 getObject 操作。
  • SCOPE_SINGLETON、SCOPE_PROTOTYPE,对象类型的创建获取方式,主要区分在于 AbstractAutowireCapableBeanFactory#createBean 创建完成对象后是否放入到内存中,如果不放入则每次获取都会重新创建。
  • createBean 执行对象创建、属性填充、依赖加载、前置后置处理、初始化等操作后,就要开始做执行判断整个对象是否是一个 FactoryBean 对象,如果是这样的对象,就需要再继续执行获取 FactoryBean 具体对象中的 getObject 对象了。整个 getBean 过程中都会新增一个单例类型的判断factory.isSingleton(),用于决定是否使用内存存放对象信息。

11. 基于观察者实现,容器事件和事件监听器

其实事件的设计本身就是一种观察者模式的实现,它所要解决的就是一个对象状态改变给其他对象通知的问题,而且要考虑到易用和低耦合,保证高度的协作。

在功能实现上我们需要定义出事件类、事件监听、事件发布,而这些类的功能需要结合到 Spring 的 AbstractApplicationContext#refresh(),以便于处理事件初始化和注册事件监听器的操作。整体设计结构如下图:

  • 在整个功能实现过程中,仍然需要在面向用户的应用上下文 AbstractApplicationContext 中添加相关事件内容,包括:初始化事件发布者、注册事件监听器、发布容器刷新完成事件。
  • 使用观察者模式定义事件类、监听类、发布类,同时还需要完成一个广播器的功能,接收到事件推送时进行分析处理符合监听事件接受者感兴趣的事件,也就是使用 isAssignableFrom 进行判断。
  • isAssignableFrom 和 instanceof 相似,不过 isAssignableFrom 是用来判断子类和父类的关系的,或者接口的实现类和接口的关系的,默认所有的类的终极父类都是Object。如果A.isAssignableFrom(B)结果是true,证明B可以转换成为A,也就是A可以由B转换而来。

12. 基于JDK和Cglib动态代理,实现AOP核心功能

在把 AOP 整个切面设计融合到 Spring 前,我们需要解决两个问题,包括:如何给符合规则的方法做代理,以及做完代理方法的案例后,把类的职责拆分出来。而这两个功能点的实现,都是以切面的思想进行设计和开发。如果不是很清楚 AOP 是啥,你可以把切面理解为用刀切韭菜,一根一根切总是有点慢,那么用手(代理)把韭菜捏成一把,用菜刀或者斧头这样不同的拦截操作来处理。而程序中其实也是一样,只不过韭菜变成了方法,菜刀变成了拦截方法。整体设计结构如下图:

  • 就像你在使用 Spring 的 AOP 一样,只处理一些需要被拦截的方法。在拦截方法后,执行你对方法的扩展操作。
  • 那么我们就需要先来实现一个可以代理方法的 Proxy,其实代理方法主要是使用到方法拦截器类处理方法的调用 MethodInterceptor#invoke,而不是直接使用 invoke 方法中的入参 Method method 进行 method.invoke(targetObj, args) 这块是整个使用时的差异。
  • 除了以上的核心功能实现,还需要使用到 org.aspectj.weaver.tools.PointcutParser 处理拦截表达式 "execution(* cn.bugstack.springframework.test.bean.IUserService.*(..))",有了方法代理和处理拦截,我们就可以完成设计出一个 AOP 的雏形了。

13. 把AOP动态代理,融入到Bean的生命周期

其实在有了AOP的核心功能实现后,把这部分功能服务融入到 Spring 其实也不难,只不过要解决几个问题,包括:怎么借着 BeanPostProcessor 把动态代理融入到 Bean 的生命周期中,以及如何组装各项切点、拦截、前置的功能和适配对应的代理器。整体设计结构如下图:

  • 为了可以让对象创建过程中,能把xml中配置的代理对象也就是切面的一些类对象实例化,就需要用到 BeanPostProcessor 提供的方法,因为这个类的中的方法可以分别作用与 Bean 对象执行初始化前后修改 Bean 的对象的扩展信息。但这里需要集合于 BeanPostProcessor 实现新的接口和实现类,这样才能定向获取对应的类信息。
  • 但因为创建的是代理对象不是之前流程里的普通对象,所以我们需要前置于其他对象的创建,所以在实际开发的过程中,需要在 AbstractAutowireCapableBeanFactory#createBean 优先完成 Bean 对象的判断,是否需要代理,有则直接返回代理对象。在Spring的源码中会有 createBean 和 doCreateBean 的方法拆分
  • 这里还包括要解决方法拦截器的具体功能,提供一些 BeforeAdvice、AfterAdvice 的实现,让用户可以更简化的使用切面功能。除此之外还包括需要包装切面表达式以及拦截方法的整合,以及提供不同类型的代理方式的代理工厂,来包装我们的切面服务。

三、 学习说明

本代码仓库 github.com/fuzhengwei/… 以 Spring 源码学习为目的,通过手写简化版 Spring 框架,了解 Spring 核心原理。

在手写的过程中会简化 Spring 源码,摘取整体框架中的核心逻辑,简化代码实现过程,保留核心功能,例如:IOC、AOP、Bean生命周期、上下文、作用域、资源处理等内容实现。


  1. 此专栏为实战编码类资料,在学习的过程中需要结合文中每个章节里,要解决的目标,进行的思路设计,带入到编码实操过程。在学习编码的同时也最好理解关于这部分内容为什么这样的实现,它用到了哪样的设计模式,采用了什么手段做了什么样的职责分离。只有通过这样的学习才能更好的理解和掌握 Spring 源码的实现过程,也能帮助你在以后的深入学习和实践应用的过程中打下一个扎实的基础。
  2. 另外此专栏内容的学习上结合了设计模式,下对应了SpringBoot 中间件设计和开发,所以读者在学习的过程中如果遇到不理解的设计模式可以翻阅相应的资料,在学习完 Spring 后还可以结合中间件的内容进行练习。
  3. 源码:此专栏涉及到的源码已经全部整合到当前工程下,可以与章节中对应的案例源码一一匹配上。大家拿到整套工程可以直接运行,也可以把每个章节对应的源码工程单独打开运行。
  4. 如果你在学习的过程中遇到什么问题,包括:不能运行、优化意见、文字错误等任何问题都可以提交issue
  5. 在专栏的内容编写中,每一个章节都提供了清晰的设计图稿和对应的类图,所以学习过程中一定不要只是在乎代码是怎么编写的,更重要的是理解这些设计的内容是如何来的。

😁 好嘞,希望你可以学的愉快!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Redis发布订阅和Stream

发表于 2021-07-26

一、前言

发布订单系统是日常开发中经常会用到的功能。简单来说,就是发布者发布消息,订阅者就会接受到消息并进行相应的处理,如下图所示。

消息队列.jpg

二、发布/订阅

Redis为我们提供了发布/订阅的功能模块PubSub,可以用于消息传递。

旅游行程制定流程图.jpg
其中发布者publisher、订阅者subscriber都是redis客户端,channel则是redis服务器。

发布者publisher向channel发送消息,订阅该channel的subscriber就会接收到消息。

2.1 常用命令

2.1.1 订阅频道subscribe

1
2
3
4
5
6
7
8
vbnet复制代码127.0.0.1:6379> subscribe test1 test2
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "test1"
3) (integer) 1
1) "subscribe"
2) "test2"
3) (integer) 2

发布消息publish

1
2
3
4
ruby复制代码127.0.0.1:6379> publish test1 hello
(integer) 1
127.0.0.1:6379> publish test2 world
(integer) 1

订阅test1、test2的客户端会收到消息

1
2
3
4
5
6
arduino复制代码1) "message"
2) "test1"
3) "hello"
1) "message"
2) "test2"
3) "world"

2.1.2 订阅模式psubscribe

按照上述这种方式,如果订阅者subscriber想要订阅多个channel则需要同时指定多个channel的名称,redis为了解决这个问题提供psubscribe模式匹配这种订阅方式,可以通过通配符的方式匹配频道。

psubscribe.jpg

1
2
3
4
5
ruby复制代码127.0.0.1:6379> psubscribe ch*
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "ch*"
3) (integer) 1

发布消息

1
2
3
4
ruby复制代码127.0.0.1:6379> publish cha hello
(integer) 1
127.0.0.1:6379> publish china world
(integer) 1

之前订阅ch*的客户端就会收到cha频道和china频道的消息,这样就一次性订阅多个频道

1
2
3
4
5
6
7
8
arduino复制代码1) "pmessage"
2) "ch*"
3) "cha"
4) "hello"
1) "pmessage"
2) "ch*"
3) "china"
4) "world"

2.2 实现原理

redis服务端存储了订阅频道/模式的客户端列表

1
2
3
4
5
6
arduino复制代码struct redisServer { 
...
dict *pubsub_channels; // redis服务端进程中维护的订阅频道的客户端信息,key就是channel,value就是客户端列表
list *pubsub_patterns; //redis server进程中维护的pattern;
...
};

相当于如果客户端订阅一个频道,那么服务端的pubsub_channels就会存储一条数据,pubsub_channels其实是一个链表,key对应channel,value对应客户端列表,根据key订阅的频道,就可以找到订阅该频道的所有客户端。

同时如果客户端订阅一个模式,pubsub_patterns也会新增一条数据,记录当前客户端订阅的模式,pubsub_patterns也有自己的数据结构,其中就包含了客户端以及模式。

1
2
3
4
arduino复制代码typedef struct pubsubPattern {
   client *client; // 客户端
   robj *pattern; // 模式
} pubsubPattern;

当发布者向某个频道发布消息时,就会遍历pubsub_channels找到订阅该频道的客户端列表,依次向这些客户端发送消息。

然后遍历pubsub_patterns找到符合当前频道的模式,同时找到模式对应的客户端,然后向客户端发送消息。

三、Stream

虽然Redis提供了发布/订阅的功能,但是并不完善,导致基本没有合适的场景能够使用。

PubSub缺点:

  • 订阅者如果部署多个节点,会出现重复消息的情况。
  • 没有ack机制,消息容易发生丢失。如果在订阅消息的期间有消费者宕机了,那么后续他重连之后也无法接收到宕机这段时间内发布的消息了。
  • 消息不会持久化。一旦Redis服务端宕机了,所有的消息都会丢失。

直到Redis5.0出现之后,出现了Stream这种数据结构,才终于完善了Redis的消息机制。

Stream实际上就是一个消息列表,只是他几乎实现了消息队列所需要的所有功能,包括:

  • 消息ID的序列化生成
  • 消息遍历
  • 消息的阻塞和非阻塞读取
  • 消息的分组消费
  • 未完成消息的处理
  • 消息队列监控

同时需要注意的是Stream只是一个数据结构,他不会主动把消息推送给消费者,需要消费者主动来消费数据。

每个Stream都有唯一的名称,它就是Redis的key,首次使用 xadd 指令追加消息时自动创建。

常见操作命令如下表:

命令名称 命令格式 描述
xadd xadd key id<*> field1 value1 将指定消息追加到指定队列(key)中,*表示自动生成id(当前时间+序列号)
xread xread [COUNT count] [BLOCK milliseconds] STREAMS key [key …] ID [ID …] 从消息队列中读取,COUNT:读取条数,BLOCK:阻塞读(默认不阻塞),key:队列名称,id:消息ID(起始ID)
xrange xrange key start end [COUNT] 读取队列中给定ID范围的消息,COUNT:返回消息条数(消息id从小到大)
xrevrange xrevrange key start end [COUNT] 读取队列中给定ID范围的消息,COUNT:返回消息条数(消息id从大到小)
xdel xdel key id 删除队列的消息
xgroup create xgroup create key groupname id 创建一个新的消费组
xgroup destroy xgroup destroy key groupname 删除指定消费组
xgroup delconsumer xgroup delconsumer key groupname cname 删除指定消费组中的指定消费者
xgroup setid xgroup setid key id 修改指定消息的最大id
xreadgroup xreadgroup group groupname consumer COUNT streams key 消费消费组的数据(consumer不存在则创建)

3.1 使用示例

3.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
makefile复制代码// 新增消息 队列名:mq 数据:score=100
127.0.0.1:6379> xadd mq * score 100
"1627225715999-0"
127.0.0.1:6379> xadd mq * score 80
"1627225761166-0"
​
// 读取一条数据
127.0.0.1:6379> xread COUNT 1 STREAMS mq 0
1) 1) "mq"
  2) 1) 1) "1627225715999-0"
        2) 1) "score"
           2) "100"
​
// 获取mq从小到大的所有数据
127.0.0.1:6379> xrange mq - +
1) 1) "1627225715999-0"
  2) 1) "score"
     2) "100"
2) 1) "1627225761166-0"
  2) 1) "score"
     2) "80"
     
// 获取指定id范围的消息
127.0.0.1:6379> xrange mq 1627225761166-0 1627225761166-0
1) 1) "1627225761166-0"
  2) 1) "score"
     2) "80"

如果客户端希望知道自身消费到第几条数据了,那么就需要记录一下当前消费的消息ID,下次再次消费的时候就从上次消费的消息ID开始读取数据即可。

3.2 消费组

消费组中多了一个游标last_delivered_id,表示当前消费到了哪一条数据。同时所有的数据都是待处理消息(PEL),只有消费者处理完毕之后使用ack指令告知redis服务器,数据才会从PEL中移除,确认后的消息就无法再次消费。

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
php复制代码// 查看当前消息队列中有多少数据
127.0.0.1:6379> xrange mq - +
1) 1) "1627225715999-0"
  2) 1) "score"
     2) "100"
2) 1) "1627226969615-0"
  2) 1) "score"
     2) "70"
     
// 消息队列mq创建消费组mqGroup
127.0.0.1:6379> xgroup create mq mqGroup 0
OK
​
// 消费消费组mqGroup中的一条数据,>的意思是指从当前消费组的游标last_delivered_id开始读取
// 读取过后消费id会递增,也就是下次读取的时候就会是下一跳数据
127.0.0.1:6379> xreadgroup GROUP mqGroup consumer1 COUNT 1 STREAMS mq >
1) 1) "mq"
  2) 1) 1) "1627225715999-0"
        2) 1) "score"
           2) "100"
127.0.0.1:6379> xreadgroup GROUP mqGroup consumer1 COUNT 1 STREAMS mq >
1) 1) "mq"
  2) 1) 1) "1627226969615-0"
        2) 1) "score"
           2) "70"
127.0.0.1:6379> xreadgroup GROUP mqGroup consumer1 COUNT 1 STREAMS mq >
(nil)
​
// 查看当前消息队列对应的消费组的情况
127.0.0.1:6379> xinfo groups mq
1) 1) "name"
  2) "mqGroup" // 消费组的名称
  3) "consumers"
  4) (integer) 1 // 消费者的数量
  5) "pending"
  6) (integer) 2 // 待处理的数据数量,如果仅仅只是读取了数据,但是没有告知redis,那么数据就依旧处于待处理状态
  7) "last-delivered-id"
  8) "1627226969615-0" // 当前已经读取到的消息ID
​
// ack确认指定消息,返回的数值就是确认的数量
127.0.0.1:6379> xack mq mqGroup 1627226969615-0
(integer) 1
​
// 查看状态
127.0.0.1:6379> xinfo groups mq
1) 1) "name"
  2) "mqGroup"
  3) "consumers"
  4) (integer) 1
  5) "pending"
  6) (integer) 1 // 待处理的消息数量从2变成了1
  7) "last-delivered-id"
  8) "1627226969615-0"
​
// 已经确认过的消息,就不能再次消费了
127.0.0.1:6379> xreadgroup GROUP mqGroup consumer1 COUNT 1 STREAMS mq 1627225715999-0
1) 1) "mq"
  2) (empty list or set)
​
// 消息列表中的消息数量变少了
127.0.0.1:6379> xreadgroup GROUP mqGroup consumer1 COUNT 1 STREAMS mq 0
1) 1) "mq"
  2) 1) 1) "1627225715999-0"
        2) 1) "score"
           2) "100"

3.2 消息队列过长

如果接收到的消息比较多,为了避免Stream过长,可以选择指定Stream的最大长度,一旦到达了最大长度,就会从最早的消息开始清除,保证Stream中最新的消息。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

MySQL系列-仅靠MVCC就能解决幻读?错!Gap锁了解一

发表于 2021-07-25

前言

上篇MySQL系列有提到间隙锁,但是我感觉没有讲清楚,仅仅只是介绍了一下间隙锁,所以再来记录一下间隙锁。

上篇传送门:juejin.cn/post/698622…

先说一下结论: 读,分为当前读和快照读,MVCC解决的是快照读的幻读问题,而当前读的幻读问题需要Gap锁+行锁来解决。

锁.png

一、Gap锁(间隙锁)的概念

Innodb支持三种行锁定方式:

  1. 行锁:锁的是索引,如果SQL没有走索引,那么会全表扫描,从而升级为表锁。
* **PS**: 想必肯定有同学会想,如果某张表没有索引怎么办?


    + 如果有主键,Innodb会把主键作为聚簇索引。
    + 如果没有主键,Innodb会选择第一个不包含有 NULL 值的唯一索引作为主键索引
    + 如果没有主键且没有唯一索引,Innodb会选择内置的rowId(Innodb内部对每一行数据维护了一个递增的rowId)作为聚簇索引。
  1. Gap锁(间隙锁):当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时(增删改查操作,Mysql会默认加锁),InnoDB会给符合条件的已有数据记录的索引项加行锁。对于键值在条件范围内但并不存在的记录加上间隙锁。Innodb 为了解决幻读问题时引入的锁机制,所以只有在 Read Repeatable 、Serializable 隔离级别才有。要取消间隙锁的话,切换隔离级别为读已提交即可。
* PS: 如果使用相等条件请求给一个不存在的记录加锁,InnoDB也会使用间隙锁!
  1. Next-Key Lock:行锁与间隙锁组合起来用就叫做Next-Key Lock。

二、快照读和当前读

快照读: 就是普通的select语句,不包含for update的和lock in share mode的select语句。

  • 当前读是通过MVCC机制(uodo_log + readView) 来实现的, 上篇有讲juejin.cn/post/698622…

当前读:读取的是最新的数据,并且需要先获取对应记录的锁,包含以下这些 SQL 类型:

  • select … lock in share mode
  • select … for update
  • 增删改(MySQL默认加锁)

当前读是通过Next-Key Lock(行锁+间隙锁) 来实现的

下面来看个例子:

数据库的原始数据如下图

image.png

在RR(可重复读)隔离级别下开启两个事物进行操作:

image.png

咦。是不是有点奇怪,SQL1是快照读,SQL4是当前读,RR隔离级别下,在同一个事务中还是发生了幻读。

当时我就想了一下,不是说MVCC解决了快照读的幻读问题,间隙锁解决了当前读的问题吗。为什么我这里还是会出现幻读的问题呢?

是这样的,MySQL的当前读,是指在一个事务中的第一个SQL就是当前读了,也就是说第一个SQL就会加锁,其他的事务拿不到锁,根本没办法对对应的记录增删改。必须要等第一个事务释放锁之后,其他的事务才能对对应的记录增删改查。虽然解决了当前读的幻读问题,但是性能受到了影响。

三、间隙锁实例分析

建立一个user表的记录如下:

image.png

事务A执行delete from user where age=3, 而age=3位于age(2,7)之间, 所以MySQL对age(2,7)区间的数据加了间隙锁,当事务B对age(2,7)区间进行增删改操作时,会发生阻塞的现象。如图:

image.png

再看个例子,原始数据如下:

image.png

同理,当事务A执行update user set money=100 where id>=5时,MySQL会对id【5, 正无穷大)进行加锁,这时,事务B只要在id【5, 正无穷大)区间发生增删改操作,都会发生阻塞。因为对id=5的这一行数据加了行锁,并且对id>5的但还不存在的数据加了间隙锁。如图:

image.png

四、Gap锁造成的死锁

历史数据如图:

image.png

开启两个事务进行操作:

image.png

发生死锁的原因:

  1. age=5的数据不存在,即对age(2,7)加上了间隙锁。
  2. age=8的数据不存在,即对age(7,9)加上了间隙锁。
  3. 增删改查语句,MySQL会默认加锁,SQL3插入的是age=8, 而age(7,9)已经被加上间隙锁,所以SQL3阻塞,等待age(7,9)的间隙锁释放。
  4. 增删改查语句,MySQL会默认加锁,SQL3插入的是age=5,而age(2,7)已经被加上间隙锁,所以SQL4死锁,等待age(2,7)的间隙锁释放。从而发生死锁。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…592593594…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%