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

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


  • 首页

  • 归档

  • 搜索

MySQL慢查询日志如何开启以及分析

发表于 2021-06-06

这是我参与更文挑战的第5天,活动详情查看: 活动链接

1、MySQL慢查询日志是什么

(1)MySQL的慢查询日志是MySQL提供的一种日志记录,它用来记录MySQL中查询时间超过(大于)设置阈值(long_query_time)的语句,记录到慢查询日志中。

(2)long_query_time的默认值是10。

2、如何开启MySQL慢查询日志

默认情况下,MySQL没有开启慢查询日志。需要手动打开,如果不是调优需要的话,不建议开启,因为开启会带来一定的性能影响,慢查询日志支持将日志记录写入文件。

(1)开启设置

1
2
sql复制代码-- 查看慢查询日志是否开启
show variables like '%slow_query_log%';

在这里插入图片描述

1
2
sql复制代码-- 开启慢查询日志,只对当前数据库生效,并且重启数据库后失效
set global slow_query_log = 1;

在这里插入图片描述

1
2
sql复制代码-- 查看慢查询日志的阈值,默认10s
show variables like '%long_query_time%';

在这里插入图片描述

1
2
sql复制代码-- 设置阈值
set long_query_time = 3;

在这里插入图片描述
(2)如果需要永久生效则修改配置文件my.cnf

1
2
3
4
5
ini复制代码[mysqld]
slow_query_log=1
slow_query_log_file=/var/lib/mysql/atguigu-slow.log
long_query_time=3
log_output=FILE

(3)运行慢查询sql,查看慢查询日志

1
sql复制代码select sleep(4);

在这里插入图片描述
(4)查询当前系统有多少条慢查询记录

1
sql复制代码show global status like '%Slow_queries%';

在这里插入图片描述

3、日志分析工具mysqldumpslow

慢查询日志多了,不利于我们进行分析。mysqldumpslow能将相同的慢SQL归类,并统计出相同的SQL执行的次数,每次执行耗时多久、总耗时,每次返回的行数、总行数,以及客户端连接信息等。

通过 –help命令,就能知道如何使用Mysqlsumpslow
在这里插入图片描述

  • -s 表示按何种方式排序。
  • c 访问次数。
  • l 锁定时间。
  • r 返回记录。
  • t 查询时间。
  • al 平均锁定时间。
  • ar 平均返回记录数。
  • at 平均查询时间。
  • -t 返回前面多少条数据。
  • -g 后面搭配一个正则匹配模式,大小写不敏感。
1
2
3
4
5
6
7
8
9
10
11
shell复制代码# 得到返回记录集最多的10 个SQL
mysqldumpslow -s r -t 10 /var/lib/mysql/atguigu-slow.log

# 得到访问次数最多的10 个SQL
mysqldumpslow -s c -t 10 /var/lib/mysql/atguigu-slow.log

# 得到按照时间排序的前10 条里面含有左连接的查询语句
mysqldumpslow -s t -t 10 -g "left join" /var/lib/mysql/atguigu-slow.log

# 另外建议在使用这些命令时结合| 和more 使用,否则有可能出现爆屏情况
mysqldumpslow -s r -t 10 /var/lib/mysql/atguigu-slow.log | more

本文转载自: 掘金

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

动态代理的的两种实现方式

发表于 2021-06-06

一、动态代理简介

动态代理实际上是JVM在运行期动态创建class字节码并加载的过程。

优势:在不修改源码的情况下,对目标方法进行相应的增强。

作用:完成程序功能之间的松耦合。

二、动态代理的两种实现

JDK代理:基于接口的动态代理技术(缺点,目标对象必须有接口,如果没有接口,则无法完成动态代理的实现)

cglib代理:基于父类的动态代理技术

两者的区别如图所示:
image.png

1. 基于JDK的实现

目标接口类:

1
2
3
4
5
csharp复制代码public interface TargetInterface {
public void save();

public void print(String str);
}

目标类:

1
2
3
4
5
6
7
8
9
10
11
typescript复制代码public class Target implements TargetInterface{

public void save() {
System.out.println("save running...");
}

public void print(String str) {
System.out.println(str);
}

}

增强类:

1
2
3
4
5
6
7
8
9
csharp复制代码public class Advice {
public void before() {
System.out.println("前置增强");
}

public void after() {
System.out.println("后置增强");
}
}

测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
java复制代码import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class ProxyTest {

public static void main(String[] args) {

//目标对象
final Target target = new Target();

//增强对象
final Advice advice = new Advice();

TargetInterface proxyInstance = (TargetInterface)Proxy.newProxyInstance(
target.getClass().getClassLoader(), //目标对象类加载器
target.getClass().getInterfaces(), //目标对象相同的接口字节码对象数组
new InvocationHandler() {
//调用代理对象的任何方法,实质执行的都是invoke方法
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable{
advice.before(); //前置增强
Object invoke = method.invoke(target, args); //执行目标方法
advice.after(); //后置增强
System.out.println();
return invoke;
}
});

//代理对象的方法测试
proxyInstance.save();

proxyInstance.print("JDK动态代理");
}

}

运行截图:

image.png

2. 基于cglib的实现

需要导入Jar包,如果是maven项目,则在pom.xml文件加入如下配置:

1
2
3
4
5
xml复制代码<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.2.4.RELEASE</version>
</dependency>

目标类:

1
2
3
4
5
6
7
8
9
csharp复制代码public class Target {
public void save() {
System.out.println("save running...");
}

public void print(String str) {
System.out.println(str);
}
}

增强类:

1
2
3
4
5
6
7
8
9
10
11
csharp复制代码public class Advice {

public void before() {
System.out.println("前置增强");
}

public void after() {
System.out.println("后置增强");
}

}

测试类:

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
java复制代码import java.lang.reflect.Method;

import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

public class ProxyTest {

public static void main(String[] args) {
final Target target = new Target();
final Advice advice = new Advice();

//返回值就是动态生成的代理对象,基于cglib
//创建增强器
Enhancer enhancer = new Enhancer();

//设置父类(目标)
enhancer.setSuperclass(Target.class);

//设置回调
enhancer.setCallback(new MethodInterceptor() {
public Object intercept(Object o, Method method, Object[] obj, MethodProxy methodProxy) throws Throwable{
advice.before();
Object invoke = method.invoke(target, obj);
advice.after();
System.out.println();
return invoke;
}
});

//创建代理对象
Target proxy = (Target)enhancer.create();

//测试代理方法
proxy.save();
proxy.print("基于cglib实现动态规划");

}

}

运行截图:

image.png

三、为什么要有基于cglib的实现

使用JDK动态代理实现时,最大限制是被增强对象必须实现接口,并且增强的方法只能是接口中声明的方法。但在实际的项目中,可能总是存在对不实现业务接口的对象进行增强的需求,这时JDK动态代理将无能为力。

本文转载自: 掘金

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

还原一个全栈开发者的开发环境

发表于 2021-06-05

这是我参与更文挑战的第4天,活动详情查看:更文挑战

已经不知道换了多少次开发环境,从 Windows 到 Linux,再到 MacOS,更多还是 Linux,但每次都是凭着记忆还原曾经熟悉的那个开发环境,这个过程要花好几天,所以这次我决定记录下这整个过程。

开发环境

最近,由于开发用的一台服务器将要到期了,所以新的服务器需要重新配置环境,借此机会记录下我还原整个开发环境的过程。

我是一个全栈后端开发者,主要写 Golang 等,使用的 IDE 是 VSCode,再加上它的利器 Remote - SSH,远程开发让我很少再使用本地的环境进行开发了,因为这可以让我在切换设备后,仍可以正常工作!

我的开发环境(远程服务器)包括以下内容:

  • Proxy
  • Oh-My-Zsh
  • Go
  • Docker
  • Git

Proxy

所有的开始,都要从配置代理开始,这将会加快整个过程。

作为一名国内开发者,很多时候都需要访问或者下载一些国外的资源,当然可以通过一些国内源代替,但是最好的解决办法还是配置一个代理。这里说明一下如何在 Linux 上配置代理,我们只需要在 ~/.bash_profile 文件里添加下面的内容:

1
2
3
4
5
6
7
8
9
10
11
12
bash复制代码proxy_ip="127.0.0.1"
proxy_port="7890"
proxy_addr="$proxy_ip:$proxy_port"
http_proxy="http://$proxy_addr"
socks5_proxy="socks5://$proxy_addr"
alias proxy='export https_proxy=$http_proxy \
http_proxy=$http_proxy \
ftp_proxy=$http_proxy \
rsync_proxy=$http_proxy \
all_proxy=$socks5_proxy \
no_proxy="127.0.0.1,localhost"'
alias unproxy='unset https_proxy http_proxy ftp_proxy rsync_proxy all_proxy no_proxy'

保存后,执行下面的命令即可生效:

1
bash复制代码source ~/.bash_profile

在需要使用代理的时候,执行 proxy 即可开启代理,不需要的时候,执行 unproxy 即可关闭。

在后面我们配置好 Oh-My-Zsh 后,可以将上面写在 ~/.bash_profile 里的内容移到 ~/.zshrc 文件中。

Oh-My-Zsh

bash 固然可以,但我更喜欢使用 zsh + Oh-My-Zsh,因为它更加好看,也拥有更多的插件可以使用,可以帮助我节省很多时间。

Oh-My-Zsh 是 zsh 的一个框架,所以安装 Oh-My-Zsh 之前,需要先安装 zsh,我用的系统是 CentOS 7,所以需要使用下面的方法进行安装:

1
bash复制代码sudo yum update && sudo yum -y install zsh

如果是其他系统,可以参考这个 文档 进行安装 zsh。

安装完 zsh 后,我们就可以安装 Oh-My-Zsh 了,只需一步:

1
bash复制代码sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"

到这里,就安装好了 Oh-My-Zsh,但我还会用到一些插件,比如 git、zsh-syntax-highlighting、zsh-autosuggestions 等,下面是我的配置文件 ~/.zshrc:

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
bash复制代码export ZSH="/home/k8scat/.oh-my-zsh"
ZSH_THEME="robbyrussell"

plugins_folder="${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/plugins"
syntax_highlight_plugin="${plugins_folder}/zsh-syntax-highlighting"
[[ ! -d "$syntax_highlight_plugin" ]] && git clone https://github.com/zsh-users/zsh-syntax-highlighting.git $syntax_highlight_plugin
autosuggestions_plugin="${plugins_folder}/zsh-autosuggestions"
[[ ! -d "$autosuggestions_plugin" ]] && git clone https://github.com/zsh-users/zsh-autosuggestions $autosuggestions_plugin
[[ -z $(pip list | grep -E "^wakatime ") ]] && pip install --user wakatime
wakatime_plugin="${plugins_folder}/wakatime"
[[ ! -d "$wakatime_plugin" ]] && git clone https://github.com/sobolevn/wakatime-zsh-plugin.git $wakatime_plugin
[[ ! -s "$HOME/.wakatime.cfg" ]] && cat > $HOME/.wakatime.cfg <<EOF
[settings]
api_key = xxx
EOF
plugins=(git zsh-syntax-highlighting zsh-autosuggestions wakatime docker docker-compose)

source $ZSH/oh-my-zsh.sh

export GOPRIVATE="github.com/private"
export GO111MODULE="auto"
export GOPROXY="https://goproxy.io,direct"
export GOROOT="/usr/local/go"
export GOPATH="$HOME/go"
export PATH=$PATH:$GOROOT/bin:$GOPATH/bin

proxy_ip="127.0.0.1"
proxy_port="7890"
proxy_addr="$proxy_ip:$proxy_port"
http_proxy="http://$proxy_addr"
socks5_proxy="socks5://$proxy_addr"
alias proxy='export https_proxy=$http_proxy \
http_proxy=$http_proxy \
ftp_proxy=$http_proxy \
rsync_proxy=$http_proxy \
all_proxy=$socks5_proxy \
no_proxy="127.0.0.1,localhost,192.168.8.152,192.168.8.154,192.168.8.155"'
alias unproxy='unset https_proxy http_proxy ftp_proxy rsync_proxy all_proxy no_proxy'
proxy

export PATH=$PATH:$HOME/.local/bin

Go

作为一名 Go 开发者,必不可少的当然是安装 Go。由于我一开始就配置了代理,所以使用的是国外的下载源,安装步骤主要如下:

1
2
bash复制代码curl -LO https://golang.org/dl/go1.16.5.linux-amd64.tar.gz
tar -C /usr/local -xzf go1.16.5.linux-amd64.tar.gz

然后需要配置一些环境变量:

1
2
3
4
5
6
bash复制代码export GOPRIVATE="github.com/private"
export GO111MODULE="auto"
export GOPROXY="https://goproxy.io,direct"
export GOROOT="/usr/local/go"
export GOPATH="$HOME/go"
export PATH=$PATH:$GOROOT/bin:$GOPATH/bin

Docker

Docker 真的是现代软件开发中必不可少的一个技术了,不使用 Docker 都 Out 了!

也确实,Docker 在日常开发中也有很大的帮助,比如需要一个 MySQL 数据库,我们只需一行命令就可以跑起来:

1
bash复制代码docker run -d -p 3306:3306 mysql:8

这比以前安装一个 MySQL 折腾半天可要简单太多了!所以如果你现在还不会使用 Docker 的话,赶紧学起来吧!

下面是如何在 CentOS 上安装 Docker:

1
2
3
4
5
6
7
8
9
10
11
12
13
bash复制代码sudo yum install -y yum-utils
sudo yum-config-manager \
--add-repo \
https://download.docker.com/linux/centos/docker-ce.repo
sudo yum install docker-ce docker-ce-cli containerd.io

# 让 Docker 服务开机自启动
sudo systemctl enable docker
# 启动 Docker
sudo systemctl start docker

# 验证
sudo docker run hello-world

其他系统安装 Docker 可以参考这个 文档。

Git

版本控制是开发过程中必不可少的一个环节,而 Git 就是这个环节的一大利器。在 CentOS 上,我们可以快速使用下面的命令安装 Git:

1
bash复制代码sudo yum install -y git

但是,这样安装的 Git 的版本比较低,或者不是我们想要的版本,我们可以选择去 GitHub 上面下载需要的发布版本,然后基于源码编译安装,下面是如何安装 Git v2.29.2 的过程:

1
2
3
4
5
6
7
8
9
bash复制代码# 安装依赖
sudo yum install -y gcc openssl-devel expat-devel curl-devel

# 下载源码
curl -LO https://github.com/git/git/archive/v2.29.2.tar.gz
tar zxf v2.29.2.tar.gz
cd git-2.29.2
make prefix=/usr
sudo make prefix=/usr install

本地 VSCode

到此,远程服务器的环境大致算是完成了,然后本地 VSCode 需要安装插件 Remote - SSH,然后配置 ~/.ssh/config 文件,添加以下内容:

1
2
3
4
5
config复制代码Host dev-server
HostName 40.18.95.22
User k8scat
Port 9232
IdentityFile ~/.ssh/id_rsa

最后,便可以开始远程开发之旅了,下班后回家再也不用带电脑了,回到家继续连上远程 Coding…

个人博客

k8scat.com/posts/dev-w…

本文转载自: 掘金

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

生成图片验证码-Google Kaptcha|Java 开发

发表于 2021-06-05

这是我参与更文挑战的第 4 天,活动详情查看:更文挑战

本文正在参加「Java主题月 - Java 开发实战」,详情查看:活动链接

序言

系统开发过程中,安全往往是系统设计的首要考虑因素。验证码认证、短信验证、邮件以及安全框架Shiro和Spring Secruity对应的jwt等各种认证方式。本文主要针对本人在开发过程中使用的图片验证码进行登录校验的第一步,简要描述下Google Kaptcha生成动态图片验证码的主要流程和使用过程。

开发流程

1.pom文件引入kaptcha对应的依赖

1
2
3
4
5
xml复制代码<dependency>
<groupId>com.google.code.kaptcha</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3</version>
</dependency>

其中,version标签中的版本号可以根据自己使用情况选择

2.controller写对应api接口生成图片验证码,并把验证码对应的key放入session,登录认证时取出key对应的验证码和前端传入的做比对。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
arduino复制代码@GetMapping("getCaptchaCode")
public void getCaptchaCode(HttpServletResponse response, HttpServletRequest request) throws ServletException, IOException {
response.setHeader("Cache-Control", "no-store, no-cache");
response.setContentType("image/jpeg");

// 生成文字验证码
String text = producer.createText();
// 生成图片验证码
BufferedImage image = producer.createImage(text);
// 保存到验证码到 session
request.getSession().setAttribute(Constants.KAPTCHA_SESSION_KEY, text);//Constants.KAPTCHA_SESSION_KEY为自己定义的存储常量

ServletOutputStream out = response.getOutputStream();
ImageIO.write(image, "jpg", out);
IOUtils.closeQuietly(out);
}

3.登录认证,将前端传送过来的验证码和session中存储的验证码做校验,匹配上返回true,然后做账号和密码的校验,反之为false,直接给出具体的错误提示信息。

1
2
3
4
5
6
7
8
scss复制代码// 从session中获取之前保存的验证码跟前台传来的验证码进行匹配
Object kaptcha = request.getSession().getAttribute(Constants.KAPTCHA_SESSION_KEY);
if(kaptcha == null){
return HttpResult.error(“验证码已失效”);
}
if(!captcha.equals(kaptcha)){
return HttpResult.error(“验证码不正确”);
}

4.此处为第2步中生成验证码引用的io关闭流工具类。众所周知,流在使用完以后,要及时关闭,比如大家常使用的文件流等,在对文件处理完毕后,要及时处理,否则将严重消耗系统资源,造成不可预知的一些问题等。此处也是如此,要对io流做及时关闭处理

1
2
3
4
5
6
7
8
9
typescript复制代码public static void closeQuietly(Closeable closeable) {
try {
if(closeable != null) {
closeable.close();
}
} catch (IOException var2) {
;
}
}

结尾

其实,验证码还有一些其他生成方式,本文只是其中一种,更普遍或者企业常用的可能是在项目中声明一个工具类,然后生成验证码对应的base64位流返回给前端,并将对应的key存入redis,然后前端利用img标签的方式,将base64位流以图片方式呈现出来。有兴趣的朋友可以参考我之前写的一篇文章(base64位流验证码生成及校验),此处不再做详细的展示。

本文转载自: 掘金

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

golang-chanel 并发设计 以及使用常见问题 1

发表于 2021-06-05
  1. channel 的设计模式CSP是什么?

1.1 CSP是什么?对于传统的共享内存有什么好处?

  • CSP:Communicating Sequential Processes,顺序通信进程,核心就是多个线程之间通过channel来进行通信。这个也对应golang社区的一句经典语** 不要通过共享内存的方式进行通信,而是应该通过通信的方式共享内存**
  • 这种设计模式带来的好处有哪些(对比共享内存)?
    • 解耦合,通过数据冗余来避免程序员频繁直接操作 锁,降低编码复杂度
    • 便于分布式通信(目前在golang channel没有体现)

1.2 对比 并发模型 Actor 和 CSP

  • 什么是Actor?
    • Actor模式是一种并发模型,与另一种模型共享内存完全相反,Actor模型share nothing。所有的线程(或进程)通过消息传递(邮箱)的方式进行合作,这些线程(或进程)称为Actor
  • actor 和 CSP 对比图 ?第一张图为actor 第二张为 channel

image.png
image.png

  • csp模型和actor模型有什么区别
    • CSP 通信中goroutine是和channel** 解耦合的**,actor 和 传递消息的 “邮箱” 是耦合的
    • CSP 通信是同步的,Actor则是异步的。golang中channel是进行优化后支持缓存
    • CSP 通信中通过channel通信的两个goroutine,由于有channel的存在,所以相互是匿名的,而Actor中的Actor实例则不是
    • CSP 模型能够保证顺序性,而Actor模型不行。比如 当生产为 一个 ,而消费者为多个的时候

​

  1. channel 底层结构是什么样的 ?

  • 具体channel里面的各种功能可以参考源码runtime/chan.go:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
go复制代码type hchan struct {
qcount uint // total data in the queue
dataqsiz uint // size of the circular queue
buf unsafe.Pointer // points to an array of dataqsiz elements
elemsize uint16
closed uint32
elemtype *_type // element type
sendx uint // send index
recvx uint // receive index
recvq waitq // list of recv waiters
sendq waitq // list of send waiters

// lock protects all fields in hchan, as well as several
// fields in sudogs blocked on this channel.
//
// Do not change another G's status while holding this lock
// (in particular, do not ready a G), as this can deadlock
// with stack shrinking.
lock mutex
}
  1. channel使用中遇到过的问题有哪些?怎么解决?

3.1 向一个关闭了的channel写入了数据, 导致Panic

  • 解决办法1: 写入的goroutineshi来关闭channel,而不是由读取的channel来进行关闭
  • 解决办法2: 当遇到多个写入的groutine的时候,通过sync.waitGroup来进行阻塞关闭

3.2 一个有缓存的channel已经关闭,还是能够读取到数据

  • 要考虑兼容此类情况,不能说已经close掉了channel就一定没有数据了,代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
go复制代码package main

import (
"fmt"
"testing"
"time"
)


func OnlyRead(testCh <-chan string) {
result, ok := <-testCh
fmt.Println(result, ok) // 这里会输出:msg1 true
}

func TestChannel(t *testing.T) {

testCh := make(chan string, 5)
testCh <- "msg1"
close(testCh)
go OnlyRead(testCh)
time.Sleep(time.Second)

}

3.3 某个函数中对channel进行写入又对channel进行读取,导致复杂度提升

  • 通过传参进行限制只读或者是只写,代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
go复制代码package main

import (
"fmt"
"testing"
"time"
)

func OnlyWrite(testCh chan<- string) {

testCh <- "msg2"
}

func OnlyRead(testCh <-chan string) {
for result := range testCh {
fmt.Println(result, "====")
}
}

func TestChannel(t *testing.T) {

testCh := make(chan string, 2)
testCh <- "msg1"
go OnlyRead(testCh)
go OnlyWrite(testCh)
time.Sleep(time.Second * 1)
close(testCh)

}
  1. 参考链接

csp 和 actor的区别:

  • ​www.quora.com/What-are-th…
  • ​engineering.universe.com/introductio…

本文转载自: 掘金

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

SQL窗口函数详解 1 概述 2 分组:PARTITION

发表于 2021-06-05

最近看完《SQL基础教程》,感觉其中的窗口函数部分讲得不太清楚,同时发现网上的许多教程也存在类似的问题,主要体现在指定窗口框架(即ROWS|RANGE BETWEEN)部分讲解得不够清楚和深入,所以在这里凭借我的个人理解把窗口函数相关内容梳理一遍,欢迎指正!

1 概述

窗口函数是只能用于SELECT子句中的,可以返回基于分组的、与指定的相邻若干行相关的统计值的函数。

  • 基本组成:
1
2
3
sql复制代码[函数名] OVER ([分组 PARTITION BY] 
[排列顺序 ORDER BY]
[窗口框架])
  • 执行顺序:分组–>按顺序排列–>对每个窗口执行函数

例如:

1
2
3
4
5
sql复制代码SELECT product_id, product_name, product_type, sale_price,
SUM(sale_price) OVER (PARTITION BY product_type
ORDER BY product_id DESC
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) sum
FROM Product;

|400

这个窗口函数按顺序执行了以下操作:

  • PARTITION BY product_type:按product_type分组
  • ORDER BY product_id DESC:每组内按product_id降序排列
  • ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING:指定当前行及相邻两行作为窗口
  • SUM(sale_price):在每个窗口内执行对sale_price的求和

2 分组:PARTITION BY

选择一个变量作为分组条件,之后的所有操作,包括排序、窗口划分、函数执行都在每个组内进行,组间互不干扰。

3 排序:ORDER BY

在某一组内,按指定变量排序,以便划分窗口。

4 (重点!)窗口框架:

指定哪些行参与当前行统计值的计算。

语法:

1
sql复制代码RANGE|ROWS BETWEEN 上限 AND 下限
  • 上下限:
1
sql复制代码[CURRENT ROW] | [<num>|UNBOUNDED PRECEDING|FOLLOWING]
+ 当前行:`CURRENT ROW`
+ 上下滑动:


    - `PRECEDING`为当前行向上滑动,`FOLLOWING`为当前行向下滑动。
    - 可以使用`<num>`指定向上/下滑动的单位数(`ROW`和`RANGE`单位含义不同),如`2 PRECEDING`代表包括本行、本行之上一单位与本行之上两单位;或者使用`UNBOUNDED`,代表选中当前行之上/下的所有。
+ 若想表示当前行及其上方若干单位,可省略`BETWEEN AND`,简写为



1
sql复制代码ROWS|RANGE <num>|UNBOUNDED PRECEDING
  • ROWS与RANGE
+ `ROWS`代表每一行在表中的绝对位置,滑动的每一单位为一行,可以脱离`ORDER BY`使用。
+ `RANGE`必须搭配`ORDER BY`使用,表示`ORDER BY`指定变量的值。滑动的单位为值变化1,上下限代表值的区间,如`1 PRECEDING`代表当前行的值向上变动1单位。注意是向”上“变动1单位而不是”加“或”减“1单位,因为这与`ORDER BY`的升降序有关。升序时为减,降序时为加,但不变的是都是朝表的上方向。例如:
1
2
3
4
5
6
7
8
sql复制代码SELECT s_id, s_score,
SUM(s_score) OVER (ORDER BY s_score
ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) row_,
SUM(s_score) OVER (ORDER BY s_score
RANGE BETWEEN 9 PRECEDING AND CURRENT ROW) range_9,
SUM(s_score) OVER (ORDER BY s_score
RANGE BETWEEN 10 PRECEDING AND CURRENT ROW) range_10
FROM Student;

|250

  • 默认值
+ 当`ORDER BY`与窗口都缺失时,默认为

1
sql复制代码ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
+ 指定`ORDER BY`,缺失窗口时,默认为
1
sql复制代码RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW

若触及边界,则将边界外的行省去,如第一个例子中的绿色部分。

5 函数

  • 专用窗口函数
    • RANK:排名,同名次的行算多个
    • DENSE_RANK:排名,排名同名次的行算一个
    • ROW_NUMBER:按顺序编号
      例如:
1
2
3
4
5
sql复制代码SELECT s_id, s_score,
RANK() OVER (ORDER BY s_score) rank_ ,
DENSE_RANK() OVER (ORDER BY s_score) dense_rank_ ,
ROW_NUMBER() OVER (ORDER BY s_score) row_num_
FROM Student;

|300

  • 聚合函数
    • COUNT()
    • SUM()
    • AVG()
    • MIN()/MAX()

本文转载自: 掘金

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

Golang中的内存模型

发表于 2021-06-05

Golang中的内存模型

The Go memory model specifies the conditions under which reads of a variable in one goroutine can be guaranteed to observe values produced by writes to the same variable in a different goroutine.

  • 翻译过来就是说,Go内存模型通过定义以下的条件,来保证在一个goroutine可以观察到另外的goroutine对这个相同变量的写操作。

Happens Before

在一个goroutine中,读和写的顺序一定是安装程序中的顺序执行的。编译器和处理器只有在不会在改变这个goroutine的行为的时候,才可能修改读和写的执行顺序。

但是由于重排,不同的gouroutine会看到不同的执行顺序。

例如,一个goroutine执行a = 1;b = 2;,另一个goroutine可能看到b在a之前更新。

为了说明读和写的必要性,go引入了Happens Before原语,来对内存模型中指定的条件进行描述。Happens Before翻译过来就是先行发生的意思。

Happens Before的定义:

  • 如果事件e1发生在事件e2之前,那么我们说e2发生在事件e1之后。同样,如果e1不发生在e2之前,也不发生在e2之后,那么我们说e1和e2同时发生。

内存模型指定了哪些条件

go的Happens Before定义了2组条件:

第一组条件:

当上面2个条件都满足的时候,对变量v的读操作r是允许对v的写入操作w进行监测的

  • r不先行发生于w
  • 在w后r前没有对v的其他写操作

第二组条件:

为了确保对变量v的读取操作r能够监测到特定的对v的写操作w, 需要确保w是r允许看到的唯一写操作。即当下面条件满足时,则r能保证监测到w

  • w先行发生于r
  • 对共享变量v的其它任何写入操作都只能发生在w之前或r之后

在单个goroutine中,这2组条件的定义是一样的。但是,如果在多goroutine的环境下,第二组条件要求会更严格,因为它需要确保没有其他的写入操作与w或者r同时发生

我们看第一组条件,为什么说它没有第二组条件这么严格,r不先行发生于w,并不意味着,r就在w之后,因为它们可以是同时发生。

因此,需要特别注意的是,第一组条件的说法,在2个并发的goroutine来说,一个goroutine能否读到另一个goroutine中写入的数据是不确定的,可能可读到,可能读不到。

第二组条件,由于r发生在w之后,对共享变量v的其它任何写入操作都只能发生在w之前或r之后,意思就是说在r到w这一段期间之间,没有其他的写操作w',也没有和r并行的写操作w''发生,所以我们可以说,r读到的值必然是w写入的值。

下面这个图是从go编译语言网站扣过来的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
diff复制代码单Go程的情形:
-- w0 ---- r1 -- w1 ---- w2 ---- r2 ---- r3 ------>

这里不仅是个偏序关系,还是一个良序关系:所有 r/w 的先后顺序都是可比较的。

双Go程的情形:
-- w0 -- r1 -- r2 ---- w3 ---- w4 ---- r5 -------->
-- w1 ----- w2 -- r3 ---- r4 ---- w5 -------->

单Go程上的事件都有先后顺序;而对于两条Go程,情况又有所不同。即便在时间上 r1 先于 w2 发生,
但由于每条Go程的执行时长都像皮筋一样伸缩不定,因此二者在逻辑上并无先后次序。换言之,即二者并发。
对于并发的 r/w,r3 读取的结果可能是前面的 w2,也可能是上面的 w3,甚至 w4 的值;
而 r5 读取的结果,可能是 w4 的值,也能是 w1、w2、w5 的值,但不可能是 w3 的值。


双Go程交叉同步的情形:
-- r0 -- r1 ---|------ r2 ------------|-- w5 ------>
-- w1 --- w2 --|-- r3 ---- r4 -- w4 --|------->

现在上面添加了两个同步点,即 | 处。这样的话,r3 就是后于 r1 ,先于 w5 发生的。
r2 之前的写入为 w2,但与其并发的有 w4,因此 r2 的值是不确定的:可以是 w2,也可以是 w4。
而 r4 之前的写入的是 w2,与它并发的并没有写入,因此 r4 读取的值为 w2。

个人理解:

通过上面的解释,我们知道,在多个goroutine的情形下,如果我们不添加同步控制,那么,所有的goroutine的运行都是”平行”并发的,类似于在2个维度运行,对于在2个纬度里面的读或者写操作,我们没办法判定谁先谁后,所以必须要引入同步控制。引入同步控制之后,在2个goroutine里面,才有了一个”相同的节点”,在这个”相同的节点”的两边,也就有了执行的先后顺序。不过两个”节点”之间的部分,同样还是可以自由伸缩,没有先后顺序的。

golang中有哪些同步的机制呢?

1. 初始化

Program initialization runs in a single goroutine, but that goroutine may create other goroutines, which run concurrently.

If a package p imports package q, the completion of q’s init functions happens before the start of any of p’s.

The start of the function main.main happens after all init functions have finished.

  • 程序的初始化运行在单个goroutine中,但该goroutine可能会创建其它并发运行的goroutine
  • 若包p导入了包q,则q的 init 函数会在 p 的任何函数启动前完成。
  • 函数 main.main 会在所有的 init 函数结束后启动。

2. goroutine的创建

The go statement that starts a new goroutine happens before the goroutine’s execution begins.

  • goroutine的创建happens before 这个goroutine的执行

3. goroutine的销毁

The exit of a goroutine is not guaranteed to happen before any event in the program.

  • Go程无法确保在程序中的任何事件发生之前退出。
1
2
3
4
5
6
go复制代码var a string

func hello() {
go func() { a = "hello" }()
print(a)
}

对 a 进行赋值后并没有任何同步事件,因此它无法保证被其它任何Go程检测到。 实际上,一个积极的编译器可能会删除整条 go 语句。

若一个Go程的作用必须被另一个Go程监测到,需使用锁或信道通信之类的同步机制来建立顺序关系。

4. channel

Channel communication is the main method of synchronization between goroutines. Each send on a particular channel is matched to a corresponding receive from that channel, usually in a different goroutine.

  • channel是goroutine之间进行同步的主要方法。在特定通道上的每一次发送都有与之对应的接收操作相匹配,这通常是在不同的goroutine上发生。

channel有4种不同的同步方式,确保A事件happens before B事件。

第一种:

A send on a channel happens before the corresponding receive from that channel completes.

对一个channel的发送操作先行发生于它的接收操作,

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
go复制代码var c = make(chan int, 10)
var a string

func f() {
a = "hello, world"
c <- 0
}

func main() {
go f()
<-c
print(a)
}

它能保证打印出hello,world,因为a的赋值,先行发生于对于c的发送;对于C的发送,先行发生于C的接收;对于C的接收先行发生于print。

第二种:

The closing of a channel happens before a receive that returns a zero value because the channel is closed.

channel的关闭先行发生于channel返回零值

例如上面的代码, c <- 0, 可以改成close(c)。 其实效果是一样的

第三种:

A receive from an unbuffered channel happens before the send on that channel completes.

在无缓冲的channel中,对其的接收操作要先行发生于对channel的发送完成

例如:

1
2
3
4
5
6
7
8
9
10
11
12
go复制代码var c = make(chan int)
var a string

func f() {
a = "hello, world"
<-c
}
func main() {
go f()
c <- 0
print(a)
}

上面也是保证能打印出hello, world, 因为对a的赋值,先行发生于对应channel C的接收;对于C的接收,先行发生于对C的发送完成;对于C的发送完成先行发生于print

第四种:

The kth receive on a channel with capacity C happens before the k+Cth send from that channel completes.

This rule generalizes the previous rule to buffered channels. It allows a counting semaphore to be modeled by a buffered channel: the number of items in the channel corresponds to the number of active uses, the capacity of the channel corresponds to the maximum number of simultaneous uses, sending an item acquires the semaphore, and receiving an item releases the semaphore. This is a common idiom for limiting concurrency.

对第k次的接收操作,先行发生于第k+1次的发送完成

此规则将前面的规则推广到缓冲通道。它允许一个计数信号量由一个缓冲通道来建模:通道中的项目数对应于活动使用的数量,通道的容量对应于同时使用的最大数量,发送一个项目获得信号量,接收一个项目释放信号量。这是限制并发的常用习惯用法。

这种方式通常可以用于控制并发的数量。

例如下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
go复制代码var limit = make(chan int, 3)

func main() {
for _, w := range work {
go func(w func()) {
limit <- 1
w()
<-limit
}(w)
}
select{}
}

就限制了每次最多只能由3个work线程进行工作。

锁

sync 包实现了两种锁的数据类型:sync.Mutex 和 sync.RWMutex。

对于任何 sync.Mutex 或 sync.RWMutex 类型的变量 l 以及 n < m ,对 l.Unlock() 的第 n 次调用在对 l.Lock() 的第 m 次调用返回前发生。

此程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
csharp复制代码var l sync.Mutex
var a string

func f() {
a = "hello, world"
l.Unlock()
}

func main() {
l.Lock()
go f()
l.Lock()
print(a)
}

可保证打印出 “hello, world”。该程序首先(在 f 中)对 l.Unlock() 进行第一次调用,然后(在 main 中)对 l.Lock() 进行第二次调用,最后执行 print 函数。

对于任何 sync.RWMutex 类型的变量 l 对 l.RLock 的调用,存在一个这样的 n,使得 l.RLock 在对 l.Unlock 的第 n 次调用之后发生(返回),且与其相匹配的 l.RUnlock 在对 l.Lock的第 n+1 次调用之前发生。

Once类型

sync 包通过 Once 类型为存在多个Go程的初始化提供了安全的机制。 多个线程可为特定的 f 执行 once.Do(f),但只有一个会运行 f(),而其它调用会一直阻塞,直到 f() 返回。

通过 once.Do(f) 对 f() 的单次调用在对任何其它的 once.Do(f) 调用返回之前发生(返回)。

在此程序中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
go复制代码var a string
var once sync.Once

func setup() {
a = "hello, world"
}

func doprint() {
once.Do(setup)
print(a)
}

func twoprint() {
go doprint()
go doprint()
}
调用 twoprint 会打印两次 "hello, world" 。 第一次对 twoprint 的调用会运行一次 setup。

Go内存模型解决了什么问题?

前面说了那么多,那我们定义内存模型,到底有什么用呢?其实说到底,go中内存模型,我们主要还是需要解决的是可见性的问题,即在多线程的的环境下,A goroutine中对变量的写,如何才能让在B goroutine中的读操作可见。

多线程的环境下为什么会出现可见性的问题呢?

因为内存重排,内存重排有两块,一个是软件层面的重排,一个是硬件层面的重排。

  • 硬件层面的重排主要说的就是编译器的优化,即编译器重排。编译器重排指的是在不改变程序原有的语义的情况下,对于编译后的指令进行了优化,以提高运行效率。
  • 软件层面的重排主要说的是CPU重排。CPU的设计者为了提高CPU的执行效率和最大化的榨干它的性能,对它使用了各种手段,有分支预测、流水线等。其中也包括CPU对内存操作的重排,以提高对内存的读写效率。

重排又是如何引起可见性的问题呢?

因为为了提高CPU对内存操作的效率,CPU提出了各种策略,比如我们经常听到的三级缓存策略。

CPU的每个核心都有自己的Store Buffer, 另外还有L1-L2-L3各级缓存,CPU为了提高性能,对某个变量的操作不一定会等到扩散到内存才会进行其他操作,即读写指令的重排。那么,这就会导致,其他核在某一个时刻,读到的不一定是最新的值。

在单线程的环境下,这不会有什么问题,但是在多线程的情况下,这就会导致数据可见性的问题。因此,所有的CPU都提供了所谓的"锁"支持,称之为barrier,或者fence。

A barrier instruction forces all memory operations before it to complete before any memory operation after it can begin.

barrier 指令要求所有对内存的操作都必须要”扩散”到 memory 之后才能继续执行其他对 memory 的操作。也就是所谓的内存屏障(memory barrier)

内存屏障(Memory Barrier)分为写屏障(Store Barrier)、读屏障(Load Barrier)和全屏障(Full Barrier),其作用有两个:

  1. 防止指令之间的重排序
  2. 保证数据的可见性

关于第一点,关于指令重排,这里不考虑架构的话,Load和Store两种操作会有Load-Store、Store-Load、Load-Load、Store-Store这四种可能的乱序结果。上面提到的三种屏障则是限制这些不同乱序的机制。

关于第二点。写屏障会阻塞直到把Store Buffer中的数据刷到Cache中;读屏障会阻塞直到Invalid Queue中的消息执行完毕。以此来保证核间各级数据的一致性。

这说的各级数据,指的是L1-L2-L3-主存之间的数据一致性,但是,解决各级cache一致性的问题并不需要程序员关注,而是CPU自身实现,例如因特尔CPU的MESI协议

所以,这里要强调,内存屏障解决的只是顺序一致性的问题,不解决Cache一致性的问题(这是Cache一致性协议的责任,也不需要程序员关注)。Store Buffer和Load Buffer等组件是属于流水线的一部分,和Cache无关。这里一定要区分清楚这两点,Cache一致性协议只是保证了Cache一致性(Cache Coherence),但是不关注顺序一致性(Sequential Consistency)的问题。比如,一个处理器对某变量A的写入操作仅比另一个处理器对A的读取操作提前很短的一点时间,那就不一定能确保该读取操作会返回新写入的值。

barrier 指令要耗费几百个CPU周期,而且容易出错。因此,我们可以用高级点的 atomic compare-and-swap,或者直接用更高级的锁,通常是标准库提供。比如go中的atomic包,就提供了原子操作。

结尾

其实CPU重排这一块,还涉及到CPU多核的情况下,数据一致性的问题,这就涉及到了CPU的缓存一致性协议,比如因特尔提出的MESI协议。

以及cpu cacheline (cpu缓存), cpu的 store buffer 和 invalid queue等等。后面再写文章说说自己的理解。

本文参考

coolshell.cn/articles/20…

www.huaweicloud.com/articles/0f…

zhuanlan.zhihu.com/p/37749443

go-zh.org/ref/mem

www.jianshu.com/p/5e44168f4…

blog.csdn.net/wll1228/art…

zhuanlan.zhihu.com/p/37749443

zhuanlan.zhihu.com/p/59055457

github.com/cch123/gola…

blog.csdn.net/qcrao/artic…

本文转载自: 掘金

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

年中了,留给CentOS8的日子不多了!

发表于 2021-06-05

原创:小姐姐味道(微信公众号ID:xjjdog),欢迎分享,转载请保留出处。

2020年12 月 8 日,CentOS 开发团队在其官博宣布,CentOS 8 将在 2021 年底结束支持,也就意味着不会有 CentOS Linux 9,CentOS 10的出现了。

当时听到这个消息的我,蛋碎了一地。

现在,随着最后通牒时间越来越近,就不得不考虑后续的路怎么走。

但是,有一个好消息是,虽然CentOS8马上就要作古,CentOS7,将被支持到2024年6月30日。所以短期对于使用CentOS7的用户来说,并无太大影响。只是苦了激进采用CentOS8的用户了。

还有3年的时间来考虑下一步到底要用啥。

CentOS是免费的、稳定的发行版,完全重建于RHEL,目前最广泛应用的是CentOS7版本。CentOS8成为CentOS最短命的发行版本,是一个没开始便已经结束的版本。

红帽做出以上举措,除了商业方面的考虑,还是因为目前的发行版本,并不太像一个操作系统。CentOS6、CentOS7、CentOS8等,具有明确的版本号,从低版本Linux升级到高版本Linux,只能通过重新安装系统,代价很大。新的CentOS Stream,可以实现滚动升级的功能,只需要联网更新既可保证系统是最新状态。

所以红帽现在主打CentOS Stream版本。

但是,这样的升级方式,对于追求稳定性为主的服务器环境来说,并不见得是一件好事。Linux上海量的应用包,要适应这种升级方式,也需要有一段过渡时间。所以,最近几年,并不推荐使用CentOS的Stream版本。

该行为得到了大多数开发者和企业的讨伐,目前社区已经开始酝酿类似CentOS的发行版本,如Rocky Linux。但后续具体如何发展,还需要时间验证。如果你花了大力气升级过系统,你就更能理解这种感觉。尤其是官方说你的系统以后的安全补丁都不给你做了,就更加让人抓狂。

除了红帽系列,Linux的发行版本众多,软件包的管理方式,大体上分为apt-get和yum等两种方式,红帽属于yum系列。

\Oracle Linux**与CentOS最类似的替代品,就是Oracle Linux,但它也是基于RHEL构建的,不太推荐

\Ubuntu** 有桌面版和server版,其桌面版光环压过了server版,在服务器环境中有应用但应用不多

\OpenSUSE** 国内使用的比较少,包管理方式比较特别,但国外应用较多

\Debian** 老牌的Linux发行版,稳定可靠

\FreeBSD** Unix分支,稳定可靠,但与Linux软件有些许兼容问题,不推荐

综上所述,建议目前采取观望态度,在2024年之前,可依然使用CentOS7版本。长远看来,新的替代品将会在最近几年产生,社区是选择CentOS Stream,还是选择开源的复制品Rocky Linux,需要一个过程。

如果CentOS被淘汰,建议使用Debian作为主要的Linux Server环境,它的包管理工具是apt,最新版本是9.9,Ubuntu等其他Linux都借鉴于此。

作者简介:小姐姐味道 (xjjdog),一个不允许程序员走弯路的公众号。聚焦基础架构和Linux。十年架构,日百亿流量,与你探讨高并发世界,给你不一样的味道。我的个人微信xjjdog0,欢迎添加好友,进一步交流。

本文转载自: 掘金

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

Redis中实现事务和乐观锁--Redis实战(五)|Jav

发表于 2021-06-05

这是我参与更文挑战的第5天,活动详情查看: 更文挑战

本文正在参加「Java主题月 - Java 开发实战」,详情查看 活动链接


相关文章

Redis实战汇总:Redis实战


前言

  • 事务
    ①原子性(atomicity)。一个事务是一个不可分割的工作单位,事务中包括的操作要么都做,要么都不做。

②一致性(consistency)。事务必须是使数据库从一个一致性状态变到另一个一致性状态。一致性与原子性是密切相关的。

③隔离性(isolation)。一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。

④持久性(durability)。持久性也称永久性(permanence),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。

在Redis事务没有没有隔离级别的概念!

在Redis单条命令式保证原子性的,但是事务不保证原子性!

  • 乐观锁
    ①当程序中可能出现并发的情况时,就需要保证在并发情况下数据的准确性,以此确保当前用户和其他用户一起操作时,所得到的结果和他单独操作时的结果是一样的。

②没有做好并发控制,就可能导致脏读、幻读和不可重复读等问题。

在Redis是可以实现乐观锁的!

一、Redis如何实现事务?

①正常执行事务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bash复制代码127.0.0.1:6379> multi  #开启事务
OK
127.0.0.1:6379> set name dingyongjun #添加数据
QUEUED
127.0.0.1:6379> set age 26 #添加数据
QUEUED
127.0.0.1:6379> set high 172 #添加数据
QUEUED
127.0.0.1:6379> exec 执行事务
1) OK
2) OK
3) OK
127.0.0.1:6379> get name #获取数据成功,证明事务执行成功
"dingyongjun"
127.0.0.1:6379> get age
"26"

②放弃事务

1
2
3
4
5
6
7
8
9
10
bash复制代码127.0.0.1:6379> multi  #开启事务
OK
127.0.0.1:6379> set name dingyongjun #添加数据
QUEUED
127.0.0.1:6379> set age 26 #添加数据
QUEUED
127.0.0.1:6379> discard #放弃事务
OK
127.0.0.1:6379> get name #不会执行事务里面的添加操作
(nil)

③编译时异常,代码有问题,或者命令有问题,所有的命令都不会被执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bash复制代码127.0.0.1:6379> multi  #开启事务
OK
127.0.0.1:6379> set name dingyongjun #添加数据
QUEUED
127.0.0.1:6379> set age 23 #添加数据
QUEUED
127.0.0.1:6379> getset name #输入一个错误的命令,这时候已经报错了,但是这个还是进入了事务的队列当中
(error) ERR wrong number of arguments for 'getset' command
127.0.0.1:6379> set high 173 #添加数据
QUEUED
127.0.0.1:6379> exec #执行事务,报错,并且所有的命令都不会执行
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get name #获取数据为空,证明没有执行
(nil)

④运行时异常,除了语法错误不会被执行且抛出异常后,其他的正确命令可以正常执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
bash复制代码127.0.0.1:6379> multi  #开启事务
OK
127.0.0.1:6379> set name dingyongjun #添加字符串数据
QUEUED
127.0.0.1:6379> incr name #对字符串数据进行自增操作
QUEUED
127.0.0.1:6379> set age 23 #添加数据
QUEUED
127.0.0.1:6379> get age #获取数据
QUEUED
127.0.0.1:6379> exec #执行事务。虽然对字符串数据进行自增操作报错了,但是其他的命令还是可以正常执行的
1) OK
2) (error) ERR value is not an integer or out of range
3) OK
4) "23"
127.0.0.1:6379> get age #获取数据成功
"23"

⑤总结:由以上可以得出结论,Redis是支持单条命令事务的,但是事务并不能保证原子性!

二、Redis如何实现乐观锁?

①watch(监视)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bash复制代码127.0.0.1:6379> set money 100  #添加金钱100
OK
127.0.0.1:6379> set cost 0 #添加花费0
OK
127.0.0.1:6379> watch money #监控金钱
OK
127.0.0.1:6379> multi #开启事务
OK
127.0.0.1:6379> DECRBY money 30 #金钱-30
QUEUED
127.0.0.1:6379> incrby cost 30 #花费+30
QUEUED
127.0.0.1:6379> exec #执行事务,成功!这时候数据没有发生变动才可以成功
1) (integer) 70
2) (integer) 30

②多线程测试watch
#线程1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
bash复制代码#线程1
127.0.0.1:6379> set money 100 #添加金钱100
OK
127.0.0.1:6379> set cost 0 #添加花费0
OK
127.0.0.1:6379> watch money #开启监视(乐观锁)
OK
127.0.0.1:6379> multi #开启事务
OK
127.0.0.1:6379> DECRBY money 20 #金钱-20
QUEUED
127.0.0.1:6379> INCRBY cost 20 #花费+20
QUEUED
#这里先不要执行,先执行线程2来修改被监视的值
127.0.0.1:6379> exec #执行报错,因为我们监视了money这个值,如果事务要对这个值进行操作前
#监视器会判断这个值是否正常,如果发生改变,事务执行失败!
(nil)

#线程2

1
2
3
bash复制代码#线程2,这个在事务执行前操作执行
127.0.0.1:6379> INCRBY money 20 #金钱+20
(integer) 120

③总结:乐观锁和悲观锁的区别。
悲观锁: 什么时候都会出问题,所以一直监视着,没有执行当前步骤完成前,不让任何线程执行,十分浪费性能!一般不使用!
乐观锁: 只有更新数据的时候去判断一下,在此期间是否有人修改过被监视的这个数据,没有的话正常执行事务,反之执行失败!


路漫漫其修远兮,吾必将上下求索~
到此关于Redis的事务和乐观锁的讲解就算告一段落了,如果你认为i博主写的不错!写作不易,请点赞、关注、评论给博主一个鼓励吧~

本文转载自: 掘金

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

22 业务模型建立

发表于 2021-06-05

这是我参与更文挑战的第5天,活动详情查看: 更文挑战

内容概要

今天我们来学习一个如何建立一个业务模型,有哪些概念是需要我们掌握的,例如:ProblemFact、PlanningEntity、ProblemSolution,以及这些对象中的字段有什么作用。

如何区分ProblemFact和PlanningEntity

我们来看一下规划问题的数据,会认识到其中的对象,每一个都可以被归类为以下的一种:

  • 无任何关联的类:不被任何分数约束所使用,从规划的角度来看,这个数据是垃圾数据。
  • Problem Fact事实类:由分数约束使用,但在求解期间不改变(只要问题保持不变)。比如说。床位、房间、班次、人员、时段….问题事实类的所有属性都是问题属性。
  • Planning Entity规划实体类:由分数约束使用,在求解期间改变。比如说。CloudProcess, TimeTable, Exam, ……在求解过程中改变的属性是Planning Variable规划变量,其他属性是问题属性。
    问问自己,在求解期间,什么类会发生变化?哪个类有我想让求解器为我改变的变量?这个类就是一个Planning Entity规划实体。大多数用例只有一个规划实体类,大多数用例的每个规划实体类也只有一个规划变量。

在OptaPlanner中,所有Problem Fact和Planning Entity都是普通的JavaBeans(POJOs)。

Problem Fact

Problem Fact问题事实类是任何带有getter方法的JavaBean(POJO),在求解过程中不会改变。例如,云资源优化例子中,Computer是Problem Fact问题事实。

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
java复制代码@XStreamAlias("CloudComputer")
public class CloudComputer extends AbstractPersistable implements Labeled {

private int cpuPower; // in gigahertz
private int memory; // in gigabyte RAM
private int networkBandwidth; // in gigabyte per hour
private int cost; // in euro per month

public CloudComputer() {
}

public CloudComputer(long id, int cpuPower, int memory, int networkBandwidth, int cost) {
super(id);
this.cpuPower = cpuPower;
this.memory = memory;
this.networkBandwidth = networkBandwidth;
this.cost = cost;
}

public int getCpuPower() {
return cpuPower;
}

public void setCpuPower(int cpuPower) {
this.cpuPower = cpuPower;
}

public int getMemory() {
return memory;
}

public void setMemory(int memory) {
this.memory = memory;
}

public int getNetworkBandwidth() {
return networkBandwidth;
}

public void setNetworkBandwidth(int networkBandwidth) {
this.networkBandwidth = networkBandwidth;
}

public int getCost() {
return cost;
}

public void setCost(int cost) {
this.cost = cost;
}

// ************************************************************************
// Complex methods
// ************************************************************************

public int getMultiplicand() {
return cpuPower * memory * networkBandwidth;
}

@Override
public String getLabel() {
return "Computer " + id;
}

}

当然,一个问题事实可以参包含他问题事实。

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public class Course {

private String code;

private Teacher teacher; // Other problem fact
private int lectureSize;
private int minWorkingDaySize;

private List<Curriculum> curriculumList; // Other problem facts
private int studentSize;

// ... getters
}

一个问题事实类不需要任何OptaPlanner的注解。

Planning entity

Planning entity annotation

Planning Entity规划实体是一个JavaBean(POJO),在求解过程中会发生变化,例如一个CloudProcess会换成另一台Computer。一个规划问题有多个规划实体,例如对于云资源优化问题,每个CloudProcess都是一个规划实体。但通常只有一个规划实体类,例如CloudProcess类。

一个规划实体类,需要增加@PlanningEntity注解。

每个规划实体类有一个或多个Planning Variable规划变量。它还应该有一个或多个定义性的属性。例如,CloudProcess类中,computer作为一个规划变量,在求解过程中会发生改变,而requiredCpuPower,requiredMemory,requiredNetworkBandwidth作为一个线程的其它信息,求解过程中不会发生改变。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@PlanningEntity(difficultyComparatorClass = CloudProcessDifficultyComparator.class)
@XStreamAlias("CloudProcess")
public class CloudProcess extends AbstractPersistable {

private int requiredCpuPower; // in gigahertz
private int requiredMemory; // in gigabyte RAM
private int requiredNetworkBandwidth; // in gigabyte per hour

// Planning variables: 规划变量:规划期间的变化,分数计算之间的变化。
private CloudComputer computer;
......
}

一个规划实体类可以有多个规划变量。例如课程时间表例子,一节课程,是有老师在某个时间段上在某个教室进行授课,所以Lesson它有两个规划变量,timeslot时段和room教室。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@PlanningEntity
public class Lesson {

private Long id;

private String subject;
private String teacher;
private String studentGroup;

@PlanningVariable(valueRangeProviderRefs = "timeslotRange")
private Timeslot timeslot;

@PlanningVariable(valueRangeProviderRefs = "roomRange")
private Room room;
....
}

求解器配置需要声明每个规划实体类。

1
2
3
4
5
6
xml复制代码<solver xmlns="https://www.optaplanner.org/xsd/solver" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://www.optaplanner.org/xsd/solver https://www.optaplanner.org/xsd/solver/solver.xsd">
...
<entityClass>org.optaplanner.examples.cloudbalancing.domain.CloudProcess</entityClass>
...
</solver>

Planning Entity比较器

一些优化算法如果能估算出哪些规划实体更难规划,就能更有效地工作。例如:在垃圾箱包装中,更大的物品更难装入,在课程安排中,有更多学生的讲座更难安排,而在n个皇后中,中间的皇后更放在棋盘上。

不要尝试使用Comparator比较器来实现业务约束。它不会影响评分函数:如果我们有无限的求解时间,返回的解决方案将是一样的。

为了达到某些实体在日程表中被安排得更早的目的,添加一个分数约束来改变分数函数,使其更倾向于这种解决方案。只有在可以使求解器更有效率的情况下,才考虑也增加规划实体的Comparator。

为了让启发式方法能够利用当前优化问题的特殊信息,需要@PlanningEntity注解设置 difficultyComparatorClass属性。

1
2
3
4
java复制代码@PlanningEntity(difficultyComparatorClass = CloudProcessDifficultyComparator.class)
public class CloudProcess {
// ...
}
1
2
3
4
5
6
7
8
9
10
java复制代码public class CloudProcessDifficultyComparator implements Comparator<CloudProcess> {

public int compare(CloudProcess a, CloudProcess b) {
return new CompareToBuilder()
.append(a.getRequiredMultiplicand(), b.getRequiredMultiplicand())
.append(a.getId(), b.getId())
.toComparison();
}

}

比较器应该从高到低实现:容易的实体比较低,困难的实体比较高。例如,在垃圾箱包装中:小物品<中物品<大物品。
虽然大多数算法都是先从较难的实体开始,但它们只是把顺序颠倒过来。

针对云平衡CloudBalance的例子,它的比较器就是将所需资源(cpuPower、memory、netWorkBandwidth)更多的进程优先排在前面,让求解器可以优先解决这些需求比较大的进程。

当前的规划变量都不应该被用来比较规划实体的难度。在构建启发式方法期间,这些变量无论如何都可能是空的。例如,不应该使用CloudProcess中的computer。

Planning variable规划变量

Planning variable注解

Planning Variable规划变量是Planning Entity上的一个JavaBean属性(所以是一个getter和setter)。它指向一个规划值,该值在规划过程中会发生变化。例如,CloudProcess的computer属性就是一个真正的规划变量。高级情况下也可以有阴影变量,后续我们会学习到。

Planning Variable规划变量getter方法需要添加@PlanningVariable注解,并且需要一个非空的valueRangeProviderRefs属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
java复制代码@PlanningEntity(difficultyComparatorClass = CloudProcessDifficultyComparator.class)
@XStreamAlias("CloudProcess")
public class CloudProcess extends AbstractPersistable {

private int requiredCpuPower; // in gigahertz
private int requiredMemory; // in gigabyte RAM
private int requiredNetworkBandwidth; // in gigabyte per hour

// Planning variables: changes during planning, between score calculations.
private CloudComputer computer;

......

@PlanningVariable(valueRangeProviderRefs = {
"computerRange" }, strengthComparatorClass = CloudComputerStrengthComparator.class)
public CloudComputer getComputer() {
return computer;
}

public void setComputer(CloudComputer computer) {
this.computer = computer;
}
......

}

valueRangeProviderRefs属性定义了此规划变量的可能规划值。它引用了一个或多个@ValueRangeProvider id。

@PlanningVariable注解需要在具有@PlanningEntity注解的类中的属性上。它在没有该注解的父类或子类中被忽略。

也可以将注解添加在字段上:

1
2
3
4
5
6
7
8
java复制代码@PlanningEntity
public class Queen {
...

@PlanningVariable(valueRangeProviderRefs = {"rowRange"})
private Row row;

}

规划变量允许为空

默认情况下,一个初始化的规划变量不能为空,所以一个初始化的解决方案永远不会对其任何规划变量使用空。在一个其它用例中,这可能会产生反作用。例如:在进行任务分配时,我们宁愿让低优先级的任务不被分配,而不是把它们分配给一个超负荷的工人。

如果要允许一个初始化的规划变量为空,请将nullable设置为true。

1
2
3
4
java复制代码@PlanningVariable(..., nullable = true)
public Worker getWorker() {
return worker;
}

ConstraintProvider约束流实现时,默认会过滤掉具有null规划变量的规划实体。使用fromUnfiltered()可以避免这种情况。

1
2
3
4
java复制代码private UniConstraintStream<RockShow> getShowWithoutDate(ConstraintFactory constraintFactory) {
return constraintFactory.fromUnfiltered(RockShow.class)
.filter(rockShow -> rockShow.getDate() == null);
}

OptaPlanner会自动将数值null添加到数值范围中。无需在一个由ValueRangeProvider提供的List中添加null。

使用一个允许为null的规划变量意味着我们的分数计算要负责惩罚(甚至奖励)有null值的变量。

总结

这一篇章主要学习了OptaPlanner的Problem Fact和Planning Entity以及Planning Variable的概念,及如何在OptaPlanner去使用它。

作业

大家可以回过头来,看看之前的两个例子,来加深对这些概念的理解,这三个概念是贯穿整个OptPlanner最核心的三个概念,一定要真正的去掌握和弄懂。

结束语

下一篇章,我们会着重讲解Planning variable规划变量的其它特性,为什么没有放在这里一起讲解,是因为下一篇章内容较多且需要有一定的基础才能理解,所以在大家学习完这三个重要的概念后,再来学习它的其它特性。

创作不易,禁止未授权的转载。如果我的文章对您有帮助,就请点赞/收藏/关注鼓励支持一下吧❤❤❤❤❤❤

本文转载自: 掘金

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

1…651652653…956

开发者博客

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