项目管理5大过程
不论大小或复杂程度,所有项目都呈现通用的生命周期结构。而过程是为完成预定的产品完成成果或服务而执行的一系列行动。在最新的《PMBOK指南》第6版中将项目的管理定义为49个过程
不论大小或复杂程度,所有项目都呈现通用的生命周期结构。而过程是为完成预定的产品完成成果或服务而执行的一系列行动。在最新的《PMBOK指南》第6版中将项目的管理定义为49个过程
场景:MySQL数据库、innoDB存储引擎
聚簇索引
叶子节点存放的是索引和整列的数据
非聚簇索引(也叫二级索引)
叶子节点存放的是主键的值
InnoDB 中,对于主键索引,只需要走一遍主键索引的查询就能在叶子节点拿到数据。而对于普通索引,叶子节点存储的是 key + 主键值,因此需要再走一次主键索引,通过主键索引找到行记录,这就是所谓的回表查询,先定位主键值,再定位行记录。
InnoDB中必然且只会有一个聚簇索引,一般是主键,如果没有主键,就会优先选择非空的唯一索引,唯一索引也没有,就会创建一个隐藏的 row_id 作为聚簇索引。
不一定,如果查询语句所要求的字段全部命中了索引,那么就不必再进行回表查询就能拿到所有的请求数据。很容易理解,有一个 user 表,主键为 id,name 为普通索引,则再执行:select id, name from user where name = ‘joonwhee’ 时,通过name 的索引就能拿到 id 和 name了,因此无需再回表去查数据行了。
当提交一个新任务到线程池时,具体的执行流程如下:
当我们提交任务,线程池会根据corePoolSize大小创建若干任务数量线程执行任务
当任务的数量超过corePoolSize数量,后续的任务将会进入阻塞队列阻塞排队
当阻塞队列也满了之后,那么将会继续创建(maximumPoolSize-corePoolSize)个数量的线程来执
行任务,如果任务处理完成,maximumPoolSize-corePoolSize额外创建的线程等待
keepAliveTime之后被自动销毁
如果达到maximumPoolSize,阻塞队列还是满的状态,那么将根据不同的拒绝策略对应处理
主要有4种拒绝策略:
在Spring框架中,循环依赖问题指的是在依赖注入时,由于Bean之间相互引用而导致的初始化问题。
这种情况下,Spring容器在创建Bean的过程中,发现Bean A依赖于Bean B,而Bean B又依赖于Bean A,形成了循环依赖关系。
当两个或多个Bean的构造函数相互依赖时,会形成构造器循环依赖。这种情况下,Spring容器在创建Bean时无法确定哪个Bean应该先被实例化,因为它们相互依赖于彼此的构造函数参数。
示例:
public class A {
private B b;
public A(B b) {
this.b = b;
}
}
public class B {
private A a;
public B(A a) {
this.a = a;
}
}
当两个或多个Bean的属性相互依赖时,会形成属性循环依赖。例如,Bean A依赖于Bean B的属性,而Bean B又依赖于Bean A的属性,形成了属性循环依赖。
示例:
public class A {
private B b;
public void setB(B b) {
this.b = b;
}
}
public class B {
private A a;
public void setA(A a) {
this.a = a;
}
}
当单例Bean之间相互依赖时,会形成单例Bean的循环依赖。由于Spring默认情况下会将单例Bean存储在容器中,这种循环依赖问题可能会导致死锁或无限递归调用。
示例 :
假设我们有两个类 A
和 B
,它们相互依赖:
@Service
public class A {
@Autowired
private B b;
}
@Service
public class B {
@Autowired
private A a;
}
假设 A
类的一个实例需要依赖 B
类的一个实例,而 B
类的一个实例又需要依赖 A
类的一个实例,形成了循环依赖。在这种情况下,如果我们试图使用 Spring 容器来管理这些类的实例,就会出现循环依赖的问题。
在初始化这些 Bean 时,Spring 会发现 A
类的实例需要 B
类的实例,而 B
类的实例又需要 A
类的实例,这样就形成了循环依赖。如果不采取措施来解决这个问题,Spring 容器就会陷入死循环或者抛出异常。
这三种循环依赖问题在Spring中都有解决方案:
可以通过使用 @Lazy
注解延迟初始化其中一个依赖,或者使用@Autowired
与@Qualifier
来指定构造器参数的具体Bean。
示例代码:
@Component
public class A {
private B b;
@Autowired
public A(@Lazy B b) {
this.b = b;
}
}
@Component
public class B {
private A a;
@Autowired
public B(A a) {
this.a = a;
}
}
可以通过@Lazy
注解延迟初始化其中一个依赖,或者将其中一个依赖设置为@Nullable
,允许属性为空。
示例代码:
@Component
public class A {
private B b;
public void setB(@Lazy B b) {
this.b = b;
}
}
@Component
public class B {
@Nullable
private A a;
public void setA(A a) {
this.a = a;
}
}
可以通过使用 @Lazy
注解延迟初始化其中一个依赖,或者使用代理对象来解决单例Bean的循环依赖。
示例代码:
<bean id="a" class="com.example.A" lazy-init="true">
<property name="b" ref="b"/>
</bean>
<bean id="b" class="com.example.B" lazy-init="true">
<property name="a">
<bean class="org.springframework.beans.factory.config.BeanReferenceFactoryBean">
<property name="beanName" value="a"/>
</bean>
</property>
</bean>
这些解决方案可以根据具体情况选择适合的方式来解决Spring中的循环依赖问题。
为了解决这个问题,Spring使用了三级缓存机制。这三级缓存分别是:
当Spring遇到循环依赖问题时,它会按照以下步骤解决:
通过三级缓存机制,Spring有效地解决了循环依赖问题,确保了Bean的正确初始化。这种机制使得Spring在处理复杂依赖关系时更加灵活和健壮。
Spring通过三级缓存机制自动解决了特定类型的循环依赖,即单例Bean通过setter方法注入形成的循环依赖。对于构造器注入、非单例Bean间的循环依赖以及在Bean生命周期回调方法中出现的循环依赖,Spring无法自动解决,需要开发者自行调整设计或代码实现以消除循环依赖,或者采取其他编程策略来妥善处理。
场景:SpringBoot项目中使用了
undertow
作为web服务,在配置accesslog后发现%D
并不能获取访问耗时
server:
undertow:
url-charset: UTF-8
accesslog:
enabled: true
dir: /opt/logs
file-date-format: .yyyy-MM-dd
prefix: access
suffix: .log
pattern: '%t %a %v %r %s %b "%{i,Referer}" "%{i,User-Agent}" "%{i,X-Forwarded-For}" %D ms'
rotate: true
通过查看文档发现如果需要获取耗时,undertow需要开启 server option的RECORD_REQUEST_START_TIME
就是在自动配置类里新建一个Bean,将属性set进去,代码如下:
@Configuration
public class UndertowConfig {
/**
* 开启undertow计时
* @return
*/
@Bean
public UndertowServletWebServerFactory undertowServletWebServerFactory() {
UndertowServletWebServerFactory factory = new UndertowServletWebServerFactory();
factory.addBuilderCustomizers(new UndertowBuilderCustomizer() {
@Override
public void customize(Undertow.Builder builder) {
builder.setServerOption(UndertowOptions.RECORD_REQUEST_START_TIME, true);
}
});
return factory;
}
}
但这种方法总感觉很奇怪,还要重新new一个新的UndertowServletWebServerFactory
, 所以我就没有采取这种做法。
感觉这种开关应该是可以通配置来解决的,通过查看源码发现确实是可以的。
通过配置文件开启undertow
的计时:
server:
undertow:
url-charset: UTF-8
options:
server:
record-request-start-time: true #记录请求开始时间
accesslog:
enabled: true
dir: /opt/logs
file-date-format: .yyyy-MM-dd
prefix: access
suffix: .log
pattern: '%t %a %v %r %s %b "%{i,Referer}" "%{i,User-Agent}" "%{i,X-Forwarded-For}" %D ms'
rotate: true
NPE问题就是,我们在开发中经常碰到的NullPointerException
user.getAddress().getProvince();
这种写法,在user为null时,是会报NullPointerException异常的。为了解决这个问题,于是采用下面的写法
if(user!=null){
Address address = user.getAddress();
if(address!=null){
String province = address.getProvince();
}
}
这种写法增加了很多if-else代码块,有什么更优雅的写法呢。JAVA8提供了Optional类来优化这种写法
Optional 类是一个可以为null的容器对象。如果值存在则isPresent()方法会返回true,调用get()方法会返回该对象。
Optional 是个容器:它可以保存类型T的值,或者仅仅保存null。Optional提供很多有用的方法,这样我们就不用显式进行空值检测。
Optional 类的引入很好的解决空指针异常。
源码:
private Optional(T value) {
this.value = Objects.requireNonNull(value);
}
Optional(T value)
,即构造函数,它是private权限的,不能由外部调用的。
源码:
public static <T> Optional<T> of(T value) {
return new Optional<>(value);
}
也就是说of(T value)函数内部调用了构造函数。根据构造函数的源码我们可以得出两个结论:
源码:
public static<T> Optional<T> empty() {
@SuppressWarnings("unchecked")
Optional<T> t = (Optional<T>) EMPTY;
return t;
}
empty() 的作用就是返回EMPTY对象,用来构造一个空的 Optional,即该 Optional 中不包含值
源码:
public static <T> Optional<T> ofNullable(T value) {
return value == null ? empty() : of(value);
}
相比较Optional.of(T value)的区别就是,当value值为null时,of(T value)会报NullPointerException异常;ofNullable(T value)不会throw Exception,ofNullable(T value)直接返回一个EMPTY对象。
由此可见,实际项目运用中Optional.ofNullable(T value)的使用场景要远远多余Optional.of(T value)。当你不想隐藏NullPointerException,而是要立即报告,这种情况下就用Of函数。
这三个函数放一组,都是在上面构造函数传入的value值为null时,进行调用的。
orElse和orElseGet的用法如下所示,相当于value值为null时,给予一个默认值:
@Test
public void test() {
User user = null;
user = Optional.ofNullable(user).orElse(createUser());
user = Optional.ofNullable(user).orElseGet(() -> createUser());
}
public User createUser(){
User user = new User();
user.setName("zhangsan");
return user;
}
这个两个函数简单解释为,我们使用Optional包装的变量如果不为空,返回它本身,否则返回我们传递进去的值。orElseGet参数为Supplier接口,它是一个函数式接口,它的形式是这样的:() -> { return computedResult },即入参为空,有返回值(任意类型的)。
这两个函数的区别:
乍一看确实有点懵,明明有值,为什么还执行,怎么都觉得跟orElse的语义违背
源码:
public T orElse(T other) {
return value != null ? value : other;
}
public T orElseGet(Supplier<? extends T> other) {
return value != null ? value : other.get();
}
两者的明显(也是唯一)区别是前者需要传递的参数是一个值(通常是为空时的默认值),后者传递的是一个函数。
其实不管是orElse还是orElseGet都会进到对应的方法里面的,所以在要执行orElse之前,那参数的值总是要知道的,所以肯定先要执行传入的方法。
简而言之:前者立即计算,后者是延迟计算
看起来可以使用orElseGet的时候,使用orElse也可以代替(因为Supplier接口没有入参),而且使用orElseGet还需要将计算过程额外包装成一个 lambda 表达式。
一个关键的点是,使用Supplier能够做到懒计算。即 使用orElseGet时,它的好处是,只有在需要的时候才会计算结果。使用orElse的时候,每次它都会执行计算结果的过程,而对于orElseGet,只有Optional中的值为空时,它才会计算备选结果。这样做的好处是可以避免提前计算结果的风险。
场景举例:
class User {
// 中文名
private String chineseName;
// 英文名
private EnglishName englishName;
}
class EnglishName {
// 全名
private String fullName;
// 简写
private String shortName;
}
假如我们现在有User类,用户注册账号时,需要提供自己的中文名或英文名,或都提供,我们抽象出一个EnglishName类,它包含英文名的全名和简写(因为有的英文名确实太长了)。现在,我们希望有一个User#getName()方法,它可以像下面这样实现:
class User {
// 中文名
private String chineseName;
// 英文名
private EnglishName englishName;
public String getName1() {
return Optional.ofNullable(chineseName)
.orElse(englishName.getShortName());
}
public String getName2() {
return Optional.ofNullable(chineseName)
.orElseGet(() -> englishName.getShortName());
}
}
可以看出getName1()方法有什么风险了吗?
当用户只提供了中文名时,此时englishName属性是null,但是在orElse中,englishName.getShortName()总是会执行,这时候就会出现空指针异常。而在getName2()中,这个风险却没有。
源码:
public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
if (value != null) {
return value;
} else {
throw exceptionSupplier.get();
}
}
它会在对象为空的时候抛出异常,而不是返回备选的值:
User user = null;
Optional.ofNullable(user).orElseThrow(()->new Exception("用户不存在"));
这个方法让我们有更丰富的语义,可以决定抛出什么样的异常,而不总是抛出NullPointerException。
源码:
public final class Optional<T> {
public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent())
return empty();
else {
return Optional.ofNullable(mapper.apply(value));
}
}
public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent())
return empty();
else {
return Objects.requireNonNull(mapper.apply(value));
}
}
}
如果当前 Optional 为 Optional.empty,则依旧返回 Optional.empty;否则返回一个新的 Optional,该 Optional 包含的是:函数 mapper 在以 value 作为输入时的输出值。
Optional<String> username = Optional
.ofNullable(user)
.map(user -> user.getUsername());
System.out.println("Username is: " + username.orElse("Unknown"));
而且我们可以多次使用map操作:
Optional<String> username = Optional
.ofNullable(user)
.map(user -> user.getUsername())
.map(name -> name.toLowerCase())
.map(name -> name.replace('_', ' '));
System.out.println("Username is: " + username.orElse("Unknown"));
flatMap 方法与 map 方法的区别在于,map 方法参数中的函数 mapper 输出的是值,然后 map 方法会使用 Optional.ofNullable 将其包装为 Optional;而 flatMap 要求参数中的函数 mapper 输出的就是 Optional。
Optional<String> username = Optional
.ofNullable(user)
.flatMap(user -> Optional.of(user.getUsername()))
.flatMap(name -> Optional.of(name.toLowerCase()));
System.out.println("Username is: " + username.orElse("Unknown"));
isPresent即判断value值是否为空,返回boolean,而ifPresent就是在value值不为空时,做一些操作。这两个函数的源码如下:
public final class Optional<T> {
//省略....
public boolean isPresent() {
return value != null;
}
//省略...
public void ifPresent(Consumer<? super T> consumer) {
if (value != null)
consumer.accept(value);
}
}
如果需要判断value不为空之后还需要do something,不要这样写:
Optional<User> user = Optional.ofNullable(user);
if (user.isPresent()){
// TODO: do something
}
可以使用ifPresent函数处理
Optional<User> user = Optional.ofNullable(user);
user.ifPresent(u -> System.out.println("Username is: " + u.getUsername()));
源码:
public final class Optional<T> {
public Optional<T> filter(Predicate<? super T> predicate) {
Objects.requireNonNull(predicate);
if (!isPresent())
return this;
else
return predicate.test(value) ? this : empty();
}
}
filter 方法接受一个 Predicate 来对 Optional 中包含的值进行过滤,如果包含的值满足条件,那么还是返回这个 Optional;否则返回 Optional.empty。
用法:
Optional<User> user1 = Optional.ofNullable(user).filter(u -> u.getName().length()<6);
如上所示,如果user的name的长度是小于6的,则返回。如果是大于6的,则返回一个EMPTY对象。
ifPresentOrElse 方法的用途是,如果一个 Optional 包含值,则对其包含的值调用函数 action,即 action.accept(value),这与 ifPresent 一致;与 ifPresent 方法的区别在于,ifPresentOrElse 还有第二个参数 emptyAction —— 如果 Optional 不包含值,那么 ifPresentOrElse 便会调用 emptyAction,即 emptyAction.run()
使用示例:
public class Tester {
public static void main(String[] args) {
Optional<Integer> optional=Optional.of(1);
optional.ifPresentOrElse( x -> System.out.println("Value: " + x),() ->
System.out.println("Not Present."));
optional=Optional.empty();
optional.ifPresentOrElse( x -> System.out.println("Value: " + x),() ->
System.out.println("Not Present."));
}
}
//输出
Value: 1
Not Present.
有时当Optional为空时,我们想执行一些其他逻辑并也返回Optional。在Java9之前Optional类仅有orElse()和orElseGet()方法,但两者都返回非包装值。
Java9引入or()方法当Optional为空时返回另一个Optional。如果Optional有定义值,则传入or方法的lambda不被执行:
String expected = "properValue";
Optional<String> value = Optional.of(expected);
Optional<String> defaultValue = Optional.of("default");
Optional<String> result = value.or(() -> defaultValue);
Optional<String> value1 = Optional.empty();
Optional<String> result = value1.or(() -> defaultValue);
stream 方法的作用就是将 Optional 转为一个 Stream,如果该 Optional 中包含值,那么就返回包含这个值的 Stream;否则返回一个空的 Stream(Stream.empty())。
举个例子,在 Java8,我们会写下面的代码:
// 此处 getUserById 返回的是 Optional<User>
public List<User> getUsers(Collection<Integer> userIds) {
return userIds.stream()
.map(this::getUserById) // 获得 Stream<Optional<User>>
.filter(Optional::isPresent)// 去掉不包含值的 Optional
.map(Optional::get)
.collect(Collectors.toList());
}
而有了 Optional.stream(),我们就可以将其简化为:
public List<User> getUsers(Collection<Integer> userIds) {
return userIds.stream()
.map(this::getUserById) // 获得 Stream<Optional<User>>
.flatMap(Optional::stream) // Stream 的 flatMap 方法将多个流合成一个流
.collect(Collectors.toList());
}
Stream 是 Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。使用Stream API 对集合数据进行操作,就类似于使用 SQL 执行的数据库查询。也可以使用 Stream API 来并行执行操作。简而言之,Stream API 提供了一种高效且易于使用的处理数据的方式。
流(Stream) 到底是什么呢?
可以这么理解流:流就是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。
“集合讲的是数据,流讲的是计算! ”
注意:
① Stream 自己不会存储元素。
② Stream 不会改变源对象。相反,他们会返回一个持有结果的新Stream。
③ Stream 操作是延迟执行的。这意味着他们会等到需要结果的时候才执行。
1.创建 Stream
一个数据源(如: 集合、数组), 获取一个流。
2.中间操作
一个中间操作链,对数据源的数据进行处理。
3.终止操作(终端操作)
一个终止操作,执行中间操作链,并产生结果 。
在Java8中,Collection 接口被扩展,提供了两个获取流的默认方法,如下所示。
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
default Stream<E> parallelStream() {
return StreamSupport.stream(spliterator(), true);
}
其中,stream()方法返回一个顺序流,parallelStream()方法返回一个并行流。
我们可以使用如下代码方式来创建顺序流和并行流。
List<String> list = new ArrayList<>();
list.stream();
list.parallelStream();
Java8 中的 Arrays类的静态方法 stream() 可以获取数组流 ,如下所示。
public static <T> Stream<T> stream(T[] array) {
return stream(array, 0, array.length);
}
上述代码的的作用为:传入一个泛型数组,返回这个泛型的Stream流。
除此之外,在Arrays类中还提供了stream()方法的如下重载形式。
public static <T> Stream<T> stream(T[] array) {
return stream(array, 0, array.length);
}
public static <T> Stream<T> stream(T[] array, int startInclusive, int endExclusive) {
return StreamSupport.stream(spliterator(array, startInclusive, endExclusive), false);
}
public static IntStream stream(int[] array) {
return stream(array, 0, array.length);
}
public static IntStream stream(int[] array, int startInclusive, int endExclusive) {
return StreamSupport.intStream(spliterator(array, startInclusive, endExclusive), false);
}
public static LongStream stream(long[] array) {
return stream(array, 0, array.length);
}
public static LongStream stream(long[] array, int startInclusive, int endExclusive) {
return StreamSupport.longStream(spliterator(array, startInclusive, endExclusive), false);
}
public static DoubleStream stream(double[] array) {
return stream(array, 0, array.length);
}
public static DoubleStream stream(double[] array, int startInclusive, int endExclusive) {
return StreamSupport.doubleStream(spliterator(array, startInclusive, endExclusive), false);
}
基本上能够满足将基本类型的数组转化为Stream流的操作。
我们可以通过下面的代码示例来使用Arrays类的stream()方法来创建Stream流。
Integer[] nums = new Integer[]{1,2,3,4,5,6,7,8,9};
Stream<Integer> numStream = Arrays.stream(nums);
可以使用静态方法 Stream.of(), 通过显示值创建一个流。它可以接收任意数量的参数。
如下:
public static<T> Stream<T> of(T t) {
return StreamSupport.stream(new Streams.StreamBuilderImpl<>(t), false);
}
@SafeVarargs
@SuppressWarnings("varargs")
public static<T> Stream<T> of(T... values) {
return Arrays.stream(values);
}
可以看到,在Stream类中,提供了两个of()方法,一个只需要传入一个泛型参数,一个需要传入一个可变泛型参数。
我们可以使用下面的代码示例来使用of方法创建一个Stream流。
Stream<String> strStream = Stream.of("a", "b", "c");
可以使用静态方法 Stream.iterate() 和Stream.generate(), 创建无限流。
先来看看Stream类中iterate()方法和generate()方法的源码,如下所示。
public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f) {
Objects.requireNonNull(f);
final Iterator<T> iterator = new Iterator<T>() {
@SuppressWarnings("unchecked")
T t = (T) Streams.NONE;
@Override
public boolean hasNext() {
return true;
}
@Override
public T next() {
return t = (t == Streams.NONE) ? seed : f.apply(t);
}
};
return StreamSupport.stream(Spliterators.spliteratorUnknownSize(
iterator,
Spliterator.ORDERED | Spliterator.IMMUTABLE), false);
}
public static<T> Stream<T> generate(Supplier<T> s) {
Objects.requireNonNull(s);
return StreamSupport.stream(
new StreamSpliterators.InfiniteSupplyingSpliterator.OfRef<>(Long.MAX_VALUE, s), false);
}
通过源码可以看出,iterate()方法主要是使用“迭代”的方式生成无限流,而generate()方法主要是使用“生成”的方式生成无限流。我们可以使用下面的代码示例来使用这两个方法生成Stream流。
Stream<Integer> intStream = Stream.iterate(0, (x) -> x + 2);
intStream.forEach(System.out::println);
运行上述代码,会在终端一直输出偶数,这种操作会一直持续下去。如果我们只需要输出10个偶数,该如何操作呢?其实也很简单,使用Stream对象的limit方法进行限制就可以了,如下所示。
Stream<Integer> intStream = Stream.iterate(0, (x) -> x + 2);
intStream.limit(10).forEach(System.out::println);
Stream.generate(() -> Math.random()).forEach(System.out::println);
上述代码同样会一直输出随机数,如果我们只需要输出5个随机数,则只需要使用limit()方法进行限制即可。
Stream.generate(() -> Math.random()).limit(5).forEach(System.out::println);
在Stream类中提供了一个empty()方法,如下所示。
public static<T> Stream<T> empty() {
return StreamSupport.stream(Spliterators.<T>emptySpliterator(), false);
}
我们可以使用Stream类的empty()方法来创建一个空Stream流,如下所示。
Stream<String> empty = Stream.empty();
多个中间操作可以连接起来形成一个流水线,除非流水线上触发终止操作,否则中间操作不会执行任何的处理!而在终止操作时一次性全部处理,称为“惰性求值” 。 Stream的中间操作是不会有任何结果数据输出的。
Stream的中间操作在整体上可以分为:筛选与切片、映射、排序。接下来,我们就分别对这些中间操作进行简要的说明。
方法 | 描述 |
---|---|
filter(Predicate p) | 接收Lambda表达式,从流中排除某些元素 |
distinct() | 筛选,通过流所生成元素的 hashCode() 和 equals() 去 除重复元素 |
limit(long maxSize) | 截断流,使其元素不超过给定数量 |
skip(long n) | 跳过元素,返回一个扔掉了前 n 个元素的流。若流中元素 不足 n 个,则返回一个空流。与 limit(n) 互补 |
列举几个简单的示例,以便加深理解。
先构造了一个对象数组,如下所示。
protected List<Employee> list = Arrays.asList(
new Employee("张三", 18, 9999.99),
new Employee("李四", 38, 5555.55),
new Employee("王五", 60, 6666.66),
new Employee("赵六", 8, 7777.77),
new Employee("田七", 58, 3333.33)
);
其中,Employee类的定义如下所示。
@Data
@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class Employee implements Serializable {
private static final long serialVersionUID = -9079722457749166858L;
private String name;
private Integer age;
private Double salary;
}
filter()方法主要是用于接收Lambda表达式,从流中排除某些元素,其在Stream接口中的源码如下所示。
Stream<T> filter(Predicate<? super T> predicate);
可以看到,在filter()方法中,需要传递Predicate接口的对象,Predicate接口又是个什么呢?点进去看下源码。
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
default Predicate<T> negate() {
return (t) -> !test(t);
}
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}
static <T> Predicate<T> isEqual(Object targetRef) {
return (null == targetRef)
? Objects::isNull
: object -> targetRef.equals(object);
}
}
可以看到,Predicate是一个函数式接口,其中接口中定义的主要方法为test()方法,test()方法会接收一个泛型对象t,返回一个boolean类型的数据。
所以:filter()方法是根据Predicate接口的test()方法的返回结果来过滤数据的,如果test()方法的返回结果为true,符合规则;如果test()方法的返回结果为false,则不符合规则。
这里,我们可以使用下面的示例来简单的说明filter()方法的使用方式。
//内部迭代:在此过程中没有进行过迭代,由Stream api进行迭代
//中间操作:不会执行任何操作
Stream<Person> stream = list.stream().filter((e) -> {
System.out.println("Stream API 中间操作");
return e.getAge() > 30;
});
上述代码在执行终止语句之后,会一边迭代,一边打印,而我们并没有去迭代上面集合,其实这是内部迭代,由Stream API 完成。
下面我们来看看外部迭代,也就是我们人为得迭代。
//外部迭代
Iterator<Person> it = list.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
主要作用为:截断流,使其元素不超过给定数量。
先来看limit方法的定义,如下所示。
Stream<T> limit(long maxSize);
limit()方法在Stream接口中的定义比较简单,只需要传入一个long类型的数字即可。
我们可以按照如下所示的代码来使用limit()方法。
//过滤之后取2个值
list.stream().filter((e) -> e.getAge() >30 ).limit(2).forEach(System.out :: println);
在这里,我们可以配合其他得中间操作,并截断流,使我们可以取得相应个数得元素。而且在上面计算中,只要发现有2条符合条件得元素,则不会继续往下迭代数据,可以提高效率。
跳过元素,返回一个扔掉了前 n 个元素的流。若流中元素 不足 n 个,则返回一个空流。与 limit(n) 互补。
源码定义如下所示。
Stream<T> skip(long n);
源码定义比较简单,同样只需要传入一个long类型的数字即可。其含义是跳过n个元素。
简单示例如下所示。
//跳过前2个值
list.stream().skip(2).forEach(System.out :: println);
筛选,通过流所生成元素的 hashCode() 和 equals() 去 除重复元素。
源码定义如下所示。
Stream<T> distinct();
旨在对流中的元素进行去重。
我们可以如下面的方式来使用disinct()方法。
list.stream().distinct().forEach(System.out :: println);
这里有一个需要注意的地方:distinct 需要实体中重写hashCode()和 equals()方法才可以使用。
关于映射相关的方法如下表所示。
方法 | 描述 |
---|---|
map(Function f) | 接收一个函数作为参数,该函数会被应用到每个元 素上,并将其映射成一个新的元素。 |
mapToDouble(ToDoubleFunction f) | 接收一个函数作为参数,该函数会被应用到每个元 素上,产生一个新的 DoubleStream。 |
mapToInt(ToIntFunction f) | 接收一个函数作为参数,该函数会被应用到每个元 素上,产生一个新的 IntStream。 |
mapToLong(ToLongFunction f) | 接收一个函数作为参数,该函数会被应用到每个元 素上,产生一个新的 LongStream |
flatMap(Function f) | 接收一个函数作为参数,将流中的每个值都换成另 一个流,然后把所有流连接成一个流 |
接收一个函数作为参数,该函数会被应用到每个元 素上,并将其映射成一个新的元素。
先来看Java8中Stream接口对于map()方法的声明,如下所示。
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
我们可以按照如下方式使用map()方法。
//将流中每一个元素都映射到map的函数中,每个元素执行这个函数,再返回
List<String> list = Arrays.asList("aaa", "bbb", "ccc", "ddd");
list.stream().map((e) -> e.toUpperCase()).forEach(System.out::printf);
//获取Person中的每一个人得名字name,再返回一个集合
List<String> names = this.list.stream().map(Person :: getName).collect(Collectors.toList());
接收一个函数作为参数,将流中的每个值都换成另 一个流,然后把所有流连接成一个流。
先来看Java8中Stream接口对于flatMap()方法的声明,如下所示。
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);
测试flatMap()方法的所有代码:
/**
* flatMap —— 接收一个函数作为参数,将流中的每个值都换成一个流,然后把所有流连接成一个流
*/
@Test
public void testFlatMap () {
StreamAPI_Test s = new StreamAPI_Test();
List<String> list = Arrays.asList("aaa", "bbb", "ccc", "ddd");
list.stream().flatMap((e) -> s.filterCharacter(e)).forEach(System.out::println);
//如果使用map则需要这样写
list.stream().map((e) -> s.filterCharacter(e)).forEach((e) -> {
e.forEach(System.out::println);
});
//打印aaabbbcccddd
}
/**
* 将一个字符串转换为流
*/
public Stream<Character> filterCharacter(String str){
List<Character> list = new ArrayList<>();
for (Character ch : str.toCharArray()) {
list.add(ch);
}
return list.stream();
}
其实map方法就相当于Collaction的add方法,如果add的是个集合得话就会变成二维数组,而flatMap 的话就相当于Collaction的addAll方法,参数如果是集合得话,只是将2个集合合并,而不是变成二维数组。
关于排序相关的方法如下表所示。
方法 | 描述 |
---|---|
sorted() | 产生一个新流,其中按自然顺序排序 |
sorted(Comparator comp) | 产生一个新流,其中按比较器顺序排序 |
从上述表格可以看出:sorted有两种方法,一种是不传任何参数,叫自然排序,还有一种需要传Comparator 接口参数,叫做定制排序。
先来看Java8中Stream接口对于sorted()方法的声明,如下所示。
Stream<T> sorted();
Stream<T> sorted(Comparator<? super T> comparator);
我们也可以按照如下方式来使用Stream的sorted()方法。
// 自然排序
List<Employee> persons = list.stream().sorted().collect(Collectors.toList());
//定制排序
List<Employee> persons1 = list.stream().sorted((e1, e2) -> {
if (e1.getAge() == e2.getAge()) {
return 0;
} else if (e1.getAge() > e2.getAge()) {
return 1;
} else {
return -1;
}
}).collect(Collectors.toList());
终止操作会从流的流水线生成结果。其结果可以是任何不是流的值,例如: List、 Integer、Double、String等等,甚至是 void 。
在Java8中,Stream的终止操作可以分为:查找与匹配、规约和收集。
Stream API中有关查找与匹配的方法如下表所示。
方法 | 描述 |
---|---|
allMatch(Predicate p) | 检查是否匹配所有元素 |
anyMatch(Predicate p) | 检查是否至少匹配一个元素 |
noneMatch(Predicate p) | 检查是否没有匹配所有元素 |
findFirst() | 返回第一个元素 |
findAny() | 返回当前流中的任意元素 |
count() | 返回流中元素总数 |
max(Comparator c) | 返回流中最大值 |
min(Comparator c) | 返回流中最小值 |
forEach(Consumer c) | 内部迭代(使用 Collection 接口需要用户去做迭代,称为外部迭代。相反, Stream API 使用内部迭代) |
同样的,这里,先建立一个Employee类,Employee类的定义如下所示。
@Data
@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class Employee implements Serializable {
private static final long serialVersionUID = -9079722457749166858L;
private String name;
private Integer age;
private Double salary;
private Stauts stauts;
public enum Stauts{
WORKING,
SLEEPING,
VOCATION
}
}
接下来,测试类中定义一个用于测试的集合employees,如下所示。
protected List<Employee> employees = Arrays.asList(
new Employee("张三", 18, 9999.99, Employee.Stauts.SLEEPING),
new Employee("李四", 38, 5555.55, Employee.Stauts.WORKING),
new Employee("王五", 60, 6666.66, Employee.Stauts.WORKING),
new Employee("赵六", 8, 7777.77, Employee.Stauts.SLEEPING),
new Employee("田七", 58, 3333.33, Employee.Stauts.VOCATION)
);
allMatch()方法表示检查是否匹配所有元素。其在Stream接口中的定义如下所示。
boolean allMatch(Predicate<? super T> predicate);
可以通过类似如下示例来使用allMatch()方法。
boolean match = employees.stream().allMatch((e) -> Employee.Stauts.SLEEPING.equals(e.getStauts()));
System.out.println(match);
注意:使用allMatch()方法时,只有所有的元素都匹配条件时,allMatch()方法才会返回true。
anyMatch方法表示检查是否至少匹配一个元素。其在Stream接口中的定义如下所示。
boolean anyMatch(Predicate<? super T> predicate);
通过类似如下示例来使用anyMatch()方法。
boolean match = employees.stream().anyMatch((e) -> Employee.Stauts.SLEEPING.equals(e.getStauts()));
System.out.println(match);
注意:使用anyMatch()方法时,只要有任意一个元素符合条件,anyMatch()方法就会返回true。
noneMatch()方法表示检查是否没有匹配所有元素。其在Stream接口中的定义如下所示。
boolean noneMatch(Predicate<? super T> predicate);
通过类似如下示例来使用noneMatch()方法。
boolean match = employees.stream().noneMatch((e) -> Employee.Stauts.SLEEPING.equals(e.getStauts()));
System.out.println(match);
注意:使用noneMatch()方法时,只有所有的元素都不符合条件时,noneMatch()方法才会返回true。
findFirst()方法表示返回第一个元素。其在Stream接口中的定义如下所示。
Optional<T> findFirst();
通过类似如下示例来使用findFirst()方法。
Optional<Employee> op = employees.stream().sorted((e1, e2) -> Double.compare(e1.getSalary(), e2.getSalary())).findFirst();
System.out.println(op.get());
findAny()方法表示返回当前流中的任意元素。其在Stream接口中的定义如下所示。
findAny()方法的行为是不确定的,它可以自由选择流中的任何元素。
findAny()方法有助于在并行操作中获得最大的性能,但它不能保证每次调用都得到相同的结果。
Optional<T> findAny();
通过类似如下示例来使用findAny()方法。
Optional<Employee> op = employees.stream().filter((e) -> Employee.Stauts.WORKING.equals(e.getStauts())).findFirst();
System.out.println(op.get());
注意:
假设我们有一个整数流。
Stream.of(10, 20, 30).findAny().ifPresent(s -> System.out.println(s));
此执行在一般情况下,并不会随机输出,而是按顺序输出,因为该流是有顺序的串行流,按照最优执行会按照顺序。
如果我们想要看到随机选择,需要改成并行流。
Stream.of(10, 20, 30).parallel().findAny().ifPresent(s -> System.out.println(s));
再次调用findAny方法时,可以自由选择流中的任何元素,这意味着findAny可以给出10、20或30的输出。
注: findAny不是为了随机而随机,而是为了进行最快速的选择,所以最好不要用在随机选择的场景。
count()方法表示返回流中元素总数。其在Stream接口中的定义如下所示。
long count();
通过类似如下示例来使用count()方法。
long count = employees.stream().count();
System.out.println(count);
max()方法表示返回流中最大值。其在Stream接口中的定义如下所示。
Optional<T> max(Comparator<? super T> comparator);
通过类似如下示例来使用max()方法。
Optional<Employee> op = employees.stream().max((e1, e2) -> Double.compare(e1.getSalary(), e2.getSalary()));
System.out.println(op.get());
min()方法表示返回流中最小值。其在Stream接口中的定义如下所示。
Optional<T> min(Comparator<? super T> comparator);
通过类似如下示例来使用min()方法。
Optional<Double> op = employees.stream().map(Employee::getSalary).min(Double::compare);
System.out.println(op.get());
forEach()方法表示内部迭代(使用 Collection 接口需要用户去做迭代,称为外部迭代。相反, Stream API 使用内部迭代)。其在Stream接口内部的定义如下所示。
void forEach(Consumer<? super T> action);
通过类似如下示例来使用forEach()方法。
employees.stream().forEach(System.out::println);
Stream API中有关规约的方法如下表所示。
方法 | 描述 |
---|---|
reduce(T iden, BinaryOperator b) | 可以将流中元素反复结合起来,得到一个值。 返回 T |
reduce(BinaryOperator b) | 可以将流中元素反复结合起来,得到一个值。 返回 Optional |
在Stream接口中的定义如下所示。
T reduce(T identity, BinaryOperator<T> accumulator);
Optional<T> reduce(BinaryOperator<T> accumulator);
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);
通过类似如下示例来使用reduce方法。
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
Integer sum = list.stream().reduce(0, (x, y) -> x + y);
System.out.println(sum);
System.out.println("----------------------------------------");
Optional<Double> op = employees.stream().map(Employee::getSalary).reduce(Double::sum);
System.out.println(op.get());
我们也可以搜索employees列表中“张”出现的次数。
Optional<Integer> sum = employees.stream()
.map(Employee::getName)
.flatMap(TestStreamAPI1::filterCharacter)
.map((ch) -> {
if(ch.equals('张'))
return 1;
else
return 0;
}).reduce(Integer::sum);
System.out.println(sum.get());
注意:上述例子使用了硬编码的方式来累加某个具体值,在实际工作中再优化代码。
方法 | 描述 |
---|---|
collect(Collector c) | 将流转换为其他形式。接收一个 Collector接口的实现,用于给Stream中元素做汇总的方法 |
collect()方法在Stream接口中的定义如下所示。
<R> R collect(Supplier<R> supplier,
BiConsumer<R, ? super T> accumulator,
BiConsumer<R, R> combiner);
<R, A> R collect(Collector<? super T, A, R> collector);
我们可以通过类似如下示例来使用collect方法。
Optional<Double> max = employees.stream()
.map(Employee::getSalary)
.collect(Collectors.maxBy(Double::compare));
System.out.println(max.get());
Optional<Employee> op = employees.stream()
.collect(Collectors.minBy((e1, e2) -> Double.compare(e1.getSalary(), e2.getSalary())));
System.out.println(op.get());
Double sum = employees.stream().collect(Collectors.summingDouble(Employee::getSalary));
System.out.println(sum);
Double avg = employees.stream().collect(Collectors.averagingDouble(Employee::getSalary));
System.out.println(avg);
Long count = employees.stream().collect(Collectors.counting());
System.out.println(count);
System.out.println("--------------------------------------------");
DoubleSummaryStatistics dss = employees.stream()
.collect(Collectors.summarizingDouble(Employee::getSalary));
System.out.println(dss.getMax());
Collector接口中方法的实现决定了如何对流执行收集操作(如收集到 List、 Set、 Map)。 Collectors实用类提供了很多静态方法,可以方便地创建常见收集器实例, 具体方法与实例如下表:
方法 | 返回类型 | 作用 |
---|---|---|
toList | List | 把流中元素收集到List |
toSet | Set | 把流中元素收集到Set |
toCollection | Collection | 把流中元素收集到创建的集合 |
counting | Long | 计算流中元素的个数 |
summingInt | Integer | 对流中元素的整数属性求和 |
averagingInt | Double | 计算流中元素Integer属性的平均 值 |
summarizingInt | IntSummaryStatistics | 收集流中Integer属性的统计值。 如:平均值 |
joining | String | 连接流中每个字符串 |
maxBy | Optional | 根据比较器选择最大值 |
minBy | Optional | 根据比较器选择最小值 |
reducing | 归约产生的类型 | 从一个作为累加器的初始值开始,利用BinaryOperator与 流中元素逐个结合,从而归约成单个值 |
collectingAndThen | 转换函数返回的类型 | 包裹另一个收集器,对其结 果转换函数 |
groupingBy | Map | 根据某属性值对流分组,属 性为K,结果为V |
partitioningBy | Map | 根据true或false进行分区 |
每个方法对应的使用示例如下表所示。
方法 | 使用示例 |
---|---|
toList | List employees= list.stream().collect(Collectors.toList()); |
toSet | Set employees= list.stream().collect(Collectors.toSet()); |
toCollection | Collection employees=list.stream().collect(Collectors.toCollection(ArrayList::new)); |
counting | long count = list.stream().collect(Collectors.counting()); |
summingInt | int total=list.stream().collect(Collectors.summingInt(Employee::getSalary)); |
averagingInt | double avg= list.stream().collect(Collectors.averagingInt(Employee::getSalary)) |
summarizingInt | IntSummaryStatistics iss= list.stream().collect(Collectors.summarizingInt(Employee::getSalary)); |
Collectors | String str= list.stream().map(Employee::getName).collect(Collectors.joining()); |
maxBy | Optionalmax= list.stream().collect(Collectors.maxBy(comparingInt(Employee::getSalary))); |
minBy | Optional min = list.stream().collect(Collectors.minBy(comparingInt(Employee::getSalary))); |
reducing | int total=list.stream().collect(Collectors.reducing(0, Employee::getSalar, Integer::sum)); |
collectingAndThen | int how= list.stream().collect(Collectors.collectingAndThen(Collectors.toList(), List::size)); |
groupingBy | Map map= list.stream() .collect(Collectors.groupingBy(Employee::getStatus)); |
partitioningBy | Mapvd= list.stream().collect(Collectors.partitioningBy(Employee::getManage)); |
public void test4(){
Optional<Double> max = emps.stream()
.map(Employee::getSalary)
.collect(Collectors.maxBy(Double::compare));
System.out.println(max.get());
Optional<Employee> op = emps.stream()
.collect(Collectors.minBy((e1, e2) -> Double.compare(e1.getSalary(), e2.getSalary())));
System.out.println(op.get());
Double sum = emps.stream()
.collect(Collectors.summingDouble(Employee::getSalary));
System.out.println(sum);
Double avg = emps.stream()
.collect(Collecors.averagingDouble(Employee::getSalary));
System.out.println(avg);
Long count = emps.stream()
.collect(Collectors.counting());
DoubleSummaryStatistics dss = emps.stream()
.collect(Collectors.summarizingDouble(Employee::getSalary));
System.out.println(dss.getMax());
这两种异常都应该去捕获
wget https://github.com/laruence/yac.git
#或者
git clone https://github.com/laruence/yac.git
cd yac
phpize
./configure --with-php-config=/opt/homebrew/opt/php@7.1/bin/php-config
make
make install
/opt/homebrew/opt/php@7.1
是要替换成你自己机器上PHP的安装目录make install
之后会返回yac.so所在的目录,同一台机器的php扩展一般都会在这个目录下extension=yac.so
如果你是第一次安装PHP的扩展,那可能改需要修改extension_dir
的指向目录,这个目录也就是上面make install
之后会返回的目录,不然会报找不到yac.so文件,例如:
extension_dir = "/opt/homebrew/lib/php/pecl/20160303"
可以通过php -i | grep ini
找到php.ini的路径
只添加扩展还是无法使用Yac的,还需要增加一些配置
还是在php.ini文件中增加yac的一些配置,可以根据自己的需要调整参数
[yac]
yac.enable = 1
yac.enable_cli = 1
yac.keys_memory_size=4M
yac.values_memory_size = 64M
service php-fpm reload
php -m
查看到yac,说明扩展安装成功,可以在代码中使用了。
我们本地使用wamp Server配置虚拟主机的时候,在访问URL的时候总是在domain后面带上入口文件index.php。这导致url多上几个字符串观看上很不舒服,而且拼写url的时候需要多敲几个。
在项目中入口文件的同级目录下建一个.htaccess,文件内容如下:
<IfModule mod_rewrite.c>
Options +FollowSymLinks
RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
# RewriteRule ^(.*)$ index-dev.php/$1 [QSA,PT,L]
RewriteRule ^(.*)$ index-dev.php [L,E=PATH_INFO:$1]
</IfModule>
<IfModule mod_deflate.c>
AddOutputFilter DEFLATE html xml php js css text/html text/plain
</IfModule>
在httpd-vhost.conf文件下配置虚拟主机的时候添加重写规则:
<VirtualHost *:80>
#开启重写
RewriteEngine on
#哪些文件路径不定义重写,css和js等文件是放在public路径下,所以在视图文件中以/public开头的url不重写路径(注意'/'需要使用'\’来进行转义)
RewriteCond $1 !^(index\.php|\/public)
#重写规则:可以不需要输入index.php来进行访问
RewriteRule ^(.*)$ /index.php/$1 [QSA,PT,L]
ServerName dev.api.com
ServerAlias dev.api.com
DocumentRoot "D:/projects/api"
<Directory "D:/projects/api/">
DirectoryIndex index.php index.html
Options +Indexes +Includes +FollowSymLinks +MultiViews
AllowOverride All
Require all granted
</Directory>
</VirtualHost>
打开主库的配置文件my.ini(不同于linux环境下的my.cnf),该文件在在mysql的安装目录下。我的是C:\*****\bin\mysql\mysql5.7.26\my.ini
。在my.ini
的最下面可以看到[mysqld]
,在它下面添加主库的配置
所以
[mysqld]
default_authentication_plugin=mysql_native_password
port = 3306
#以下是新增配置
server-id=1 #配置唯一server-id,主库和从库不能重复
log-bin=mysql-bin #开启二进制日志
binlog-do-db=test #同步的数据库
binlog-ignore-db=mysql #不需要同步的数据库
重启主库mysql
查看主库的server-id以及其他配置信息
打开Navicat Premium连接到主库,切换到命令行模式(快捷键:F6),分别执行下面的sql语句:
show variables like 'server_id';
show master status;
为从库创建用于同步的帐号(一般不会使用root帐号)
我是直接使用Navicat Premium来创建帐号
用户名:slave
密码:123456
并且在服务器权限里选中全部的权限
打开从库的配置文件my.ini
,同样在my.ini
的[mysqld]
下面添加主库的配置
[mysqld]
default_authentication_plugin=mysql_native_password
port = 3306
#下面是新加配置
server-id=2 #和主库不一致
#master-host=10.1.100.93 #主库ip
#master-user=slave #主库用户名
#master-password=123456 #主库密码
#master-port=3306 #主库端口号
#master-connect-retry=60
replicate-do-db=test #需要同步的表
replicate-ignore-db=mysql #不需要同步的表
重启从库MySQL
注意: 可以看到配置中我注释了master-*
这部分的配置,是因为MySQL版本从5.1.7以后开始就不支持“master-*”类似的参数。
如果不注释这块配置则会导致从库MySQL在重启的时候报错:
unknown variable 'master-host=10.1.100.93'
该部分配置已改为执行命令的方式进行配置:
change master to master_host='110.1.100.93',
master_port=3306,master_user='slave',
master_password='123456',
master_log_file=’mysql-bin.000001’,
master_log_pos=1437;
其中master_log_file
和master_log_pos
的配置值是在主库中执行下面命令后可以查看:
show master status;
启动
执行命令启动从库同步:
start slave;
查看是否成功
在从库命令行执行(Navicat Premium命令行下执行该命令会报错):
show slave status\G
可以看到主从已经成功打通
在主库的test
库中建表user
CREATE TABLE `user`(
`id` int(11) NOT NULL AUTO_INCREMENT ,
`name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户名' ,
`age` int(3) UNSIGNED NOT NULL COMMENT '年龄' ,
PRIMARY KEY (`id`)
);
查看从库
可以发现从库在没有执行上面的sql语句的情况下,也生成了user
表:
同样:在主库中添加数据,也会自动同步到从库