2023年4月

begin 2023年04月02日15:56:19

引子

悲观者往往正确,乐观者往往成功

定义

Represent an operation to be performed on the elements of an object structure.
Visitor lets you define a new operation without changing the classes of the elements
on which it operates.

表示在对象结构的元素上执行的操作。访问者模式(Visitor)允许您定义一种新操作,而无需更改它所作用的元素的类。——《设计模式:可复用面向对象软件的基础》

访问者模式是一种行为型设计模式。

使用场景

  • an object structure contains many classes of objects with differing
    interfaces, and you want to perform operations on these objects that depend
    on their concrete classes.
  • many distinct and unrelated operations need to be performed on objects in
    an object structure, and you want to avoid "polluting" their classes with
    these operations. Visitor lets you keep related operations together by
    defining them in one class. When the object structure is shared by many
    applications, use Visitor to put operations in just those applications that
    need them.
  • the classes defining the object structure rarely change, but you often want
    to define new operations over the structure. Changing the object structure
    classes requires redefining the interface to all visitors,which is
    potentially costly. If the object structure classes change often, then it's
    probably better to define the operations in those classes.
  • 对象机构包含了许多具有不同接口的对象类,您希望对这些对象执行依赖于其具体类的操作。
  • 需要对对象结构中的对象执行许多不同且不相关的操作,并且您希望避免这些操作“污染”它们的类。Visitor允许将相关操作定义在一个类中,从而将它们放在一起。当对象结构被许多应用程序共享时,使用Visitor将操作放在需要它们的应用程序中。
  • 定义对象结构的类很少改变,但是您经常希望在类上定义新的操作。更改对象结构类需要为所有访问者重新定义接口,这可能成本很高。如果对象结构类经常变化,那么最好在这些类中定义操作。

图示

访问者模式结构图:

访问者模式结构图

角色

抽象访问者角色(Visitor):

  • 为对象结构中的每个 ConcreteElement 类声明一个 visit 操作。
  • 操作的名称和签名(visitConcreteElementA、visitConcreteElementB)标识向访问者(ConcreteVisitorA、ConcreteVisitor)发送 visit 请求的类,让访问者可以确定所访问元素的具体类(ConcreteElementA、ConcreteElementB)。
  • 然后访问者可以通过元素特定的接口直接访问该元素。

具体访问者角色(ConcreteVisitorA、ConcreteVisitorB):

  • 实现访问者声明的每个操作。
  • 每个操作实现为对象结构中对应的对象类定义的算法片段(针对具体元素所执行的操作,如获取ConcreteElementA的名字等)
  • ConcreteVisitor 为算法提供了上下文,并存储其本地状态。该状态通常在遍历对象结构时积累结果。

对象结构角色(ObjectStructure):

  • 可以枚举其元素。
  • 可以提供一个高层接口,以允许访问者访问其元素。
  • 可以是一个组合(参见组合模式)或者一个集合,如列表或集合

抽象元素角色(Element):

  • 定义一个接受访问者作为参数的 Accept 操作。

具体元素角色(ConcreteElementA、ConcreteElementB):

  • 实现一个接受访问者作为参数的 Accept 操作。

代码示例

生命主要分为过去、现在、未来,乐观者看到不念过去,不畏将来,立足现在努力,悲观者看到悔恨过去,迷茫未来,或不知所措的现在,同样的生命不同的人看到不同的世界。

代码示例类图:
代码示例类图

代码示例:

抽象访问者角色:

// 人类
public interface Man {
    void visitPast(Past past);
    void visitPresent(Present present);
    void visitFuture(Future future);
}

具体访问者角色:

// 乐观主义者
public class Optimist implements Man {
    @Override
    public void visitPast(Past past) {
        System.out.println("不念" + past.getName());
    }

    @Override
    public void visitPresent(Present present) {
        System.out.println("享受" + present.getName());
    }

    @Override
    public void visitFuture(Future future) {
        System.out.println("不畏" + future.getName());
    }
}
// 悲观主义者
public class Pessimist implements Man {
    @Override
    public void visitPast(Past past) {
        System.out.println("悔恨" + past.getName());
    }

    @Override
    public void visitPresent(Present present) {
        System.out.println("焦虑" + present.getName());
    }

    @Override
    public void visitFuture(Future future) {
        System.out.println("迷茫" + future.getName());
    }
}

对象结构角色:

// 生命
public class Life {
    private List<Time> timeList = new ArrayList<>();

    public Life() {
        timeList.add(new Past());
        timeList.add(new Present());
        timeList.add(new Future());
    }

    public void visitTime(Man man) {
        for (Time time : timeList) {
            if (time instanceof Past) {
                man.visitPast((Past) time);
            } else if (time instanceof Present) {
                man.visitPresent((Present) time);
            } else if (time instanceof Future) {
                man.visitFuture((Future) time);
            }
        }
    }
}

抽象元素角色:

// 时间
public interface Time {
    void accept(Man man);
    String getName();
}

具体元素角色:

// 过去
public class Past implements Time {

    private String name = "过去";

    @Override
    public void accept(Man man) {
        man.visitPast(this);
    }

    public String getName() {
        return name;
    }
}
// 现在
public class Present implements Time {

    private String name = "现在";

    @Override
    public void accept(Man man) {
        man.visitPresent(this);
    }
    public String getName() {
        return name;
    }
}
// 未来
public class Future implements Time {

    private String name = "未来";

    @Override
    public void accept(Man man) {
        man.visitFuture(this);
    }

    public String getName() {
        return name;
    }
}

客户端:


public class Client {
    public static void main(String[] args) {
        Optimist optimist = new Optimist();
        Pessimist pessimist = new Pessimist();

        Life life = new Life();
        System.out.println("乐观者:");
        life.visitTime(optimist);
        System.out.println("悲观者:");
        life.visitTime(pessimist);
    }
}

结果:

乐观者:
不念过去
享受现在
不畏未来
悲观者:
悔恨过去
焦虑现在
迷茫未来

优点

  • 操作和结构分开,易于新增操作

缺点

  • 修改结构需要修改所有操作类,成本极大

总结

访问者莫斯允许您定义一种新操作,而无需更改它所作用的元素的类。访问者模式适用于结构固定,但是操作不固定的对象,它把对象结构和作用于结构的操作耦合解开,方便增加新的操作。

end 2023年04月05日15:22:07

Redis系列1:深刻理解高性能Redis的本质
Redis系列2:数据持久化提高可用性
Redis系列3:高可用之主从架构
Redis系列4:高可用之Sentinel(哨兵模式)
Redis系列5:深入分析Cluster 集群模式
追求性能极致:Redis6.0的多线程模型
追求性能极致:客户端缓存带来的革命
Redis系列8:Bitmap实现亿万级数据计算
Redis系列9:Geo 类型赋能亿级地图位置计算
Redis系列10:HyperLogLog实现海量数据基数统计
Redis系列11:内存淘汰策略

1 复习下何为事务机制?

Transaction(事务)是计算机的特有术语,它一般指单个逻辑工作单位,由一系列的操作组合而成,在这些操作执行的时候,要么都执行成功,要么都不执行,防止数据结果的不一致性。
简而言之,事务是一个不可分割的工作逻辑单位。为了衡量工作单元是否具备事务能力,需要满足四个特征:ACID,即 原子性(Atomicity,或称不可分割性)、一致性(Consistency)、隔离性(Isolation,又称独立性)、持久性(Durability)。

  • 原子性(Atomicity):一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
  • 一致性(Consistency):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的数据必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。
    • 实体完整性,存在唯一的主键
    • 列完整性:字段类型、字段长度等符合所有的预设规则
    • foreign key 外键约束
    • 用户自定义完整性(如用户购物支付前后,商家收入和用户的余额总和不变)
  • 隔离性(Isolation):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。
  • 持久性(Durability):事务处理结束后,对数据的修改就是永久的,会持久化到硬盘上,即便系统故障也不会丢失。

2 Redis模式下如何实现事务机制?

Redis 支持事务机制,他实现事务的关键命令包括:

MULTI、EXEC、DISCARD 、 WATCH
  • MULTI 开启事务,总是返回OK
  • EXEC 提交事务
  • DISCARD 放弃事务(即放弃提交执行)
  • WATCH 监控
  • QUEUED 命令加入执行的队列,没操作一个动作的时候,都先加入Queue

根据上述命令,Redis 事务的执行过程包含三个步骤:

  • 开启事务:MULTI
  • 命令入队:QUEUE
  • 执行事务或丢弃:EXEC 或者 DISCARD

2.1 显式开启一个事务

Client 通过 MULTI 命令显式开启一个事务,随后执行的操作将会暂时缓存在Queue中,实际并没有立即执行。

2.2 将命令入队列Queue

Client 端 把事务中的要执行的一系列操作指令发送到Service 端。 Redis服务端 实例接收到指令之后,并不是马上执行,而是暂存在命令队列中。

2.3 执行事务或丢弃

当Client端向Service端发送的命令都Ready了之后,可以发送提交执行或者丢弃事务的命令,如果是执行则操作队列中的具体指令,如果是丢弃则是清空队列命令。

  • EXEC:执行队列中的指令
  • DISCARD:丢弃保存在队列中的命令

2.4 EXEC命令执行示例

通过 MULTI 和 EXEC 执行一个事务过程:

#开启事务
> MULTI
OK
# 定义一系列指令
> set 'name' 'brand'
QUEUED
> set 'age' 18
QUEUED
> INCR 'age'
QUEUED
> GET 'name'
QUEUED
> GET 'age'
QUEUED
# 实际执行事务
> EXEC
# 获取执行结果
1) OK
2) OK
3) 19
4) "brand"
5) "19"

从上面可以看出来,每个读写指令执行后的返回结果都是 QUEUED,代表这些操作只是暂存在指令队列中,并没有实际执行。
当发送了 EXEC 命令之后,才真正执行并获取结果。

2.5 DISCARD命令:放弃事务

通过 MULTI 和 DISCARD 丢弃执行,清空指令队列:

# 初始化订数据
> SET 'name' 'brand'
OK
> SET 'age' 18
OK
# 开启事务
> MULTI
OK
# 数据增量1
> INCR 'age'
QUEUED
# 丢弃
> DISCARD
OK
# 执行结果是增量前的数据
> get 'age'
"18"

2.6 因为命令错误导致的事务回滚

体现原子性,再发生故障的时候,要么执行都成功,要么执行都失败

# 开启事务
> MULTI
OK
# 初始一个数据
> SET 'age' 18
OK
# 对该数据进行更新,但Redis不支持该命令,返回报错信息
> UPD 'age' 17
(error) ERR unknown command `UPD`, with args beginning with: `age`, `17`,
# 继续发送一个指令 ,降低age的值,该指令是正确的
> DECR 'age'
QUEUED
# 执行exec,但是之前有错误,所以Redis放弃了事务,不再执行
> EXEC
(error) EXECABORT Transaction discarded because of previous errors.

3 Redis事务机制能实现哪些属性?

类似MySQL的事务,Redis 事务一次性可以执行多个指令, 而这多个指令通过以下的方式来保证:

  • EXEC 命令执行之前,所有的指令都是被暂存(Queued)在队列中;
  • Service端接收到EXEC命令后开始执行事务,事务中某些命令执行失败,其余命令依旧执行;
  • 在事务执行的时候具备隔离性,其他Client端执行的指令不会乱入到当前指令的执行顺序中的。

3.1 原子性(Atomicity)

在事务执行的过程中,可能遇到这几种命令执行错误:

  • 在执行 EXEC 命令前,指令本身错误:
    • 参数数量不一致构成的错误
    • 命令名称构成的错误,使用了不存在或者错误的命令:比如上面的 'UPD'
    • 超过MaxMemory内存限制,导致内存不足
  • 在执行 EXEC 命令后,命令的不合理操作导致的失败。比如数据类型不匹配(对 String 类型 的 value 执行了 INCR 或者 DECR 之类的操作)
  • 在执行事务的 EXEC 命令时,实例故障导致的失败,这种情况比较少一点。

3.1.1 EXEC 执行前报错

执行前错误是指命令入队(Queue)时,Redis 就会发现并记录报错。
即使执行了 EXEC命令之后,Redis也会拒绝执行指令队列中的所有指令,返回事务失败的结果。
这样一来,所有的指令都不会被执行,保持了原子性。下面是指令入队列的报错的实例,跟上面的举例一致:

# 开启事务
> MULTI
OK
# 初始一个数据
> SET 'age' 18
OK
# 对该数据进行更新,但Redis不支持该命令,返回报错信息
> UPD 'age' 17
(error) ERR unknown command `UPD`, with args beginning with: `age`, `17`,
# 继续发送一个指令 ,降低age的值,该指令是正确的
> DECR 'age'
QUEUED
# 执行exec,但是之前有错误,所以Redis放弃了事务,不再执行
> EXEC
(error) EXECABORT Transaction discarded because of previous errors.

3.1.2 EXEC 执行后报错

这个跟上面的情况正好相反,指令入Queue时,命令的类型虽然不匹配,但是并没有在预编译的时候检查出。
只有在EXEC 命令之后,实际执行指令的时候才会报错。其他正确的指令还是会执行成功,不保证原子性。 参考下面:

# 开启事务
> MULTI
OK
> set age 18
QUEUED
> set name 'brand'
QUEUED
> INCR age
QUEUED
# 这边对String类型进行DECR,没有报错,但是在执行指令的时候会报错误
> DECR name
QUEUED
# 执行,会发现其他三条执行执行成功,只有一条执行失败,返回报错信息
> EXEC
1) OK
2) OK
3) 19
4) ERR value is not an integer or out of range
# 查看结果
> get name
"brand"
> get age
"19"

3.1.3 在EXEC执行时发生实例故障

可以使用AOF日志,把未完成的事务操作从AOF日志中去除,之后使用AOF进行恢复时就不会被再次执行,以此保证整个操作的原子性。
这个需要Redis启用AOF日志这个持久化能力。

3.1.4 对于上述几种错误特征的总结

  • 指令入队列时有报错(所有指令中只要有一条不是QUEUED),就会放弃事务执行,保证原子性。如 3.1.1
  • 指令入队列时没报错(所有指令都是QUEUED),但在实际执行EXEC时报错,则不保证原子性。如 3.1.2
  • EXEC执行时出现故障,如果开启了 AOF 日志,可以保证原子性。如 3.1.3

3.2 一致性

跟原子性类似,一致性会受到错误指令、执行异常、Redis故障等情况的影响,主要有如下几种情况:

  • 指令入队列时有报错,事务被放弃执行,所以可以保证一致性。
  • 指令入队列时正产,实际执行EXEC时报错,则是错误部分不会执行,正确指令依旧正常执行,也可以保证一致性。
  • Redis实例故障分成几种:
    • 未开启持久化情况,故障重启后数据都清空,结果是一致的。
    • RDB快照:事务命令操作的结果不被保存到 RDB 快照中,所以在恢复时,数据结果是一致的。
    • AOF 日志:发生故障时,使用 redis-check-aof 清除事务中对应操作,数据库恢复后也保持一致。

3.3 隔离性

从隔离性这个角度,事务执行的时机可以分成两种:

  • 一种是操作在EXEC执行之前(纯入队期间),这时候采用 WATCH 的机制来保障
  • 另一种是开始执行EXEC之后(实际开始执行命令了),这时候本身具备隔离性了。

3.3.1 WATCH监测对象是否有变化

如果前后有变化,说明被修改了,这时就放弃事务执行,避免事务的隔离性被破坏。
image

3.3.2 对操作进行顺序,并发操作排在 EXEC 之后

Redis 操作命令是单线程执行的,所以在EXEC 命令执行后,不会乱入其他操作,Redis 会保证把指令队列中的所有指令都操作完成之后。
在执行后续的命令,所以,这种模式并发操作不会破坏事务的隔离性。它具有天然的隔离能力。
image

3.4 持久性

因为Redis的持久化特性,所以有如下三种可能性:

  • 未开启 RDB快照 或 AOF日志,事务肯定不具备持久化能力。
  • RDB快照模式:我们在Redis持久化那一篇中聊过,RDB具有快照间隙,事务执行在快照之间则不会被保障。
  • AOF日志:无论日志持久化选项是 no、everysec 和 always 都会存在数据丢失的情况,所以也是无法完全保障的。
    所以不管 Redis 采用什么持久化模式,事务的持久性属性是得不到完全保证的。

4 总结

  • Redis 具备了一定的原子性,但不支持回滚。DISCARD 主要负责清空指令列表,放弃操作。
  • Redis 具备一致性的能力
  • Redis 具备隔离性的能力
  • Redis 无法保证持久性

1. JNI原理概述

通常为了更加灵活高效地实现计算逻辑,我们一般使用C/C++实现,编译为动态库,并为其设置C接口和C++接口。用C++实现的一个库其实是一个或多个类的简单编译链接产物。然后暴露其实现类构造方法和纯虚接口类。这样就可以通过多态调用到库内部的实现类及其成员方法。进一步地,为了让不同库之间调用兼容,可以将C++接口进一步封装为一组C接口函数,C接口编译时不会添加复杂的函数签名,也不支持函数重载,可以方便其他C或C++客户程序调用。C接口的封装需要有"extern C{}"标识,以告诉编译器请使用C编译方式编译这些函数。
进一步地,为了方便上层应用调用C/C++库, 如Android应用,可以为C++库封装Java接口。jdk中地jni组件可以方便地实现在java中调用c++库函数。基本调用原理如下:

  • Java客户代码实现和native方法声明属于java层,使用java编译器编译;
  • JNI接口实现代码和c++库属于c++层,使用G++编译。

这里假定C++类库已经预编译好了,有现成的so库和c接口使用。首先明确一点就是,我们要为C++库封装一个java接口,也即在java层使用C++库暴露的所有函数,那么:

  1. 第一步就是创建一个java类,并按照c++库的接口函数声明,创建所有的native本地接口函数声明(可以是static的)。
  2. 第二步,将这些本地接口声明映射为C++ JNI接口声明,这一步是通过java提供的工具按照既定的映射机制自动生成。这也就保证了java层能正确找到c++实现。
  3. 第三步,实现第二步自动生成的c++ JNI接口函数,在这些接口实现中,按照需要调用c++类库的接口函数,以使用特定的功能并拿到需要的结果。所以,这里要注意的一点是,c++ JNI接口函数实现会编译为一个单独的动态库,并且该动态库动态链接C++类动态库。(这里没有尝试过静态库,按道理应该也是可以的)。此外,在c++ JNI函数实现中按照类型签名规则,我们可以获取到从java层传入的参数,也可以返回特定的数据到Java层。
  4. 第四步,在java应用层使用
    system.loadLibrary("libname.so");
    加载第三步编译生成的jni so库,即可间接调用到c++库函数。


PS:

  1. jni层类型和java类型的对应关系,基本数据类型只是简单地加了前缀
    j
    ,如
    int<=>jint, double<=>jdouble
    ,下面是一些对象类型(包含数组)的类型映射关系:

  2. 签名规则对应表

  3. String 字符串函数操作

    // 在jni实现函数中把jstring类型的字符串转换为C风格的字符串,会额外申请内存
    const char *str = env->GetStringUTFChars(string,0);
    // 做检查判断
    if (str == NULL){
        return NULL;
    }
    // do something;
    // 使用完之后释放申请的内存
    env->ReleaseStringUTFChars(string,str);
  • JNI 支持将 jstring 转换成 UTF 编码和 Unicode 编码两种。因为 Java 默认使用 Unicode 编码,而 C/C++ 默认使用 UTF 编码。所以使用
    GetStringUTFChars(jstring string, jboolean* isCopy)
    将 jstring 转换成 UTF 编码的字符串。其中,jstring 类型参数就是我们需要转换的字符串,而 isCopy 参数的值在实际开发中,直接填 0或NULL就好了,表示深拷贝。

  • 当调用完
    GetStringUTFChars
    方法时别忘了做完全检查。因为 JVM 需要为产生的新字符串分配内存空间,如果分配失败就会返回 NULL,并且会抛出 OutOfMemoryError 异常,所以要对 GetStringUTFChars 结果进行判断。

  • 当使用完 UTF 编码的字符串时,还不能忘了释放所申请的内存空间。调用
    ReleaseStringUTFChars
    方法进行释放。

  • 除了将 jstring 转换为 C 风格字符串,JNI 还提供了将 C 风格字符串转换为 jstring 类型。

  • 通过
    NewStringUTF
    函数可以将 UTF 编码的 C 风格字符串转换为 jstring 类型,通过
    NewString
    函数可以将 Unicode 编码的 C 风格字符串转换为 jstring 类型。这个 jstring 类型会自动转换成 Java 支持的 Unicode 编码格式。

  • 除了 jstring 和 C 风格字符串的相互转换之外,JNI 还提供了其他的函数。

    参考:
    https://blog.csdn.net/TLuffy/article/details/123994246

2. JNI封装示例

实践出真知,分别建立一个c++工程和java工程,
源码github地址

结构目录如下:

├── cpp_project
│   ├── build.sh
│   ├── CMakeLists.txt
│   ├── include
│   │   ├── c_api.h
│   │   ├── com_Student.h
│   │   └── student.h
│   ├── jni_impl
│   │   └── jni_impl.cpp
│   ├── src
│   │   ├── c_api.cpp
│   │   └── student.cpp
│   └── test
│       └── main.cpp
└── java_project
    ├── com
    │   ├── Student.java
    │   └── Test.java
    ├── com_Student.h
    └── run.sh

整体构建流程如下:

  1. 在java工程下创建和C++类库同名(非必须)的java类源文件,并声明和c++工程接口统一的native成员函数;
    使用
    javac -encoding utf8 -h ./ com/Student.java
    命令生成naive本地接口.h头文件。将其拷贝到c++工程下。
  2. 在c++工程下实现jni接口头文件中的函数声明,实现中调用c接口间接完成特定能力调用,编译为libjnilib.so,并链接原始c++库的动态库。
  3. 回到java工程中,在native接口所在的那个类中,添加jni库加载代码:
    // 加载jni库
    static {
        try {
            System.loadLibrary("jnilib");
        }
        catch(UnsatisfiedLinkError e) {
			System.err.println(">>> Can not load library: " + e.toString());
		}
    }
  1. java 测试代码调用,使用如下脚本:
# 编译java文件
javac -encoding utf8 com/Test.java -d bin

# 运行java文件
java -Djava.library.path=/root/project/lzq/jni_demo/cpp_project/build/bin -cp bin com.Test

PS: 编译脚本分别在cpp工程和java工程目录下

3. 思考

  1. 目前即使编译debug版本,调试还是无法进入到jni实现层。有博客说可以通过attach进程可以进入,我尝试并没有成功。
  2. JNI接口传参和返回数据到java层要注意数据类型匹配,签名要一致,否则会直接崩溃掉。
  3. 类似的为C++库封装Python接口,并生成一个安装包可以直接使用pip安装也是常见的封装方式,有时间也可以尝试一下。
  4. 为C++库实现JNI接口可以用Android studio,IDEA,更加方便。也可以直接在Linux上进行,只要有jdk和gcc就可以,但正常人一般不会在linux上写JAVA代码。

前言

大部分人在日常的业务开发中,其实很少去关注数据库的事务相关问题,基本上都是 CURD 一把梭。正好最近在看 MySQL 的相关基础知识,其中对于幻读问题之前一直没有理解深刻,今天就来聊聊「InnoDB 是如何解决幻读的」,话不多说,下面进入主题。

事务隔离级别

事务隔离是数据库处理的基础之一,是
ACID
中的
I
。在 MySQL 的 InnoDB 引擎中支持在
SQL:1992
标准中的四种事务隔离级别,如下图所示,其中 P1 表示脏读(Dirty read),P2 表示不可重复读(Dirty read),P3 表示幻读(Phantom)。

SQL1992_transaction_isolation_levels.jpg

为什么需要定义这么多隔离呢?从上图中也能猜出一二了,InnoDB 提供多个隔离级别主要原因是:让使用者可以在
多个事务
同时进行更改和执行查询时微调性能与结果的可靠性、一致性和可再现性之间的平衡的设置。是一种性能与结果可靠性间的
trade off

什么是幻读

在聊「InnoDB 解决幻读方式」前我们需要先了解
幻读是什么

官方文档
的描述如下:

A row that appears in the
result set
of a query, but not in the
result set
of an earlier query.

其中我加粗的「result set」是关键的地方,两次查询返回的是结果集,说明必须是一个
范围查询
操作。总结下,幻读就是:在同一个事务中,在前后两次查询相同范围时,两次查询得到的结果是不一致的。所以幻读会产生数据一致性问题。

Xnip2023-04-05_16-05-06.jpg

InnoDB 解决幻读方式

为了解决上述的幻读问题,InnoDB 引入了两种锁,分别是「间隙锁」和「next-key 锁」。下面通过一个示例来描述这两种锁的作用分别是什么。假如存在一个这样的 B+ Tree 的索引结构,结构中有 4 个索引元素分别是:9527、9530、9535、9540。

Xnip2023-04-05_16-16-53.jpg

此时当我们使用如下 SQL 通过主键索引查询一条记录,并且加上 X 锁(排它锁)时:

select * from user where id = 9527 for update;

这时就会产生一个记录锁(也就是行锁),锁定
id = 9527
这个索引。

Xnip2023-04-05_16-26-17.jpg

在被锁定的记录(这里是 id = 9527)的锁释放之前,其它事务无法对这条被锁定记录做任何操作。再回忆一下,前面说的幻读定义「在同一个事务中,在前后两次查询相同
范围
时,两次查询得到的结果是不一致」。注意,这里强调的是范围查询。

InnoDB 要解决幻读问题,就必须得保证在如果在一个事务中,通过如下这条语句进行锁定时:

select * from user where id > 9530 and id < 9535 for update;

此时,另外一个语句再执行一如下这条 insert 语句时,需要被阻塞,直到上面这个获得锁的事务释放锁后才能执行。

insert into user(id, name, age) values(9533, 'Jack', 44);

为此,InnoDB 引入了「间隙锁」,它的主要功能是
锁定一段范围内的索引记录
。比如上面查询
id > 9530 and id < 9535
的时候,对 B+ Tree 中的(9530,9535)这个开区间范围的索引加间隙锁。

在这种加了间隙锁的情况下,其它事务对这个区间的数据进行插入、更新、删除都会被锁住直到这个获取到锁的事务释放。

Xnip2023-04-05_16-44-36.jpg

这种是在区间之间的情况,你可能想到另外的一种情况:锁定多个区间,如下的一条语句:

select * from user where id > 9530 for update;

上面这条查询语句是针对
id > 9530
这个条件加锁,那么此时它需要锁定多个索引区间,所以在这种情况下 InnoDB 引入了「next-key 锁」机制。其实 next-key 锁的效果相当于间隙锁和记录锁的合集,记录锁锁定存在的记录行,间隙锁锁住记录行之间的间隙,而 next-key 锁它锁住的是两者之和。

Xnip2023-04-05_16-56-55.jpg

在 InnoDB 中,每个数据行上的
非唯一索引
列上都会存在一把 next-key 锁,当某个事务持有该数据行的 next-key 锁时,会锁住一段
左开右闭区间
的数据。因此,当通过
id > 9530
这样一种范围查询加锁时,会加 next-key 锁,锁定区间是范围是:

(9530,9535] (9535,9540] (9540,+∞]

Xnip2023-04-05_17-04-07.jpg

间隙锁(也叫 Gap 锁)和 next-key 锁的区别在于
加锁的范围
,间隙锁只锁定两个索引之间的引用间隙,而 next-key 锁会锁定多个索引区间,它包含「记录锁」和「间隙锁」。所以,当我们使用了范围查询,不仅仅命中了已存在的 Record 记录,还包含了 Gap 间隙。

总结

虽然在 InnoDB 引擎中通过间隙锁和 next-key 锁的方式解决了幻读问题,但是加锁之后会影响到数据库的并发性能,因此,如果对性能要求较高的业务场景中,建议把隔离级别设置成 RC(READ COMMITTED),这个级别中不存在间隙锁,但是需要考虑到幻读问题会导致的数据一致性。

Lambda

前言 之前在学校,老师说,最好不要使用jdk8的一些新特性....代码阅读不方便。
然后我天真的以为,是新特性不好用,是新特性阅读体验不好,所以,我就从未使用,也从未了解。
直到参加工作,发现了同事使用新特性,跟同事交流了这个新特性的事情,才知道是大学老师怕我们糊涂,于是在假日深入研究了一下

从JDK1.8开始为了简化使用者进行代码的开发,专门提供有Lambda表达式的支持,利用此操作可以实现函数式的编程,对于函数式编程比较著名的语言有:Haskell、Scala,利用函数式的编程可以避免掉面向对象编程之中一些繁琐的处理问题。

Lambda入门小案例:

这是我自己写的工具方法

public static int calculateNum(IntBinaryOperator operator){
        int a = 10;
        int b = 20;
        return operator.applyAsInt(a, b);
    }

在用这个方法的时候,发现参数的类型是 IntBinaryOperator ,那么就直接查看源码

发现有个 方法叫applyAsInt ,含义就是说 接收 int 类型的 left、right 两个参数,最终我要返回得是 int 类型

那么我先用一个老式得方式来调用 这个 calculateNum 工具方法

       //  calculateNum 原始写法
        /*
            * IntBinaryOperator   是一个接口   在 idea 中,如果先写常规的语法的话,此时我想要用 Lambda 的话  ,
        */
        int param = calculateNum(new IntBinaryOperator() {
            @Override
            public int applyAsInt(int left, int right) {
                return left + right;
            }
        });

这是老式得写法了。

(小技巧:如果你使用得IDEA 那么可以使用 Alt + enter 快速生成匿名内部类 转变为Lambda表达式 或者 是Lambda表达式 生成匿名内部类)

那么我使用Lambda如何写呢?

        int params = calculateNum( ( int a,int b) -> {
            return a+b;
        });

函数式接口 Functional Interface

抛出一个疑问:在我们书写一段 Lambda 表达式后(比如上一章节中匿名内部类的 Lambda 表达式缩写形式),Java 编译器是如何进行类型推断的,它又是怎么知道重写的哪个方法的?

需要说明的是,不是每个接口都可以缩写成 Lambda 表达式。只有那些函数式接口(Functional Interface)才能缩写成 Lambda 表示式。

那么什么是函数式接口(Functional Interface)呢?

所谓函数式接口(Functional Interface)
就是只包含一个抽象方法的声明
。针对该接口类型的所有 Lambda 表达式都会与这个抽象方法匹配。

注意:你可能会有疑问,Java 8 中不是允许通过 defualt 关键字来为接口添加默认方法吗?那它算不算抽象方法呢?答案是:不算。因此,你可以毫无顾忌的添加默认方法,它并不违反函数式接口(Functional Interface)的定义。

总结一下:只要接口中仅仅包含一个抽象方法,我们就可以将其改写为 Lambda 表达式。为了保证一个接口明确的被定义为一个函数式接口(Functional Interface),我们需要为该接口添加注解:
@FunctionalInterface
。这样,一旦你添加了第二个抽象方法,编译器会立刻抛出错误提示。

示例代码:

@FunctionalInterface
interface Converter<F, T> {
    T convert(F from);
}

注意:上面的示例代码,即使去掉
@FunctionalInterface
也是好使的,它仅仅是一种约束而已。

Lambda错误示例

interface IMessage {
    public void send(String str);
    public void say();
}

public class JavaDemo {
    public static void main(String[] args) {
        IMessage i = (str) -> {
            System.out.println(str);
        };
        i.send("www.baidu.com");
    }
}

Lambda表达式的几种格式

  • 方法没有参数: () -> {};
  • 方法有参数::(参数,…,参数) -> {};

demo

package com.yhn.Lambda;

import java.util.function.Function;
import java.util.function.IntBinaryOperator;
import java.util.function.IntConsumer;
import java.util.function.IntPredicate;
/**

    一、什么情况下可以使用 Lambda 表达式?
        Lambda表达式如果要想使用,那么必须要有一个重要的实现要求:SAM(Single Abstract Method),接口中只有一个抽象方法。
        以IMessage接口为例,这个接口里面只是提供有一个send()方法,
         interface IMessage {
         public void send(String str);
         }
        除此之外没有任何其他方法定义,所以这样的接口就被称为函数式接口,
        而只有函数式接口才能被Lambda表达式所使用。


    二、 Lambda 表达式的格式
        Lambda 表达式在Java语言中引入了一个操作符**“->”**,该操作符被称为Lambda操作符或箭头操作符。它将Lambda分为两个部分:
        左侧:指定了Lambda表达式需要的所有参数
        右侧:制定了Lambda体,即Lambda表达式要执行的功能。
        方法没有参数: () -> {};
        方法有参数::(参数,…,参数) -> {};

        省略规则
          参数类型可以省略
          方法体只有一句代码时大括号return和唯一一句代码的分号可以省略
          方法只有一个参数时小括号可以省略
          以上这些规则都记不住也可以省略不记
 *
 */

public class LambdaDemo {

    public static void main(String[] args) {
        printNum2(value -> value % 2 == 0, value -> value>4);

        /* ------------------------------------------------------------------------------------------ */

        //  calculateNum 原始写法
        /*
            * IntBinaryOperator   是一个接口   在 idea 中,如果先写常规的语法的话,此时我想要用 Lambda 的话  ,
            * 可以使用 Alt + enter 快速生成匿名内部类 转变为Lambda表达式 或者 是Lambda表达式 生成匿名内部类
        */
        int param = calculateNum(new IntBinaryOperator() {
            @Override
            public int applyAsInt(int left, int right) {
                return left + right;
            }
        });


        // 使用 Lambda 写法
        int params = calculateNum( ( int a,int b) -> {
            return a+b;
        });

        /* ------------------------------------------------------------------------------------------ */

        /*
         *  printNum    IntPredicate 接口只有一个方法 test
         */
        printNum(new IntPredicate() {
            @Override
            public boolean test(int value) {
                return value%2==0;
            }
        });

        printNum( (int a) -> {
            return a%2 == 0;
        });

        /* ------------------------------------------------------------------------------------------ */

        // 先解释  Function<T, R> 接口  - >  Function<String, R>
        // Function 接口 有个方法叫 apply   含义就是说   R 传什么类型 , 我最终都是要返回的是 T 类型  在此处是  String 类型


        //  此时 我只需要 设定一个泛型即可
        Integer result  = typeConver(new Function<String, Integer>() {

            @Override
            public Integer apply(String s) {
                return Integer.valueOf(s);
            }
        });

        Integer results  = typeConver( (String s) ->{
            return Integer.valueOf(s);
        });

        // 扩展
        String s1 = typeConver(s -> {
            return s + "new";
        });


        /* ------------------------------------------------------------------------------------------ */

        foreachArr(new IntConsumer() {
            @Override
            public void accept(int value) {
                System.out.println(value);
            }
        });

        foreachArr((int value ) -> {
            System.out.println(value);
        });

        // 进行省略
        foreachArr( value -> System.out.println(value));


        /* ------------------------------------------------------------------------------------------ */
    }



    public static void foreachArr(IntConsumer consumer){
        int[] arr = {1,2,3,4,5,6,7,8,9,10};
        for (int i : arr) {
            consumer.accept(i);
        }
    }




    public static <R> R typeConver(Function<String,R> function){
        String str = "1235";
        R result = function.apply(str);
        return result;
    }



    public static void printNum2(IntPredicate predicate,IntPredicate predicate2){
        int[] arr = {1,2,3,4,5,6,7,8,9,10};
        for (int i : arr) {
            if(predicate.and(predicate2).test(i)){
                System.out.println(i);
            }
        }
    }

    public static void printNum(IntPredicate predicate){
        int[] arr = {1,2,3,4,5,6,7,8,9,10};
        for (int i : arr) {
            if(predicate.test(i)){
                System.out.println(i);
            }
        }
    }

    public static int calculateNum(IntBinaryOperator operator){
        int a = 10;
        int b = 20;
        return operator.applyAsInt(a, b);
    }
}

总结:

​ Lambda 表达式在Java语言中引入了一个操作符
“->”
,该操作符被称为Lambda操作符或箭头操作符。它将Lambda分为两个部分:
​ 左侧:指定了Lambda表达式需要的所有参数
​ 右侧:制定了Lambda体,即Lambda表达式要执行的功能。
​ 方法没有参数: () -> {};
​ 方法有参数::(参数,…,参数) -> {};

​ 省略规则
​ 参数类型可以省略
​ 方法体只有一句代码时大括号return和唯一一句代码的分号可以省略
​ 方法只有一个参数时小括号可以省略
​ 以上这些规则都记不住也可以省略不记

方法引用

方法引用的三种形式

​ 对象 :: 非静态方法

​ 类 :: 静态方法

​ 类 :: 非静态方法

类引用静态方法

语法格式: 类名称::static方法名称

第一步,我们自定义一个接口,该接口中只有一个抽象方法,是一个函数式接口。

第二步,随便建立一个类,创建一个方法。这里要注意,创建的方法返回值类型和形参列表必须和函数式接口中的抽象方法相同。

第三步,创建函数式接口的实现类,我们可以使用方法引用。相当于实现类里的重写的方法,就是方法引用的方法。这样才能方法引用。

public class LambdaDemoLei {
    /**
     *   @FunctionalInterface,主要用于编译级错误检查,加上该注解,当你写的接口不符合函数式接口定义的时候,编译器会报错。
     */
    @FunctionalInterface
    interface IMessage<T,P> {
        public T transfer(P p);
    }

    static class Supplier{
        public static String getStr(Integer integer) {
            return String.valueOf(integer);
        }
    }
    public static void main(String[] args) {
    //        类引用静态方法   类名称::static方法名称
        IMessage<String, Integer> msg = Supplier::getStr;
        System.out.println(msg.transfer(31415926));
    }
    
    // 输出 31415926

对象引用非静态方法

语法格式: 实例化对象::普通方法;

有了类引用静态方法的基础,相信大家已经有了一点感觉。

对象引用非静态方法,和类引用静态方法一致。要求我们对象引用的方法,返回值和形参列表要和函数式接口中的抽象方法相同。

public class LambdaDemoLei {
    @FunctionalInterface
    interface IMessage1 {
        public double get();
    }

    static class Supplier1{
        private Double salary;

        public Supplier1() {
        }

        public Supplier1(Double salary){
            this.salary = salary;
        }
        public Double getSalary() {
            return this.salary;
        }
    }
    
        public static void main(String[] args) {
//        对象引用非静态方法   语法格式: 实例化对象::普通方法
        Supplier1 supplier = new Supplier1(9999.9);
        IMessage1 msg1 = supplier::getSalary;
        System.out.println(msg1.get());
    }
    
    // 输出 9999.9
}

类引用非静态方法

语法格式: 类::普通方法

类引用普通方法就有点难以理解了。

当抽象方法中有两个参数,且第一个参数是调用者,第二个参数是形参,则可以使用类::实例方法。

package com.yhn.Lambda;

import lombok.Data;

/**
 * @Description
 * @Author TuiMao
 * @Date 2023/4/4 16:19
 * @Version 1.0
 */
@Data
public class Person {
    @FunctionalInterface
    interface IMessage<T, P> {
        // 要看成 T res = p1.compare(p2);
        public T compare(P p1, P p2);
    }

    @FunctionalInterface
    interface IMessage2<T, P, V> {
        //  public T create(P p1, V p2); 符合抽象方法的要求
        public T create(P p1, V p2);
    }



    @FunctionalInterface
    interface IMessage1<T, P, V> {
        // 看成 T res = p1.compare(p2);
        // public int compareTo(String anotherString){} 符合抽象方法的格式
        // int res = str1.compare(str2);
        public T compare(P p1, V p2);
    }


    public Person() {
    }

    public Person(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    private String name;
    private Integer age;

    public boolean equal(Person per) {
        return this.name.equals(per.getName()) && this.age.equals(per.getAge());
    }

    public static void main(String[] args) {
        Person person1 = new Person("张三", 22);
        Person person2 = new Person("张三", 23);
        // 符合T res = p1.compare(p2);
        IMessage<Boolean, Person> msg = Person::equal;
        System.out.println(msg.compare(person1, person2));

        System.out.println("----------");

        // 类引用普通方法  语法格式: 类::普通方法       当抽象方法中有两个参数,且第一个参数是调用者,第二个参数是形参,则可以使用类::实例方法。
        IMessage1<Integer,String,String> stringCompare = String::compareTo;
        Integer compare = stringCompare.compare("adc", "abd");
        System.out.println(compare);

        System.out.println("----------");

        // 构造引用  语法格式: 类名称::new
        IMessage2<Person,String,Integer> msg1 = Person::new;
        Person person = msg1.create("张三", 20);
        System.out.println(person);

    }

}

Lambda 访问外部变量及接口默认方法

在本章节中,我们将会讨论如何在 lambda 表达式中访问外部变量(包括:局部变量,成员变量,静态变量,接口的默认方法.),它与匿名内部类访问外部变量很相似。

访问局部变量

在 Lambda 表达式中,我们可以访问外部的
final
类型变量,如下面的示例代码:

// 转换器
@FunctionalInterface
interface Converter<F, T> {
    T convert(F from);
}
final int num = 1;
Converter<Integer, String> stringConverter =
        (from) -> String.valueOf(from + num);

stringConverter.convert(2);     // 3

与匿名内部类不同的是,我们不必显式声明
num
变量为
final
类型,下面这段代码同样有效:

int num = 1;
Converter<Integer, String> stringConverter =
        (from) -> String.valueOf(from + num);

stringConverter.convert(2);     // 3

但是
num
变量必须为隐式的
final
类型,何为隐式的
final
呢?就是说到编译期为止,
num
对象是不能被改变的,如下面这段代码,就不能被编译通过:

int num = 1;
Converter<Integer, String> stringConverter =
        (from) -> String.valueOf(from + num);
num = 3;

在 lambda 表达式内部改变
num
值同样编译不通过,需要注意, 比如下面的示例代码:

int num = 1;
Converter<Integer, String> converter = (from) -> {
	String value = String.valueOf(from + num);
	num = 3;
	return value;
};

访问成员变量和静态变量

上一章节中,了解了如何在 Lambda 表达式中访问局部变量。与局部变量相比,在 Lambda 表达式中对成员变量和静态变量拥有读写权限:

    @FunctionalInterface
    interface Converter<F, T> {
        T convert(F from);
    }
class Lambda4 {
        // 静态变量
        static int outerStaticNum;
        // 成员变量
        int outerNum;

        void testScopes() {
            Converter<Integer, String> stringConverter1 = (from) -> {
                // 对成员变量赋值
                outerNum = 23;
                return String.valueOf(from);
            };

            Converter<Integer, String> stringConverter2 = (from) -> {
                // 对静态变量赋值
                outerStaticNum = 72;
                return String.valueOf(from);
            };
        }
    }

访问接口的默认方法

@FunctionalInterface
interface Formula {
	// 计算
	double calculate(int a);

	// 求平方根
	default double sqrt(int a) {
		return Math.sqrt(a);
	}
}

当时,我们在接口中定义了一个带有默认实现的
sqrt
求平方根方法,在匿名内部类中我们可以很方便的访问此方法:

Formula formula = new Formula() {
	@Override
	public double calculate(int a) {
		return sqrt(a * 100);
	}
};

但是在 lambda 表达式中可不行:

Formula formula = (a) -> sqrt(a * 100);

带有默认实现的接口方法,是
不能
在 lambda 表达式中访问的,上面这段代码将无法被编译通过。

JDK8自带函数式接口

在JDK1.8之中,提供有Lambda表达式和方法引用,但是你会发现如果由开发者自己定义函数式的接口,往往都需要使用@FunctionalInterface来进行大量的声明,于是很多的情况下如果为了方便则可以引用系统中提供的函数式接口。

在系统之中专门提供有一个java.util.functional的开发包,里面可以直接使用函数式接口,在这个包下面一共有如下几个核心的接口供我们使用。

功能型函数式接口

接口定义 接口作用 接口使用
@FunctionalInterface
public interface Function<T,R>
消费 T 类型参数,返回 R 类型结果 如下所示

function 相当于是给一个参数,然后返回一个结果。
如果是给两个参数,返回一个结果,那么就是 BiFunction。Bi 前缀即使 binary 的缩写。

import java.util.function.*;
/*
  @FunctionalInterface
  T是参数类型
  R是返回类型
  public interface Function<T,R>{
  	public R apply(T t);
  }
 */
class StringCompare {
	// 给一个 String 类型的参数,返回布尔类型,符合功能性函数式接口的抽象方法
    public static boolean test(String t) {
        return t == null;
    }
}
public class JavaDemo {
    public static void main(String[] args) {
    	// 直接静态引用
        Function<String,Boolean> func1 = StringCompare::test;
        System.out.println(func1.apply(null));
    }
}

// true

消费型函数式接口

消费性函数式接口,只能进行数据的处理操作,而没有返回值

· 在进行系统输出的时候使用的是:System.out.println();这个操作只是进行数据的输出和消费,而不能返回,这就是消费性接口。

接口定义 接口作用 接口使用
@FunctionalInterface
public interface Consumer
接收一个 T 类型参数,但是不返回任何东西,消费型接口 如下

其实最常见的消费型接口的实现,就是 System.out.println(xxx) 了。 我们只管往方法中输入参数,但是并没有返回任何值。
Consumer 相当于是有来无回,给一个参数,但是无返回。
而如果是两个参数,无返回,那么就是 BiConsumer。

public class JavaDemo {
    public static void main(String[] args) {
        Consumer<String> consumer = System.out::println;
        consumer.accept("Hello World!");
    }
}

// Hello World!

当然我们也可以自定义消费性接口

class StringCompare {
	// 接收 StringBuilder ,但是不返回任何数据。
    public void fun(StringBuilder sb) {
        sb.append("World!");
    }
}

public class JavaDemo {
    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder();
        sb.append("Hello ");
        Consumer<StringBuilder> consumer = new StringCompare()::fun;
        consumer.accept(sb);
        System.out.println(sb.toString());
    }
}

供给型函数式接口

接口定义 接口作用 接口使用
@FunctionalInterface
public interface Supplier
啥也不接受,但是却返回 T 类型数据,供给型接口 如下

Supplier 相当于是无中生有,什么也不传,但是返回一个结果。
像 String 类里的 toUpperCase() 方法,也是不接受参数,但是返回 String 类型,就可以看成这个供给型函数式接口的一个实现。

public String toUpperCase() {
        return toUpperCase(Locale.getDefault());
    }

import java.util.function.*;
public class Demo01 {
	public static void main(String[] args) {
		Supplier <String> sup = "WWW.BAIDU.COM" :: toLowerCase;
		System.out.println(sup.get());
	}
}

断言型函数式接口

接口定义 接口作用 接口使用
@FunctionalInterface public interface Predicate 传入 T 类型参数,返回布尔类型,常常用于对入参进行判断 如下
class StringFilter {
	// 对集合中的数据进行过滤,传入断言型接口进行判断
    public static List<String> filter(List<String> list, Predicate<String> predicate) {
        List<String> stringList = new ArrayList<>();
        for (String str : list) {
            if (predicate.test(str)) {
                stringList.add(str);
            }
        }
        return stringList;
    }

}

public class JavaDemo {
    public static void main(String[] args) {
        List<String> stringList = Arrays.asList("好诗", "好文", "好评", "好汉", "坏蛋", "蛋清", "清风", "风间");
        List<String> filterList = StringFilter.filter(stringList, list -> list.contains("好"));
        System.out.println(filterList);
    }
}

如果JDK本身提供的函数式接口可以被我们所使用,那么就没必要重新去定义了。