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

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


  • 首页

  • 归档

  • 搜索

IIS部署Net5全流程 介绍 安装环境 发布项目 托管方

发表于 2021-05-23

介绍

Internet Information Services (IIS) 是一种灵活、安全且可管理的 Web 服务器,用于托管 Web 应用(包括 ASP.NET Core)。虽然我们的程序可以跨平台了,不过还是有些服务是部署在windows服务器下的,下面我们就从头开始部署下我们的程序到IIS.

本次示例环境:Windows Server 2012 R2 、vs2019、MySQL、.net5

安装环境

支持平台

  • Windows 7 或更高版本
  • Windows Server 2012 R2 或更高版本

本次代码将安装在Windows Server 2012 R2 版本上,感觉这个版本使用的公司还不少。

安装ASP.NET Core托管捆绑包

安装的文件应该和项目对应的版本相同,现在我项目使用的.net版本是5,那么我应该也用5的,下载地址是:此处

安装其他版本的请参考官网地址:.NET Core托管捆绑包

捆绑包可安装 .NET Core 运行时、.NET Core 库和 ASP.NET Core 模块。 该模块允许 ASP.NET Core 应用在 IIS 运行。

image.png

安装后查看应用程序目录

image.png

发布项目

新建一个net5 WebAPI程序,当前程序主要包含一个用户控制器(包含用户信息的增删改查)并且连接MySQL数据库。

项目结构如下

image.png

源码地址:gitee.com/AZRNG/my-ex… 需要自取

通过vs2019发布我们的项目,然后将发布后的项目拷贝到要部署的服务器上面。

image.png

发布后如下

image.png

为了正确设置 ASP.NET Core 模块,web.config 文件必须存在于已部署应用的根路径中。里面可以设置一些环境、日志等配置。

托管方式

进程内托管(IIS HTTP 服务器)

自 ASP.NET Core 3.0 起,默认情况下已为部署到 IIS 的所有应用启用进程内托管。

进程内托管在与其 IIS 工作进程相同的进程中运行 ASP.NET Core 应用。 进程内承载相较进程外承载提供更优的性能,因为请求并不通过环回适配器进行代理,环回适配器是一个网络接口,用于将传出的网络流量返回给同一计算机。

image

该图说明了 IIS、ASP.NET Core 模块和进程内托管的应用之间的关系

显式配置进行内托管,需要在项目文件(.csproj)中增加如下配置

1
2
3
xml复制代码<PropertyGroup>
<AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
</PropertyGroup>

进程外托管(Kestrel服务器)

由于运行 ASP.NET Core 进程与 IIS 工作进程分开,所以ASP.NET Core 模块会负责进程管理。

image

该图说明了 IIS、ASP.NET Core 模块和进程外托管的应用之间的关系

进程外托管配置,在项目文件 ( .csproj) 中将 属性的值设置为 OutOfProcess

1
2
3
xml复制代码<PropertyGroup>
<AspNetCoreHostingModel>OutOfProcess</AspNetCoreHostingModel>
</PropertyGroup>

关于两种托管方式的差异:此处

部署项目

将项目进行发布,然后拷贝到我们的服务器一个文件夹内。

打开IIS添加网站,选择物理路径为我们项目文件

image.png

修改应用程序池为无托管模式

image.png

启动程序转到swagger页面

image.png

因为当前我并没有连接数据库,直接调用接口应该报错,我们看下错误日志。启动输出日志

image.png

说明我们项目已经部署成功了

image.png

如果出现了错误可以查看点此处查看常见错误解决方案:此处

题外话:当初部署2.1版本时候,windows server 2012r2需要打好几个补丁,并且需要重启多次,没想到这次安装net5这么顺利(服务器是从朋友那借的,我自己的是linux),如果你所在公司需要部署.net,还是推荐linux进行部署。

参考文档

docs.microsoft.com/zh-cn/aspne…

微信公众号[鹏祥]

本文转载自: 掘金

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

Springboot集成GraphicsMagick

发表于 2021-05-23

上一节我们已经本地安装了GM这个工具:手把手教你本地安装GraphicsMagick,本章看看如何将这个工具集成到项目中进行开发。

图片处理服务的技术选型

技术 性能 可靠性 可维护性 活跃度 总分
JDK Image IO 7 1 6 2 16
ImageMagick 2 7 3 5 17
GraphicsMagick 10 8 6 5 29

结论:

  • JDK Image IO 有一个非常明显的问题是,在处理大图片时,很容易在内存中存在很多大对象而导致OOM
  • ImageMagick 性能着实不容乐观
  • GraphicsMagick 从 ImageMagick 中派生出来,在稳定性和性能上都更加优秀

以什么方式集成?

JNI / 命令行(im4java)

在im4java官网中提到:

image.png
翻译过来就是: 从Java内部使用JNI运行本机代码始终会带来其他风险,对于长时间运行的进程(通常是Web应用程序服务器)尤其危险。内存损坏或分段错误(可能由故意操纵的图像触发)可能会使整个服务器瘫痪。

所以我们选择使用命令行的方式进行调用。

项目集成

1、将gm命令行工具引入到项目中

在SpringBoot集成Linux可执行命令的时候,我们将可执行文件放在了项目的resource目录下:

image.png

这里需要有一步操作就是将文件复制到宿主机:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
java复制代码    private void initGM() throws Exception {
String osName = System.getProperty("os.name").toLowerCase();
log.info("os name: {}", osName);
String gmPath;
if (osName.contains("mac")) {
gmPath = "gm/mac/gm";
} else if (osName.contains("linux")) {
// 初始化容器的环境
initPodEnv();
gmPath = "gm/linux/gm";
} else {
throw new RuntimeException("非法操作系统:"+osName);
}
InputStream fisInJar = new ClassPathResource(gmPath).getInputStream();
File file = File.createTempFile("GraphicsMagick", "_gm");
file.setExecutable(true);
GM_PATH = file.getAbsolutePath();
//将jar包里的gm复制到操作系统的目录里
OutputStream fosInOs = new FileOutputStream(file);
byte[] buffer = new byte[1024];
int readLength = fisInJar.read(buffer);
while (readLength != -1) {
fosInOs.write(buffer, 0, readLength);
readLength = fisInJar.read(buffer);
}
IOUtils.closeQuietly(fosInOs);
IOUtils.closeQuietly(fisInJar);
log.info("gm初始化完毕");
}

2、在项目启动的时候自动初始化环境

下面只对Linux进行了自动化环境安装,mac环境主要是本地开发,自己安装环境即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码    /**
* 初始化容器的环境
*
* 安装gm所依赖的库
*/
private void initPodEnv() throws Exception {
log.info("============ start init pod env ============");
Process exec1 = Runtime.getRuntime().exec("yum install -y gcc make");
this.printLog(exec1);
log.info("cmd 1 exec success");

Process exec2 = Runtime.getRuntime().exec("yum install -y libpng-devel libjpeg-devel libtiff-devel jasper-devel freetype-devel libtool-ltdl-devel*");
this.printLog(exec2);
log.info("cmd 2 exec success");
// 打水印时缺少依赖
Process exec3 = Runtime.getRuntime().exec("yum -y install ghostscript");
this.printLog(exec3);
log.info("cmd 3 exec success");
log.info("============ init pod env success ============");
}

3、gm进程池化

想象下,如果在每次进行图片处理都去 fork gm子进程,不仅代价大,而且在高并发情况下,容易造成子进程过多,导致系统负载飙高,上下文切换频繁。

所以将 gm进程 池化是很有必要的。

前提: gm提供batch批量模式,运行在此模式下的gm进程,会一直读取标准输入,逐行接收命令实时进行处理。

池化思路: 预先 fork 一批 gm 子进程,每次要运行命令时,从子进程池中挑选一个子进程,进行图片处理,处理完毕后归还连接。

具体架构:

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
java复制代码/**
* GM 进程池参数
*/
@ConfigurationProperties(prefix = "gm.pool")
@Data
public class GMPoolProperties {

/**
* 连接池最大活跃数
*/
private int maxActive = 4;

/**
* 连接池最大空闲连接数
*/
private int maxIdle = 4;

/**
* 连接池最小空闲连接数
*/
private int minIdle = 2;

/**
* 资源池中资源最小空闲时间(单位为毫秒),达到此值后空闲资源将被移
*/
private long minEvictableIdleTimeMillis = 300000L;

/**
* 连接池连接用尽后执行的动作
*/
private WhenExhaustedAction whenExhaustedAction = WhenExhaustedAction.BLOCK;

/**
* 连接池没有对象返回时,最大等待时间(毫秒)
*/
private long maxWait = 5000;

/**
* 定时对线程池中空闲的链接进行校验
*/
private boolean testWhileIdle = false;

/**
* 空闲资源的检测周期(单位为毫秒)
*/
private long timeBetweenEvictionRunsMillis = 10000L;

}

性能初测

1、单线程测试: 单线程循环100次

技术 耗时 平均耗时
GraphicsMagick + im4java 2110 ms 21 ms
GraphicsMagick + im4java + 池化技术 1478 ms 15 ms
总结:性能提升约29%

2、多线程并发测试: 并发100个线程请求

技术 耗时 平均耗时
GraphicsMagick + im4java 37901 ms 379 ms
GraphicsMagick + im4java + 池化技术 22456 ms 224 ms
总结:性能提升约41%

写在最后

目前主流的是使用openresty(nginx + lua)来搭建图片处理服务,使用Java的话性能可能会比较差。因为对Java技术栈比较熟悉,前期会先使用Java实现。

本文的demo版本已经上传到github上,感兴趣的小伙伴可以去看下: github.com/Shanbw/Grap…

本文转载自: 掘金

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

大数据开发-Flink-113新特性 介绍 深入解读 Fl

发表于 2021-05-22

介绍

大概4月,Flink1.13就发布了,参加 了Flink1.13 的Meetup,收获还是挺多,从大的方面讲就是FlingSql的改进和优化,资源调度管理方面的优化,以及流批一体Flink在运行时与DataStream API的优化,另外就是State backend 模块的优化,本篇文章既是当时做的笔记,又是在后续查阅官网等做的补充,

Flink 的一个主要目标取得了重要进展,即让流处理应用的使用像普通应用一样简单和自然。Flink 1.13 新引入的被动扩缩容使得流作业的扩缩容和其它应用一样简单,使用者仅需要修改并行度即可。

这个版本还包括一系列重要改动使使用者可以更好理解流作业的效能。当流作业的效能不及预期的时候,这些改动可以使使用者可以更好的分析原因。这些改动包括用于识别瓶颈节点的负载和反压视觉化、分析运算元热点程式码的 CPU 火焰图和分析 State Backend 状态的 State 存取效能指标

深入解读 Flink SQL 1.13

在刚刚发布的 1.13 版本中,Flink SQL 带来了许多新 feature 和功能提升,在这里围绕 Winddow TVF,时区支持,DataStream & Table API 交互,hive 兼容性提升,SQL Client 改进 五个方面

  • flip-145 window tvf
+ 完整关系代数表达
+ 输入是一个关系,输出是一个关系
+ 每个关系对应一个数据集
+ cumulater window eg: 每10分钟一次统计uv,,结果准确,不会有跳变
+ window 性能优化


    - 内存,切片,算子,迟到数据
    - benchmark 测试 2x提升
+ 多维数据分析:grouping sets ,rollup,cube等
  • flip-162时区分析
+ 时区问题:proctime未考虑时区,timestamp 也没有时区,各种current\_time,now未考虑时区
+ 时间函数:current\_timestamp 返回utc+0
+ 支持 tiestamp——ltz类型 timestamp vs timestamp\_ltz
+ 纠正proctime()函数
+ 夏令时支持-同timestamp\_ltz
  • flip-163 改进sql-client,hive兼容性
+ 支持更多实用配置
+ 支持statement set
  • flip-136 增强datastrem 和 table的转换
+ 支持ds 和table转换时传递 event time 和 watermark
+ 支持changelog数据流在table和datastream间相互转换

Flink 1.13: Towards Scalable Cloud Native Application

Flink 1.13 新增了被动资源管理模式与自适应调度模式,具备灵活的伸缩能力,与云原生的自动伸缩技术相结合,能够更好地发挥云环境下弹性计算资源的优势,是 Flink 全面拥抱云原生技术生态的又一重要里程碑。本次议题将对 Flink 1.13 中的被动资源管理、自适应调度、自定义容器模板等新特性,我觉得这个的扩展才是Flink此次版本特别重要的一个feature

  • 云原生 时代 flink,k8s,声明api,可弹性扩展
  • k8s高可用-(zk,k8s可选)
  • Rescale (reactive mode → adaptive mdoe → autoscaling mode(TBD,还未支持))ci.apache.org/projects/fl…
  • Flip-158 generalized incremental checkpoints 让checkpoint更短时间
  • Pod Template 自定义Pod模板支持
  • Fine-细粒度资源管理-featrue 大概1.14支持
  • 纵向扩展资源和横向扩展资源,tm cpu → k8s, mem→no

面向流批一体的 Flink 运行时与 DataStream API 优化

在 1.13 中,针对流批一体的目标,Flink 优化了大规模作业调度以及批执行模式下网络 Shuffle 的性能,从而进一步提高了流作业与批作业的执行性能;同时,在 DataStream API 方面,Flink也正在完善有限流作业的退出语义,从而进一步提高不同执行模式下语义与结果的一致性

api下面的shuffle架构实现

  • 有限作业和无限作业,和预期结果一致
  • 大规模作业优化 consumerVetexGroup partitionGroup
  • 有限流作业结束一致性,2pc😁😁
  • 流批-数据回流
  • piplien and block-缓存主要是,离线处理

State backend Flink-1.13 优化及生产实践

  • 统一savepoint 可以 切换rocksdb
  • state-backend 内存管控,
  • checkpoint save point zhuanlan.zhihu.com/p/79526638
  • 更快速的checkpoint & falover

flink1.14 的展望

  • 删除legacy planner
  • 完善window tvf
  • 提升 schema handing
  • 增强cdc

参考

更多可以查看Flink官网 ci.apache.org/projects/fl…

参考部分:tw511.com/a/01/34869.…
查看个人资料,可以关注更多。

本文转载自: 掘金

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

Redisson 应用于复杂业务注意点和优化点

发表于 2021-05-22

为什么要使用Redis?

在游戏的跨服业务中:

  1. 如果需要多个服联动需要自定义多条跨服协议通讯(至少4条,跨服请求、返回,个人跨服请求返回),调试起来非常麻烦。
  2. 比较难保证每一个服的数据是同步的,经常发现某些服跟主服数据不一致的情况。
  3. 旧项目的跨服架构不支持玩家不在跨服发跨服个人请求,如果需要发生跨服请求,需要带大量的本服数据进行跨服请求。

是否能有一个应用在不消耗过多性能的情况独立在这些服外面的第三方应用做到统一调配几个服的数据,并且能屏蔽大多数跨服的细节专注本服的业务。所以我们使用了Redis。

先把结论放在前面:

  • 在 Redisson 框架使用过程中,问题都是出现在自己的使用方法上而并非Redis上。
  • Redis作为一个内存数据库还是很快。

1.在同一业务中,多次使用同步查询

错误发生的场景:
在同一业务中,误认为redis是内存数据库很快(事实上也是很快,但是会影响本机性能),不慎使用了多次请求。

1
2
3
4
5
6
7
8
9
10
csharp复制代码/**
* Redisson错误示范2
* 在同一业务中,多次使用同步查询
*/
@Test
public void test2() {
for (int i = 0; i < 10000; i++) {
client.getMap("test").getOrDefault(1, 0);
}
}
1
复制代码结果:同一结果多次同步查询,消耗性能

2.在异步RFuture里面用同步方法,导致同时阻塞两个线程,最终导致所有线程阻塞

错误发生的场景:
同时查询多个参数的时候,需要起多个查询
如:同时需要A、B、C数据的时候,我们需要连续起查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
arduino复制代码/**
* Redisson错误示范1,在异步RFuture里面用同步方法
* 导致同时阻塞两个线程
*/
@Test
public void test() {
for (int i = 0; i < 1000; i++) {
RMap<Integer, Integer> map = client.getMap("test");
RFuture<Integer> future = map.addAndGetAsync(1, 1);
future.onComplete((value1, throwable) -> {
System.out.println("value1 ==============" + value1);
Object value2 = client.getMap("test").getOrDefault(1, 0);
System.out.println("value2 ==============" + value2);
});
}

结果:
简单来说,就是netty线程都被阻塞了。Redisson希望你增加网络线程缓和一下并发

1
2
3
4
vbscript复制代码new RedisTimeoutException("Command still hasn't been written into connection! Increase nettyThreads and/or retryInterval settings. Payload size in bytes: " + totalSize
+ ". Node source: " + source + ", connection: " + connectionFuture.getNow()
+ ", command: " + LogHelper.toString(command, params)
+ " after " + attempt + " retry attempts");

根据上面两个问题的解决方案:

总结来说就是要解决两件事:减少与redis服务端的通讯、到本地服务器尽量异步执行

基于查询的优化方案:

方案1:如果这个请求是用于了锁,那只需要查询一次,如果有修改最后在加回去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ini复制代码/**
* Redisson解决方案1
* 问题:在同一业务中,多次使用同步查询
* 方案:如果这个请求是用于了锁,那只需要查询一次,如果有修改最后在加回去
*/
@Test
public void solve1() {
RLock lock = client.getLock("testLock");
boolean isLock = lock.tryLock();
if (isLock) {
try {
RMap<Integer, Integer> map = client.getMap("test");
Integer num = map.getOrDefault(1, 0);
Result result = new Result(num);
for (int i = 0; i < 10000; i++) {
result.addValue(1);
}
map.put(1, result.getValue());
} finally {
lock.unlock();
}
}
}

方案2:如果改数据不是频繁修改,建议使用缓存(RLocalCachedMap)

1
2
3
4
5
6
7
8
9
10
11
12
13
ini复制代码/**
* Redisson解决方案2
* 问题:在同一业务中,多次使用同步查询
* 方案:如果改数据不是频繁修改,建议使用缓存(RLocalCachedMap)
*/
@Test
public void solve2() {
RLocalCachedMap<Integer, Integer> map = client.getLocalCachedMap("test", options);
for (int i = 0; i < 1000; i++) {
Integer value = map.getOrDefault(1, 0);
System.out.println("value :" + value);
}
}

RLocalCachedMap 源码简析

  1. RLocalCachedMap 初始化的时候就会向redis服务器发送一个以自己命名的SUBSCRIBE 命令。
  2. RLocalCachedMap 在修改的时候,除了修改也会发一条以自己命名的PUBLISH 命令。接到这个命令的时候就会修改在本机的缓存 updateCache()。

RLocalCachedMap.init(初始化)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
ini复制代码private void init(String name, LocalCachedMapOptions<K, V> options, RedissonClient redisson, EvictionScheduler evictionScheduler) {
// 创建监听器
listener = new LocalCacheListener(name, commandExecutor, this, codec, options, cacheUpdateLogTime) {
@Override
protected void updateCache(ByteBuf keyBuf, ByteBuf valueBuf) throws IOException {
CacheKey cacheKey = toCacheKey(keyBuf);
Object key = codec.getMapKeyDecoder().decode(keyBuf, null);
Object value = codec.getMapValueDecoder().decode(valueBuf, null);
cachePut(cacheKey, key, value);
}
};
// 发送监听到redis服务端
listener.add(cache);
}


@Override
public <M> int addListener(Class<M> type, MessageListener<? extends M> listener) {
RFuture<Integer> future = addListenerAsync(type, (MessageListener<M>) listener);
commandExecutor.syncSubscription(future);
return future.getNow();
}


RLocalCachedMap.putOperationAsync(修改值)
@Override
protected RFuture<V> putOperationAsync(K key, V value) {
ByteBuf mapKey = encodeMapKey(key);
CacheKey cacheKey = toCacheKey(mapKey);
CacheValue prevValue = cachePut(cacheKey, key, value);
broadcastLocalCacheStore(value, mapKey, cacheKey);

if (storeMode == LocalCachedMapOptions.StoreMode.LOCALCACHE) {
V val = null;
if (prevValue != null) {
val = (V) prevValue.getValue();
}
return RedissonPromise.newSucceededFuture(val);
}

ByteBuf mapValue = encodeMapValue(value);
byte[] entryId = generateLogEntryId(cacheKey.getKeyHash());
ByteBuf msg = createSyncMessage(mapKey, mapValue, cacheKey);
// 向所有订阅了这个map的主题的服发生已经被修改的信息
return commandExecutor.evalWriteAsync(getName(), codec, RedisCommands.EVAL_MAP_VALUE,
"local v = redis.call('hget', KEYS[1], ARGV[1]); "
+ "redis.call('hset', KEYS[1], ARGV[1], ARGV[2]); "
+ "if ARGV[4] == '1' then "
+ "redis.call('publish', KEYS[2], ARGV[3]); "
+ "end;"
+ "if ARGV[4] == '2' then "
+ "redis.call('zadd', KEYS[3], ARGV[5], ARGV[6]);"
+ "redis.call('publish', KEYS[2], ARGV[3]); "
+ "end;"
+ "return v; ",
Arrays.<Object>asList(getName(), listener.getInvalidationTopicName(), listener.getUpdatesLogName()),
mapKey, mapValue, msg, invalidateEntryOnChange, System.currentTimeMillis(), entryId);
}



Redisson没有提供队列的缓存,我们可以根据上面的逻辑实现一个自己的带缓存的队列
public class RLocalCachedQueue<V> {
private ConcurrentLinkedQueue<V> cQueue = new ConcurrentLinkedQueue<V>();
private RQueue<V> rQueue;
private RTopic rTopic;

public RLocalCachedQueue(Class clazz, String name, RedissonClient client) {
super();
this.rQueue = client.getQueue(name);
this.rTopic = client.getTopic(name);
this.rTopic.addListener(clazz, (channel, msg) -> {
if (msg == null) {
return;
}
V v = (V) msg;
cQueue.add(v);
});
this.cQueue.addAll(rQueue.readAll());
}

public RFuture<Boolean> add(V v) {
RFuture<Boolean> future = rQueue.offerAsync(v);
rTopic.publish(v);
return future;
}

public ConcurrentLinkedQueue<V> getQueue() {
return cQueue;
}
}

==========================================================================================================================================================

基于修改的优化方案

以上说的就是 Redisson 查询上的优化,那添加(修改)。是不是也有优化的方案呢?
添加(修改)主要是从 减少与redis服务端的通讯 处理的。

方案1:使用Redis的管道
1
2
3
4
5
6
7
8
9
10
11
12
13
ini复制代码这个比较简单,就是把几条命令合成一条命令发给redis服务端
/**
* Redisson优化写的方案 1
* 方案:使用redis的管道功能
*/
@Test
public void write_solve1() {
RBatch batch = client.createBatch();
for (int i = 0; i < 100; i++) {
batch.getMap("test").addAndGetAsync(1, 1);
}
batch.executeAsync();
}
方案2:使用Redis的脚本

首先说下RBatch的局限,如果我想在管道里面做运算的操作是不能做到的。
如: 卖商品,当库存到0的时候,就不需要再减了。不然就会变负数了。

上面这种场景RBatch是做不到的,这个时候就需要用到Redis的lua脚本了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
swift复制代码/**
* Redisson优化写的方案 2
* 方案:使用Redis的脚本
*/
@Test
public void write_solve2() {
List<Object> keys = new ArrayList<>();
keys.add("\"stock\"");
keys.add("\"amount\"");
client.getScript().evalAsync(RScript.Mode.READ_ONLY, RedisScript.STOCK_SCRIPT, RScript.ReturnType.INTEGER, keys);
}


// 扣库存脚本
public static final String STOCK_SCRIPT =
"if (redis.call('hexists', KEYS[1], KEYS[2]) == 1) then\n" +
"local stock = tonumber(redis.call('hget', KEYS[1], KEYS[2]));\n" +
"if (stock > 0) then\n" +
" redis.call('hincrby', KEYS[1], KEYS[2], -1);\n" +
"return stock;\n" +
"end;\n" +
"return 0;\n" +
"end;";

本文转载自: 掘金

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

Android 适配 64 位架构 背景 32 和 64 位

发表于 2021-05-21

背景

64位的应用性能更好,也能运行在未来仅支持 64 位架构的设备上。目前各个应用市场也对 64 适配提出了要求。

Google Play:

自 2019 年 8 月 1 日起,在 Google Play 上发布的应用必须支持 64 位架构。

国内:

小米应用商店与OPPO应用商店、vivo应用商店等已经发出通知

  • 2021年12月底:现有和新发布的应用/游戏,需上传包含64位包体的APK包(支持双包在架,和64位兼容32位的两个形式,不再接收仅支持32位的APK包)
  • 2022年8月底:硬件支持64位的系统,将仅接收含64位版本的APK包
  • 2023年底:硬件将仅支持64位APK,32位应用无法在终端上运行

32 和 64 位区别

这里需要先说一下 CPU 类型,每种 CPU 类型对应了一种 ABI(Application Binary Interface),常见的 abi 有 armeabi、armeabi-v7a、arm64-v8a、x86、x86_64 等。

  • armeabi: 第5代、第6代的ARM处理器,早期的手机用的比较多,基本可以淘汰了
  • armeabiv-v7a: 第7代及以上的 ARM 处理器。2011年15月以后的生产的大部分Android设备都使用它
  • arm64-v8a: 第8代、64位ARM处理器
  • x86: 平板、模拟器用得比较多
  • x86_64: 64位的平板

这里主要看 arm 架构的,新的架构能够兼容旧的 abi 对应的 so,例如 arm64-v8a 架构的 CPU 能够运行 armeabi-v7a 架构的 so,反过来不行,这就是为什么现在很多 APP 只包含 armeabi-v7a 的包但是能够在最新的 CPU 上运行。如果以后的 CPU 不再兼容旧的架构了的话,现在只包含 armeabi 或者 armeabi-v7a 架构的 APP 就不能再运行了。

那如果同时包含两种架构呢?如果是支持 64 位系统的机器,会有两个Zygote(一个32位,一个64位)进程同时运行。APP 安装的时候根据 lib 目录里面支持的架构和机器自己的 CPU 类型来决定 primaryCpuAbi,在启动的时候会根据安装时候确定的 primaryCpuAbi 的值来决定是从64位还是32位的Zygote进程fork出子进程,如果从 64的 fork,则是以64位模式运行。

是否已满足 64 位要求

如果没有使用任何原生代码,那就已经满足 64 位的要求了。如何查看是否有使用原生库?,比较快捷的方法是使用 Android Studio 提供的 APK 分析器。入口在菜单 Build > Analyze APK…

查看 lib 文件夹,如果里只有 armeabi 或者 armeabi-v7a 文件夹,就是只支持 32 位,不支持 64 位。
如果同时还有 arm64-v8a 文件夹,则说明有 64 位原生库,是否与 32 位有相同的功能和质量,还需要进行测试。

快速找出不支持 64 位的原生库

应用内的原生库的来源一般有三处:

  • 第三方库
  • 工程内的 so 文件
  • C/C++ 源码模块

目前很多第三方库已经同时支持 32 和 64 位了,但是有些还不支持,如何找出这部分不支持的库或者文件呢?如果项目中的 so 文件数量很多,就很难通过肉眼的方式来查找,这里提供一个 gradle 脚本可以很方便快速得找出那些不支持 64 位的库。
在主模块的 build.gradle 最后面添加如下代码:

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
Groovy复制代码tasks.whenTaskAdded { task ->
if (task.name=='mergeDebugNativeLibs') {
task.doFirst {
println("==========================================================")
def v7a = []
def arm64 = []
it.inputs.files.each { file ->
if (file.absolutePath.endsWith("/jni")) {
// println("==========" + file.absolutePath)
if (file.isDirectory()) {
file.listFiles().each { soFileDir ->
if (soFileDir.absolutePath.contains("armeabi-v7a")) {
if (soFileDir.isDirectory()) {
soFileDir.listFiles().each {
println(it.absolutePath)
v7a.add(it.name)
}
}
}
if (soFileDir.absolutePath.contains("arm64-v8a")) {
if (soFileDir.isDirectory()) {
soFileDir.listFiles().each {
println(it.absolutePath)
arm64.add(it.name)
}
}
}
}
}
}
}
println("v7a size: ${v7a.size()}")
println("arm64 size: ${arm64.size()}")
println("so in v7a, but not in arm64:")
v7a.each {
if (!arm64.contains(it)) {
println("$it")
}
}
println("==========================================================")
}
}
}

然后执行 .\gradlew assembleDebug,这里的 Debug 可以根据实际项目中的 Flavor 进行替换。Demo 工程的输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
markdown复制代码> Task :app:mergeDebugNativeLibs
==========================================================
xxx\5d0e00f6a703ec622708978bebed0322\mmkv-static-1.2.8\jni\arm64-v8a\libmmkv.so
xxx\5d0e00f6a703ec622708978bebed0322\mmkv-static-1.2.8\jni\armeabi-v7a\libmmkv.so
xxx\7206efbbb5863dbe5969c1c384b81177\openDefault-4.2.7\jni\armeabi-v7a\libweibosdkcore.so
xxx\ccd11b53aab95a933b06ef9e74f9fb44\sentry-android-ndk-3.1.3\jni\arm64-v8a\libsentry-android.so
xxx\ccd11b53aab95a933b06ef9e74f9fb44\sentry-android-ndk-3.1.3\jni\arm64-v8a\libsentry.so
xxx\ccd11b53aab95a933b06ef9e74f9fb44\sentry-android-ndk-3.1.3\jni\armeabi-v7a\libsentry-android.so
xxx\ccd11b53aab95a933b06ef9e74f9fb44\sentry-android-ndk-3.1.3\jni\armeabi-v7a\libsentry.so
v7a size: 4
arm64 size: 3
so in v7a, but not in arm64:
libweibosdkcore.so
==========================================================

从输出可以快速看出哪些已经支持 64 位,找到哪些还不支持 64 位的 so 文件,以及他们所在的路径。

Demo 工程见:github.com/callmepeanu…

适配 64 位

找到不支持 64 位的 so 列表后,如果是第三方库或者从外部引入的 so 文件,需要看是否有适配过 64 位的新版本,或者找提供方获取支持 64 位的版本。
如果是自己项目中的 C/C++ 源码编译出来的,需要在编译选项和源码层面做对 64 位架构的适配并生成对应架构的 so 库文件。

打包

可以把所有支持的 abi 的 so 都打在一个包里,这样一个安装包就可以适配所有设备,缺点就是包体积会增大,特别是原生库比较多的情况。
目前应用市场提供了分别上传32位兼容包和64位包的能力,所以可以利用构建多个 APK 的能力来打出支持不同 abi 的包,应用市场根据用户手机 CPU 类型分发对应的包,可以减少用户下载包的大小。
支持构建多个 APK 只需要在 build.gradle 中添加如下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
Groovy复制代码android {

...

splits {
abi {
enable true
reset()
include 'armeabi-v7a', 'arm64-v8a'
universalApk false
}
}
}

生成的安装包如下图所示:

image.png

参考:

  • developer.android.com/distribute/…
  • www.fresco-cn.org/docs/multip…
  • www.infoq.cn/article/8wa…
  • dev.mi.com/distribute/…
  • dev.mi.com/distribute/…
  • developer.android.com/studio/buil…

本文转载自: 掘金

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

小米手环解锁MacOS系统笔记本MacBookPro

发表于 2021-05-21

通过小米手环解锁笔记本

官方windows是提供了方法的。
我目前用的MacBookPro,所以说下苹果笔记本的解锁方式。

安装软件BLEUnlock

库

安装方式:

brew 安装 brew install bleunlock

或下载程序 下载发布的程序

安装好打开软件:

image

设备列表选择手环,如果发现不到就在小米运动app中打开实验室选项里小米笔记本解锁开关。

实验室功能

设备列表选择你的小米手环。

选择设备

解锁RSSI与锁定RSSI 是根据你dBM值来判断是否锁定/解锁笔记本。是一个阈值。

锁定RSSI

延迟锁定,无信号超时是时间阈值。功能顾名思义。

我这里选择开启了屏保来锁定、以及开机启动。

通过以上配置之后,我们就可以通过小米手环来解锁MacOS笔记本了。

请注意一点

如果你是每天背着本上下班的话,那我建议上下班前后别开启此功能。
为什么呢,因为你设定的RSSI值肯定是离近笔记本的。这时候你带着手环和笔记本的时候。他很容易就吧本解锁了。然后你发现从书包拿出来本巨热无比。

为啥呢,他唤醒了设备啊 还解锁了~

这点我不知道怎么搞定呢,好 结束~

祝好

拜拜~

本文转载自: 掘金

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

Sublime Text 4 首个稳定版本终于来了!

发表于 2021-05-21

下载Sublime Text 4

许可证变化

Sublime Text的许可证密钥不再与单一的主要版本绑定,相反,它们现在对购买后3年内的所有更新有效。之后,你仍然可以完全访问3年内发布的每个版本的Sublime Text,但较新的版本需要一个许可证升级。这些是我们对Sublime Merge使用的相同的许可条款,它们使我们能够在更新准备好后立即提供更频繁和令人兴奋的更新,而不必将它们卷进一个新的主要版本。

标签多选

文件标签已经得到增强,使分割视图毫不费力,整个界面和内置命令都支持。侧边栏、标签栏、Goto Anything、Goto Definition、自动完成等都经过调整,使代码导航比以往更容易、更直观。

image.png

Apple Silicon 和 Linux ARM64

Sublime Text for Mac现在包括对Apple Silicon处理器的本地支持。Linux ARM64构建也可用于Raspberry Pi等设备。

刷新的用户界面

默认和自适应主题已经被刷新了,有了新的标签样式和非活动窗格的调光。主题和配色方案支持自动黑暗模式切换。Windows和Linux上的自适应主题现在具有自定义标题栏。

上下文感知的自动填充

自动填充的引擎已被重写,以提供基于项目中现有代码的智能填充。建议还增加了有关其种类的信息,并提供定义的链接。

image.png

TypeScript, JSX和TSX支持

对最流行的新编程语言之一的支持现在是默认提供的。在现代JavaScript生态系统中利用Sublime Text的所有基于语法的智能功能。

超级强大的语法定义

语法高亮引擎得到了极大的改进,具有处理非确定性语法、多行结构、懒人嵌入和语法继承等新功能。内存用量已经减少,加载时间也比以前快。

GPU渲染

Sublime Text现在可以在Linux、Mac和Windows上利用你的GPU来渲染界面。这使得流畅的用户界面一直到8K分辨率,同时比以前使用更少的电力。

  • 关于OpenGL渲染的博文

更新了Python API

Sublime Text API已经更新到Python 3.8,同时保持与为Sublime Text 3构建的软件包的向后兼容性。该API已被大大扩展,增加了一些功能,使LSP等插件比以前更好地工作。阅读更新后的文档这里。

兼容性

Sublime Text 4 与版本 3 完全兼容。它将自动拾取你的会话和配置。当然也可以分开。

完整的 changelog

www.sublimetext.com/blog/articl…

本文转载自: 掘金

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

GitHub收藏最高的10个Java练手项目推荐

发表于 2021-05-21

前言

很多初学者想小试牛刀的时候都会有找不到适合自己的项目的困境,要不就是太难要么就是没有写的必要,很是让人头大。

所以本文收集了GitHub上星星最多排名最靠前的十个Java练手项目,源码和文档也都整理好了,需要的朋友可以直接点击领取。

  • 项目源码和开发文档

好了,话不多说,坐稳扶好,开车喽!

1、图灵商城

star:25.8k

包括前台商城系统及后台管理系统,基于SpringBoot+MyBatis实现。

前台商城系统包含首页门户、商品推荐、商品搜索、商品展示、购物车、订单流程、会员中心、客户服务、帮助中心等模块。

后台管理系统包含商品管理、订单管理、会员管理、促销管理、运营管理、内容管理、统计报表、财务管理、权限管理、设置等模块。

2、秒杀系统设计

star:11.5k

关于高并发大流量如何进行秒杀架构的项目。学习之前,先快速入门MQ、SpringBoot、Redis、Dubbo、ZK、Maven,lua,效果会更好!

3、spring-boot-api-project-seed

star:5.5k

基于Spring Boot & MyBatis的种子项目,用于快速构建中小型API、RESTful API~可以帮助我们摆脱重复劳动,专注于业务代码的编写,告别996。

4、微人事管理系统

star:11.9k

前后端分离的人力资源管理系统,项目采用SpringBoot+Vue开发。

5、MarkdownEditors

star:1.1k

基于Android的Markdown编辑器,项目功能本身不难,但是细节很多。

6、博客系统

star:1.3k

基于SSM实现的个人博客系统,适合初学SSM和个人博客制作的同学学习。主要涉及技术包括的包括Maven,Spring,SpringMVC,MyBatis,Redis,JSP等。

7、MyBatis-Spring-Boot

star:2.9k

是Spring Boot集成MyBatis的基础项目。

8、webporter

star:2.1k

基于 webmagic 的 Java 爬虫项目。核心简单,但是涵盖爬虫应用的完整流程,是爬虫应用的实践样例。

9、shopping-management-system

star:2.9k

该项目为多个小项目的集合,并且在持续更新中。内容包括网购管理系统、图书管理系统、超市管理系统等。适合Java基础到入门的爱好者。

10、会议系统

star:2.6k

支持音频、视频、幻灯片(带有白板控件),聊天和屏幕的实时共享。用于在线学习可以实现:

  • 在线辅导(一对一)
  • 课堂翻转(在会议前记录内容)
  • 小组协作(多对多)
  • 在线课程(一对多)


往期热文:

  • Java基础知识总结
  • 性能调优系列专题(JVM、MySQL、Nginx and Tomcat)
  • 从被踢出局到5个30K+的offer,一路坎坷走来,沉下心,何尝不是前程万里
  • 100个Java项目解析,带源代码和学习文档!

end

本文转载自: 掘金

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

面试官:谈谈你对geohash的理解和如何实现附近人功能呢?

发表于 2021-05-21

前言

Hello,掘金的小伙们好,我是阿沐!一个喜欢通过实际项目实践来分享技术点的程序员!

你们有没有遇到被面试官嘲讽的场景;之前有位刚毕业的小学弟在上海魔都某某某大公司面试,二面主要是问了关于redis的相关知识点,回答的也是磕磕绊绊的,其中一个问题是如何实现搜索附近人加好友功能;想跟掘金的小伙伴们一起分享、一起探讨下。如果有不正确的地方,欢迎指正批评,共同进步~

面试官的主要考点

  • 考点一:面试官考点之Geohash是什么 知识存储量,没用过但是不能不知道
  • 考点二:面试官考点之原理与算法 考验算法基本功
  • 考点三:面试官考点之redis的geohash的基础命令 考察底子
  • 考点四:面试官考点之实现想法与方案 考察思维能力
  • 考点五:面试官考点之实际项目应用 实战经验

当你看到面试官想考验你这些知识点的时候;你在面试官问的过程中,就脑海在飞快的转动着,组合一系列的数据场景准备应战。

Geohash概念介绍

geohash就是一种地理位置编码。用来查询附近的POI(POI是“Point of Interest”的缩写,中文可以翻译为“兴趣点”。在地理信息系统中,一个POI可以是一栋房子、一个商铺、一个邮筒、一个公交站等),也是一种算法的思想。

通过将地球看成一个二维的平面图,然后将平面递归切分成更小的模块,然后将空间经纬度数据进行编码生成一个二进制的字符串,再通过base32将其转换为一个字符串。最终是通过比较geohash的值的相似程度查询附近目标元素。

Geohash能实现什么功能?

  • 地图导航; 高德地图、百度地图、腾讯地图
  • 附近人功能;微信附近人、微信摇一摇、拼夕夕附近人、扣扣附近人

Geohash 算法原理

讲真地,当我要准备讲解原理和算法的时候,也很纠结,毕竟算法不是我的强项且百度一下千篇一律;并且都是大神级人物总结,且不敢妄自菲薄,所以还是站在前人的肩膀上来理解下geohash原理与算法。

“附近的人”也就是常说的 LBS (Location Based Services,基于位置服务),它围绕用户当前地理位置数据而展开的服务,为用户提供精准的增值服务。

“附近的人” 核心思想如下:

① 以“自己”为中心,搜索附近的用户

② 以“自己”当前的地理位置为准,计算出别人和 “我” 之间的距离

③ 按“自己”与别人距离的远近排序,筛选出离我最近的用户或者商店等

那么我们按照我们以往的操作方式:我们在搜索附近人时,会将整个站点的用户信息塞到一个list中,然后去遍历所有节点,检查哪一个节点在自己的范围内;时间复杂度就变成了n*m(n搜索次数,m用户数据)这谁顶得住啊,就是全部放redis一旦数据量上来也顶不住,搜索效率极低。

附近人

上面大家应该可以看出来吧,其实就是把自己的坐标作为一个中心;哎,然后我们要找到围绕我们方圆10公里以内的附近小伙伴:

X轴:我们可以看做是纬度,左半边范围是-180°~ 0°;右半边是0° ~ 180°
Y轴:我们可以看做是经度,上半边范围是0° ~ 90°;下半边是-90° ~ 0°
原点:我们就看做是(0,0)位置;就好像是我们自己的位置

举例子

假如我们现在地点在广州字节跳动有限公司(广州市天河区珠江东路6号)经纬度是:113.326059(经度),23.117596(纬度)

geohash实质就是将经纬度进行二分法的形式落于相对应的区间中,越分越细一直到趋近于某一个临界值,那么分的层数越多,精确度越准确。

原则是:左区间标注 0;右区间标注 1。

例如我们用代码实现上面经纬度二分法生成的二进制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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
php复制代码/**
* @desc 利用递归思想 找出经纬度的二进制编码
* @param float $place 经度或纬度
* @param string $binary_array 每次递归拿到的bit值
* @param int $max_separate_num递归总次数
* @param array $section 区间值
* @param int $num 递归次数
* @return array
*/
public function binary($place = 0, $binary_array = [], $max_recursion_num = 20, $section = [], $num = 1)
{
if (!$section) return $binary_array;

// 获取中间值
$count = ($section['max'] - $section['min']) / 2;

// 左半边区间
$left = [
'min' => $section['min'],
'max' => $section['min'] + $count
];

// 右半边区间
$right = [
'min' => $section['min'] + $count,
'max' => $section['max']
];

// 假如给点的经纬度值大于右边最小值 则属于右半边区间为1 否则就是左半边区间 0
array_push($binary_array, $place > $right['min'] ? 1 : 0);

// 如果递归次数已经达到最大值 则直接返回结果集
if ($max_recursion_num <= $num) return $binary_array;

// 下一次递归我们需要传入的经纬度 区间值
$section = $place > $right['min'] ? $right : $left;

// 继续针对自身做的递归处理 一直到出现结果
return $this->binary($place, $binary_array, $max_recursion_num, $section, $num + 1);
}

// 实例化调用该方法
require_once './Geohash.php';

$geohash = new Geohash();

echo json_encode($geohash->binary(23.117596,[],$geohash->baseLengthGetNums(4, 1), $geohash->interval[0],1));

//结果集
[1,0,1,0,0,0,0,0,1,1]-> 二进制 101000 00011 这样是不是很清晰

//再看不明白的话 那么久打印出范围值:
[{"min":-90,"max":90},{"min":0,"max":90},{"min":0,"max":45},{"min":22.5,"max":45},{"min":22.5,"max":33.75},{"min":22.5,"max":28.125},{"min":22.5,"max":25.3125},{"min":22.5,"max":23.90625},{"min":22.5,"max":23.203125},{"min":22.8515625,"max":23.203125}]

从上面的脚本实现来看是不是更清晰了呢?那么我在使用lua语言给大家实现展示一下,本身原理基本一致:

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

-- 定义经纬度区间范围
local interval = {
{ min = -90, max = 90 },
{ min = -180, max = 180 }
}

--- @desc 利用递归思想 找出经纬度的二进制编码
--- @param place string 经度或纬度
--- @param binary_array table 每次递归拿到的bit值
--- @param max_separate_num number 递归总次数
--- @param section table 区间值
--- @param num number 递归次数
function binary(place, binary_array, max_recursion_num, section, num)
-- body
place = tonumber(place) or 0
binary_array = binary_array and binary_array or {}
max_recursion_num = tonumber(max_recursion_num) or 20
section = section and section or {}
num = tonumber(num) or 1

if not next(section) then
return binary_array
end
print(cjson.encode(section))
-- 获取中间值
local count = (section["max"] - section["min"]) / 2
-- 左半边区间
local left = {
min = section["min"],
max = section["min"] + count
}
-- 右半边区间
local right = {
min = section["min"] + count,
max = section["max"]
}

-- 假如给点的经纬度值大于右边最小值 则属于右半边区间为1 否则就是左半边区间 0
binary_array[#binary_array+1] = place > right["min"] and 1 or 0

-- 如果递归次数已经达到最大值 则直接返回结果集
if max_recursion_num <= num then
return binary_array
end

-- 下一次递归我们需要传入的经纬度 区间值
local _section = place > right["min"] and right or left

return binary(place, binary_array, max_recursion_num, _section, num + 1)
end


local res = binary(113.326059, {}, _base_length_get_nums(4, 1), interval[2], 1)

print(cjson.encode(res))

//打印结果
{"max":180,"min":-180}
{"max":180,"min":0}
{"max":180,"min":90}
{"max":135,"min":90}
{"max":135,"min":112.5}
{"max":123.75,"min":112.5}
{"max":118.125,"min":112.5}
{"max":115.3125,"min":112.5}
{"max":113.90625,"min":112.5}
{"max":113.90625,"min":113.203125}
[1,1,0,1,0,0,0,0,1,0]

我们可以实际手动打一遍执行下,聪明的朋友应该看到一个函数php($geohash->baseLengthGetNums)和lua中(_base_length_get_nums)私有方法,这个是干嘛用的,通过方法注释我们看到大概意思是我们二分层数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
perl复制代码--- @desc 根据指定编码长度获取经纬度的 二分层数
--- @param int $length 编码精确度
--- @param int $type 类型 0-纬度;1-经度
--- @return mixed
local function _base_length_get_nums(length, typ)
-- 第一种方法写死
local list = { {2, 3}, {5, 5}, {7, 8}, {10, 10}, {12, 13}, {15, 15}, {17, 18}, {20, 20}, {22, 23}, {25, 25}, {27, 28}, {30, 30} }

-- 第二种通过规律计算纬度位数组合成list
local cycle_num = 12
local list_res = {}
local lat, lng = 0, 0
for i = 1, 12, 1 do
lat = i % 2 == 0 and lat + 3 or lat + 2
lng = i % 2 == 0 and lng + 2 or lng + 3
list_res[#list_res + 1] = {lat, lng}
end

return list[length][typ]
end

🤡 大家是不是还会有疑问,我的天啊,这个list变量哪里来的呀?是不是感觉跟奇怪?下面看下图GeoHash Base32编码长度与精度表格展示:

1
2
3
4
复制代码整体的计算方式:
latitude的范围是:-90° 到 90°
longitude范围是:-180° 到 180°
地球参考球体的周长:40075016.68米
Geohash长度 Lat位数 Lng位数 Lat误差 Lng误差 km误差
1 2 3 ±23 ±23 ±2500
2 5 5 ±2.8 ±5.6 ±630
3 7 8 ±0.7 ±0.7 ±78
4 10 10 ±0.087 ±0.18 ±20
5 12 13 ±0.022 ±0.022 ±2.4
6 15 15 ±0.0027 ±0.0055 ±0.61
7 17 18 ±0.00068 ±0.00068 ±0.076
8 20 20 ±0.000086 ±0.000172 ±0.01911
9 22 23 ±0.000021 ±0.000021 ±0.00478
10 25 25 ±0.00000268 ±0.00000536 ±0.0005871
11 27 28 ±0.00000067 ±00000067 ±0.0001492
12 30 30 ±0.00000008 ±00000017 ±0.0000186
在纬度相等的情况下:
  • 经度每隔0.00001度,距离相差约1米;
  • 每隔0.0001度,距离相差约10米;
  • 每隔0.001度,距离相差约100米;
  • 每隔0.01度,距离相差约1000米;
  • 每隔0.1度,距离相差约10000米。
在经度相等的情况下:
  • 纬度每隔0.00001度,距离相差约1.1米;
  • 每隔0.0001度,距离相差约11米;
  • 每隔0.001度,距离相差约111米;
  • 每隔0.01度,距离相差约1113米;
  • 每隔0.1度,距离相差约11132米。

现在是不是一目了然了,规定Geohash长度最大长度是12层,每一层对应位数。哦哦哦!!!原来是这样来的呀,是不是超级简单。我们得到了经纬度的编码之后要干什么?肯定要对其进行组码了:

组合编码:

通过上述计算,纬度产生的编码为10100 00011,经度产生的编码为11010 00010。偶数位放经度,奇数位放纬度,把2串编码组合生成新串:11100 11000 00000 01101。是不是又有点懵了,它是如何组合的呢?下面一张图带你明白它的组合流程:

经纬度编码组合

有木有豁然开朗的赶脚,组合就是这么简单;可能有的小伙伴在之阅读很多文章有点迷惑,奇偶交叉组合怎么组合的会有点一头雾水;但是仔细看来就是这么简单啦!

代码实现编码组合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
php复制代码/**
* @desc 编码组合
* @param $latitude_str 纬度
* @param $longitude_str 经度
* @return string
*/
public function combination($latitude_str, $longitude_str)
{
$result = '';
//循环经度数字 作为偶数位置
for ($i = 0; $i < strlen($longitude_str); $i++) {
// 拼接经度作为偶数位置
$result .= $longitude_str{$i};
// 维度存在则拼接字符串
if (isset($latitude_str{$i})) $result .= $latitude_str{$i};
}
return $result;
}
// 结果集
var_dump($geohash->combination('1010000011', '1101000010'));
11100110000000001101
1
2
3
4
5
6
7
8
9
10
11
12
13
lua复制代码function combination(latitude_arr, longitude_arr)
local result = ''
for i = 1, #longitude_arr do
result = result..longitude_arr[i]
if latitude_arr[i] then
result = result..latitude_arr[i]
end
end
return result
end
//结果集
print(cjson.encode(combination({1,0,1,0,0,0,0,0,1,1}, {1,1,0,1,0,0,0,0,1,0})))
11100110000000001101

真的到了这时候,将经纬度转GeoHash字符串的工作已经完成了一半了,是不是赶脚超级无敌很简单~

base32算法:用0-9、b-z(去掉a, i, l, o)这32个字母进行base32编码,首先将11100 11000 00000 01101转成十进制,对应着十进制对应的编码就是可以组合成字符串了。我相信学习过编程的小伙们肯定都用过base64编码加解密,但是不确定是否去研究看过怎么加解密的。base32和base64的区别就在于:base32对应的二进制序列是5位,base64对应的二进制序列是6位。

1
css复制代码又会有小伙们问了为啥要去掉(a, i, l, o)这四个字母? 有没有疑问的,有的请下方扣1!!!!!
  • 属于容易混淆的字符,例如:[1, I(大写i), l(小写L)],[0,O];实际编码的时候,也会看错的
  • 元音,去除元音防止密码泄露,增加可靠性

编码组合成十进制再转换为字符串

原理:将组合之后的二进制序列每5位一组进行拆分转换;例如:

1
ini复制代码11100 = 2^4+2^3+2^2 = 16+8+4 = 28  也就是对应w字符串;看下下面对应的维基百科的字典表
decimal(十进制) 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
base32编码 0 1 2 3 4 5 6 7 8 9 b c d e f g h
decimal(十进制) 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
base32编码 j k m n p q r s t u v w x y z

是不是更加清晰明了了,照着对就可以啦;下面是编码实现获取组合字符串(解码这里就不写了,文末会有代码地址):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
php复制代码/**
* @desc 将二进制字符串转为十进制 再转换为对应的编码
* @param $str
* @return string
*/
public function encode($str = '')
{
$string = '';

// 按照5位分割字符串
$array = str_split($str, 5);
if (!$array) return $string;
foreach ($array as $va) {
//二进制转换为十进制
$decimal = bindec($va);
$string .= $this->base_32[$decimal];
}
return $string;
}
// 结果集是ws0e
var_dump($geohash->encode('11100110000000001101'));

注意:将经纬度转换成二进制序列的过程中,转换的次数越多,所表示的精度越细,标识的范围越小。

Geohash 实战系列

  • 基于mysql实现附近人查询
  • 基于mysql + GeoHash实现附近人查询
  • 基于redis + GeoHash实现附近人查询
  • 基于mongoDB实现附近人查询
  • 基于es搜索引擎实现附近人查询(说下方案)

基于mysql实现附近人查询

创建一个用户地理位置上报的表用来存放的经、纬度属性:

1
2
3
4
5
6
7
8
9
sql复制代码CREATE TABLE `user_place` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(10) DEFAULT NULL DEFAULT '0' COMMENT '用户id',
`longitude` double DEFAULT NULL DEFAULT '' COMMENT '经度',
`latitude` double DEFAULT NULL DEFAULT ''COMMENT '纬度',
`create_time` int(10) DEFAULT NULL DEFAULT '0' COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `idx_longe_lat` (`longitude`,`latitude`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

① 第一种方案:查询附近符合要求的附近人,然后再算距离(实习时用过)

1
2
3
sql复制代码SELECT * FROM `user_place` WHERE (longitude BETWEEN minlng(最小经度) AND maxlng(最大经度)) AND (latitude BETWEEN minlat(最小纬度) AND maxlat(最大纬度))

查询之后结果集之后,通过经纬度计算距离

② 第二种方案:直接通过复杂的sql语句计算结果(实习时用过)

1
2
3
4
5
6
7
sql复制代码// 当前自己的经纬度坐标
$latitude = 23.117596
$longitude = 113.326059
//一系列的复杂计算用到了 mysql中的 三角函数 ASIN函数:反正弦值; POWER函数:用于计算 x 的 y 次方。其他的小伙伴们百度查吧 正弦、余弦
fields = "user_id,ROUND(6378.138*2*ASIN(SQRT(POW(SIN(($latitude*PI()/180-latitude*PI()/180)/2),2)+COS($latitude*PI()/180)*COS(latitude*PI()/180)*POW(SIN(($longitude*PI()/180-longitude*PI()/180)/2),2)))*1000,2) AS distance";

selecet fields from `user_place` having distance <= 10 order by distance asc limit 10

③ 第三方案:mysql的四个内置函数

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
sql复制代码 1、ST_GeoHash(longitude, latitude, max_length -- 生成geohash值

mysql> SELECT ST_GeoHash(116.336506,39.978729,10);
+-------------------------------------+
| ST_GeoHash(116.336506,39.978729,10) |
+-------------------------------------+
| wx4ermcsvp |
+-------------------------------------+
1 row in set (0.00 sec)

2、ST_LongFromGeoHash() -- 从geohash值返回经度

mysql> SELECT ST_LongFromGeoHash('wx4ermcsvp');
+----------------------------------+
| ST_LongFromGeoHash('wx4ermcsvp') |
+----------------------------------+
| 116.33651 |
+----------------------------------+
1 row in set (0.00 sec)

3、ST_LatFromGeoHash() -- 从geohash值返回纬度

mysql> SELECT ST_LatFromGeoHash('wx4ermcsvp');
+---------------------------------+
| ST_LatFromGeoHash('wx4ermcsvp') |
+---------------------------------+
| 39.97873 |
+---------------------------------+
1 row in set (0.00 sec)

4、ST_PointFromGeoHash() -- 将geohash值转换为point值

mysql> SELECT ST_AsText(ST_PointFromGeoHash('wx4ermcsvp',0));
+------------------------------------------------+
| ST_AsText(ST_PointFromGeoHash('wx4ermcsvp',0)) |
+------------------------------------------------+
| POINT(116.33651 39.97873) |
+------------------------------------------------+
1 row in set (0.00 sec)

具体用法其实都差不多的,小伙伴们可以查查相关资料看看!用的不是很多

注意:单单基于 mysql 实现 “附近的人”;优点:简单,一张表存储经纬度即可;缺点:数据量比较小时可以使用,同时可以配合redis缓存查询结果集,效果也是ok的;但是数据量比较大的时候,我们可以看到需要大量的计算两个点之间的距离,对性能有很大的影响。(不推荐使用了)

基于mysql + GeoHash实现附近人查询

① 设计思路

在原本存储用户经纬度的表中:入库时计算经纬度对应的geohash字符串存储到表中;那么存储时需要我们明确字符串的长度。

那么我们查询的时候就不需要用经纬度查询,可以这样: select * from xx where geohash like 'geohash%'进行模糊查询,查询到结果集在通过经纬度计算距离;然后筛选指定的距离例如1000m以内,则是附近人。

② 代码实现

1
2
3
4
5
6
sql复制代码-- 先添加字段 geohash
alter table `user` add `geohash` varchar(64) NOT NULL DEFAULT NULL COMMENT '经纬度对应geohash码值',
-- 再添加geohash的普通索引
alter table `user` add index idx_geohash ( `geohash` )
-- 查询sql
select * from `user` where `geohash` like 'geohash%' order by id asc limit 100

③ 问题分析

小伙们都知道geohash算法是将地图划分为多个矩形块,然后在对矩形块编码得到geohash字符串。那是不是会出现这种情况,明明这个人离我很近,但是我们又不在同一个矩形块里,那是不是我搜索的时候就搜不到这个人,那不是血亏(万一是一个漂亮的妹子呢)

④ 解决方案

我们在搜索时,可以根据当前的编码计算出附近的8个区域块的geohash码,然后全部拿到,再一个个的筛选比较;这样那个妹子不就被我搜到加好友了嘛!

⑤ 再次实现

1
2
3
sql复制代码-- 例如有一下8个值 geohash1、geohash2、geohash3、geohash4、geohash5、geohash6、geohash7、geohash8
-- 查询sql
select * from `user` where `geohash` regexp 'geohash1|geohash2|geohash3|geohash4' order by id asc limit 100

然后查询的结果集进行距离计算,过滤掉大于指定距离的附近人。

基于redis + GeoHash实现附近人查询

① 设计思路

1、查找select指令操作:

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
less复制代码1、geopos指令:geopos key member [member ...] 获取指定key里返回所有指定名称的位置(经度和纬度);时间复杂度O(log(n)),n是排序集中的元素数

注意事项:
① geopos命令返回的是一个数组,每个数组中的都由两个元素组成:第一个是位置的经度, 而第二个是位置的纬度。
② 若给定的元素不存在,则对应的数组项为nil(不要搞错以为是一个空数组)。

2、geodist指令:geodist key member1 member2 [m|km|ft|mi] 获取两个给定位置之间的距离;时间复杂度O(log(n)),n是排序集中的元素数

注意事项:
① member1和member2 为两个地理位置名称,例如用户id标识。
② [m|km|ft|mi]尾部参数:
m :米,默认单位
km :千米
mi :英里
ft :英尺

③ 计算出的距离会以双精度浮点数的形式被返回;位置不存在,则返回nil。

3、georadius指令:georadius key longitude latitude radius [m|km|ft|mi] [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]

根据用户给定的经纬度坐标来获取指定范围内的地理位置集合;时间复杂度: O(n+log(m)),n是圆形区域的边界框内的元素数,该元素由中心和半径定界,m是索引内的项数。

注意事项:
① 以给定的经纬度为中心
② [m|km|ft|mi]单位说明
m :米,默认单位
km :千米
mi :英里
ft :英尺
③ withdist: 在返回位置元素的同时, 将位置元素与中心之间的距离也一并返回。
④ withcoord: 将位置元素的经度和维度也一并返回。
⑤ withhash: 以 52 位有符号整数的形式,返回位置元素经过原始geohash编码的有序集合分值。这个选项主要用于底层应用或者调试, 实际中的作用并不大。
⑥ count 限定返回的记录数。
⑦ asc: 查找结果根据距离从近到远排序。
⑧ desc: 查找结果根据从远到近排序。

4、georadiusbymember指令:georadiusbymember key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
获取指定范围内的元素数据,中心点是由给定的位置元素决定的,不是使用经度和纬度来决定中心点。时间复杂度: O(n+log(m)),n是圆形区域的边界框内的元素数,该元素由中心和半径定界,m是索引内的项数。

注意事项同上面georadius指令!!!

5、geohash指令:geohash key member [member ...] 获取一个或多个位置元素的geohash值;时间复杂度O(log(n)),n是排序集中的元素数

注意事项:
① 该命令返回的是一个数组格式,位置不存在则返回nil
② 数组结果集的值跟给出位置一一对应,说白了就是下标一致

2、添加insert指令操作:

1
2
3
4
5
6
7
sql复制代码geoadd指令:geoadd key longitude latitude member [longitude latitude member ...] 
添加地理位置的坐标;时间复杂度O(log(n)),n是排序集中的元素数。

注意事项:
① 实际存储的数据类型是zset,member是zset的value,score是根据经纬度计算出geohash
② geohash是52bit的长整型,计算距离使用的公式是Haversine
③ geoadd添加的坐标会有少许的误差,因为geohash对二维坐标进行一维映射是有损耗的

大家是不是感觉到有点奇怪,怎么这次的redis命令的时间复杂度都是O(log(n)),这是个啥意思呢?那我们么来一起科普一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
scss复制代码① O(1)解析:

O(1)就是最低的时空复杂度了,也就是耗时/耗空间与输入数据大小无关,无论输入数据增大多少倍,耗时/耗空间都不变。

② O(n)解析:
比如时间复杂度为O(n),就代表数据量增大几倍,耗时也增大几倍。比如常见的遍历算法。 

要找到一个数组里面最大的一个数,你要把n个变量都扫描一遍,操作次数为n,那么算法复杂度是O(n).

③ O(n^2)解析:

比如时间复杂度O(n^2),就代表数据量增大n倍时,耗时增大n的平方倍,这是比线性更高的时间复杂度。比如冒泡排序,就是典型的O(n^2)的算法,对n个数排序,需要扫描n×n次。 

用冒泡排序排一个数组,对于n个变量的数组,需要交换变量位置次,那么算法复杂度就是O().

③ O(logn)解析:

比如O(logn),当数据增大n倍时,耗时增大logn倍(这里的log是以2为底的,比如,当数据增大256倍时,耗时只增大8倍,是比线性还要低的时间复杂度),二分查找就是O(logn)的算法,每找一次排除一半的可能,256个数据中查找只要找8次就可以找到目标。 


④ O(nlogn)解析:
O(nlogn)同理,就是n乘以logn,当数据增大256倍时,耗时增大256*8=2048倍。这个复杂度高于线性低于平方。归并排序就是O(nlogn)的时间复杂度。

② 优缺点

  • 优点:效率高、实现简单、支持排序。
  • 缺点:结果存在误差;若需要精准,则需要手动再筛选一次;大数量情况下,需要解耦缓存。若是集群环境,缓存key不宜过大,否则集群迁移会有问题;所以针对redis要细致解耦缓存,拆分为多个小key

③ 代码实现

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
php复制代码/**
* @desc 添加用户地理位置 可以添加多个 大家可以测试下 修改下方法
* @param int $user_id
* @param int $latitude
* @param int $longitude
* @return int
*/
public function insert($user_id = 0, $latitude = 0, $longitude = 0)
{
$result = $this->redis->geoAdd($this->key, $longitude, $latitude, $user_id);

return $result;
}

/**
* @desc 搜索附近人结果集
* @param int $distance
* @param int $latitude
* @param int $longitude
* @return mixed
*/
public function searchNearby($distance = 300, $latitude = 0, $longitude = 0)
{
$result = $this->redis->georadius(
$this->key,
$longitude,
$latitude,
$distance,
'mi',
['WITHDIST','count' => 10,'DESC']
);
return $result;
}

// 调用 获取结果集
$res = new GeoRedis();
var_dump($res->searchNearby(300, 21.306,-157.858));
array(2) {
[0]=>
array(2) {
[0]=>
string(4) "1001" //用户id
[1]=>
string(8) "104.5615" //距离
}
[1]=>
array(2) {
[0]=>
string(4) "1002"
[1]=>
string(8) "104.5615"
}
}

那么是不是有小伙伴们问:我要分页可咋办?其实在上面已经给出了答案,使用georadiusbymember命令中的 STOREDIST将排好序的数据存入一个zset集合中,以后分页查直接从zset集合中取数据即可:

1
2
3
4
5
6
7
8
9
10
11
redis复制代码localhost:6379> zrange user:nearby 0 10 withscores
1) "1001"
2) "1184565520453603"
3) "1002"
4) "1184565520453603"
5) "1003"
6) "1231646528010636"
7) "1004"
8) "1231732998886639"
9) "1005"
10) "1235058932387089"

不过也有点不足的地方,就是我们不能根据筛选条件来直接查询,而是要查询到之后手动过滤;比如我们要查询18岁的美少女附近好友;

① 要么按照搜索条件细化分存储一份数据

② 要么就查询之后过滤

基于mongoDB实现附近人查询

① 设计思路

目前阿沐有一个类似直播项目首页推荐就有一个附近好友的功能;它就是基于MongoDB来实现附近好友功能。主要是通过它的两种地理空间索引2d和2dsphere,这两种索引底层还是基于geohash来构建的。

2dsphere索引支持:球星表面点、线、面、多点、多线、多面和几何集合;创建2dsphere索引语法:

1
2
3
4
5
6
7
sql复制代码-- 创建索引
db.coll.createIndex({'location':'2dsphere'})

-- 空间查询语法
① 位置相交 db.coll.map.find({'location':{'$geoIntersects':{'$geometry':area}})
② 位置包含 db.coll.map.find({'location':{'$within':{'$geometry':area}})
③ 位置接近 db.coll.map.find({'location':{'$near':{'$geometry':area}})

2d索引支持平面几何形状和一些球形查询;支持球面查询但是不太友好,更适合平面查询;创建2d索引语法:

1
2
3
4
5
6
7
8
9
sql复制代码-- 创建索引  索引的精度通过bits来指定,bits越大,索引的精度就越高
db.coll.createIndex({'location':"2d"}, {"bits":30})

-- 查询语法
① 位置包含 db.coll.map.find({'location':{'$within':[[10,10],[20,20]]})
② 矩形包含 db.coll.map.find({'location':{'$within':{'$box':[[10,10],[20,20]]}})
③ 中心包含 db.coll.map.find({'location':{'$within':{'$center':[[20,20],5]}})
④ 多边形包含 db.coll.map.find({'location':{'$within':{'$polygon':[[20,20],[10,10],[10,18],[13,21]]}})
⑤ 位置接近 db.coll.map.find({'location':{'$near':[10,20]})

② 代码实现

1、创建数据库,插入几条数据;collection为起名 user(相当于mysql里面的表名)。三个字段user_id用户id,user_name名称,location 为经、纬度数据。

1
2
3
4
5
6
7
8
sql复制代码db.user.insertMany([
{'user_id':1001, 'user_name':'阿沐1', loc:[122.431, 37.773]},
{'user_id':1002, 'user_name':'阿沐2', location:[157.858, 21.315]},
{'user_id':1003, 'user_name':'阿沐3', location:[155.331, 21.798]},
{'user_id':1004, 'user_name':'阿沐4', location:[157.331,22.798]},
{'user_id':1005, 'user_name':'阿沐5', location:[151.331, 25.798]},
{'user_id':1006, 'user_name':'阿沐6', location:[147.942428, 28.67652]},
])

2、因为我们以二维平面上点的方式存储的数据,想要进行LBS查询,那么还是选择设置2d索引:

1
sql复制代码db.coll.createIndex({'location':"2d"}, {"bits":30})

3、根据自己当前的坐标经纬度查询

1
2
3
4
5
6
7
8
9
sql复制代码db.user.aggregate({
$geoNear:{
near: [157.858, 21.306], // 当前自己坐标
spherical: true, // 计算球面距离
distanceMultiplier: 6378137, // 地球半径,单位是米,默认6378137
maxDistance: 300/6378137, // 过滤条件300米内,需要弧度
distanceField: "distance" // 距离字段别名
}
})

4、查看结果集中是否有符合条件的数据,若有数据则会多出刚刚设置的距离字段名distance,表示两点间的距离:

1
2
3
sql复制代码//当前结果集为模拟结果,因本机电脑docker没有安装mongo
{ "_id" : ObjectId("4e96b3c91b8d4ce765381e58"), 'user_id':1001, 'user_name':'阿沐1', "location" : [ 122.431, 37.773 ], "distance" : 5.10295397457355 }
{ "_id" : ObjectId("4e96b5c91b8d4ce765381e51"), 'user_id':1002, 'user_name':'阿沐2', "location" : [ 157.858, 21.315 ], "distance" : 255.81213803417531 }

那么到这里是不是对mongo存储经纬度,然后查询附近人获取距离是不是有点了解了;其实性能还是很好地,只不过压力大的时候会出现mongo连接超时,我们可以针对mongo做一个集群;基本上算是比较稳定的了。

基于es搜索引擎实现附近人查询

① 设计思路

其实用es/sphinx/solr等等(后面也会具体聊聊搜索引擎)这类搜索引擎大都能支持查询附近人,因为效率高,查询速度快,而且结果集比较精准;所以还是比较推荐使用这个。

1
2
3
4
5
6
7
8
9
10
11
12
sql复制代码GET api/query/search/
{
"query": {
"geo_distance": {
"distance": "1km",
"location": {
"lat": 21.306,
"lon": 157.858
}
}
}
}

大致流程就是这样:① 用户请求查询附近好友 ② 服务端收到请求,然后通过api(http或者rpc)请求上游搜索引擎组的数据 ③ 搜索组拿到请求参数解析查询对应关系链 ④ 高效率返回给调用者

支持分页查询以及更多条件的查询方案;性能优越、可分页;尤其在大数据量的情况下,其性能更友好。阿沐之前公司就是这样处理,类似个性化推荐;通过用户喜好从几百万商品中检索,整个流程也就是服务端请求搜索组接口。搜索组基本上都是Java开发者,由sorl搜索过度elasticcsearch引擎,再加上使用k8s部署es集群;挺好的!!!

仓库代码地址:github.com/woshiamu/am…

🐣 总结

哇哇哇,能有幸看到这里的小伙伴,我很服气你们了,我花了三天的时间去想去画去构思写好的文章;实话实说,写这篇文章压力挺大的;首先底层的算法原理,再者需要实践验证。网络上都是复制来复制去,我就想着走不一样的路线;既然大家都已经看过那么多跟geohash相关的文章,原理算法应该都ok的;假如我再去纠结讲着写,那岂不是有点自取其辱(大牛们都已经讲的很好了);所以决定通过不同的角度去思考一个问题;以及引发的思考。

本文主要还是通过实践动手结合实际的项目以及过往经验;展示geohash的不同场景下的使用,量级大和量极小怎么选择等等?也通过整理文章时,去解析小伙们会遇到的疑问,这些疑问估计很多文章都没有的,因为都是千篇一律。阿沐要做的就是复杂简单化,能动手实践的绝不只看不练!

座右铭:不管做什么,只要坚持下去就会看到不一样!

最后,欢迎关注我的个人公众号「我是阿沐」,会不定期的更新后端知识点和学习笔记。也欢迎直接公众号私信或者邮箱联系我,我们可以一起学习,一起进步。

好了,我是阿沐,一个不想30岁就被淘汰的打工人 ⛽️ ⛽️ ⛽️ 。创作不易觉得「阿沐」写的有点料话:👍 关注一下,💖 分享一下,我们下期再见。

本文转载自: 掘金

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

盘点 JPA 基于 Spring 的事务管理

发表于 2021-05-21

总文档 :文章目录

Github : github.com/black-ant

一 . 前言

这一篇讲事务管理相关的流程 , 以 SpringDataJPA 为例.

文章的目的 :

  • 梳理 Spring Transaction 的主流程
  • 说明 Spring Transaction 的核心原理
  • 流程中的参数传递

一句话原理 :

  • 通过 SQL START TRANSACTION; + COMMIT; 实现数据库级的事务管理
  • 通过 代理的方式进行整体的管控 , 通过代理类开启 START TRANSACTION
  • 通过 Mysql rollback 事务回滚

主流程 :

  • TransactionManager
  • JpaTransactionManager
  • PlatformTransactionManager

二 . 事务处理流程

事务管理的核心是对象 TransactionManager , 我们来看一下他的家族体系 :

PlatformTransactionManager.png

大概可以看到 , 主要的实现类有 :

1
2
3
4
5
6
7
8
java复制代码C- DataSourceTransactionManager
C- JdoTransactionManager
C- JpaTransactionManager
C- HibernateTransactionManager
C- JtaTransactionManager
C- OC4JjtaTransactionManager
C- WebSphereUowTransactionManager
C- WebLogicJtaTransactionManager

2.1 事务的拦截入口

事务的起点是通过 Interceptor 进行拦截的 , 其代理方式也是通过 AOP 实现的 ,其入口类为 CglibAopProxy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码C01- TransactionInterceptor
E- TransactionAspectSupport -> PS:001
M01_01- invoke(MethodInvocation invocation)


// M01_01
@Override
@Nullable
public Object invoke(MethodInvocation invocation) throws Throwable {
// 向TransactionAttributeSource传递目标类和方法
Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);

// 此处调用 invokeWithinTransaction 正式处理
return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
}

PS:001 TransactionAspectSupport 的作用

类的作用 : 事务基础类 , 子类负责以正确的顺序调用该类中的方法

特点 : 基于 策略模式 设计

2.2 流程的拦截

  • Step 1 : 属性准备
    • TransactionAttributeSource : TransactionInterceptor用于元数据检索的策略接口
    • TransactionAttribute : 该接口将rollbackOn规范添加到TransactionDefinition。
    • PlatformTransactionManager : 事务管理平台
    • joinpointIdentification
  • Step 2 : 分为 2 种类型处理
    • 存在用于在事务中执行给定回调的方法
    • 不存在回调方法

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
java复制代码C02- TransactionAspectSupport
M02_01- invokeWithinTransaction(Method method, Class<?> targetClass, InvocationCallback invocation)
P- method : 当前原始方法
P- targetClass : 当前原始类
P- invocation : 代理对象
// 1- 属性准备
- getTransactionAttributeSource() : 获取 TransactionAttributeSource
- tas.getTransactionAttribute(method, targetClass) : 获取 TransactionAttribute
- determineTransactionManager(txAttr) : 获取 PlatformTransactionManager
- methodIdentification(method, targetClass, txAttr) :
// 类型 A : -------------
A2- 主调操作
- createTransactionIfNecessary(tm, txAttr, joinpointIdentification)
?- 如果需要,根据给定的TransactionAttribute创建一个事务 -> M02_02
- invocation.proceedWithInvocation() : 核心接口 -> PS:M02_01_01
?- 进行目标调用的简单回调接口 , 具体的拦截器/方面由它们的调用机制决定
A3- catch 操作
- completeTransactionAfterThrowing(txInfo, ex)
A4- finally 操作
- cleanupTransactionInfo(txInfo) -> PS:M02_04_01
A5- 提交事务
- commitTransactionAfterReturning(txInfo) -> PS:M02_05_01
// 类型 B :------------------------
B2- CallbackPreferringPlatformTransactionManager.execute
B3- prepareTransactionInfo(tm, txAttr, joinpointIdentification, status) : 构建 TransactionInfo
B4- invocation.proceedWithInvocation()
M02_02- createTransactionIfNecessary(PlatformTransactionManager tm,TransactionAttribute txAttr, String joinpointIdentification)
1- tm.getTransaction(txAttr) : 获取 TransactionStatus -> M03_01
2- prepareTransactionInfo(tm, txAttr, joinpointIdentification, status)
?- 处理
M02_03- prepareTransactionInfo
1- new TransactionInfo(tm, txAttr, joinpointIdentification) : 构建一个 TransactionInfo
2- txInfo.newTransactionStatus(status) : 如果不兼容的tx已经存在,事务管理器将标记一个错误
3- txInfo.bindToThread() : 即使没有在这里创建一个新的事务 , 也会将TransactionInfo绑定到线程
?- 这保证了即使这个方面没有创建任何事务,TransactionInfo堆栈也将被正确管理
M02_04- cleanupTransactionInfo : 重置TransactionInfo ThreadLocal
- txInfo.restoreThreadLocalStatus() -> PS:M02_04_01

// PS:M02_01_01 核心处理
// 该方法会执行最终的 state 方法 , 主要流程为

C- RepositoryComposition # invoke(Method method, Object... args)
C- RepositoryFactorySupport # doInvoke
C- SimpleJpaRepository # save
C- Loader # loadEntity
C- Loader # doQuery
C- ConnectionImpl # prepareStatement


// PS:M02_04_01 的作用
TODO : 这个属性的作用
protected void cleanupTransactionInfo(@Nullable TransactionInfo txInfo) {
if (txInfo != null) {
txInfo.restoreThreadLocalStatus();
}
}

参数详情

这里看一看相关的参数详情

image.png

M02_01 源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
java复制代码    protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
final InvocationCallback invocation) throws Throwable {

// Step 1 : 属性准备
TransactionAttributeSource tas = getTransactionAttributeSource();
final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
final PlatformTransactionManager tm = determineTransactionManager(txAttr);
final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);

if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
// Standard transaction demarcation with getTransaction and commit/rollback calls.
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
Object retVal = null;
try {
// This is an around advice: Invoke the next interceptor in the chain.
// This will normally result in a target object being invoked.
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// target invocation exception
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
cleanupTransactionInfo(txInfo);
}
commitTransactionAfterReturning(txInfo);
return retVal;
}

else {
final ThrowableHolder throwableHolder = new ThrowableHolder();

// It's a CallbackPreferringPlatformTransactionManager: pass a TransactionCallback in.
try {
Object result = ((CallbackPreferringPlatformTransactionManager) tm).execute(txAttr, status -> {
TransactionInfo txInfo = prepareTransactionInfo(tm, txAttr, joinpointIdentification, status);
try {
return invocation.proceedWithInvocation();
}
catch (Throwable ex) {
if (txAttr.rollbackOn(ex)) {
// A RuntimeException: will lead to a rollback.
if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
}
else {
throw new ThrowableHolderException(ex);
}
}
else {
// A normal return value: will lead to a commit.
throwableHolder.throwable = ex;
return null;
}
}
finally {
cleanupTransactionInfo(txInfo);
}
});

// Check result state: It might indicate a Throwable to rethrow.
if (throwableHolder.throwable != null) {
throw throwableHolder.throwable;
}
return result;
}
catch (ThrowableHolderException ex) {
throw ex.getCause();
}
catch (TransactionSystemException ex2) {
if (throwableHolder.throwable != null) {
logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
ex2.initApplicationException(throwableHolder.throwable);
}
throw ex2;
}
catch (Throwable ex2) {
if (throwableHolder.throwable != null) {
logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
}
throw ex2;
}
}
}

3.3 流程的处理

如何调用到该方法 :

  • TransactionAspectSupport # invokeWithinTransaction
  • TransactionAspectSupport # createTransactionIfNecessary
  • AbstractPlatformTransactionManager # getTransaction

M02_02 源代码 , 先来看一下第二步怎么到 getTransaction 的

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复制代码
protected TransactionInfo createTransactionIfNecessary(@Nullable PlatformTransactionManager tm,
@Nullable TransactionAttribute txAttr, final String joinpointIdentification) {

// 如果没有指定名称,则将方法标识应用为事务名称
if (txAttr != null && txAttr.getName() == null) {
txAttr = new DelegatingTransactionAttribute(txAttr) {
@Override
public String getName() {
return joinpointIdentification;
}
};
}

TransactionStatus status = null;
if (txAttr != null) {
if (tm != null) {
// txAttr 和 tm 均存在时 , 调用 AbstractPlatformTransactionManager
status = tm.getTransaction(txAttr);
} else {
// 仅打 log
}
}
return prepareTransactionInfo(tm, txAttr, joinpointIdentification, status);
}

可以看到 , 相关代码为 status = tm.getTransaction(txAttr)

TransactionManager 总共有2个 :

  • AbstractPlatformTransactionManager : 总的抽象事务管理器
  • JpaTransactionManager : 因为是基于 JPA , 此处是 JpaTransactionManager
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
java复制代码C03- AbstractPlatformTransactionManager
M03_01- getTransaction(TransactionDefinition definition)
1- doGetTransaction() : 调用子类事务管理 -> M04_01
2- newTransactionStatus(definition, transaction, true, newSynchronization, debugEnabled, suspendedResources) -> PS:M03_01_01
3- doBegin(transaction, definition)
?- 根据给定的事务定义,使用语义开始一个新的事务 -> PS:M03_01_02
4- prepareSynchronization(status, definition)
M03_02- commit(TransactionStatus status)
1- processRollback(defStatus, false) : 处理回调操作 -> M03_03
2- processCommit(defStatus) : -> M03_04
M03_04- processCommit(defStatus) : 主要的 commit 流程
1- 前置处理
- prepareForCommit(status);
- triggerBeforeCommit(status);
- triggerBeforeCompletion(status);
2- 通过不同的 DefaultTransactionStatus 执行不同的逻辑
3- 在commit后触发回调,并将在那里抛出的异常传播给调用方,但事务仍被视为已提交
- triggerAfterCommit(status)
- triggerAfterCompletion(status, TransactionSynchronization.STATUS_COMMITTED)
4- 完成后进行清理,必要时清除同步,并调用doCleanupAfterCompletion
- cleanupAfterCompletion(status)

// 调用子类事务管理 , 此处使用 JpaTransactionManager
C04- JpaTransactionManager
M04_01- doGetTransaction()
?- 该方法主要是对 JpaTransactionObject 的创建和处理
1- setSavepointAllowed
- TransactionSynchronizationManager.getResource : 获取 EntityManagerHolder
2- setEntityManagerHolder :
- TransactionSynchronizationManager.getResource(getDataSource()) : ConnectionHolder
3- setConnectionHolder : 包装JDBC的资源持有者
M04_02- doBegin(Object transaction, TransactionDefinition definition)
1- 获取 M04_01 创建的 JpaTransactionObject
2- txObject.getEntityManagerHolder().getEntityManager() : 获取 EntityManager
3- determineTimeout(definition) : 准备超时时间
4- getJpaDialect().beginTransaction : 委托给JpaDialect以开始实际的事务 -> PS:M04_02_05
5- txObject.getEntityManagerHolder().setTimeoutInSeconds(timeoutToUse) : 如果超时 , 设置超时时间
6-IF- 如果设置了JPA EntityManager的JDBC连接
- getJpaDialect().getJdbcConnection(em, definition.isReadOnly()) : 获取 ConnectionHandle
- 通过 ConnectionHandle 生成 ConnectionHolder
- TransactionSynchronizationManager.bindResource(getDataSource(), conHolder) :
- txObject.setConnectionHolder(conHolder)
7- txObject.getEntityManagerHolder().setSynchronizedWithTransaction(true) : 将资源标记为与事务同步


// PS:M04_02_05 实际事务处理流程 : 主要是2句话
Object transactionData = getJpaDialect().beginTransaction(em,new JpaTransactionDefinition(definition, timeoutToUse, txObject.isNewEntityManagerHolder()));
txObject.setTransactionData(transactionData);


// 流程处理的参数情况关注类
C- NativeSession # execSQL
// 1
select orgentity0_.id as id1_0_0_, orgentity0_.org_name as org_name2_0_0_, orgentity0_.org_type as org_type3_0_0_ from org orgentity0_ where orgentity0_.id=?
select userentity0_.userid as userid1_1_0_, userentity0_.isactive as isactive2_1_0_, userentity0_.orgid as orgid3_1_0_, userentity0_.remark as remark4_1_0_, userentity0_.userlink as userlink5_1_0_, userentity0_.username as username6_1_0_, userentity0_.usertype as usertype7_1_0_ from user userentity0_ where userentity0_.userid=2177281

select orgentity0_.id as id1_0_0_, orgentity0_.org_name as org_name2_0_0_, orgentity0_.org_type as org_type3_0_0_ from org orgentity0_ where orgentity0_.id=5052474
select userentity0_.userid as userid1_1_0_, userentity0_.isactive as isactive2_1_0_, userentity0_.orgid as orgid3_1_0_, userentity0_.remark as remark4_1_0_, userentity0_.userlink as userlink5_1_0_, userentity0_.username as username6_1_0_, userentity0_.usertype as usertype7_1_0_ from user userentity0_ where userentity0_.userid=4412226

.......

PS:M03_01_01 newTransactionStatus

作用 : 用给定的参数创建一个TransactionStatus实例

1
2
3
4
5
6
java复制代码protected DefaultTransactionStatus newTransactionStatus(TransactionDefinition definition, @Nullable Object transaction, boolean newTransaction,boolean newSynchronization, boolean debug, @Nullable Object suspendedResources) {

boolean actualNewSynchronization = newSynchronization &&!TransactionSynchronizationManager.isSynchronizationActive();
return new DefaultTransactionStatus(transaction, newTransaction, actualNewSynchronization,
definition.isReadOnly(), debug, suspendedResources);
}

S:M03_01_02 具体的流程
作用 : 根据给定的事务定义,使用语义开始一个新的事务 。 由于已经由抽象管理器处理过 , 所以不需要关心传播行为的应用。当事务管理器决定实际启动一个新事务时,将调用此方法。要么之前没有任何交易,要么之前的交易已经暂停。

S:M02_05_01 中干了什么 ?
txInfo.getTransactionManager().commit(txInfo.getTransactionStatus())

可以看到 , 这里通过 TransactionManager 调用具体的 Commit -> AbstractPlatformTransactionManager

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码C03- AbstractPlatformTransactionManager
M03_02- commit(TransactionStatus status)


// commit 主要逻辑
public final void commit(TransactionStatus status) throws TransactionException {
if (status.isCompleted()) {
throw new IllegalTransactionStateException(".....");
}

DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;
if (defStatus.isLocalRollbackOnly()) {
processRollback(defStatus, false);
return;
}

if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) {
processRollback(defStatus, true);
return;
}

// 核心调用逻辑 , 提交 commit 处理
processCommit(defStatus);
}

3.5 异常的处理

异常的处理主要是回退的相关操作 , 该操作主要在 C03- AbstractPlatformTransactionManager 中

Rollback 的调用 :
观察发现 , Rollback 不仅仅在出现异常时调用 , 这里会有不同的场景 , 主要的有2种 type :

  • 事务代码请求回滚 : processRollback(defStatus, false)
  • 全局事务被标记为仅回滚但请求提交事务代码 : processRollback(defStatus, true)
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
java复制代码// 事务的回退主流程
C03- AbstractPlatformTransactionManager
M03_03- processRollback(DefaultTransactionStatus status, boolean unexpected) : 处理实际回退, 检查已完成标志
// Type 1 : 正常执行情况下的 Rollback
1- triggerBeforeCompletion(status)
2- triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK)
// Type 2 : 调用 doCallback 回滚的情况
1- triggerBeforeCompletion(status)
2- doRollback(status)
3- triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK)
M03_05- triggerBeforeCompletion(status)
1- TransactionSynchronizationUtils.triggerBeforeCompletion()
M03_06- triggerAfterCompletion(status)
M03_07- doRollback(status)
?- 这是一个需要子类实现的接口 , 以 JPA 为例 , 这里使用的 JpaTransactionManager

C04- JpaTransactionManager
F04_01- EntityManagerFactory entityManagerFactory;
M04_03- doRollback
// Step 1 : 属性获取 -> PS:M04_03_01
1- status.getTransaction() : 获取 JpaTransactionObject
// Step 2 : 执行 EntityTransaction
2- txObject.getEntityManagerHolder().getEntityManager().getTransaction() -> PS:M04_03_02
3- tx.rollback() -> PS:M04_03_03
- 主要调用对象
// Step 3 : finally 强制处理
4- txObject.getEntityManagerHolder().getEntityManager().clear()


C05- TransactionImpl
M05_01- rollback
- TransactionStatus status = getStatus() : 获取 TransactionStatus --> PS:M05_01_01
- status.canRollback() : 是否可以回退 , 不可则抛出异常 --> PS:M05_01_02
- internalGetTransactionDriverControl().rollback() : 执行回退 --> PS:M05_01_03

// 发起命令 callback

PS:M04_03_01 参数详情

JpaTransactionObject 参数详情

这里可以看到 , 每一个操作都会形成一个 EntityInsertAction

JPA_Transaction_object.jpg

PS:M05_01_03 回退逻辑详情

核心类为 JdbcResourceLocalTransactionCoordinatorImpl , 回退有如下调用栈 (中间有部分省略) :

setAutoCommit 环节

  • JpaTransactionManager # doBegin
  • HibernateJpaDialect # beginTransaction
  • TransactionImpl # begin
  • JdbcResourceLocalTransactionCoordinatorImpl # begin
  • HikariProxyConnection # setAutoCommit
  • ConnectionImpl # setAutoCommit
  • NativeProtocol # sendQueryString

SQL 执行环节

  • HikariProxyPreparedStatement # executeQuery
  • NativeSession # execSQL
  • NativeProtocol # sendQueryString

PS :最终执行语句均为 :

  • NativeProtocol # sendQueryString
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
java复制代码C11- JdbcResourceLocalTransactionCoordinatorImpl
M- rollback()

C12- AbstractLogicalConnectionImplementor
M- rollback()
- getConnectionForTransactionManagement().rollback()
?- 调用 ProxyConnection
- status = TransactionStatus.ROLLED_BACK

C13- ProxyConnection
M13_01- rollback()

// M13_01 rollback() 源代码
public void rollback() throws SQLException{
delegate.rollback();
isCommitStateDirty = false;
lastAccess = currentTime();
}

// 这里就涉及到 com.mysql.cj.jdbc
C14- ConnectionImpl
M14_01- rollback()
- rollbackNoChecks()

private void rollbackNoChecks() throws SQLException {
synchronized (getConnectionMutex()) {
if (this.useLocalTransactionState.getValue()) {
if (!this.session.getServerSession().inTransactionOnServer()) {
return; // effectively a no-op
}
}
// 回退核心逻辑
this.session.execSQL(null, "rollback", -1, null, false, this.nullStatementResultSetFactory, this.database, null, false);
}
}


// 最终处理
C- NativeProtocol # sendQueryString
return sendQueryPacket(callingQuery, sendPacket, maxRows, streamResults, catalog, cachedMetadata, getProfilerEventHandlerInstanceFunction,
resultSetFactory);

sendPacket -> rollback

// PS : 这里就涉及到 MySQL 的事务了
MySQL的 ROLLBACK 命令用来回退(撤销)MySQL语句

START TRANSACTION or BEGIN 启动新事务.
COMMIT 提交当前事务,使其更改永久化.
ROLLBACK 回滚当前事务,取消其更改
SET 自动提交禁用或启用当前会话的默认自动提交模式。

默认情况下,MySQL 运行时启用了自动提交模式。这意味着,如果事务中没有其他语句,那么每个语句都是原子的,就好像它被 START TRANSACTION 和 COMMIT 包围了一样。不能使用 ROLLBACK 撤消该效果; 但是,如果在语句执行期间发生错误,将回滚该语句。

// 若要对一系列语句隐式禁用自动提交模式,请使用 START TRANSACTION 语句:

START TRANSACTION;
SELECT @A:=SUM(salary) FROM table1 WHERE type=1;
UPDATE table2 SET summary=@A WHERE type=1;
COMMIT;

// 具体的可以参考官方文档 : https://dev.mysql.com/doc/refman/5.7/en/commit.html

总结

整体大概是把 JPA Transaction 的流程过了一遍 , 不过一直有一点怀疑 , 现阶段看出来的就是通过 SQL 其本身的事务管理来做的 , 不确定是否有其他的环节控制或者通过业务的方式处理 .

附录

Mysql 事务回滚机制

1
java复制代码官方文档 @ https://dev.mysql.com/doc/refman/8.0/en/innodb-autocommit-commit-rollback.html

先来看一下 , 官方提供的案例 :

在 InnoDB 中,所有的用户活动都发生在一个事务中。如果启用了自动提交模式,则每个 SQL 语句自己形成一个事务。

注意 , 在 MyMyISAM 中 ,这种处理是失效的!!

1
2
3
4
SQL复制代码CREATE TABLE customer (a INT, b CHAR (20), INDEX (a));

# 这里可以看到 , 此时运行可以直接插入
INSERT INTO customer VALUES (10, 'Heikki');

类型一 : 多语句事务提交

  • 如果这里不执行 commit , 当前操作是不会提交的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
SQL复制代码
# Step 1 :开启事务和自动提交
# 启用自动提交的会话可以通过以显式的 START TRANSACTION 或 BEGIN 语句开始并以 COMMIT 或 ROLLBACK 语句结束来执行多语句事务。
START TRANSACTION;


# Step 2 : 插入一条记录 , 在自动提交的前提下 , 一个 SQL 就是一个事务
INSERT INTO customer VALUES (10, 'Heikki');


# Step 3 : 提交 (提交之后 , 数据才正式添加)
# 如果一个禁用自动提交的会话在没有显式提交最终事务的情况下结束,MySQL 将回滚该事务。
# COMMIT 意味着当前事务中所做的更改是永久性的,并且对其他会话可见
COMMIT;

类型二 :回退操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
SQL复制代码# Step 1 : 设置非自动提交
# 如果在 setautocommit = 0的会话中禁用自动提交模式,则会话始终打开一个事务。COMMIT 或 ROLLBACK 语句结束当前事务并启动新事务。
SET autocommit=0;

# Step 2 : 插入所有的数据
INSERT INTO customer VALUES (15, 'John');
INSERT INTO customer VALUES (20, 'Paul');
DELETE FROM customer WHERE b = 'Heikki';

# Step 3 : 回滚事务
ROLLBACK;

# 可以看到 , DELETE Heikki 没有执行 , 第一条 插入成功
SELECT * FROM customer;

对于本案例的事务处理

1
2
3
4
5
6
7
8
9
10
11
12
sql复制代码START TRANSACTION;
SET autocommit=0;
# Step 2 : 插入一条记录 , 在自动提交的前提下 , 一个 SQL 就是一个事务
INSERT INTO org VALUES (10,'111', 'Heikki');
INSERT INTO org VALUES (20,'222','Paul');
#DELETE FROM org WHERE id = 'Heikki';

# Step 3 : 回滚事务
ROLLBACK;

# 可以看到 , DELETE Heikki 没有执行 , 第一条 插入成功
SELECT * FROM org;

观察到的本案例的 SQL 操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
sql复制代码# 项目启动时
SET autocommit=1

# Step 1 : 进入方法
SET autocommit=0

# Step 2 : 执行 SQL
select userentity0_.userid as userid1_1_0_, userentity0_.isactive as isactive2_1_0_, userentity0_.orgid as orgid3_1_0_, userentity0_.remark as remark4_1_0_, userentity0_.userlink as userlink5_1_0_, userentity0_.username as username6_1_0_, userentity0_.usertype as usertype7_1_0_ from user userentity0_ where userentity0_.userid=3138559
select orgentity0_.id as id1_0_0_, orgentity0_.org_name as org_name2_0_0_, orgentity0_.org_type as org_type3_0_0_ from org orgentity0_ where orgentity0_.id=7614913tity0_.userlink as userlink5_1_0_, userentity0_.username as username6_1_0_, userentity0_.usertype as usertype7_1_0_ from user userentity0_ where userentity0_.userid=3138559

// Step 3 : 插入操作
insert into user .....
insert into org ........

commit
SET autocommit=1


// 如果出现异常 , 会调用 rollback
// PS : 若要保留自动提交 (SET autocommit=1),请以 start transaction 开始每个事务,并以 COMMIT 或 ROLLBACK 结束

PS : 这里可以 Debug C- NativeProtocol # sendQueryString 拿到

M04_01 : JpaTransactionManager # doGetTransaction()源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码protected Object doGetTransaction() {
JpaTransactionObject txObject = new JpaTransactionObject();
txObject.setSavepointAllowed(isNestedTransactionAllowed());
// JPA中资源持有者的封装
EntityManagerHolder emHolder = (EntityManagerHolder)
// 管理每个线程的资源和事务同步的中央委托
TransactionSynchronizationManager.getResource(obtainEntityManagerFactory());
if (emHolder != null) {
txObject.setEntityManagerHolder(emHolder, false);
}

if (getDataSource() != null) {
// ConnectionHolder :包装JDBC的资源持有者
// 对于特定的javax.sql. datasource,将这个类的实例绑定到线程。
// 注意:这是一个SPI类,不打算被应用程序使用。
ConnectionHolder conHolder = (ConnectionHolder)
TransactionSynchronizationManager.getResource(getDataSource());
txObject.setConnectionHolder(conHolder);
}

return txObject;
}

M04_02 : JpaTransactionManager # doBegin()源码

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
java复制代码protected void doBegin(Object transaction, TransactionDefinition definition) {

// JPA事务对象,表示EntityManagerHolder。JpaTransactionManager用作事务对象。
JpaTransactionObject txObject = (JpaTransactionObject) transaction;

if (txObject.hasConnectionHolder() && !txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
throw new IllegalTransactionStateException(....);
}

try {
if (!txObject.hasEntityManagerHolder() ||txObject.getEntityManagerHolder().isSynchronizedWithTransaction()) {
// 实体类管理器
EntityManager newEm = createEntityManagerForTransaction();
txObject.setEntityManagerHolder(new EntityManagerHolder(newEm), true);
}

EntityManager em = txObject.getEntityManagerHolder().getEntityManager();
final int timeoutToUse = determineTimeout(definition);
// 开始一个事务
Object transactionData = getJpaDialect().beginTransaction(em,new JpaTransactionDefinition(definition, timeoutToUse, txObject.isNewEntityManagerHolder()));
// 为当前事务对象标明事务属性
txObject.setTransactionData(transactionData);

if (timeoutToUse != TransactionDefinition.TIMEOUT_DEFAULT) {
// 控制超时时间
txObject.getEntityManagerHolder().setTimeoutInSeconds(timeoutToUse);
}

if (getDataSource() != null) {
// 连接管理 , 执行相关的连接操作
ConnectionHandle conHandle = getJpaDialect().getJdbcConnection(em, definition.isReadOnly());
if (conHandle != null) {
ConnectionHolder conHolder = new ConnectionHolder(conHandle);
if (timeoutToUse != TransactionDefinition.TIMEOUT_DEFAULT) {
conHolder.setTimeoutInSeconds(timeoutToUse);
}
// 这里绑定了资源 , 把一个数据库数据绑定为了映射对象
// TODO : 这里应该就是三态变化的核心 , 以后看看
TransactionSynchronizationManager.bindResource(getDataSource(), conHolder);
txObject.setConnectionHolder(conHolder);
} else {
//log.....
}
}

if (txObject.isNewEntityManagerHolder()) {
TransactionSynchronizationManager.bindResource(obtainEntityManagerFactory(), txObject.getEntityManagerHolder());
}
// 将资源标记为与事务同步
txObject.getEntityManagerHolder().setSynchronizedWithTransaction(true);
} catch (TransactionException ex) {
closeEntityManagerAfterFailedBegin(txObject);
throw ex;
} catch (Throwable ex) {
closeEntityManagerAfterFailedBegin(txObject);
throw new CannotCreateTransactionException("Could not open JPA EntityManager for transaction", ex);
}
}

本文转载自: 掘金

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

1…663664665…956

开发者博客

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