Nicksxs's Blog

What hurts more, the pain of hard work or the pain of regret?

最近也看到了一篇文章,结合一些实际的经验来看下索引的基数和可选择性,
这个基数指的是啥呢,就是索引我们一直在讲的是要字段值的差异度比较大的那种,因为假如这个字段的所有值都是比如0和1的话,那索引的结构BTree就没办法高效的找到所查询的值,这个基数就是可以作为它差异度大小的一个参考,当然这是一种反过来的说法,理论上应该从写入这个表的数据的逻辑去看,这些字段会出现哪些值以及它的取值范围是怎么样的
那么这个会在mysql中存起来的基数值可以怎么看呢
还是用我之前建的一个简单的表

1
2
3
4
5
6
7
8
9
10
CREATE TABLE `students` (
`id` bigint(32) NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(64) NOT NULL COMMENT '姓名',
`age` int(11) NOT NULL COMMENT '年纪',
`class` int(11) NOT NULL COMMENT '班级',
`created_at` datetime NOT NULL COMMENT '创建时间',
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_class` (`class`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=608 DEFAULT CHARSET=utf8mb4 COMMENT='学生表';

表里的数据也比较简单

因为升成name和class的时候我都用了一定的规律取余,所以会有一些重复的,
这是我们就可以在information库的查一下这个基数

1
select table_schema,table_name,index_name,cardinality from information_schema.statistics where table_schema='database' and table_name = 'students' order by cardinality desc

database是你的库,可以来这么查询

我的这个表差不多是这样,主键刚好对应的就是数据量,另一个班级的索引就是这个class的数量是一致的
所以这就是个明确的区分度的值,而之前也提到过,如果一个表的某个索引字段他的区分度低,跟全表扫描差不多的话
优化器当然也可能会选择不使用索引,直接使用全表扫描,这样就不用质疑说为啥没用起来索引等等

我们日常在使用github的时候经常会碰到访问比较慢的问题,一方面是github的打开慢,还有就是相关的数据传输很慢,比如从github上clone代码,有时候仓库比较大,经常是clone到一半就卡住了,或者速度几乎跌0了,还有比如是下载一些软件的软件包,特别是这个让人特别眼熟的地址raw.githubusercontent.com,经常是龟速下载,时间一长还经常断了就没法继续下载了
最近看到了这个项目,https://github.com/521xueweihan/GitHub520
我们可以把项目的内容复制下来,追加写入到我们的hosts文件里,但是这种方式不够一劳永逸,而且如果出现了访问缓慢,又去这个仓库下载的话,就变成先有鸡还是先有蛋的问题了,
所以刚好有switchhosts这款软件,它可以新建一个远程模式的hosts配置文件

同时可以设置更新频率,仓库维护着推荐是1小时更新,我觉得其实1天问题也不大,这样也能减轻维护者服务器的压力
还有就是通过crontab任务,比如

1
sudo sed -i "" "/# GitHub520 Host Start/,/# Github520 Host End/d" /etc/hosts && curl https://raw.hellogithub.com/hosts | sudo tee -a /etc/hosts

但是相对来说不太推荐,crontab维护比较原始,出现问题也不好恢复,switchhosts是个不错的选择

上次简单体验了下koupleless框架,可以发现ark模块的加载运行是非常有优势的,
这次就来看下这个模块式怎么加载的
我们可以通过启动基座使用调试模式,然后打下断点在ArkClient这个类的几个核心方法
比如com.alipay.sofa.ark.api.ArkClient#installBiz(java.io.File)
核心方法是在这

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
private static ClientResponse doInstallBiz(File bizFile, String[] args, Map<String, String> envs) throws Throwable {
AssertUtils.assertNotNull(bizFactoryService, "bizFactoryService must not be null!");
AssertUtils.assertNotNull(bizManagerService, "bizManagerService must not be null!");
AssertUtils.assertNotNull(bizFile, "bizFile must not be null!");
long start = System.currentTimeMillis();
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss,SSS");
String startDate = sdf.format(new Date(start));
Biz biz = bizFactoryService.createBiz(bizFile);
ClientResponse response = new ClientResponse();
if (bizManagerService.getBizByIdentity(biz.getIdentity()) == null && bizManagerService.registerBiz(biz)) {
try {
biz.start(args, envs);
long end = System.currentTimeMillis();
response.setCode(ResponseCode.SUCCESS).setMessage(String.format("Install Biz: %s success, cost: %s ms, started at: %s", biz.getIdentity(), end - start, startDate)).setBizInfos(Collections.singleton(biz));
getLogger().info(response.getMessage());
return response;
} catch (Throwable var15) {
long end = System.currentTimeMillis();
response.setCode(ResponseCode.FAILED).setMessage(String.format("Install Biz: %s fail,cost: %s ms, started at: %s", biz.getIdentity(), end - start, startDate));
getLogger().error(response.getMessage(), var15);
boolean autoUninstall = Boolean.parseBoolean(ArkConfigs.getStringValue("sofa.ark.auto.uninstall.when.failed.enable", "true"));
if (autoUninstall) {
try {
getLogger().error(String.format("Start Biz: %s failed, try to unInstall this biz.", biz.getIdentity()));
biz.stop();
} catch (Throwable var14) {
getLogger().error(String.format("UnInstall Biz: %s fail.", biz.getIdentity()), var14);
}
}

throw var15;
}
} else {
return response.setCode(ResponseCode.REPEAT_BIZ).setMessage(String.format("Biz: %s has been installed or registered.", biz.getIdentity()));
}
}

具体的代码在

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
public Biz createBiz(File file) throws IOException {
boolean unpackBizWhenInstall = Boolean.parseBoolean(ArkConfigs.getStringValue("sofa.ark.unpack.biz.when.install", "true"));
Object bizArchive;
if (ArkConfigs.isEmbedEnable() && unpackBizWhenInstall) {
File unpackFile = FileUtils.file(file.getAbsolutePath() + "-unpack");
if (!unpackFile.exists()) {
unpackFile = FileUtils.unzip(file, file.getAbsolutePath() + "-unpack");
}

if (file.exists()) {
file.delete();
}

file = unpackFile;
bizArchive = new ExplodedBizArchive(unpackFile);
} else {
JarFile bizFile = new JarFile(file);
JarFileArchive jarFileArchive = new JarFileArchive(bizFile);
bizArchive = new JarBizArchive(jarFileArchive);
}

BizModel biz = (BizModel)this.createBiz((BizArchive)bizArchive);
biz.setBizTempWorkDir(file);
return biz;
}

接下来是创建biz模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public Biz createBiz(BizArchive bizArchive) throws IOException {
AssertUtils.isTrue(this.isArkBiz(bizArchive), "Archive must be a ark biz!", new Object[0]);
BizModel bizModel = new BizModel();
Attributes manifestMainAttributes = bizArchive.getManifest().getMainAttributes();
String mainClass = manifestMainAttributes.getValue("Main-Class");
String startClass = manifestMainAttributes.getValue("Start-Class");
bizModel.setBizState(BizState.RESOLVED, StateChangeReason.CREATED).setBizName(manifestMainAttributes.getValue("Ark-Biz-Name")).setBizVersion(manifestMainAttributes.getValue("Ark-Biz-Version")).setMainClass(!StringUtils.isEmpty(startClass) ? startClass : mainClass).setPriority(manifestMainAttributes.getValue("priority")).setWebContextPath(manifestMainAttributes.getValue("web-context-path")).setDenyImportPackages(manifestMainAttributes.getValue("deny-import-packages")).setDenyImportClasses(manifestMainAttributes.getValue("deny-import-classes")).setDenyImportResources(manifestMainAttributes.getValue("deny-import-resources")).setInjectPluginDependencies(this.getInjectDependencies(manifestMainAttributes.getValue("inject-plugin-dependencies"))).setInjectExportPackages(manifestMainAttributes.getValue("inject-export-packages")).setDeclaredLibraries(manifestMainAttributes.getValue("declared-libraries")).setClassPath(bizArchive.getUrls()).setPluginClassPath(this.getPluginURLs());
if (!(bizArchive instanceof DirectoryBizArchive)) {
bizModel.setBizUrl(bizArchive.getUrl());
}

BizClassLoader bizClassLoader = new BizClassLoader(bizModel.getIdentity(), this.getBizUcp(bizModel.getClassPath()), bizArchive instanceof ExplodedBizArchive || bizArchive instanceof DirectoryBizArchive);
bizClassLoader.setBizModel(bizModel);
bizModel.setClassLoader(bizClassLoader);
return bizModel;
}

这里就会读取对应biz包的启动类等,以及创建对应的biz模块的类加载器 BizClassLoader
接下去回到上一层

1
biz.start(args, envs);

接下来就是BizModel的start方法

1
2
3
public void start(String[] args, Map<String, String> envs) throws Throwable {
this.doStart(args, envs);
}

实际的就是doStart方法

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
private void doStart(String[] args, Map<String, String> envs) throws Throwable {
AssertUtils.isTrue(this.bizState == BizState.RESOLVED, "BizState must be RESOLVED", new Object[0]);
if (envs != null) {
String mainClassFromEnv = (String)envs.get("sofa.ark.biz.main.class");
if (mainClassFromEnv != null) {
this.mainClass = mainClassFromEnv;
ArkLoggerFactory.getDefaultLogger().info("Ark biz {} will start with main class {} from envs", this.getIdentity(), mainClassFromEnv);
}
}

if (this.mainClass == null) {
throw new ArkRuntimeException(String.format("biz: %s has no main method", this.getBizName()));
} else {
ClassLoader oldClassLoader = ClassLoaderUtils.pushContextClassLoader(this.classLoader);
EventAdminService eventAdminService = (EventAdminService)ArkServiceContainerHolder.getContainer().getService(EventAdminService.class);

try {
eventAdminService.sendEvent(new BeforeBizStartupEvent(this));
this.resetProperties();
if (!this.isMasterBizAndEmbedEnable()) {
long start = System.currentTimeMillis();
ArkLoggerFactory.getDefaultLogger().info("Ark biz {} start.", this.getIdentity());
MainMethodRunner mainMethodRunner = new MainMethodRunner(this.mainClass, args, envs);
mainMethodRunner.run();
eventAdminService.sendEvent(new AfterBizStartupEvent(this));
ArkLoggerFactory.getDefaultLogger().info("Ark biz {} started in {} ms", this.getIdentity(), System.currentTimeMillis() - start);
}
} catch (Throwable var11) {
this.setBizState(BizState.BROKEN, StateChangeReason.INSTALL_FAILED, getStackTraceAsString(var11));
eventAdminService.sendEvent(new AfterBizStartupFailedEvent(this, var11));
throw var11;
} finally {
ClassLoaderUtils.popContextClassLoader(oldClassLoader);
}

BizManagerService bizManagerService = (BizManagerService)ArkServiceContainerHolder.getContainer().getService(BizManagerService.class);
if (bizManagerService.getActiveBiz(this.bizName) == null) {
this.setBizState(BizState.ACTIVATED, StateChangeReason.STARTED);
} else {
boolean activateMultiBizVersion = Boolean.parseBoolean(ArkConfigs.getStringValue("sofa.ark.activate.multi.biz.version.enable", "false"));
if (activateMultiBizVersion) {
this.setBizState(BizState.ACTIVATED, StateChangeReason.STARTED);
} else {
if (Boolean.getBoolean("activate.new.module")) {
Biz currentActiveBiz = bizManagerService.getActiveBiz(this.bizName);
((BizModel)currentActiveBiz).setBizState(BizState.DEACTIVATED, StateChangeReason.SWITCHED, String.format("switch to new biz %s", this.getIdentity()));
this.setBizState(BizState.ACTIVATED, StateChangeReason.STARTED, String.format("switch from old biz: %s", currentActiveBiz.getIdentity()));
} else {
this.setBizState(BizState.DEACTIVATED, StateChangeReason.STARTED, "start but is deactivated");
}

}
}
}
}

主要是看这个方法

1
2
3
ArkLoggerFactory.getDefaultLogger().info("Ark biz {} start.", this.getIdentity());
MainMethodRunner mainMethodRunner = new MainMethodRunner(this.mainClass, args, envs);
mainMethodRunner.run();

看到这个run方法

1
2
3
4
5
public Object run() throws Exception {
Class<?> mainClass = Thread.currentThread().getContextClassLoader().loadClass(this.mainClassName);
Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
return mainMethod.invoke((Object)null, this.args);
}

最终就是到这个,它就用了这个上下文的类加载器,而这个类加载器就是用的前面设置的bizClassLoader

1
2
3
4
5
public static ClassLoader pushContextClassLoader(ClassLoader newClassLoader) {
ClassLoader oldClassLoader = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(newClassLoader);
return oldClassLoader;
}

在挺久之前serverless架构还是很火的,大概是在这一波人工智能热潮之前,结合云原生,但似乎这个框架似乎没有一个大一统的,类似于Java中的spring,springboot这种框架
前阵子体验到了这款Koupleless框架,前身是sofa serverless,总结下来主要是对于它的模块级开发和部署的机制觉得非常不错
首先就是我们可以下一个samples来体验
可以通过git也可以去下载zip包(因为实在有点大,网络不好可能还是zip包比较快)

1
git clone git@github.com:koupleless/samples.git

比较方便的是基于仓库下的springboot-samples/web/tomcat/样例来体验

目录下的 base-web-single-host 是基座应用
biz1-web-single-hostbiz2-web-single-host 是业务模块的demo
代码clone下来以后首先要构建这几个模块

1
mvn -pl web/tomcat/biz1-web-single-host,web/tomcat/biz2-web-single-host -am clean package -DskipTests

这里会碰到几个问题,一个是需要maven版本 >= 3.9.x,于是我就单独下了个,另外就是会碰到

1
2
 Plugin com.alipay.sofa.koupleless:koupleless-base-build-plugin:1.4.2-SNAPSHOT or one of its dependencies could not be resolved:
[ERROR] Could not find artifact com.alipay.sofa.koupleless:koupleless-base-build-plugin:jar:1.4.2-SNAPSHOT in ark-snapshot (https://oss.sonatype.org/content/repositories/snapshots)

这个问题,然后就去这个仓库看了下缔约没这个版本的包,已经是正式包了,
找到这个版本是在根pom定义的

1
<koupleless.runtime.version>1.4.2</koupleless.runtime.version>

把它改成正式版本构建就好了,当然也是对国内网络不太友好,需要等一会
还需要准备好模块包的构建工具,arkctl v0.2.1+
可以用go来安装

1
go install github.com/koupleless/arkctl@v0.2.1

或者直接在这下载 https://github.com/koupleless/arkctl/releases
安装后需要设置下PATH,也可以直接用全路径,比如mac下安装完了是在用户目录下的/go/bin/arkctl
对了,还需要maven拉下依赖包,也可以用命令行的
然后就可以启动基座了,基座启动就类似于普通的springboot应用,
接下去启动biz模块

1
~/go/bin/arkctl deploy web/tomcat/biz1-web-single-host/target/biz1-web-single-host-0.0.1-SNAPSHOT-ark-biz.jar

1
~/go/bin/arkctl deploy web/tomcat/biz2-web-single-host/target/biz2-web-single-host-0.0.1-SNAPSHOT-ark-biz.jar 

启动之后就可以用curl或者浏览器访问来验证,是否两个模块都被加载了
使用curl查看biz1

1
curl http://localhost:8080/biz1/

可以看到输出

1
hello to biz1 deploy, run in com.alipay.sofa.ark.container.service.classloader.BizClassLoader@4850d96f%

再看下biz2

1
curl http://localhost:8080/biz2/ 

可以看到输出

1
hello to biz2 deploy, run in com.alipay.sofa.ark.container.service.classloader.BizClassLoader@6c5136fe

结合代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.alipay.sofa.web.biz1.rest;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDate;
import java.time.LocalDateTime;

@RestController
public class SampleController {

@Autowired
private ApplicationContext applicationContext;

@RequestMapping(value = "/", method = RequestMethod.GET)
public String hello() {
String appName = applicationContext.getId();
return String.format("hello to %s deploy, run in %s", appName, Thread.currentThread()
.getContextClassLoader());
}

可以看到这是biz1的输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.alipay.sofa.web.biz2.rest;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDate;
import java.time.LocalDateTime;

@RestController
public class SampleController {

@Autowired
private ApplicationContext applicationContext;

@RequestMapping(value = "/", method = RequestMethod.GET)
public String hello() {
String appName = applicationContext.getId();
return String.format("hello to %s deploy, run in %s", appName, Thread.currentThread()
.getContextClassLoader());
}

biz2也会答应对应的
这里其实主体就是启动一个应用基座,模块都是直接在基座上热加载的,不用重新启动应用,对于需要快捷开发并行开发的还是很方便的,也是种变相的serverless下,模块的代码结构模式也跟普通springboot应用很类似,就常规的开发,但是部署启动都变成了秒级的,非常方便

前阵子在微博看到一个说nocodb这个工具,一开始以为是类似于飞书的多维表格,那个对于很多办公党来说真的很好用,但是对于我好像没啥吸引力,但是这次看到的介绍是能替代很多db工具,就相当于是个后台系统,可以查看表的数据以及编辑等
首先就是很简单的安装,不过需要再支持docker且安装了的环境,

1
bash <(curl -sSL http://install.nocodb.com/noco.sh) <(mktemp)

在高级选项中我选择了

1
2
3
4
1. db (1 replicas)
2. nocodb (1 replicas)
3. redis (1 replicas)
4. traefik (1 replicas)

这几个,第一个是db,可以选pgsql,2是本体,3是作为缓存,4是作为网络路由组件

可以在左边菜单中添加外部数据源集成

添加以后
就可以在这里

连接外部数据源,选择刚才新增的数据源

接下来就能在tables里查看数据表里的数据了

比如像新增数据,编辑数据都是支持

还能搜索过滤数据

真的是只要有了表,里面的数据查看管理都直接支持了,非常的方便,省去了我们自己搞个后台系统,并且也是非常美观的展示样式,对比起phpmyadmin这种,真的优秀太多了
如果有外键关联还可以查看表结构关系,类似于er图的功能了,还有webhook,数据出现变动就会发送hook通知

甚至还能直接升成swagger的rest api文档,真的强的有点离谱

感觉有了这种工具,一般的crud工作基本就完全省掉了,建好表,直接连接nocodb,然后导出api,前端就可以干活了,
如果是基本的数据crud就完全不用开发了,需要前端定制的就直接对接前端
感觉对于小工作室或者个人开发者来说还是很方便了的,当然结合最新的人工智能技术编写代码也能做得很好
现在这样子相对简单的功能是变得越来越方便

0%