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

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


  • 首页

  • 归档

  • 搜索

Mysql字段类型

发表于 2021-11-01

Mysql数据类型

1. 数值类型:

包括整数类型TINYINT、SMALLINT、MEDIUMINT、INT、BIGINT、浮点小数数据类型FLOAT和DOUBLE,定点小数类型DECIMAL。

类型 存储 范围(有符号) 无符号
TINYINT 1字节 -128~127 0~255()
SMALLINT 2字节 32768~32767 0~65535
MEDIUMINT 3字节 -8388608~8388607 0~16777215
INT 4字节 -2147483648~2147483647 0~4294967295
BIGINT 8字节
FLOAT 4字节
DOUBLE 8字节
DECIMAL(M,N) M+2字节

注意:

1
2
3
4
5
sql复制代码CREATE TABLE test1(
id INT(3),
`name` VARCHAR(5),
age INT(3)
);

id INT(3)括号内的3不是限制存储数据的大小,而是指示显示宽度.显示宽度和数据类型的取值范围是无关的

显示宽度只用于显示,并不能限制取值范围和占用空间。例如:INT(3)会占用4字节的存储空间,并且允许的最大值不会是999,而是INT整型所允许的最大值。显示宽度只是指明MySQL最大可能显示的数字个数,数值的位数小于指定的宽度时会由空格填充

例如:向test1入id = 999999的数据还是会成功.

1
2
3
4
5
6
sql复制代码INSERT INTO test1 VALUES(999999,'小明',12);

select * from test1 where id = 999999;

id name age
999999 小明 12

DECIMAL。浮点数类型和定点数类型都可以用(M,N)来表示。其中,M称为精度,表示总共的位数;N称为标度,表示小数的位数.DECIMAL若不指定精度则默认为(10,0)

不论是定点数还是浮点数类型,如果用户指定的精度超出精度范围,则会四舍五入

1
2
3
4
5
6
7
8
9
10
11
12
13
sql复制代码CREATE TABLE test2(
id INT(4),
score DECIMAL(3,2)
);

INSERT INTO test2 VALUES(1,5.123),(2, 5.236),(3,5.1);

SELECT * FROM test2;
-- 结果
id score
1 5.12
2 5.24
3 5.10

如果插入大于999.99的数就会报错了

1
2
3
4
5
6
7
sql复制代码insert into test2 values(4, 1000.4567);
-- 结果

查询:insert into test2 values(4, 1000.4567)

错误代码: 1264
Out of range value for column 'score' at row 1

2. 日期/时间类型:

包括YEAR、TIME、DATE、DATETIME和TIMESTAMP

类型 日期格式 范围
YEAR YYYY 1901-2155 1字节
TIME HH:MM:SS 3字节
DATE YYYY-MM-DD 3字节
DATETIME YYYY-MM-DD HH:MM:SS 8字节
TIMESTAMP YYYY-MM-DD HH:MM:SS 4字节

注意: TIMESTAMP的范围是1970年到2038年

TIMESTAMP与DATETIME除了存储字节和支持的范围不同外,还有一个最大的区别就是:DATETIME在存储日期数据时,按实际输入的格式存储,即输入什么就存储什么,与时区无关;而TIMESTAMP值的存储是以UTC(世界标准时间)格式保存的,存储时对当前时区进行转换,检索时再转换回当前时区。查询时,不同时区显示的时间值是不同的。

DATE:

(1)以‘YYYY-MM-DD’或者‘YYYYMMDD’字符串格式表示的日期,取值范围为‘1000-01-01’~‘9999-12-3’。例如,输入‘2012-12-31’或者‘20121231’,插入数据库的日期都为2012-12-31。

(2)以‘YY-MM-DD’或者‘YYMMDD’字符串格式表示的日期,在这里YY表示两位的年值。包含两位年值的日期会令人模糊,因为不知道世纪。MySQL使用以下规则解释两位年值:‘00~69’范围的年值转换为‘2000~2069’;‘70~99’范围的年值转换为‘1970~1999’。例如,输入‘12-12-31’,插入数据库的日期为2012-12-31;输入‘981231’,插入数据的日期为1998-12-31。

(3)以YY-MM-DD或者YYMMDD数字格式表示的日期,与前面相似,00~69范围的年值转换为2000~2069,70~99范围的年值转换为1970~1999。例如,输入12-12-31插入数据库的日期为2012-12-31;输入981231,插入数据的日期为1998-12-31

3. 字符串类型:

包括CHAR、VARCHAR、BINARY、VARBINARY、BLOB、TEXT、ENUM和SET等。字符串类型又分为文本字符串和二进制字符串

类型 存储 大小
CHAR(M) M字节,1 <= M <=255
VARCHAR L+1字节, L<= M ,
TINYTEXT 0-255 字节
TEXT 0-65535 字节
MEDIUMTEXT 0-16,777,215 字节
LONGTEXT 0-4,294,967,295 or 4GB 字节
ENUM 1或2字节
TINYBLOB 0-255 字节
BLOB 0-65535 字节
MEDIUMBLOB 0-16,777,215 字节
LONGBLOB 0-4,294,967,295 or 4GB 字节

varchar(M)说明 括号内的M和INT(4)类型的限制不一样,这里M对插入数据的长度有限制,超长就会报错

1
2
3
4
5
6
7
8
9
10
11
sql复制代码CREATE TABLE test3(
id INT(4),
`name` VARCHAR(5),
`remark` varchar(1000)
);

insert into test3 values(1, '小红','第一条数据');
-- 数据正常插入

id name remark
1 小红 第一条数据

数据超长情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
sql复制代码insert into test3 values(1, '这个名字真长','第二条数据');
-- 显示name字段超长
<e>查询:insert into test3 values(1, '这个名字真长','第二条数据')

错误代码: 1406
Data too long for column 'name' at row 1


INSERT INTO test3 VALUES(1, 'abcdef','第三条数据');
-- 显示还是超长
查询:INSERT INTO test3 VALUES(1, 'abcdef','第三条数据')

错误代码: 1406
Data too long for column 'name' at row 1

varchar字段长度直接按字符计算不区分中英文字符

本文由博客一文多发平台 OpenWrite 发布!

本文转载自: 掘金

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

Dubbo集群容错

发表于 2021-11-01

简介

通常生产环境中服务消费方和服务提供方都是以集群模式部署的,服务消费者会根据一定的策略选择服务提供方集群中的某个实例进行调用。当调用失败时,Dubbo 提供了多种容错方案,缺省为 failover (失败重试)。

以下为Dubbo提供的集群容错模式

容错模式 描述
Failover Cluster 失败自动重试。当调用出现失败,会自动切换到其它服务提供者实例进行重试。通常用于幂等的操作,但重试会带来更长延迟。 该配置为缺省配置。
Failfast Cluster 快速失败。只发起一次调用,失败立即报错。通常用于非幂等性的操作。
Failsafe Cluster 安全失败。出现调用出现异常时,直接忽略异常。通常用于写入审计日志等操作。
Forking Cluster 并行调用。并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的操作,但需要浪费更多服务资源。
Failback Cluster 失败自动恢复。后台记录失败请求,定时重发。通常用于消息通知操作。
Broadcast Cluster 广播调用。逐个调用所有提供者,任意一台报错则标志本次调用失败。通常用于通知所有提供者更新缓存或日志等本地资源信息。

配置示例

服务消费方和服务提供方共用配置

1
2
3
4
5
6
7
8
arduino复制代码/**
* Cluster strategy, legal values include: failover, failfast, failsafe, failback, forking
*/
String cluster() default "";
​
public class ForkingCluster implements Cluster {
   public final static String NAME = "forking";
}

服务消费方配置

1
2
ini复制代码@Reference(interfaceName = "com.xxx.XxxService", cluster = ForkingCluster.NAME)
private XxxService xxxService;
1
ini复制代码<dubbo:reference cluster="failsafe" />

服务提供方配置

1
2
java复制代码@Service(cluster = FailoverCluster.NAME)
public class XxxServiceImpl implements XxxService
1
ini复制代码<dubbo:service cluster="failsafe" />

源码分析

下面以Dubbo默认的集群容错模式FailoverCluster为例,分析集群容错模式的源码。实现失败自动重试的是FailoverClusterInvoker类。

org.apache.dubbo.rpc.cluster.support.FailoverClusterInvoker

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
ini复制代码public Result doInvoke(Invocation invocation, final List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
   
   // 所有服务提供者
   List<Invoker<T>> copyInvokers = invokers;
   checkInvokers(copyInvokers, invocation);
   String methodName = RpcUtils.getMethodName(invocation);
   
   // 获取重试次数。值=设置的重试次数+1
   int len = getUrl().getMethodParameter(methodName, Constants.RETRIES_KEY, Constants.DEFAULT_RETRIES) + 1;
   if (len <= 0) {
       len = 1;
  }
   
   RpcException le = null;
   List<Invoker<T>> invoked = new ArrayList<Invoker<T>>(copyInvokers.size());
   Set<String> providers = new HashSet<String>(len);
   
   // 进行循环调用,如果调用成功,则不再重试
   for (int i = 0; i < len; i++) {
       // 如果调用过程中,服务提供者列表发生改变,则获取新的提供者列表
       if (i > 0) {
           checkWhetherDestroyed();
           copyInvokers = list(invocation);
           // check again
           checkInvokers(copyInvokers, invocation);
      }
       
       // 根据负载均衡机制选取一个服务提供者
       Invoker<T> invoker = select(loadbalance, invocation, copyInvokers, invoked);
       invoked.add(invoker);
       RpcContext.getContext().setInvokers((List) invoked);
       try {
           // 调用成功,则返回,不再进行循环重试
           Result result = invoker.invoke(invocation);
           return result;
      } catch (RpcException e) {
           if (e.isBiz()) { // biz exception.
               throw e;
          }
           le = e;
      } catch (Throwable e) {
           le = new RpcException(e.getMessage(), e);
      } finally {
           providers.add(invoker.getUrl().getAddress());
      }
  }
   
   // 全部重试失败,抛出异常,标志调用失败
   throw new RpcException("Failed to invoke the method ");
}

校验服务提供者是否为空,如果为空则抛出异常

1
2
3
4
5
typescript复制代码protected void checkInvokers(List<Invoker<T>> invokers, Invocation invocation) {
   if (CollectionUtils.isEmpty(invokers)) {
       throw new RpcException("");
  }
}

自定义集群容错模式

Dubbo内置了丰富的集群容错模式,如果在实际开发中,开发者有更个性化的的使用需求,那么也可以根据org.apache.dubbo.rpc.cluster.Cluster扩展接口进行自定义实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
swift复制代码/**
* Cluster. (SPI, Singleton, ThreadSafe)
*/
@SPI(FailoverCluster.NAME)
public interface Cluster {
​
   /**
    * Merge the directory invokers to a virtual invoker.
    */
   @Adaptive
   <T> Invoker<T> join(Directory<T> directory) throws RpcException;
​
}

步骤

1.定义扩展接口Cluster的自定义实现类

1
2
3
4
5
6
7
8
9
10
java复制代码public class CustomCluster implements Cluster {
​
   public final static String NAME = "custom";
​
   @Override
   public <T> Invoker<T> join(Directory<T> directory) throws RpcException {
       return new CustomClusterInvoker<>(directory);
  }
​
}

2.结合AbstractClusterInvoker,编写ClusterInvoker

1
2
3
4
5
6
7
8
9
10
11
12
13
14
scala复制代码public class CustomClusterInvoker <T> extends AbstractClusterInvoker<T> {
​
   public CustomClusterInvoker(Directory<T> directory) {
       super(directory);
  }
​
   @Override
   public Result doInvoke(Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
       checkInvokers(invokers, invocation);
       // 实际场景中的容错逻辑...
       return invoker.invoke(invocation);
  }
​
}

3.在META-INF/dubbo文件夹下,编写名称为扩展接口全路径的文件org.apache.dubbo.rpc.cluster.Cluster,内容如下

1
ini复制代码custom=com.xxx.cluster.CustomCluster

4.消费方配置

1
2
ini复制代码@Reference(interfaceName = "com.xxx.XxxService", cluster = CustomCluster.NAME)
private XxxService xxxService;

本文转载自: 掘金

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

实战小技巧18:BigDecimal除法使用不当导致精度问题

发表于 2021-11-01

「这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战」。

在使用BigDecimal的除法时,遇到一个鬼畜的问题,本以为的精度计算,结果使用返回0,当然最终发现还是使用姿势不对导致的,因此记录一下,避免后面重蹈覆辙

I. 问题抛出

在使用BigDecimal做高精度的除法时,一不注意遇到了一个小问题,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码@Test
public void testBigDecimal() {
BigDecimal origin = new BigDecimal(541253);
BigDecimal now = new BigDecimal(12389431);

BigDecimal val = origin.divide(now, RoundingMode.HALF_UP);
System.out.println(val);

origin = new BigDecimal(541253);
now = new BigDecimal(12389431.3);
val = origin.divide(now, RoundingMode.HALF_UP);
System.out.println(val);

origin = new BigDecimal(541253.4);
now = new BigDecimal(12389431);
val = origin.divide(now, RoundingMode.HALF_UP);
System.out.println(val);
}

上面的输出是什么 ?

1
2
3
sh复制代码0
0
0.043686703610520937021487456961257

为什么前面两个会是0呢,如果直接是 541253 / 12389431 = 0 倒是可以理解, 但是BigDecimal不是高精度的计算么,讲道理不应该不会出现这种整除的问题吧

我们知道在BigDecimal做触发时,可以指定保留小数的参数,如果加上这个,是否会不一样呢?

1
2
3
4
5
java复制代码BigDecimal origin = new BigDecimal(541253);
BigDecimal now = new BigDecimal(12389431);

BigDecimal val = origin.divide(now, 5, RoundingMode.HALF_UP);
System.out.println(val);

输出结果为:

1
sh复制代码0.04369

所以说在指定了保留小数之后,则没有问题,所以大胆的猜测一下,是不是上面的几种case中,由于scale值没有指定时,默认值不一样,从而导致最终结果的精度不同呢?

简单的深入源码分析一下,执行的方式为 origin.divide(now, RoundingMode.HALF_UP);, 所以这个scale参数就瞄准origin对象,而这个对象,就只能去分析它的构造了,因为没有其他的地方使用

II. 源码定位

1. 整形传参构造

分析下面这一行, 直接进入源码

1
java复制代码BigDecimal origin = new BigDecimal(541253);

很明显的int传参构造,进去简单看一下

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码// java.math.BigDecimal#BigDecimal(int)
public BigDecimal(int val) {
this.intCompact = val;
this.scale = 0;
this.intVal = null;
}

public BigDecimal(long val) {
this.intCompact = val;
this.intVal = (val == INFLATED) ? INFLATED_BIGINT : null;
this.scale = 0;
}

so,很明确的知道默认的scale为0,也就是说当origin为正数时,以它进行的除法,不现实指定scale参数时,最终返回的都是没有小数的,同样看一眼,还有long的传参方式, BigInteger也一样

2. 浮点传参

接下来就是浮点的scale默认值确认了,这个构造相比前面的复杂一点,源码就不贴了,太长,也看不太懂做了些啥,直接用猥琐一点的方式,进入debug模式,单步执行

1
2
3
4
5
6
java复制代码@Test
public void testBigDecimal() {
BigDecimal origin = new BigDecimal(541253.0);
BigDecimal now = new BigDecimal(12389431.1);
BigDecimal tmp = new BigDecimal(0.0);
}

根据debug的结果,第一个,scale为0; 第二个scale为29, 第三个scale为0

image.png

image.png

image.png

3. String传参

依然是一大串的逻辑,同样采用单步debug的方式试下

1
2
3
4
5
6
java复制代码@Test
public void testBigDecimal() {
BigDecimal origin = new BigDecimal("541253.0");
BigDecimal now = new BigDecimal("12389431.1");
BigDecimal t = new BigDecimal("0.0");
}

上面三个的scale都是1

image.png

4. 小结

  • 对于BigDecimal进行除法运算时,最好指定其scale参数,不然可能会有坑
  • 对于BigDecimla的scale初始化的原理,有待深入看下BigDecimal是怎么实现的

最后贴一张乘法的图作为收尾

mul

系列博文:

  • 实战小技巧1:字符串占位替换-JDK版
  • 实战小技巧2:数组与list互转
  • 实战小技巧3:字符串与容器互转
  • 实战小技巧4:优雅的实现字符串拼接
  • 实战小技巧5:驼峰与下划线互转
  • 实战小技巧6:枚举的特殊用法
  • 实战小技巧7:排序比较需慎重
  • 实战小技巧8:容器的初始化大小指定
  • 实战小技巧9:List.subList使用不当StackOverflowError
  • 实战小技巧10:不可变容器
  • 实战小技巧11:数组拷贝
  • 实战小技巧12:数字格式化
  • 实战小技巧13:进制转换很简单
  • 实战小技巧14:配置文件Properties
  • 实战小技巧15:如何判断类为基础类型or基础类型的包装类
  • 实战小技巧17:随机数生成怎么选

II. 其他

1. 一灰灰Blog: liuyueyi.github.io/hexblog

尽信书则不如无书,已上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

  • 微博地址: 小灰灰Blog
  • QQ: 一灰灰/3302797840
  • 微信公众号: 一灰灰blog

本文转载自: 掘金

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

涨知识了,服务注册居然是这样实现的?【eureka源码系列N

发表于 2021-11-01

前言

我计划写一个SpringCloud源码系列的文章,毕竟最有效的学习方式,就是尝试把知识教授给其他人,在这个过程中如果遇到问题,就需要反思、再学习、再反思、再学习,渐渐就进步了。

虽然eureka已经停止维护了,但是它的设计理念、系统架构设计都是值得学习的,是有学习的价值的,所以springCloud源码系列的探秘之旅,我们从此处开始。

服务端和客户端的概念

eureka由服务端和客户端两部分组成,服务端作为单独的一个服务运行,而客户端则是需要和我们自己的业务系统进行集成。

服务端负责维护各个服务的地址信息,客户端则需要和服务端进行通信,获取其他服务的地址。

客户端,调用服务端

源码概览

本系列文章通过源码+图解说明方式,力求用简单的、轻松的方式辅助大家理解eureka的核心机制。

springCloud-eureka是对netflix-eureka的封装,所以我们直接通过学习Netflix的eureka源码就ok了,当然最后还是会研究一下springCloud是怎么对netflix进行封装的。

本文使用的是eureka 1.7.2版本,下载地址为:github.com/Netflix/eur…

下载下来的源码如图:

image-20210811211421769

eureka-server和eureka-client是怎样通信的

eureka服务端作为一个单独的系统,可独立部署。就像平时我们自己开发的web服务一样,打成war包后,直接放入web容器中,再启动web容器(比如tomcat),eureka服务端就可以被客户端访问了。

eureka服务端提供了大量的接口,能够让客户端通过http请求进行调用,客户端一旦调用相应的接口(比如服务注册接口、服务下线接口等),服务端就可以做出相应的响应。

接下来通过一个服务注册的例子,来讲解eureka服务端在启动的过程中做了什么,eureka客户端的启动又做了些什么,服务注册又是怎么实现的。

eureka-server在项目启动期间做了些什么

首先,是eureka-server的启动过程,我们在eureka-core的源码中,查看一下eureka服务端启动时做了什么。

类名:EurekaBootStrap.java

主要涉及的方法:contextInitialized() ,initEurekaEnvironment() ,initEurekaServerContext()

服务端初始化1

说明:源代码太长,不适合放在文章里,所以用图片的方式做了一些简化。具体代码细节,建议大家可以从github下载源码,对照文章进行阅读。文章里只能梳理主要流程,展现主要的代码逻辑。

简单总结一下,eureka-server的启动,大概做了以下的事情:

1.做了一些初始化操作:读取配置文件、创建ApplicationInfoManager、eurekaClient、PeerAwareInstanceRegistry、PeerEurekaNodes等对象。具体查看上图。

2.启动了一堆定时任务。 具体有哪些定时任务,我们后续查看到对应的机制时再讲,避免蒙圈。

图中的简化代码,大家有一个简单的理解即可,这里我们主要讲服务注册,所以只关注 PeerAwareInstanceRegistry对象的创建即可。

来看看最关键的东西,注册表

客户端想要从服务端获取其他服务的信息,那么在服务端必定也必须要有一个地方保存所有的服务信息,对不对?

所以eureka的设计里,服务端的内部就维护了一张注册表,每当有新的服务注册时,服务端就会去更新这张注册表,注册表的初始化方法就是 new PeerAwareInstanceRegistry() 。

追进源码我们发现,实际创建的是AbstractInstanceRegistry.java类的对象,还是用一张图来简化该类。

注册表

AbstractInstanceRegistry.java中包含一些成员变量,暂时我们主要关注registry对象,其余的部分还是等到学习对应的机制时再讲解。

registry是一个ConcurrentHashMap对象,它就是真正的注册表。至此我们已经知道在eureka-server里,是通过一个Map来维护服务注册信息。

服务注册是怎样实现的

接下来,来看看eureka-client调用服务注册接口时,eureka-server做了什么。

eureka-server端接收请求的类是eureka-core工程的resources包下的ApplicationResource.java,由于eureka的开发没有使用我们熟悉的springmvc,为了便于理解,我们可以把resources包下的类当做我们平常开发对应的Controller。

image-20210818213520273

首先,我们先来看看InstanceInfo对象,也就是eureka-client调用服务注册接口时传递了什么参数。

在这里为了更直观的演示,我们借助于eureka-server项目中自带的单元测试来断点调试一下。

单元测试项目截图:

image-20210821105529299

服务注册的单元测试代码,贴一小段,大概看一下eureka-server的测试代码长什么样:

1
2
3
4
5
6
7
8
java复制代码public class EurekaClientServerRestIntegrationTest {    
@Test
   public void testRegistration() throws Exception {
       InstanceInfo instanceInfo = instanceInfoIt.next();
       EurekaHttpResponse<Void> httpResponse = jerseyEurekaClient.register(instanceInfo);
       assertThat(httpResponse.getStatusCode(), is(equalTo(204)));
  }
}

断点看看InstanceInfo中都是些什么数据:

image-20210821110851041

InstanceInfo中的数据很多,我标注了一些常用的大家一眼就能看明白的参数。

前面提到过引进注册中心就是为了解决服务之间自动获取通信地址的难题,解决的方法就是所有的服务都把ip和端口号放在一个统一的地方(eureka-server),需要其他服务的地址时再从同一个地方去取即可。

现在大家就能知道,每个服务是怎么把自己的ip和端口号交给eureka-server的了。

看明白了服务注册时的请求参数,下面通过一张流程图来理解eureka-server在服务注册中具体做了哪些事情。

eureka-server服务注册流程如下

注册接口流程图

图中列出了服务注册过程中eureka-server端主要执行的逻辑,暂时我们只需要知道,eureka-server接收到服务注册的请求后,会将eureka-client传递过来的客户端的数据,保存进registry中即可。其他逻辑暂时可忽略。

通过断点调试,看看registry内部的数据长什么样的。

image-20210821114939476

画一张图,方便理解registry的数据结构。

注册表的数据结构

至此,服务注册的相关流程就结束了,下一篇,我们来看看eureka-client是怎么获取注册表的。

本文转载自: 掘金

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

事务是如何影响你的系统(一)

发表于 2021-11-01

「这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战」

事务,我们通常都听到这个词语,一般是用于系统的中一组逻辑操作开始的,这组逻辑操作要求是原子性的,比如转账功能,我必须要转账成功,需要两个条件,

本质上: 转账成功,

  • 操作一: A账户向B账户,转账100元
  • 操作二: B账户增加100元,同时A账户少100元;

事务:

逻辑操作中,一组逻辑共同的操作,或者单个逻辑单元执行的一系列操作,共同是为了某一个目标而执行的原子性操作;

其实对于事务,我们已经不是很陌生了,基本的属性ACID ,原子,一致,隔离,持久,这也是一般博客中都能提到的东西,但是具体我们来对比一下;

这些到底是用作干什么的;

这里我们换个视角去了解一下对于 MYSQL的事务,是的,你没有听错,就是MySQL的事务;

MYSQL事务的信息

对于数据库,我们基本是对于很多操作,类型基本分为读和写的操作,根据对于具体数据类型以及关系表的操作,我们可以解决很多复杂的问题,比如数据状态的更新,类型转化,添加数据等操作。

但是当数据库同时对于相同时间并发的处理执行多个事务的时候,就会导出很多意想不到的问题,

并发事务带来的问题

当前这些会在事务并发的时候,数据会经常性的遇到这些问题;

1.数据丢失

数据丢失,其实也比较好理解,因为事务具体隔离性,所以每个事务在操作的时候,并不被对方所知晓, 设想一个场景,如果是 事务A,和事务B同时都有给C转钱, 依次是100元和200元,但是事务C 账户是100元,

如果能转帐成功,并且事务都可以运行成功,那就账户C就会有400元, 但其实只会有300元,

原因:

1
css复制代码因为A事务未完成的时候,事务B就读取到,C就100元,在这个基础上更新,就是最后的300元

解决方法:

本质上是读写问题,要写之前,确定读的数据,保证不被任何事务修改,然后才进行写操作,
因为都是写操作,所以进行串行化的方式,解决脏写的问题。

2.脏读

这个其实是很好理解的,就是A事务,读到了B事务未提交的数据,导致你以后更新数据之后,万一A事务将数据回滚了,就会导致数据读取的不准确,有很大的数据漏洞风险存在

解决方法:

1
复制代码 读已提交

3.不可重复读

如果你每次去读取特定条件的数据,但是发现得到的数据结果不一致,主要是数据被修改或者删除操作中会遇到,

这种我们举个例子,如果说我要去查询某个班级的最高分, 第一次是100,第二次是99分,你会发现数据不一致,

同样的条件下,数据不一致,本质上就是读写顺序的问题,

解决方法: 先读后写, (可重复读取)

对于MYSQL事务来说,对于可重复读的隔离级别中,SQL语句读到数据之后,会将数据进行加锁,使得其他事物无法删除和修改此数据,此时就可以实现了可重复读(保证每次读取的结果都一致,对结果进行加锁)

4.幻读

幻读其实还挺玄乎的,因为很多时候都会出现这个问题, 这个问题常见的是,

事务读取之前查询SQL的数据,发现其他事务插入满足当前事务条件的新数据,导致我感觉看错了,眼花了(幻读出现)
本质上:读写问题,先读后写;

解决方法:

因为可重复读隔离级别,可以解决不可重复读取中,数据不一致被改变的问题,行锁

幻读,只能是串行化操作来解决,但是在解决这个问题之后带来了数据并发的大大折扣

image.png

疑惑要点:

为什么感觉幻读和不可重复读详细呢,他们的区别能细节说一下吗?

先说结论:

本质上:

1
2
3
4
5
6
7
8
9
10
11
12
markdown复制代码 相同点:
- 都是数据由于数据读写顺序,造成的数据读取前后不一致造成的


不同点:

- 1. 不可重复读-->因为特定条件下,读取的数据因为被其他事务修改或者删除,导致的数据不一致的情况(行锁)


- 2. 幻读--> 在特定条件下,读取之前已经读取过的数据,但是发现有新的符合条件的数据(存在新数据),导致数据不一致。

串行化解决

卢卡寄语:

今天是对于数据库事务展示 并发数据带来的问题, 以及解决方式, 下期我们讲关于事务隔离级别细节的解读。

本文转载自: 掘金

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

索引(一)

发表于 2021-11-01

这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战

1、什么是索引 index

定义:帮助MySQL提高查询效率的数据结构;

优点:

​ 1、大大加快数据查询速度;

缺点:

​ 1、维护索引需要耗费数据库资源;

​ 2、索引需要占用磁盘空间;

​ 3、当对表的数据进行增删改的时候,因为要维护索引,速度会受到影响;

​ (所以索引一般建立在不经常更新的字段、常用的搜索字段)

2、索引分类

1、主键索引

设定为主键后数据库会自动建立索引,Innodb为聚簇索引;

2、单值索引

即一个索引只包含单个列,一个表可以有多个单列索引;

3、唯一索引

索引列的值必须唯一,但允许有空值(可有多个 null)

4、复合索引

即一个索引包含多个列

5、Full Text 全文索引(MySQL版本之前,只能由于 MYISAM引擎)

全文索引类型为 FULL TEXT,在定义索引的列上支持值的全文查找,允许在这些索引列中插入重复值和空值。

全文索引可以再CHAR、VARCHAR、TEXT类型列上创建。

MYSQL只有MYISAM存储引擎支持全文索引。

3、索引的基本操作

– 查看索引

1
sql复制代码show index from t_user;

– 删除索引

1
sql复制代码drop index 索引名 on 表名

1.主键索引

创建表时加上主键,自动创建主键索引。

2.普通索引

建表时创建:

1
sql复制代码create table t_user(id varchar(20) primary key,name varchar(20),key(name)

建表后创建:

1
sql复制代码create index name_index on t_user(name);

3.唯一索引

建表时创建

1
sql复制代码create table t_user(id varchar(20) primary key,name varchar(20),unique(name))

建表后创建

1
sql复制代码create unique index on t_user

4.复合索引

建表时创建

1
2
3
4
5
6
sql复制代码CREATE TABLE tb_test2(
id INT,
`name` VARCHAR(200),
age INT,
KEY(`name`,age)
)

建表后创建

1
sql复制代码create index name_age_index on t_user(name,age)

复合索引的使用(面试题)

name age bir

1.最左前缀原则;

2.mysql 引擎在查询为了更好利用索引,在查询过程中会动态调整查询顺序以便利用索引

以下查询字段能否使用到符合索引:

1
2
3
4
5
sql复制代码name bir age	可以
name age bir 可以
age bir 不可以
bir age name 可以
age bir 不可以

4、聚簇索引和非聚簇索引

InnoDB的数据直接存储在叶子节点;

MyISAM叶子节点存的是数据的磁盘地址;

聚簇索引

将数据存储与索引放到了一块,找到索引也就找到了数据

非聚簇索引

将数据存储于索引分开结构,索引结构的叶子节点指向了数据的对应行,myisam通过key_buffer把索引先缓存到内存中,当需要访问数据时(通过索引访问数据),在内存中直接搜索索引,然后通过索引找到磁盘相应数据,这也就是为什么索引不在key buffer命中时,速度慢的原因。

澄清一个概念:innodb中,在聚簇索引之上创建的索引称之为辅助索引,辅助索引访问数据总是需要二次查找,非聚簇索引都是辅助索引,像复合索引、前缀索引、唯一索引,辅助索引叶子节点存储的不再是行的物理位置,而是主键值

何时使用聚簇索引与非聚簇索引

非聚簇索引一定会回表查询吗?
不一定,这涉及到查询语句所要求的字段是否全部命中了索引,如果全部命中了索引,那么就不必再进行回表查询。

举个简单的例子,假设我们在员工表的年龄上建立了索引,那么当进行select age from employee where age < 20的查询时,在索引的叶子节点上,已经包含了age信息,不会再次进行回表查询。

本文转载自: 掘金

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

MySQL 索引实践 使用的表 最佳实践 小总结 参考文档

发表于 2021-11-01

「这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战」

使用的表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sql复制代码CREATE TABLE employees (
id int(11) NOT NULL AUTO_INCREMENT,
name varchar(24) NOT NULL DEFAULT '' COMMENT '姓名',
age int(11) NOT NULL DEFAULT '0' COMMENT '年龄',
position varchar(20) NOT NULL DEFAULT '' COMMENT '职位',
hire_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '入职时间',
PRIMARY KEY (id),
KEY idx_name_age_position USING BTREE (name, age, position)
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARSET = utf8 COMMENT '员工记录表';

INSERT INTO employees (name, age, position, hire_time) VALUES ('LiLei', 22, 'manager', NOW());

INSERT INTO employees (name, age, position, hire_time) VALUES ('HanMeimei', 23, 'dev', NOW());

INSERT INTO employees (name, age, position, hire_time) VALUES ('Lucy', 23, 'dev', NOW());

最佳实践

  1. 全值匹配

1
sql复制代码EXPLAIN SELECT * FROM employees WHERE name= 'LiLei';

image.png

1
sql复制代码EXPLAIN SELECT * FROM employees WHERE name= 'LiLei' AND age = 22;

image.png

1
sql复制代码EXPLAIN SELECT * FROM employees WHERE name= 'LiLei' AND age = 22 AND position ='manager';

image.png

2.最佳左前缀法则

如果索引了多列,要遵守最左前缀法则。指的是查询从索引的最左前列开始并且不跳过索引中的列。

1
sql复制代码EXPLAIN SELECT * FROM employees WHERE age = 22 AND position ='manager';

image.png

1
sql复制代码EXPLAIN SELECT * FROM employees WHERE position = 'manager';

image.png

1
sql复制代码EXPLAIN SELECT * FROM employees WHERE name = 'LiLei';

image.png

3.不在索引列上做任何操作(计算、函数、(自动or手动)类型转换),会导致索引失效而转向全表扫描

1
sql复制代码EXPLAIN SELECT * FROM employees WHERE name = 'LiLei';

image.png

1
sql复制代码EXPLAIN SELECT * FROM employees WHERE left(name,3) = 'LiLei';

image.png

4.存储引擎不能使用索引中范围条件右边的列

1
sql复制代码EXPLAIN SELECT * FROM employees WHERE name= 'LiLei' AND age = 22 AND position ='manager';

image.png

1
sql复制代码EXPLAIN SELECT * FROM employees WHERE name= 'LiLei' AND age > 22 AND position ='manager';

image.png

5.尽量使用覆盖索引(只访问索引的查询(索引列包含查询列)),减少select *语句

image.png

1
sql复制代码EXPLAIN SELECT * FROM employees WHERE name= 'LiLei' AND age = 23 AND position ='manager';

image.png

6.mysql在使用不等于(!=或者<>)的时候无法使用索引会导致全表扫描

1
sql复制代码EXPLAIN SELECT * FROM employees WHERE name != 'LiLei';

image.png

7.is null,is not null 也无法使用索引

1
sql复制代码EXPLAIN SELECT * FROM employees WHERE name is null;

image.png

8.like以通配符开头(’$abc…’)mysql索引失效会变成全表扫描操作

1
sql复制代码EXPLAIN SELECT * FROM employees WHERE name like '%Lei';

image.png

1
sql复制代码EXPLAIN SELECT * FROM employees WHERE name like 'Lei%';

image.png
问题:解决like’%字符串%’索引不被使用的方法?
a)使用覆盖索引,查询字段必须是建立覆盖索引字段

1
sql复制代码EXPLAIN SELECT name,age,position FROM employees WHERE name like '%Lei%';

b)当覆盖索引指向的字段是varchar(380)及380以上的字段时,覆盖索引会失效!

9.强制类型转换导致索引失效

强制类型转换导致索引失效, 比如:字符串不加单引号索引失效

1
sql复制代码EXPLAIN SELECT * FROM employees WHERE name = '1000';

image.png

1
sql复制代码EXPLAIN SELECT * FROM employees WHERE name = 1000;

image.png

10.少用or,用它连接时很多情况下索引会失效

1
sql复制代码EXPLAIN SELECT * FROM employees WHERE name = 'LiLei' or name = 'HanMeimei';

image.png

小总结

like KK%相当于=常量,%KK和%KK% 相当于范围

总结 1

总结 2

参考文档

  • blog.csdn.net/qq_38138069…

本文转载自: 掘金

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

理解 Mysql 存储过程的使用

发表于 2021-11-01

存储过程和函数是 事先经过编译并存储在数据库中的一段 SQL 语句的集合,调用存储过程和函数可以简化应用开发人员的很多工作,减少数据在数据库和应用服务器之间的传输,对于提高数据处理的效率是有好处的。

存储过程和函数的区别在于函数必须有返回值,而存储过程没有。

函数:是一个有返回值的过程;

过程:是一个没有返回值的函数;

简单的来说就是为了以后的使用而保存的一条或多条Mysql语句的集合。

作用:

  1. 使用存过过程,很多相似性的删除,更新,新增等操作就变得轻松了,并且以后也便于管理!
  2. 存储过程因为SQL语句已经预编绎过了,因此运行的速度比较快。
  3. 存储过程可以接受参数、输出参数、返回单个或多个结果集以及返回值。可以向程序返回错误原因。
  4. 存储过程运行比较稳定,不会有太多的错误。只要一次成功,以后都会按这个程序运行。
  5. 存储过程主要是在服务器上运行,减少对客户机的压力。
  6. 存储过程可以包含程序流、逻辑以及对数据库的查询。同时可以实体封装和隐藏了数据逻辑。
  7. 存储过程可以在单个存储过程中执行一系列SQL语句。
  8. 存储过程可以从自己的存储过程内引用其它存储过程,这可以简化一系列复杂语句。

为什么使用存储过程

  • 通过把处理封装在容易使用的单元中,简化复杂的操作(正如前面例子所述)。
  • 由于不要求反复建立一系列处理步骤,这保证了数据的完整性。如果所有开发人员和应用程序都使用同一(试验和测试)存储过程,则所使用的代码都是相同的。这一点的延伸就是防止错误。需要执行的步骤越多,出错的可能性就越大。防止错误保证了数据的一致性。
  • 简化对变动的管理。如果表名、列名或业务逻辑(或别的内容)有变化,只需要更改存储过程的代码。使用它的人员甚至不需要知道这些变化。这一点的延伸就是安全性。通过存储过程限制对基础数据的访问减少了数据讹误(无意识的或别的原因所导致的数据讹误)的机会。
  • 提高性能。因为使用存储过程比使用单独的SQL语句要快。
  • 存在一些只能用在单个请求中的MySQL元素和特性,存储过程可以使用它们来编写功能更强更灵活的代码。

总结就是:简单、安全、高性能。

不过在将SQL代码转换为存储过程前,也必须知道其的缺陷:

  • 一般来说,存储过程的编写比基本SQL语句复杂,编写存储过程需要更高的技能,更丰富的经验。
  • 你可能没有创建存储过程的安全访问权限。许多数据库管理员限制存储过程的创建权限,允许用户使用存储过程,但不允许他们创建存储过程。

Mysql将编写存储过程的安全和访问与执行存储过程的安全和访问区分开了。这样你不能(或不想)编写自己的存储过程,也仍然可以在适当的时候执行别的存储过程。

创建存储过程

语法格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
mysql复制代码CREATE
[DEFINER = { user | CURRENT_USER }]
 PROCEDURE sp_name ([proc_parameter[,...]])
[characteristic ...] routine_body

proc_parameter:
[ IN | OUT | INOUT ] param_name type

characteristic:
COMMENT 'string'
| LANGUAGE SQL
| [NOT] DETERMINISTIC
| { CONTAINS SQL | NO SQL | READS SQL DATA | MODIFIES SQL DATA }
| SQL SECURITY { DEFINER | INVOKER }

routine_body:
  Valid SQL routine statement

[begin_label:] BEGIN
  [statement_list]
    ……
END [end_label]
  • IN 输入参数:表示调用者向过程传入值(传入值可以是字面量或变量)
  • OUT 输出参数:表示过程向调用者传出值(可以返回多个值)(传出值只能是变量)
  • INOUT 输入输出参数:既表示调用者向过程传入值,又表示过程向调用者传出值(值只能是变量)

声明语句结束符:

1
2
3
mysql复制代码DELIMITER $$
或
DELIMITER //

声明存储过程:

1
mysql复制代码CREATE PROCEDURE demo_in_parameter(IN p_in int)

存储过程开始和结束符号: – 可嵌套多个

每个嵌套块及其中的每条语句,必须以分号结束,表示过程体结束的begin-end块(又叫做复合语句compound statement),则不需要分号。

1
mysql复制代码BEGIN .... END

为语句块贴标签:

1
2
3
mysql复制代码[begin_label:] BEGIN
  [statement_list]
END [end_label]

变量赋值:

1
mysql复制代码SET @p_in=1

变量定义:

1
2
mysql复制代码-- Declere 变量名 数据类型...
DECLARE l_int int unsigned default 4000000;

创建Mysql存储过程、存储函数:

1
mysql复制代码CREATE PROCEDURE 存储过程名(参数)

存储过程体:

1
mysql复制代码CREATE FUNCTION 存储函数名(参数)

Ex:

  • 首先,在你选择的数据库下的出发过程中创建文件,并将你要写的存储过程写在源中并保存。
1
2
3
4
5
6
7
8
9
10
11
mysql复制代码CREATE DEFINER=`root`@`localhost` 
PROCEDURE `mysql_scirpt`.`01.CreateStorage`( IN `01.创建存储过程` VARCHAR ( 50 ) )

BEGIN
#Routine body goes here...
SELECT
AVG( prod_price ) AS "商品平均值"
FROM
products;

END
  • 然后,选择你所在的database点击存储过程

image-20211027171017065

  • 最后,会生成一段SQL语句,直接执行后就可以看到结果了
1
mysql复制代码{ CALL mysql_scirpt.`01.CreateStorage`(:01_创建存储过程) }
  • 运行结果,如图:

创建存储过程

如何在sql文件中直接书写存储过程

1
2
3
4
5
6
7
8
9
10
11
12
mysql复制代码USE mysql_scirpt;

DROP PROCEDURE IF EXISTS mysql_scirpt.myTry;

DELIMITER //
CREATE PROCEDURE myTry(IN 表名 varchar(50))

BEGIN
pass...
END

DELIMITER ;

删除存储过程

语法格式:

1
mysql复制代码DROP PROCEDUCE/FUNCTION xxx
1
mysql复制代码DROP PROCEDURE myTry

image-20211027174519517

如果指定的过程不存在,则Drop Procedure 将产生一个错误。当过程存在则删除。可以使用Drop Procedure If Exists

检查存储过程

为显示用来创建一个存储过程的CREATE语句,使用SHOW CREATEPROCEDURE语句:

1
mysql复制代码SHOW CREATE PROCEDURE XXX;

为了获得包括何时、由谁创建等详细信息的存储过程列表,使用SHOW PROCEDURE STATUS。

限制过程状态结果: SHOW PROCEDURE STATUS列出所有存储过程。为限制其输出,可使用LIKE指定一个过滤模式,例如:

1
mysql复制代码SHOW PROCEDURE STATUS LIEK 'XXX';

存储过程语法

存储过程是可以编程的,意味着可以使用变量,表达式,控制结构 , 来完成比较复杂的功能。

变量

变量(variable)内存中一个特定的位置,用来临时存储数据。

在调用时,这条语句并不显示是任何数据。它返回以后可以显示(或在其他处理中使用)的变量。

语法格式:

1
mysql复制代码DECLARE var_name[,...] type [DEFAULT value]

通过 DECLARE 可以定义一个局部变量,该变量的作用范围只能在 BEGIN…END 块中,,并且必须在复合语句的开头,在任何其它语句之前;可以被用在嵌套的块中,除了那些用相同名字声明变量的块。

果要给变量提供一个默认值,使用DEFAULT子句(值可以是常数,也可以指定为一个表达式);如果没有DEFAULT子句,初始值为NULL。

1
2
3
4
5
6
7
8
9
10
11
12
13
mysql复制代码DROP PROCEDURE IF EXISTS mysql_scirpt.`03.CreateVariable`;

DELIMITER $$
$$
CREATE PROCEDURE mysql_scirpt.`03.CreateVariable`(OUT num1 int)
BEGIN
DECLARE num2 int DEFAULT 200;
SET num1 = num2;
END$$
DELIMITER ;

CALL `03.CreateVariable`(@num);
SELECT @num;

局部变量的作用域:
  也就是变量能正常使用而不出错的程序块的范围。

在嵌套块的情况下,
  在外部块中声明的变量可以在内部块中直接使用;
  在内部块中声明的变量只能在内部块中使用。

image-20211027210926499

@ 所声明的变量相当于在外部起到一个占位符的作用。且必须要使用 call方法进行调用后。

1
2
mysql复制代码CALL `03.CreateVariable`(@num);
SELECT @num;

IF 条件判断

语法格式:

  • IF … then单次判断:

语法格式:

1
2
3
mysql复制代码IF expression THEN 
statements;
END IF;

如果表达式(expression)计算结果为TRUE,那么将执行statements语句,否则控制流将传递到END IF之后的下一个语句。

执行流程图:

img

  • Else 双判断:

语法格式:

1
2
3
4
5
mysql复制代码IF expression THEN
statements;
ELSE
else-statements;
END IF;

如果表达式计算结果为FALSE时执行语句,请使用IF ELSE语句

流程图:

img

  • 多条件判断:
1
2
3
4
5
6
7
8
mysql复制代码IF expression THEN
statements;
ELSEIF elseif-expression THEN
elseif-statements;
...
ELSE
else-statements;
END IF;

如果表达式(expression)求值为TRUE,则IF分支中的语句(statements)将执行;如果表达式求值为FALSE,则如果elseif_expression的计算结果为TRUE,MySQL将执行elseif-expression,否则执行ELSE分支中的else-statements语句。

流程图:

img

  • 案例:
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
mysql复制代码DROP PROCEDURE IF EXISTS mysql_scirpt.`04.CreateIF`;

/* 需求:
* 180身高 -- 高
* 170-180身高 - 中
* 170以下 - 一般
* */

DELIMITER $$
$$
CREATE PROCEDURE mysql_scirpt.`04.CreateIF`()
BEGIN
DECLARE height int DEFAULT 175;
DECLARE description varchar(50);

IF height >= 180 THEN
SET description = "高";
ELSEIF height >=170 AND height < 180 THEN
SET description = "中";
ELSE
SET description = "一般";
END IF;

SELECT description;
END$$
DELIMITER ;

CALL mysql_scirpt.`04.CreateIF`();

image-20211027224447072

传递参数

开发中的存储过程几乎都需要参数,也是这些参数使得传递参数更加灵活。

Mysql中,参数有三种模式:IN、OUT、INOUT

  • IN:是默认模式。在存储过程中定义 IN 参数时,调用程序必须将参数传递给存储过程。另外, IN 参数的值是被保护。这意味着即使在存储过程中更改了 IN 参数的值,在存储过程结束后仍保留其原始值。存储过程只是用 IN 参数副本。
  • OUT:可以在从存储过程中更改 OUT 参数的值,并将其更改后新值传递回调用程序。请注意,存储过程在启动时无法访问OUT参数的初始值。
  • INOUT:INOUT参数是IN和OUT参数的组合。这意味着调用程序可以传递参数,并且存储过程可以修改INOUT参数并将新值传递回调用程序。

语法格式:

1
mysql复制代码create procedure procedure_name([in/out/inout] 参数名   参数类型)
  • 案例1:IN 参数的使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
mysql复制代码DROP PROCEDURE IF EXISTS mysql_scirpt.`05.CreateParameter`;

DELIMITER $$
$$
CREATE PROCEDURE mysql_scirpt.`05.CreateParameter`(IN height int)
BEGIN
DECLARE description varchar(50) DEFAULT '';

IF height >= 180 THEN
SET description = "高";
ELSEIF height >=170 AND height < 180 THEN
SET description = "中";
ELSE
SET description = "一般";
END IF;

SELECT concat('身高:',height,'身材:',description) AS '描述';

END$$
DELIMITER ;

执行存储过程:

1
mysql复制代码{ CALL mysql_scirpt.`05.CreateParameter`(这里输入身高) }

image-20211028083937908

  • 案例2:OUT 参数使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
mysql复制代码DROP PROCEDURE IF EXISTS mysql_scirpt.`06.CreateParamOut`;

DELIMITER $$
$$
CREATE DEFINER=`root`@`localhost` PROCEDURE `mysql_scirpt`.`06.CreateParamOut`(
OUT id int
)
BEGIN
SELECT id AS id_inner_1; -- 1.结果 1
IF id IS NOT NULL THEN -- 3.因为 OUT参数,我们无法获取外部传来的值
SET id = id + 1;
SELECT id AS id_inner_2;
ELSE
SELECT 1 INTO id; -- 4.进入ELSE语句,将id赋值为 1
END IF;

SELECT id AS id_inner_3; -- 2.结果 2

END$$
DELIMITER ;

SET @id = 10;
CALL `mysql_scirpt`.`06.CreateParamOut`(@id);
SELECT @id AS id_out; -- 5. 最后拿到 id 为 1

运行结束后我们可以看到三个结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
mysql复制代码mysql> set @id = 10;
mysql> call pr_param_out(@id);
+------------+
| id_inner_1 |
+------------+
| NULL |
+------------+
+------------+
| id_inner_3 |
+------------+
| 1 |
+------------+

mysql> select @id as id_out;
+--------+
| id_out |
+--------+
| 1 |
+--------+

可以看出,虽然我们设置了用户定义变量 @id 为 10,传递 @id 给存储过程后,在存储过程内部,id 的初始值总是 null(id_inner_1)。最后 id 值(id_out = 1)传回给调用者。所以 OUT 参数无法获取外部传来的参数。

  • 案例:INOUT 参数使用
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
mysql复制代码DROP PROCEDURE IF EXISTS mysql_scirpt.`07.CreateParamInOut`;

DELIMITER $$
$$
CREATE PROCEDURE mysql_scirpt.`07.CreateParamInOut`(
INOUT id int
)
BEGIN
SELECT id AS id_inner_1; -- 2. id = 10

IF id IS NOT NULL THEN
SET id = id+1; - 2. id+1 = 11
SELECT id AS id_inner_2; -- 3. id = 11
ELSE
SELECT 1 INTO id;
END IF;

SELECT id AS id_inner_3; -- 4. id = 11

END$$
DELIMITER ;

SET @id = 10;
CALL mysql_scirpt.`07.CreateParamInOut`(@id); -- 1. 传入参数
SELECT @id AS id_out; -- 5. id = 11

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
mysql复制代码mysql> set @id = 10;
mysql> call pr_param_inout(@id);
+------------+
| id_inner_1 |
+------------+
| 10 |
+------------+
+------------+
| id_inner_2 |
+------------+
| 11 |
+------------+
+------------+
| id_inner_3 |
+------------+
| 11 |
+------------+
mysql> select @id as id_out;
+--------+
| id_out |
+--------+
| 11 |
+--------+

如果仅仅想把数据传给 MySQL 存储过程,那就使用“in” 类型参数;如果仅仅从 MySQL 存储过程返回值,那就使用“out” 类型参数;如果需要把数据传给 MySQL 存储过程,还要经过一些计算后再传回给我们,此时,要使用“inout” 类型参数。

case 结构

  • 简易的语法格式:
1
2
3
4
5
mysql复制代码CASE case_value
WHEN when_value THEN statement_list
[WHEN when_value THEN statement_list] ...
[ELSE statement_list]
END CASE

case_value是一个表达式,该值和每个when子句中的when_value值进行相等比较:

  • 如果和某个when子句中的when_value值相等,则执行相应的then子句后面的语句statement_list;
  • 如果没有when_value值相等,则执行else子句后面的statement_list。

案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mysql复制代码DROP PROCEDURE IF EXISTS mysql_scirpt.`08.CreateCaseOne`;

DELIMITER $$
$$
CREATE PROCEDURE mysql_scirpt.`08.CreateCaseOne`()
BEGIN
DECLARE num int DEFAULT 3; -- 1. 给了num一个默认值 3

CASE num -- 2. 条件判断 num
WHEN 2 THEN SELECT num; -- num为2是结果为其本身
WHEN 3 THEN SELECT 0; -- num为3是结果输出0
END CASE;

END$$
DELIMITER ;
  • 检索case语法:
1
2
3
4
5
mysql复制代码CASE
WHEN search_condition THEN statement_list
[WHEN search_condition THEN statement_list] ...
[ELSE statement_list]
END CASE

对于每个when子句,判断后面的布尔表达式search_condition是否为true:  

  • 如果某个when子句的条件为true,则执行相应的then子句后面的语句statement_list;
  • 如果所有的when子句的条件都不为true,则执行else后面的语句statement_list。

案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
mysql复制代码DROP PROCEDURE IF EXISTS mysql_scirpt.`09.CreateCaseTwo`;

DELIMITER $$
$$
CREATE PROCEDURE mysql_scirpt.`09.CreateCaseTwo`(
IN p1 int,
IN p2 int,
OUT p3 int
)
BEGIN
CASE -- 2. 进行判断
WHEN p1 > p2 THEN SET p3 = 1;
WHEN p1 = p2 THEN SET p3 = 2;
ELSE SET p3 = 3;
END CASE;
SELECT p3; -- 输出
END$$
DELIMITER ;
CALL mysql_scirpt.`09.CreateCaseTwo`(200,100,@p3); -- 1. 传入参数
SELECT @p3; -- 输出

注意:

  • 如果在case中,没有一个when子句的比较结果为true,并且没有写else部分,那么就抛出异常:‘Case not found for CASE statement’;
  • statement_list如果有多条语句,使用begin…end块包围起来(复合语句)。

while 循环

语法结构:

1
2
3
mysql复制代码[begin_label:] WHILE search_condition DO
statement_list;
END WHILE [end_label];

首先判断循环开始条件search_condition是否为true(循环结束条件):

  • 如果为true,则执行循环体中的语句statement_list。每执行完一次,都要重新判断条件search_condition是否为true;
  • 如果条件search_condition为false,则循环结束。

特点:先判断,后执行。

案例:计算从1加到n的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
mysql复制代码DROP PROCEDURE IF EXISTS mysql_scirpt.`10.CreateWhile`;

DELIMITER $$
$$
CREATE PROCEDURE mysql_scirpt.`10.CreateWhile`(IN n int)
BEGIN
DECLARE total int DEFAULT 0;
DECLARE num int DEFAULT 1;

WHILE num<=n do -- 条件语句
SET total = total + num;
SET num = num + 1; -- num 递增,不写则进入死循环
END WHILE;

SELECT total;

END$$
DELIMITER ;

CALL mysql_scirpt.`10.CreateWhile`(10);

image-20211028093603285

repeat 循环

语法格式:

1
2
3
4
mysql复制代码[begin_label:] REPEAT
statement_list
UNTIL search_condition
END REPEAT [end_label]

反复执行循环体中的语句statement_list,直到until条件search_condition 为true时,循环结束

特点:先执行,后判断

有条件的循环控制语句, 当满足条件的时候退出循环 。while 是满足条件才执行,repeat 是满足条件就退出循环。

案例:计算从1加到n的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
mysql复制代码DROP PROCEDURE IF EXISTS mysql_scirpt.`11.CcreateRepeat`;

DELIMITER $$
$$
CREATE PROCEDURE mysql_scirpt.`11.CcreateRepeat`(IN n int)
BEGIN
DECLARE total int DEFAULT 0;

REPEAT
SET total = total + n;
SET n = n - 1;
until n = 0
END REPEAT;

SELECT total;

END$$
DELIMITER ;

CALL mysql_scirpt.`11.CcreateRepeat`(10)

image-20211028094720133

loop 循环

语法格式:

1
2
3
mysql复制代码[begin_label:] LOOP
statement_list ;
END LOOP [begin_label];

反复执行循环体中的语句,直到循环结束;循环的结束使用leave语句。

如果不在 statement_list 中增加退出循环的语句,那么 LOOP 语句可以用来实现简单的死循环。

Leave 语句

作用:用来退出带标签的语句块或者循环
用处:用在 BEGIN … END中或者循环中 (LOOP, REPEAT, WHILE)

语法格式:

1
mysql复制代码LEAVE  label ;

案例:Loop 和 Leave

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
mysql复制代码DROP PROCEDURE IF EXISTS mysql_scirpt.`12.CreateLoopAndLeave`;

DELIMITER $$
$$
CREATE PROCEDURE mysql_scirpt.`12.CreateLoopAndLeave`(
IN n int
)
BEGIN
DECLARE total int DEFAULT 0;
-- ins:begin_label
ins: LOOP
IF n <= 0 THEN
LEAVE ins;
END IF;

SET total = total + n;
SET n = n -1;

END LOOP ins;

SELECT total;

END$$
DELIMITER ;

CALL mysql_scirpt.`12.CreateLoopAndLeave`(10);

image-20211028101803523

iterate 语句

只能出现在循环LOOP、REPEAT和WHILE 中(有标签)

含义:跳出本次循环,开始一次新的循环。

语法格式:

1
mysql复制代码ITERATE  label;

游标/光标

游标:只能用来存储查询结果集的数据类型。

  • 只读:无法通过光标更新基础表中的数据。
  • 不可滚动:只能按照select语句确定的顺序获取行。不能以相反的顺序获取行。 此外,不能跳过行或跳转到结果集中的特定行。
  • 敏感:有两种游标:敏感游标和不敏感游标。敏感游标指向实际数据,不敏感游标使用数据的临时副本。敏感游标比一个不敏感的游标执行得更快,因为它不需要临时拷贝数据。但是,对其他连接的数据所做的任何更改都将影响由敏感游标使用的数据,因此,如果不更新敏感游标所使用的数据,则更安全。 MySQL游标是敏感的。

光标:在存储过程或函数中,可以使用光标对结果集进行循环处理。

有时,需要在检索出来的行中前进或后退一行或多行。这就是使用游标的原因。游标(cursor)是一个存储在MySQL服务器上的数据库查询,它不是一条SELECT语句,而是被该语句检索出来的结果集。在存储了游标之后,应用程序可以根据需要滚动或浏览其中的数据。

游标主要用于交互式应用,其中用户需要滚动屏幕上的数据,并对数据进行浏览或做出更改。

游标涉及几个明确的步骤:

  • 在能够使用游标前,必须声明(定义)它。这个过程实际上没有检索数据,它只是定义要使用的SELECT语句。
  • 一旦声明后,必须打开游标以供使用。这个过程用前面定义的SELECT语句把数据实际检索出来。
  • 对于填有数据的游标,根据需要取出(检索)各行。
  • 在结束游标使用时,必须关闭游标。

光标的使用:

  • 声明光标:
1
mysql复制代码DECLARE cursor_name CURSOR FOR select_statement;

cursor_name:光标名,用户自己设定,最好见名知意。
select_statement:完整的查询语句,查询表中的列名。

  • 开启光标:
1
mysql复制代码OPEN cursor_name;

cursor_name:声明时的光标名。

  • 捕获光标
1
mysql复制代码FETCH cursor_name INTO var_name...;(...表示可以有多个)

cursor_name:声明时的光标名。
var_name:自定义的变量名

  • 关闭光标
1
mysql复制代码CLOSE cursor_name;

示例:

  1. 初始化脚本
1
2
3
4
5
6
7
8
9
mysql复制代码create table emp(
id int(11) not null auto_increment ,
name varchar(50) not null comment '姓名',
age int(11) comment '年龄',
salary int(11) comment '薪水',
primary key(`id`)
)engine=innodb default charset=utf8 ;

insert into emp(id,name,age,salary) values(null,'金毛狮王',55,3800),(null,'白眉鹰王',60,4000),(null,'青翼蝠王',38,2800),(null,'紫衫龙王',42,1800);
  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
mysql复制代码DROP PROCEDURE IF EXISTS mysql_scirpt.`13.CreateCursor`;

DELIMITER $$
$$
-- 查询emp表中数据, 并逐行获取进行展示
CREATE PROCEDURE mysql_scirpt.`13.CreateCursor`()
BEGIN
-- 声明变量
DECLARE e_id int(11);
DECLARE e_name varchar(50);
DECLARE e_age int(11);
DECLARE e_salary int(11);
-- 设置光标
DECLARE emp_result CURSOR FOR SELECT * FROM emp;
-- 开启光标
OPEN emp_result;

-- 捕获光标
FETCH emp_result INTO e_id,e_name,e_age,e_salary;
SELECT concat('id=',e_id,',name=',e_name,',age=',e_age,',salary=',e_salary);

-- 关闭光标
CLOSE emp_result;
END$$
DELIMITER ;
  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
mysql复制代码DROP PROCEDURE IF EXISTS mysql_scirpt.`14.CreateGetCursor`;

DELIMITER $$
$$
CREATE PROCEDURE mysql_scirpt.`14.CreateGetCursor`()
BEGIN

DECLARE id int(11);
DECLARE name varchar(50);
DECLARE age int(11);
DECLARE salary int(11);
DECLARE has_data int default 1;

DECLARE emp_result CURSOR FOR SELECT * FROM emp;
DECLARE EXIT handler FOR NOT FOUND SET has_data = 0;

OPEN emp_result;

REPEAT
FETCH emp_result INTO id ,name,age,salary;
SELECT concat('id=',id,',name=',name,',age=',age,',salary=',salary);
until has_data = 0
END REPEAT;

CLOSE emp_result;

END$$
DELIMITER ;

逐条读取,无论表中的数据是什么都会全部读取出来。

存储过程函数

直接在sql文件中写的话会报错

1
mysql复制代码ERROR 1418 (HY000): This function has none of DETERMINISTIC, NO SQL, or READS SQL DATA in its declaration and binary logging is enabled (you might want to use the less safe log_bin_trust_function_creators variable)

原因:
这是我们开启了bin-log, 我们就必须指定我们的函数是否是
1 DETERMINISTIC 不确定的
2 NO SQL 没有SQl语句,当然也不会修改数据
3 READS SQL DATA 只是读取数据,当然也不会修改数据
4 MODIFIES SQL DATA 要修改数据
5 CONTAINS SQL 包含了SQL语句

其中在function里面,只有 DETERMINISTIC, NO SQL 和 READS SQL DATA 被支持。
如果我们开启了 bin-log, 我们就必须为我们的function指定一个参数。

输入语句:

1
mysql复制代码set global log_bin_trust_function_creators=TRUE;

语法格式:

1
2
3
mysql复制代码CREATE FUNCTION fn_name ([func_parameter[...]])
RETURNS type
[characteristic ...] routine_body

参数:

  • fn_name 参数:表示存储函数的名称;
  • func_parameter:表示存储函数的参数列表;
  • RETURNS type:指定返回值的类型;
  • characteristic 参数:指定存储函数的特性,该参数的取值与存储过程是一样的;
  • routine_body 参数:表示 SQL 代码的内容,可以用 BEGIN…END 来标示 SQL 代码的开始和结束。

注意:在具体创建函数时,函数名不能与已经存在的函数名重名。除了上述要求外,推荐函数名命名(标识符)为 function_xxx 或者 func_xxx。

存储过程和存储函数的区别

函数只能返回一个变量的限制。而存储过程可以返回多个。
函数限制比较多,比如不能用临时表,只能用表变量。还有一些函数都不可用等等.而存储过程的限制相对就比较少。

特性区别:

  • 一般来说,存储过程实现的功能要复杂一点,而函数的实现的功能针对性比较强。存储过程,功能强大,可以执行包括修改表等一系列数据库操作;用户定义函数不能用于执行一组修改全局数据库状态的操作。
  • 对于存储过程来说可以返回参数,如记录集,而函数只能返回值或者表对象。函数只能返回一个变量;而存储过程可以返回多个。存储过程的参数可以有IN,OUT,INOUT三种类型,而函数只能有IN类~~存储过程声明时不需要返回类型,而函数声明时需要描述返回类型,且函数体中必须包含一个有效的RETURN语句。
  • 存储过程,可以使用非确定函数,不允许在用户定义函数主体中内置非确定函数。
  • 存储过程一般是作为一个独立的部分来执行( EXECUTE 语句执行),而函数可以作为查询语句的一个部分来调用(SELECT调用),由于函数可以返回一个表对象,因此它可以在查询语句中位于FROM关键字的后面。 SQL语句中不可用存储过程,而可以使用函数。

使用

案例: 定义一个存储过程, 请求满足条件的总记录数;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mysql复制代码DROP FUNCTION IF EXISTS mysql_scirpt.`15.CreateFunction`;

DELIMITER $$
$$
CREATE FUNCTION mysql_scirpt.`15.CreateFunction`(vendID int)
RETURNS INT
BEGIN

DECLARE num int;
SELECT count(*) INTO num FROM products WHERE vend_id = vendID;

RETURN num;
END$$
DELIMITER ;

SELECT mysql_scirpt.`15.CreateFunction`(1001);

本文转载自: 掘金

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

前后端分离开发模式

发表于 2021-11-01

「这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战」

在做任何项目或者架构之前,我们都需要去先确定几件事情,比如技术选型、开发模式等,都是需要去做斟酌和确定的。本期我们来探讨一下前后端分离的这种开发模式。

早期传统 Java Web 开发

在探讨前后端分离开发模式之前,我们先来看一看早期传统 Java Web 的一种开发模式。

首先我们有用户,用户会访问浏览器,随后请求才会到达我们的后端。在我们的后端里面,那么这是一个单体项目,并且它是一个 war 包。在这个包里面它包含了一些相应的内容,比如 Servlet,也就是 MVC 的这一层架构的后端代码。当然也包含一部分前端代码,比如 JS、CSS 以及 HTML,但是 HTML 其实是 JSP。它是由 JSP 渲染而来的,它会渲染成 HTML,它的这个渲染的过程是在我们的后端去做的,也就是说只要有用户来访问我们的这个服务器,所有用户请求后所得到的页面,它全部都是在服务器去进行一个渲染的,对于我们的服务器是有一定的压力的。

如果在前期一开始用户量比较少的情况下,基本上是没有什么问题,一旦我们的用户越来越多,比如上万、百万的时候,我们所有页面都在服务器去渲染的话,这对于我们的服务器会造成非常大的性的影响。要去注意在我们做传统 Java Web 开发的时候,我们浏览器里面请求到所有的页面都是通过 URL 去进行跳转的。

前后端单页面交互,MVVM 开发模式

前后单页面的一种交互,也就是 MVVM 开发模式,是在前端的。

首先用户会访问浏览器,当然浏览器会请求到我们的后端。在这里面我们后端所有的一些内容其实都是以一个接口形式所存在的,但是浏览器所访问到的内容都是一些静态页面。这些静态页面都是一些 H5 的页面。这些静态资源全部都在我们的一个静态资源服务器,也就是 Nginx。在早期可能会用到 Apache 服务器,现如今我们都会使用非常主流的反向代理服务器,也就是 Nginx。

我们的用户访问到我们的页面以后,这些页面会发起一些请求,这些请求就是用户请求,它是会请求我们的后端,我们的 Servlet 的这一端,也就是 MVC 模式。后端代码全部都是在另外一台服务器,这台服务器的就是一个 Tomcat,静态资源和我们的后台的一些动态代码就做了一层分割,就是分开来,这个就是一个前后端分离的一种开发模式。

在我们的前端和后端进行交互的时候,他们之间其实是通过 Restful 这样的一种请求,就是 Restful Web Service,通过这种方式去请求到之后,会获得一些相应的数据,它们之间的一个数据交互形式全部都是以 JSON 的形式去进行交互的。

这个是用户通过浏览器去进行访问,现如今我们很多用户在使用手机的时候,手机端也是非常主流的一个客户端,我们会有一些页面,如果我们的页面兼容 H5,也能够在手机浏览器上去进行打开的,所以我们手机也是可以通过 H5 的形式去访问到 Nginx 里面所有的一些静态页面的。

还有一种方式是手机 APP,不管你是小程序、IOS 还是 Android,只要在我们手机端上有相应的客户端软件,我们也可以发起请求来访问到我们的服务器的。手机端和我们服务器之间的通讯,它也是通过 Restful 去进行通信,然后交互也是通过 JSON。不管我们的客户端的样式有多种对于我们后端来讲的话,后端只需要一套代码去维护就可以了,不需要复制多份去进行开发,我们只需要去针对不同的客户端去做一些接口上的微调就可以了。

使用前后端分离有很多的好处的,这样子做可以使得分工明确,前端和后端开发人员他们的效率可以提升一个档次,使得各自领域的开发人员术业有专攻。并且我们的团队也是只有了前端归前端,后端归后端,不同团队所关注的内容都是不一样的,后端代码只需要一套就可以了,团队是可以进行并行开发的。换成之前的传统开发模式,所有人都是偶合在一起,并且我们是相互阻塞的,我们必须完成上一个接口才能够对接下一个页面。在前后端分离的这种开发模式里面,其实也有一些小问题,比如沟通,因为团队多了,人也多了,我们一定会少不了沟通。另外,我们在发布的时候,一般是各自发布的,稍微会有一些复杂,需要做到版本的一些统一。

前后端也分离了,如果会产出一些 bug,我们也可以精准定位,理清我们的边界。有些 bug 是前端的就是前端,也不会踢给后端啊,就不会造成这种踢皮球的现象。另外,需要注意,我们所有的一些静态资源文件全部都是部署在 Nginx 里面的,这种方式就是一个前后端分离模式。

再强调一下,前后端分离不仅它是一种开发模式,也是一种架构模式,可以称之为是前后端分离架构。

使用前后端分离的时候,我们是需要去注意我们的前端和后端项目是要区分开来的,前端和后端的项目是两个不同的项目,放在两个不同的服务器,并且各自也需要去独立地去部署,都有两套不同的不一样的代码库,而且会有两个不同的开发团队共同的去进行开发和维护。这个就是我们所需要去理解的前后端分离开发模式。

本文转载自: 掘金

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

Java中的流与IO 一、流是什么 二、BIO 三、NIO

发表于 2021-11-01
  • J3 - 白起
  • 技术(I/O流)

最近在看 Netty 相关的内容,以后就会写一些和 Netty 相关技术的文章。

而 Netty 作为业界最流行的 NIO 框架之一,在开始之前就自然要全面的介绍一下 BIO、NIO 以及 AIO 相关的内容了。

所以在开始 Netty 之前,我就来介绍介绍 I/O 的基本体系,以此来向你们构建出 Netty 的魅力。

一、流是什么

百度概念:

流是一组有顺序的,有起点和终点的字节集合,是对数据传输的总称或抽象。即数据在两设备间的传输称为流。流的本质是数据传输,根据数据传输特性将流抽象为各种类,方便更直观的进行数据操作。

总结就是:==流就是传输,传输的内容就是字节==

在 Java 中我们常说的字节流、字符流其实本质就是对流传输内容的不同而划分的两种操作。字节流操作单位是字节,字符流操作单位是一个个字符。

前面说过,流是有起点和终点的。而又因为起点和终点的各不相同,流又可分为:输入流、输出流。

理解:

内存 -> 硬盘 = 输出流(OutputStream、Writer)

硬盘 -> 内存 = 输入流(InputStream、Reader)

Snipaste_2021-10-31_15-55-26.png

对于流的操作 Java 提供了非常多的 API 操作,包位置:java.io、java.nio。

因为本篇不是教大家如何使用 API 的,所以其中的使用方法就不过多的介绍了,但我岂是那种不负责任的男人😀,已经帮你们找好要复习 IO 流操作的基础教程了👉👉👉点这里。

但为了引出 BIO、NIO及 AIO 相关概念,小小案例还是要写一下的,如下:

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
java复制代码@Slf4j
public class BioTest {

/**
* 流的形式操作文件
*/
@Test
public void streamTest() throws Exception {

// 定义两个文件,in.txt 和 out.txt(提前在src目录下创建好两个文件)
File inFile = new File("src/in.txt");
File outFile = new File("src/out.txt");

// 定义一个对 in.txt 操作的流对象
InputStream inputStream = new FileInputStream(inFile);
// 开始读取文件
byte[] bytes = new byte[8];
inputStream.read(bytes);
System.out.println(new String(bytes));

// 定义 out.txt 操作的流对象
OutputStream outputStream = new FileOutputStream(outFile);
// 开始写出文件
outputStream.write(bytes);
outputStream.write("\nout-write".getBytes(StandardCharsets.UTF_8));

}

/**
* 阻塞形式操作网络编程
*/
@Test
public void blockTest() throws Exception {
/*
1、运行服务端发现,服务端在获取客户端连接及获取客户端数据时,都会堵塞
*/
}

@Test
public void server() throws Exception {
// 创建服务端对象
ServerSocket serverSocket = new ServerSocket(9528);
log.info("创建了一个服务端对象,{}", serverSocket);
// 获取客户端链接的 soclet 对象
Socket accept = serverSocket.accept();
log.info("获取到了客户端连接对象,{}", accept);
InputStream inputStream = accept.getInputStream();
byte[] bytes = new byte[100];
inputStream.read(bytes);
log.info("客户端发来的内容:{}", new String(bytes));
}

@Test
public void client() throws Exception {
// 创建客户端,指定服务端ip及端口,进行连接
Socket client = new Socket("localhost", 9528);
log.info("客户端创建完毕,{}", client);
// 开始向务端发送消息
OutputStream outputStream = client.getOutputStream();
outputStream.write("hello world!".getBytes(StandardCharsets.UTF_8));
}
}

上面案例展示了两种效果,一流操作、二阻塞操作。

而对于流和阻塞正是 Java 传统 IO(BIO) 的一种拙劣表现,流在数据的运输上效率比不上带有通道的缓冲区;而阻塞更比不上非阻塞的 Selector 操作。

二、BIO

同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。

而且也是 Java 1.4 之前唯一的 IO 模式。

上面出现了两个名词:同步,阻塞,那么下面我先解释一下。

名词 解释 例子
同步 指的是用户进程触发IO操作并等待或者轮询的去查看IO操作是否就绪 苦逼程单身程序员A,为了能找到相亲对象天天上班下班搜罗各大单身小姐姐联系方式,这种亲自出马忽略工作的就是同步。
异步 异步是指用户进程触发IO操作以后便开始做自己的事情,而当IO操作已经完成的时候会得到IO完成的通知(异步的特点就是通知) 聪明灵活单身程序员B,为了能找到各种相亲小姐姐的联系方式,直接联系了某大型婚介公司,交由婚介公司获取小姐姐联系方式,这种委托形式而不耽误正常工作的就是异步。
阻塞 所谓阻塞方式的意思是指, 当试图对该文件描述符进行读写时, 如果当时没有东西可读,或者暂时不可写, 程序就进入等待 状态, 直到有东西可读或者可写为止 程序员A,好不容易找到几个身材条件都不错的小姐姐,但是就是一直约不上人家,只能等人家空闲时间,这种等待就是阻塞。
非阻塞 非阻塞状态下, 如果没有东西可读, 或者不可写, 读写函数马上返回, 而不会等待 程序员B,因为是大型婚介公司,找得到联系方式,肯定也是约会相亲一条龙服务,所以就顺利的和小姐姐见面谈人生谈理想谈…,这种直接约的就是非阻塞。

通过上面我写的代码案例和这张表格,相信大家对与同步及阻塞有了很清晰的认识了。下面我们一起看看 BIO 的模型图:

Snipaste_2021-10-31_18-00-06.png

从图中可以看出,一个服务器会对应这多个客户端,每个客户端都对着不同的线程,这就导致了单线程环境下客户端 A 与服务端通信时,B、C 都需要进行等待阻塞,只有 A 通信完毕 B、C 客户端才能进行后续步骤。

案例代码,见第一小节。

三、NIO

同步非阻塞I/O模式,Java 1.4 之后开始支持,并提供了像 Channel , Selector,Buffer等抽象(后面会重点介绍这三大组件)。

我们都知道传统 BIO 是面向流的,而 NIO 意识到流的效率问题就提出了面向缓冲(块)的方式进行 IO 操作大大提升了传输效率。

而且 NIO 提供了与传统 BIO 模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO的非阻塞模式来开发。

NIO模型图如下:

Snipaste_2021-10-31_19-09-06.png

案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
java复制代码@Slf4j
public class NioTest {

@Test
public void serverTest() throws Exception{
//创建serverSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//绑定端口
serverSocketChannel.socket().bind(new InetSocketAddress(6666));
//设置为非阻塞
serverSocketChannel.configureBlocking(false);
//得到Selector对象
try (Selector selector = Selector.open()) {
//把ServerSocketChannel注册到selector,事件为OP_ACCEPT
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//如果返回的>0,表示已经获取到关注的事件
while (selector.select() > 0) {
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
//获得到一个事件
SelectionKey next = iterator.next();
//如果是OP_ACCEPT,表示有新的客户端连接
if (next.isAcceptable()) {
//给该客户端生成一个SocketChannel
SocketChannel accept = serverSocketChannel.accept();
accept.configureBlocking(false);
//将当前的socketChannel注册到selector,关注事件为读事件,同时给socket Channel关联一个buffer
accept.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
log.info("获取到一个客户端连接");
//如果是读事件
} else if (next.isReadable()) {
//通过key 反向获取到对应的channel
SocketChannel channel = (SocketChannel) next.channel();
//获取到该channel关联的buffer
ByteBuffer buffer = (ByteBuffer) next.attachment();
while (channel.read(buffer) != -1) {
buffer.flip();
log.info(new String(buffer.array(), 0, buffer.limit()));
buffer.clear();
}
}
// 这个很重要,在处理完事件之后,要移除该事件
iterator.remove();
}
}
}

}

@Test
public void clientTest() throws Exception{
//得到一个网络通道
SocketChannel socketChannel = SocketChannel.open();
//设置为非阻塞
socketChannel.configureBlocking(false);
//提供服务器端的IP和端口
InetSocketAddress inetSocketAddress = new InetSocketAddress("localhost", 9528);
//连接服务器
if (!socketChannel.connect(inetSocketAddress)) {
while (!socketChannel.finishConnect()) {
log.info("连接需要时间,客户端不会阻塞...你可以去干别的事情了");
}
}
//连接成功,发送数据
String str = "J3-白起";
ByteBuffer byteBuffer = ByteBuffer.wrap(str.getBytes());
socketChannel.write(byteBuffer);
socketChannel.close();
log.info("客户端退出");
}

}

这段代码,可以体现服务端获取客户端连接时时不需要阻塞的,并且在读取客户端发来的数据时也是不需要阻塞有数据就读没有就往下执行,这也是 Netty 框架流行原因之一。

四、AIO

异步非阻塞I/O模式,在 Java 7 中引入了 NIO 的改进版 NIO 2。

异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。

对于 AIO 在网上资料还不是很多,并且应用还不是很广泛,所以就…

五、最后

本篇主要是让大家对 Java IO 的模型体系有个大致了解,知道有 BIO、NIO、AIO 这一回事和了解同步、阻塞的区别就行。

对于具体的应用,我是没有具体展开说的,因为 BIO 是基础我已经贴过教程地址了☝☝☝。而 NIO 是学 Netty 的前提我后面会对这部分持续的输出,至于 AIO 应用还不是非常广泛我也不会,所以可以先不用关注了解就行。

好了,介绍了这篇,那下篇就是 NIO 讲解了,关注我,咱们下期见。

查阅或扩展资料:

  • 《Netty权威指南第二版》
  • 《漫话:如何给女朋友解释什么是Linux的五种IO模型?》
  • www.zhihu.com/question/64…
  • segmentfault.com/a/119000003…

  • 由于博主才疏学浅,难免会有纰漏,假如你发现了错误或偏见的地方,还望留言给我指出来,我会对其加以修正。
  • 如果你觉得文章还不错,你的转发、分享、点赞、留言就是对我最大的鼓励。
  • 感谢您的阅读,十分欢迎并感谢您的关注。

^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

CSDN:J3 - 白起

掘金:J3-白起

知乎:J3-白起

这是一个技术一般,但热衷于分享;经验尚浅,但脸皮够厚;明明年轻有颜值,但非要靠才华吃饭的程序员。

^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

本文转载自: 掘金

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

1…440441442…956

开发者博客

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