2024年1月

当我们成功登录SAP的某个连接后,在SAP GUI起始页(
SAP轻松访问
),我们可以通过点击【收藏夹】或者在界面左上角的输入框输入对应的事务代码,直接进入对应事务的界面。但是下面列举的场景,你是否知道如何快速应对?

场景一:当前已处于某个事务代码中,如何进入新的事务代码?

场景二:当前SAP左下角有错误消息,如何进入新的事务代码?

场景三:如何快捷关闭当前会话窗口?

下面提到的技巧相信即使是老司机 ,也未必曾经用过。

在SAP系统中,执行事务代码通常有几种方式:

1. 使用
`/n`
前缀:当在SAP中输入 `/n` 后跟事务代码时,这将尝试关闭当前的事务,并打开一个新的会话以运行该代码;

2. 强制新会话
`/o`
:可以试着用 `/o`(代表打开新窗口)来启动一个全新的会话窗口。比如,输入 `/oSE80` 将会打开一个新的ABAP开发工作台事务SE80窗口,而不关闭当前的事务;

3. 使用
`/i`
关闭会话:它将关闭所有会话(包括抛出错误消息的会话),如果当前只开启了一个会话,执行该操作后,会弹出是否【需要注销SAP】的窗口;

4. 使用
`/nend`

`/nex`
:人为结束当前的SAP会话,包括所有的事务。这还将进行日志记录。“end” 或者 "exit"(例:`/nend` 或 `/nex`)将退出SAP系统。不过这是一个较为极端的步骤,请谨慎使用;

5. 尝试根据错误提示,修复错误来源:如果频繁出现错误提示,可能是因为业务流程中某些步骤没按预期进行。需要检查错误信息,解决根本问题后,系统就可以正常运行(强烈推荐,遇到错误时,应该解决错误,而不是尽可能忽略它);

6. 联系系统管理员:如果上述方法都不能解决问题或你不确定怎么操作,可以联系SAP系统管理员或者支持团队寻求帮助。 需要注意的是优先确认是否可以忽略错误消息而无影响地继续工作。在忽略错误消息或尝试绕过它时,应考虑错误的严重性和业务流程的完整性,以免造成数据不一致或工作未能正确完成。

如果我们是借助SAP GUI Scripiting 来自动化实现上述效果,代码如下(python版):

session.findById("wnd[0]/tbar[0]/okcd").text = "/nfb03"session.findById("wnd[0]").sendVKey(0)

翻阅SAP GUI Scripiting API文档可知,对于Gui Session Object对象,存在方法:
StartTransaction

所以,下面三种方法效果完全等同,都是关闭当前的事务代码,在当前会话下进入FB03事务中:

#方法一
session.findById("wnd[0]/tbar[0]/okcd").text = "/nfb03"session.findById("wnd[0]").sendVKey(0)#方法二
session.SendCommand( "/nfb03")#方法三
session.StartTransaction( "fb03")

快来关注本公众号 获取更多爬虫、数据分析的知识!

前言

前段时间摸了下机器学习,然后我发现其实openCV还是一个很浩瀚的库的,现在也正在写一篇有关yolo的博客,不过感觉理论偏多,所以在学yolo之前先摸一下opencv,简单先写个项目感受感受opencv。

流程

openCV实际上已经有一个比较完整的模型了,下载在
haarcascades

这里我们下haarcascade_frontalface_default.xml以备用。

在做人脸识别的时候流程就比较简单了

  1. 读取图片
  2. 创建Haar级联器
  3. 图片转灰度图(可以不转,转了能更快而已)
  4. 通过Haar级联分类器来检测人脸面部特征,返回faces结构
  5. 使用openCV的接口,在原图上框选出结果,并展示

编码

这里代码偏简单,就不过多介绍了

import numpy as np
import cv2

img = './faces/lena.bmp'
#脸部Haar级联器
facer_path = './faces/haarcascade_frontalface_default.xml'
facer = cv2.CascadeClassifier(facer_path)

img = cv2.imread(img)

gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)


#执行人脸识别

#现在可以使用Haar级联分类器来检测人脸和其他面部特征

faces = facer.detectMultiScale(gray,1.35,3)

for(x,y,w,h) in faces:
    cv2.rectangle(img,(x,y),(x+w,y+h),(0,0,255),2)
    roi_img = img[y:y+h, x:x+w]

cv2.imshow('img', img)
cv2.waitKey()

结果:
在这里插入图片描述

进阶

且不看运行的结果如何,但从结果你就可以看出来,这里只是把人脸从图片中框出来了。但这还不够,我们实际上不仅希望你可以把人脸圈出来,还希望能把人脸分类,比如A是A,B是B这样。

OpenCV提供了3种人脸识别方法,分别是Eigenfaces、Fisherfaces和LBPH。这3种方法都是通过对比样本的特征最终实现人脸识别。因为这3种算法提取特征的方式不一样,侧重点不同,所以不能分出孰优孰劣,只能说每种方法都有各自的识别风格。 OpenCV为每一种人脸识别方法都提供了创建识别器、训练识别器和识别3种方法,这3种方法的语法非常相似。我这里只简单说说Eigenfaces怎么调用,至于其他的两种读者感兴趣可以自己去搜索。

Eigenfaces人脸识别器

Eigenfaces也叫作“特征脸”。Eigenfaces通过PCA(主成分分析)方法将人脸数据转换到另外一个空间维度做相似性计算。在计算过程中,算法可以忽略一些无关紧要的数据,仅识别一些具有代表性的特征数据,最后根据这些特征识别人脸。 开发者需要通过以下3种方法完成人脸识别操作。

(1)通过cv2.face.EigenFaceRecognizer_create()方法创建Eigenfaces人脸识别器,其语法如下:

recognizer = cv2.face.EigenFaceRecognizer_create(num_components, threshold)

参数说明:

num_components:可选参数,PCA方法中保留的分量个数,建议使用默认值。

threshold:可选参数,人脸识别时使用的阈值,建议使用默认值。

返回值说明:

recognizer:创建的Eigenfaces人脸识别器对象。

(2)创建识别器对象后,需要通过对象的train()方法训练识别器。建议每个人都给出2幅以上的人脸图像作为训练样本。train()方法的语法如下:

recognizer.train(src, labels)

对象说明:

recognizer:已有的Eigenfaces人脸识别器对象。 参数说明:

src:用来训练的人脸图像样本列表,格式为list。样本图像必须宽、高一致。

labels:样本对应的标签,格式为数组,元素类型为整数。数组长度必须与样本列表长度相同。样本与标签按照插入顺序一一对应。

(3)训练识别器后可以通过识别器的predict()方法识别人脸,该方法对比样本的特征,给出最相近的结果和评分,其语法如下:

label, confidence = recognizer.predict(src)

对象说明:

recognizer:已有的Eigenfaces人脸识别器对象。 参数说明:

src:需要识别的人脸图像,该图像宽、高必须与样本一致。 返回值说明:

label:与样本匹配程度最高的标签值。

confidence:匹配程度最高的信用度评分。评分小于5000匹配程度较高,0分表示2幅图像完全一样。 下面通过一个实例来演示Eigenfaces人脸识别器的用法。

确定流程

最后我们来确定一下流程:

  1. 读取数据
  2. 创建特征脸识别器
  3. 输入图片和labels开始训练
  4. 输入需要识别的人脸图像
  5. 得到输出

我这里把我的训练集整理了以下,名称打在前面。我这里照片是我自己找的,具体的图片训练集大伙可以自己去设定。

在这里插入图片描述

具体代码如下,代码的功能可以参考注释

import numpy as np
import cv2
import os

face_path = './faces'
photos = list()
labels = list()

# 设置期望的图像大小
desired_size = (811, 843)
#定义labels
names = {"0":"mengzi","1":"qy","2":"lx","3":"qq"}
# 从当前路径中读取到所有的file
for root, dirs, files in os.walk(face_path):
    for file in files:
        if '.xml' in file:
            continue
        img_path = os.path.join(root, file)
        img = cv2.imread(img_path, 0)
        # 图片需要设置到期望大小,因为模型输入的图片大小都必须是统一值,否则会无法训练
        img_resized = cv2.resize(img, desired_size) 
        # 根据图片的人脸,对应到names,插入到labels  
        if 'mengzi' in file: 
            # 读取图像并调整大小 
            labels.append(0)
        elif 'qy' in file:
            labels.append(1)
        elif 'lx' in file:
            labels.append(2)
        elif 'qq' in file:
            labels.append(3)
        else:
            continue
        photos.append(img_resized)



#创建人脸识别器
recognizer = cv2.face.EigenFaceRecognizer_create()
recognizer.train(photos, np.array(labels))

# 读取测试图像并调整大小
target = cv2.imread(face_path + '/test5.jpg', 0)
target_resized = cv2.resize(target, desired_size)

#输出待识别对象
label, confidence = recognizer.predict(target_resized)

print('confidence = ' + str(confidence))
print(names[str(label)])

结果

这里代码其实也能看得出,最后之能输出图片中人物的label和执行度,这样我们倒是完成了一个分类的工作。但是这里有个问题,就是我们不仅仅需要分类,还需要知道人脸的具体位置。

结合Haar级联器和Eigenfaces人脸识别器实现人脸划分

前言

刚刚说了两种,一个是划分区域,一个是打标签,那么能不能即划分区域,又打上标签呢?当然是可以的,接下来就简单说说怎么做。

流程

实际上流程就是把两个模式结合起来,先使用Haar级联器划分出脸部区域,然后再用Eigenfaces人脸识别器去检查分出的脸部区域的对象名称,然后将标签放在图片上即可。

  1. 加载Haar级联分类器
  2. 读取文件并训练Eigenfaces人脸识别器
  3. 读取目标图片
  4. 通过Haar级联分类器扫描得到目标图片中的所有人脸框
  5. 对所有人脸框使用Eigenfaces人脸识别器进行识别
  6. 画上方框,并打上标签

代码

代码如下:

import numpy as np
import cv2
import os

# 加载Haar级联分类器
face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')
face_path = './faces'
photos = list()
labels = list()

# 读取学习图片
desired_size = (811, 843)
names = {"0":"mengzi","1":"qy","2":"lx","3":"qq","4":"ch"}
for root, dirs, files in os.walk(face_path):
    for file in files:
        if '.xml' in file:
            continue
        img_path = os.path.join(root, file)
        img = cv2.imread(img_path, 0)
        img_resized = cv2.resize(img, desired_size)
        
        if 'mengzi' in file: 
            # 读取图像并调整大小 
            labels.append(0)
        elif 'qy' in file:
            labels.append(1)
        elif 'lx' in file:
            labels.append(2)
        elif 'qq' in file:
            labels.append(3)
        elif 'ch' in file:
            labels.append(4)
        else:
            continue
        photos.append(img_resized)



# 训练人脸识别器
recognizer = cv2.face.EigenFaceRecognizer_create()
recognizer.train(photos, np.array(labels))
# 读取测试图像
target = cv2.imread(face_path + '/test_ch.jpg')
target_gray = cv2.cvtColor(target, cv2.COLOR_BGR2GRAY)

# 检测人脸
faces = face_cascade.detectMultiScale(target_gray, scaleFactor=1.2, minNeighbors=5, minSize=(30, 30))

# 对于每个检测到的人脸
for (x, y, w, h) in faces:
    # 在原图上绘制矩形框出人脸
    cv2.rectangle(target, (x, y), (x+w, y+h), (255, 0, 0), 2)
    
    # 提取人脸区域并调整到期望大小
    face_region = target_gray[y:y+h, x:x+w]
    face_resized = cv2.resize(face_region, desired_size)
    
    # 使用EigenFaceRecognizer进行预测
    label, confidence = recognizer.predict(face_resized)
    
    # 将识别的名字和置信度打印在图像上方
    cv2.putText(target, f'{names[str(label)]} - {confidence:.2f}', (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (36,255,12), 2)

# 显示图像
cv2.imshow('Face Recognition', target)
cv2.waitKey(0)
cv2.destroyAllWindows()

测试结果:
虽然打了马赛克,但是实际上结果是可以置信的。在这里,还可以根据置信程度来对不同的人脸进行筛选,比如不想要的脸或者置信程度不高的脸可以再进行舍弃。
在这里插入图片描述

前言

在上一篇文章中,介绍了什么是锁,以及锁的使用场景,本文继续给大家继续做深入的介绍,介绍JAVA为我们提供的不同种类的锁。

JAVA为我们提供了种类丰富的锁,每种锁都有不同的特性,锁的使用场景也各不相同。由于篇幅有限,在这里只给大家介绍比较常用的几种锁。我会通过锁的定义,核心代码剖析,以及使用场景来给大家介绍JAVA中主流的几种锁。

乐观锁 与 悲观锁

乐观锁与悲观锁应该是每个开发人员最先接触的两种锁。小编最早接触的就是这两种锁,但是不是在JAVA中接触的,而是在数据库当中。当时的应用场景主要是在更新数据的时候,更新数据这个场景也是使用锁的非常主要的场景之一。更新数据的主要流程如下:

  1. 检索出要更新的数据,供操作人员查看;
  2. 操作人员更改需要修改的数值;
  3. 点击保存,更新数据;

这个流程看似简单,但是我们用多线程的思维去考虑,这也应该算是一种互联网思维吧,就会发现其中隐藏着问题。我们具体看一下

  1. A检索出数据;
  2. B检索出数据;
  3. B修改了数据;
  4. A修改数据,系统会修改成功吗?

当然啦,A修改成功与否,要看程序怎么写。咱们抛开程序,从常理考虑,A保存数据的时候,系统要给提示,说“您修改的数据已被其他人修改过,请重新查询确认”。那么我们程序中怎么实现呢?

  1. 在检索数据,将数据的版本号(version)或者最后更新时间一并检索出来;
  2. 操作员更改数据以后,点击保存,在数据库执行update操作
  3. 执行update操作时,用步骤1检索出的版本号或者最后更新时间与数据库中的记录作比较;
  4. 如果版本号或最后更新时间一致,则可以更新;
  5. 如果不一致,就要给出上面的提示;

上述的流程就是乐观锁的实现方式。在JAVA中乐观锁并没有确定的方法,或者关键字,它只是一个处理的流程、策略。咱们看懂上面的例子之后,再来看看JAVA中乐观锁。

乐观锁呢,它是假设一个线程在取数据的时候不会被其他线程更改数据,就像上面的例子那样,但是在更新数据的时候会校验数据有没有被修改过。它是一种比较交换的机制,简称CAS (Compare And Swap)机制。一旦检测到有冲突产生,也就是上面说到的版本号或者最后更新时间不一致,它就会进行重试,直到没有冲突为止。乐观锁的机制如图所示:

咱们看一下JAVA中最常用的i++,咱们思考一个问题,i++它的执行顺序是什么样子的?它是线程安全的吗?当多个线程并发执行i++的时候,会不会有问题?接下来咱们通过程序看一下:

package cn.pottercoding.lock;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author 程序员波特
 * @since 2024年01月12日
 *
 * i++ 线程安全问题测试
 */
public class ThreadTest {

    private int i = 0;

    public static void main(String[] args) {
        ThreadTest test = new ThreadTest();

        // 线程池,50个固定线程
        ExecutorService executorService = Executors.newFixedThreadPool(50);
        CountDownLatch countDownLatch = new CountDownLatch(5000);

        for (int i = 0; i < 5000; i++) {
            executorService.execute(() -> {
                test.i++;
                countDownLatch.countDown();
            });
        }

        executorService.shutdown();

        try {
            countDownLatch.await();
            System.out.println("执行完成后,i = " + test.i);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

上面的程序中,我们模拟了50个线程同时执行i++,总共执行5000次,按照常规的理解,得到的结果应该是5000,我们运行一下程序,看看执行的结果如何?

执行完成后,i=4975

执行完成后,i=4986

执行完成后,i=4971

这是运行3次以后得到的结果,可以看到每次执行的结果都不一样,而且不是5000,这是为什么呢?这就说明i++并不是一个原子性的操作,在多线程的情况下并不安全。我们把i++的详细执行步骤拆解一下:

  1. 从内存中取出i的当前值;
  2. 将i的值加1;
  3. 将计算好的值放入到内存当中;

这个流程和我们上面讲解的数据库的操作流程是一样的。在多线程的场景下,我们可以想象一下,线程A和线程B同时从内存取出的值,假如i的值是1000,然后线程A和线程B再同时执行+1的操作,然后把值再放入内存当中,这时,内存中的值是1001,而我们期望的是1002,正是这个原因导致了上面的错误。那么我们如何解决呢?在JAVA1.5以后,JDK官方提供了大量的原子类,这些类的内部都是基于CAS机制的,也就是使用了乐观锁。我们将上面的程序稍微改造一下,如下:

package cn.pottercoding.lock;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author 程序员波特
 * @since 2024年01月12日
 *
 * 原子类测试
 */
public class AtomicTest {

    private AtomicInteger i = new AtomicInteger(0);

    public static void main(String[] args) {
        AtomicTest test = new AtomicTest();

        // 线程池,50个固定线程
        ExecutorService executorService = Executors.newFixedThreadPool(50);
        CountDownLatch countDownLatch = new CountDownLatch(5000);

        for (int i = 0; i < 5000; i++) {
            executorService.execute(() -> {
                test.i.incrementAndGet();
                countDownLatch.countDown();
            });
        }

        executorService.shutdown();

        try {
            countDownLatch.await();
            System.out.println("执行完成后,i = " + test.i);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

我们将变量的类型改为AtomicInteger ,AtomicInteger 是一个原子类。我们在之前调用i++的地方改成了i.incrementAndGet(),incrementAndGet()方法采用了CAS机制,也就是说使用了乐观锁。我们再运行一下程序,看看结果如何。

执行完成后,i=5000

执行完成后,i=5000

执行完成后,i=5000

我们同样执行了3次,3次的结果都是5000,符合了我们预期。这个就是乐观锁。我们对乐观锁稍加总结,乐观锁在读取数据的时候不做任何限制,而是在更新数据的时候,进行数据的比较,保证数据的版本一致时再更新数据。根据它的这个特点,可以看出乐观锁适用于读操作多,而写操作少的场景。

悲观锁与乐观锁恰恰相反,悲观锁从读取数据的时候就显示的加锁,直到数据更新完成,释放锁为止。在这期间只能有一个线程去操作,其他的线程只能等待。在JAVA中,悲观锁可以使用
synchronized
关键字或者
ReentrantLock
类来实现。还是,上面的例子,我们分别使用这两种方式来实现一下。首先是使用
synchronized
关键字来实现:

package cn.pottercoding.lock;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author 程序员波特
 * @since 2024年01月12日
 *
 * 使用 synchronized 关键字来实现自增
 */
public class SynchronizedTest {

    private int i = 0;

    public static void main(String[] args) {
        SynchronizedTest test = new SynchronizedTest();

        // 线程池,50个固定线程
        ExecutorService executorService = Executors.newFixedThreadPool(50);
        CountDownLatch countDownLatch = new CountDownLatch(5000);

        for (int i = 0; i < 5000; i++) {
            executorService.execute(() -> {
                // 修改部分,开始
                synchronized (test) {
                    test.i++;
                }

                // 修改部分结束
                countDownLatch.countDown();
            });
        }

        executorService.shutdown();

        try {
            countDownLatch.await();
            System.out.println("执行完成后,i = " + test.i);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

我们唯一的改动就是增加了
synchronized
块,它锁住的对象是
test
,在所有线程中,谁获得了
test
对象的锁,谁才能执行
i++
操作。我们使用了
synchronized
悲观锁的方式,使得
i++
线程安全我们运行一下,看看结果如何。

执行完成后,i=5000

执行完成后,i=5000

执行完成后,i=5000

我们运行3次,结果都是5000,符合预期。接下来,我们再使用Reent rantLock类来实现悲观锁。代码如下:

package cn.pottercoding.lock;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author 程序员波特
 * @since 2024年01月12日
 */
public class LockTest {
    private int i = 0;

    Lock lock = new ReentrantLock();

    public static void main(String[] args) {
        LockTest test = new LockTest();

        // 线程池,50个固定线程
        ExecutorService executorService = Executors.newFixedThreadPool(50);
        CountDownLatch countDownLatch = new CountDownLatch(5000);

        for (int i = 0; i < 5000; i++) {
            executorService.execute(() -> {
                // 修改部分,开始
                test.lock.lock();
                test.i++;
                test.lock.unlock();

                // 修改部分结束
                countDownLatch.countDown();
            });
        }

        executorService.shutdown();

        try {
            countDownLatch.await();
            System.out.println("执行完成后,i = " + test.i);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

我们在类中显示的增加了
Lock lock= new ReentrantLock();
,而且在
i++
之前增加了
lock.lock()
,加锁操作,在
i++
之后增加了
lock.unlock()
释放锁的操作。我们同样运行3次,看看结果。

执行完成后,i=5000

执行完成后,i=5000

执行完成后,i=5000

3次运行结果都是5000,完全符合预期。我们再来总结一下悲观锁,悲观锁从读取数据的时候就加了锁,而且在更新数据的时候,保证只有一个线程在执行更新操作,没有像乐观锁那样进行数据版本的比较。所以悲观锁适用于读相对少,写相对多的操作。

公平锁与非公平锁

前面我们介绍了乐观锁与悲观锁,这一小节我们将从另外一个维度去讲解锁一公平锁与非公平锁。从名字不难看出,公平锁在多线程情况下,对待每一个线程都是公平的;而非公平锁恰好与之相反。从字面上理解还是有些晦涩难懂,我们还是举例说明,场景还是去超市买东西,在储物柜存储东西的例子。储物柜只有一个,同时来了3个人使用储物柜,这时A先抢到了柜子,A去使用,B和C自觉进行排队。A使用完以后,后面排队中的第一个人将继续使用柜子,这就是公平锁。在公平锁当中,所有的线程都自觉排队,一个线程执行完以后,排在后面的线程继续使用。

非公平锁则不然,A在使用柜子的时候,B和C并不会排队,A使用完以后,将柜子的钥匙往后一抛,B和C谁抢到了谁用,甚至可能突然跑来一个D,这个D抢到了钥匙,那么D将使用柜子,这个就是非公平锁。

公平锁如图所示:

多个线程同时执行方法,线程A抢到了锁,A可以执行方法。其他线程则在队列里进行排队,A执行完方法后,会从队列里取出下一个线程B,再去执行方法。以此类推,对于每一个线程来说都是公平的,不会存在后加入的线程先执行的情况。

非公平锁入下图所示:

多个线程同时执行方法,线程A抢到了锁,A可以执行方法。其他的线程并没有排队,A执行完方法,释放锁后,其他的线程谁抢到了锁,谁去执行方法。会存在后加入的线程,反而先抢到锁的情况。

公平锁与非公平锁都在
ReentrantLock
类里给出了实现,我们看一下
ReentrantLock
的源码。

ReentrantLock
有两个构造方法,默认的构造方法中,
sync=new NonfairSync();
我们可以从字面意思看出它是一个非公平锁。再看看第二个构造方法,它需要传入一个参数,参数是一个布尔型
true
是公平锁,
false
是非公平锁。从上面的源码我们可以看出
sync
有两个实现类,分别是
FairSync

NonfairSync
,我们再看看获取锁的核心方法,首先是公平锁
FairSync
的,

然后是非公平锁
NonfairSync
的,

通过对比两个方法,我们可以看出唯一的不同之处在于
!hasQueuedPredecessors()
这个方法,很明显这个方法是一个队列,由此可以推断,公平锁是将所有的线程放在一个队列中,一个线程执行完成后,从队列中取出下一个线程,而非公平锁则没有这个队列。这些都是公平锁与非公平锁底层的实现原理,我们在使用的时候不用追到这么深层次的代码,只需要了解公平锁与非公平锁的含义,并且在调用构造方法时,传入
true

false
即可。

总结

JAVA中锁的种类非常多,在这一节中,我们找了非常典型的几个锁的类型给大家做了介绍。乐观锁与悲观锁是最基础的,也是大家必须掌握的。大家在工作中不可避免的都要使用到乐观锁和悲观锁。从公平锁与非公平锁这个维度上看,大家平时使用的都是非公平锁,这也是默认的锁的类型。如果要使用公平锁,大家可以在秒杀的场景下使用,在秒杀的场景下,是遵循先到先得的原则,是需要排队的,所以这种场景下是最适合使用公平锁的。

本文已收录至的我的公众号【程序员波特】,关注我,第一时间获取我的最新动态。

项目场景:

因为该项目比较复杂庞大,在此就简单介绍一下:
通过Three.js创建若干个物体进行了组装,从而形成了一个类似眼球模拟模型的项目,用户可以通过拖动鼠标来达到控制视角(摄像机)的目的,以此来观察整个眼球状态。


在这里插入图片描述
Image1 Three.js眼球模型

注:下面所说的正视为从红线正轴往瞳孔(黑色圆形)看去的视角,左视为从蓝线正轴往负轴看去,右视则与其相反


问题描述

左视该眼球可以看到红色的圆形平面,但是左视则发现红色平面消失。


在这里插入图片描述
Image2 左视可以正常看到红色的圆形

在这里插入图片描述
Image3 右视发现红色圆形消失



创建外部白色球体、内部蓝色不规则球体、红色平面代码:

    createSphere() {
      const radius = 2.2;
      const outerGeometry = new THREE.SphereGeometry(radius, 120, 120);
      const outerMaterial = new THREE.MeshPhongMaterial({
        color: 0xffffff, // 定义外部的球为白色
        transparent: true,
        opacity: 0.6, // 降低不透明度以减少反射
        metalness: 0.5,
        roughness: 0.3,
      });
      const outerSphere = new THREE.Mesh(outerGeometry, outerMaterial);

      // 创建一个具有水平曲面的不完全球体
      const waterSurfaceGeometry = new THREE.CircleGeometry(radius - 0.1)
      const innerGeometry = new THREE.SphereGeometry(
        radius - 0.1,
        240,
        240,
        0,
        2 * Math.PI,
        Math.PI,
        Math.PI / 2
      );
      const innerMaterial = new THREE.MeshPhongMaterial({
        color: 0x02c0f5, // 定义内部的球体为蓝色
        opacity: 1,
        metalness: 0.5,
        roughness: 0.3,
      });
      const waterSurfaceMaterial = new THREE.MeshPhongMaterial({
        color: 0xf60404, // 定义去曲面水面为红色
        opacity: 1,
        transparent:false, // 设置成不透明
        metalness: 0.5,
        roughness: 0.3,
      });
      const innerSphere = new THREE.Mesh(innerGeometry, innerMaterial);
      const waterSurface = new THREE.Mesh(waterSurfaceGeometry, waterSurfaceMaterial)
      innerSphere.scale.set(1, 1, 1); 
      innerSphere.add(waterSurface)

      this.outerSphere = outerSphere;
      this.innerSphere = innerSphere;

      const sphereGroup = new THREE.Group();
      sphereGroup.add(outerSphere);
      sphereGroup.add(innerSphere);
      this.sphere = sphereGroup; 
      this.innerSphere = innerSphere; 

      return sphereGroup;
    },


原因分析:

  1. 材质透明度问题:通过调整内部圆形的材质透明度,使其更透明,这样可以确保在摄像机视角不理想的情况下仍然能够看到内部。
  2. 光照效果:通过调整光照效果,可以改变内部圆形的明暗度,使其更加清晰可见。
  3. 内部圆形的尺寸:通过调整内部圆形的尺寸,使其在不同视角下都能够完整显示。
  4. 摄像机位置:确保摄像机的位置不会完全遮挡要显示的内容。可以尝试将摄像机向后移动或调整其位置,使其不会完全遮挡内部的圆形。

很明显,上面四种解决方案都不可行,首先红色平面的
transparent
属性为false,并且不透明度也为1;其次肯定不是光照问题,因为白色外部球体和蓝色内部球体都能正常显示;最后更不是摄像机位置问题,无论怎么调整方位都不能显示出红色圆形。


解决方案:

因此我们需要使用
双面渲染
,双面渲染能够确保从内部看到外部的表面。默认情况下,Three.js 只会渲染面的正面,通过启用 side: THREE.DoubleSide 可以使其渲染双面。
所以我们只需要在上面的代码中添加一行就能解决这个问题。

      const waterSurfaceMaterial = new THREE.MeshPhongMaterial({
        color: 0xf60404, // 定义去曲面水面为红色
        opacity: 1,
        transparent:false, // 设置成不透明
        metalness: 0.5,
        roughness: 0.3,
        side: THREE.DoubleSide  // 允许双面渲染
      });

最后我们来看看效果!!


在这里插入图片描述
Image4 右视能够正常看到红色平面

在日常工作中,数据库是我们必须使用的,其中使用最多的也是大部分中小公司的选择是Mysql,跳槽面试中也是必问的,今天我们就说一下Mysql事务

MySQL中的事务实现原理主要涉及以下几个方面:

  1. ACID特性:MySQL支持事务的原因之一是它遵循ACID(原子性、一致性、隔离性和持久性)特性。这意味着在一个事务中的所有操作要么全部成功地提交,要么全部失败回滚。这确保了数据的一致性和可靠性。
  2. 日志:MySQL使用日志来记录事务的操作和变化。MySQL有两种主要的日志类型:重做日志(Redo Log)和回滚日志(Undo Log)。
  3. 锁机制:MySQL使用锁机制来实现事务的隔离性,保证并发事务的正确执行。MySQL支持多种类型的锁,如共享锁(Shared Lock)和排他锁(Exclusive Lock),以及行级锁和表级锁等。锁机制可以防止多个事务同时修改同一个数据,保证数据的一致性。
  4. MVCC(多版本并发控制):MVCC是MySQL中的一种并发控制机制,用于在并发事务执行时保证数据的隔离性。MVCC通过在每个数据行上维护多个版本来实现。每个事务在读取数据时,会根据自己的事务ID和数据行的版本信息来确定可见的数据版本,从而实现不同事务之间的隔离性。
  5. 事务管理器:MySQL有一个事务管理器来协调和管理事务的执行。事务管理器负责事务的开始、提交、回滚和并发控制等。它还负责处理并发事务之间的冲突和死锁等问题。

综上所述,MySQL通过使用日志、锁机制、MVCC和事务管理器等技术来实现事务的原子性、一致性、隔离性和持久性。这些机制保证了数据的完整性和一致性,并提供了高并发的支持。

其中ACID四大特性,实际上分为两个部分,其中的
原子性、一致性、持久性
,实际上是由InnoDB中的两份日志来保证的,一份是
redo log日志
,一份是
undo log日志
。而隔离性是通过数据库的

,加上
MVCC
来保证的。


我们在讲解事务原理的时候,主要就是来研究一下redolog,undolog以及MVCC

事务基础ACID

事务是一组操作的集合,它是一个不可分割的工作单位,事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败。
特性
• 原子性(Atomicity):事务是不可分割的最小操作单元,要么全部成功,要么全部失败。
• 一致性(Consistency):事务完成时,必须使所有的数据都保持一致状态。
• 隔离性(Isolation):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行。
• 持久性(Durability):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的。
那实际上,我们研究事务的原理,就是研究MySQL的InnoDB引擎是如何保证事务的这四大特性的。

redo log重做日志

记录的是事务提交时数据页的物理修改,是用来实现事务的持久性。
该日志文件由两部分组成:重做日志缓冲(redo log buffer)以及重做日志文件(redo log file),前者是在内存中,后者在磁盘中。当事务提交之后会把所有修改信息都存到该日志文件中, 用于在刷新脏页到磁盘,发生错误时, 进行数据恢复使用。

如果没有redolog,可能会存在什么问题的?我们一起来分析一下。我们知道,在InnoDB引擎中的内存结构中,主要的内存区域就是缓冲池,在缓冲池中缓存了很多的数据页。当我们在一个事务中,执行多个增删改的操作时,InnoDB引擎会先操作缓冲池中的数据,如果缓冲区没有对应的数据,会通过后台线程将磁盘中的数据加载出来,存放在缓冲区中,然后将缓冲池中的数据修改,修改后的数据页我们称为脏页。而脏页则会在一定的时机,通过后台线程刷新到磁盘中,从而保证缓冲区与磁盘的数据一致。而缓冲区的脏页数据并不是实时刷新的,而是一段时间之后将缓冲区的数据刷新到磁盘中,假如刷新到磁盘的过程出错了,而提示给用户事务提交成功,而数据却没有持久化下来,这就出现问题了,没有保证事务的持久性。

那么,如何解决上述的问题呢?在InnoDB中提供了一份日志 redo log,接下来我们再来分析一下,通过redolog如何解决这个问题。

有了redolog之后,当对缓冲区的数据进行增删改之后,会首先将操作的数据页的变化,记录在redo log buffer中。在事务提交时,会将redo log buffer中的数据刷新到redo log磁盘文件中。过一段时间之后,如果刷新缓冲区的脏页到磁盘时,发生错误,此时就可以借助于redo log进行数据恢复,这样就保证了事务的持久性。而如果脏页成功刷新到磁盘或或者涉及到的数据已经落盘,此时redolog就没有作用了,就可以删除了,所以存在的两个redolog文件是循环写的。那为什么每一次提交事务,要刷新redo log 到磁盘中呢,而不是直接将buffer pool中的脏页刷新到磁盘呢 ?
因为在业务操作中,我们操作数据一般都是随机读写磁盘的,而不是顺序读写磁盘。而redo log在往磁盘文件中写入数据,由于是日志文件,所以都是顺序写的。顺序写的效率,要远大于随机写。这种先写日志的方式,称之为 WAL(Write-Ahead Logging 预写日志)。

undo log回滚日志

用于记录数据被修改前的信息 , 作用包含两个 : 提供回滚(保证事务的原子性) 和MVCC(多版本并发控制
undo log和redo log记录物理日志不一样,它是逻辑日志。可以认为当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然,当update一条记录时,它记录一条对应相反的update记录。当执行rollback时,就可以从undo log中的逻辑记录读取到相应的内容并进行回滚。
Undo log销毁:undo log在事务执行时产生,事务提交时,并不会立即删除undo log,因为这些日志可能还用于MVCC。
Undo log存储:undo log采用段的方式进行管理和记录,存放在前面介绍的 rollback segment 回滚段中,内部包含1024个undo log segment。

MVCC

全称 Multi-Version Concurrency Control,
多版本并发控制
。指维护一个数据的多个版本,使得读写操作没有冲突,快照读为MySQL实现MVCC提供了一个非阻塞读功能。MVCC的具体实现,还需要依赖于数据库记录中的
三个隐式字段、undo log日志、readView。
接下来介绍一下InnoDB引擎的表中涉及到的隐藏字段、undolog 以及 readview。

隐藏字段

当我们创建了上面的这张表,我们在查看表结构的时候,就可以显式的看到这三个字段。实际上除了这三个字段以外,InnoDB还会自动的给我们添加三个隐藏字段及其含义分别是:

隐藏字段 含义
DB_TRX_ID 最近修改事务ID,记录插入这条记录或最后一次修改该记录的事务ID。
DB_ROLL_PTR 回滚指针,指向这条记录的上一个版本,用于配合undo log,指向上一个版本。
DB_ROW_ID 隐藏主键,如果表结构没有指定主键,将会生成该隐藏字段。

而上述的前两个字段是肯定会添加的,是否添加最后一个字段DB_ROW_ID,得看当前表有没有主键,如果有主键,则不会添加该隐藏字段。

undolog

介绍

回滚日志,在insert、update、delete的时候产生的便于数据回滚的日志。
当insert的时候,产生的undo log日志只在回滚时需要,在事务提交后,可被立即删除。
而update、delete的时候,产生的undo log日志不仅在回滚时需要,在快照读时也需要,不会立即被删除。

版本链

有一张表原始数据为:

DB_TRX_ID : 代表最近修改事务ID,记录插入这条记录或最后一次修改该记录的事务ID,是自增的。
DB_ROLL_PTR :由于这条数据是才插入的,没有被更新过,所以该字段值为null。然后,有四个并发事务同时在访问这张表。


最终生成记录数据:

最终我们发现,不同事务或相同事务对同一条记录进行修改,会导致该记录的undolog生成一条记录版本链表,链表的头部是最新的旧记录,链表尾部是最早的旧记录。

readview

ReadView(读视图)是快照读 SQL执行时MVCC提取数据的依据,记录并维护系统当前活跃的事务(未提交的)id。
ReadView中包含了四个核心字段:

字段 含义
m_ids 当前活跃的事务ID集合
min_trx_id 最小活跃事务ID
max_trx_id 预分配事务ID,当前最大事务ID+1(因为事务ID是自增的)
creator_trx_id ReadView创建者的事务ID

而在readview中就规定了版本链数据的访问规则:trx_id 代表当前undolog版本链对应事务ID。

条件 是否可以访问 说明
trx_id == creator_trx_id 可以访问该版本 成立,说明数据是当前这个事务更改的。
trx_id < min_trx_id 可以访问该版本 成立,说明数据已经提交了。
trx_id > max_trx_id 不可以访问该版本 成立,说明该事务是在

ReadView生成后才开启。 |
| min_trx_id <= trx_id <= max_trx_id | 如果trx_id不在m_ids中,是可以访问该版本的 | 成立,说明数据已经提交。 |

不同的隔离级别,生成ReadView的时机不同:

  • READ COMMITTED :在事务中每一次执行快照读时生成ReadView。
  • REPEATABLE READ:仅在事务中第一次执行快照读时生成ReadView,后续复用该ReadView。

MVCC的实现原理就是通过 InnoDB表的隐藏字段、UndoLog 版本链、ReadView来实现的。而MVCC + 锁,则实现了事务的隔离性。而一致性则是由redolog 与 undolog保证。