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

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


  • 首页

  • 归档

  • 搜索

Redis架构实战:高并发情况下并发扣减库存 第一种方案:纯

发表于 2021-08-12

相信大家从网上学习项目大部分人第一个项目都是电商,生活中时时刻刻也会用到电商APP,例如淘宝,京东等。做技术的人都知道,电商的业务逻辑简单,但是大部分电商都会涉及到高并发高可用,对并发和对数据的处理要求是很高的。这里我今天就讲一下高并发情况下是如何扣减库存的?

我们对扣减库存所需要关注的技术点如下:

  1. 当前剩余的数量大于等于当前需要扣减的数量,不允许超卖
  2. 对于同一个数据的数量存在用户并发扣减,需要保证并发的一致性
  3. 需要保证可用性和性能,性能至少是秒级
  4. 一次的扣减包含多个目标数量
  5. 当次扣减有多个数量时,其中一个扣减不成功即不成功,需要回滚
  6. 必须有扣减才能有归还
  7. 返还的数量必须要加回,不能丢失
  8. 一次扣减可以有多次返还
  9. 返还需要保证幂等性

第一种方案:纯MySQL扣减实现

顾名思义,就是扣减业务完全依赖MySQL等数据库来完成。而不依赖一些其他的中间件或者缓存。纯数据库实现的好处就是逻辑简单,开发以及部署成本低。(适用于中小型电商)。

纯数据库的实现之所以能够满足扣减业务的各项功能要求,主要依赖两点:

  1. 基于数据库的乐观锁方式保证并发扣减的强一致性
  2. 基于数据库的事务实现批量扣减失败进行回滚

基于上述方案,它包含一个扣减服务和一个数量数据库

image.png

如果数据量单库压力很大,也可以做主从和分库分表,服务可以做集群等。

image.png
一次完整的流程就是先进行数据校验,在其中做一些参数格式校验,这里做接口开发的时候,要保持一个原则就是不信任原则,一切数据都不要相信,都需要做校验判断。其次,还可以进行库存扣减的前置校验。比如当前库存中的库存只有8个,而用户要购买10个,此时的数据校验中即可前置拦截,减少对于数据库的写操作。纯读不会加锁,性能较高,可以采用此种方式提升并发量。

1
mysql复制代码update xxx set leavedAmount=leavedAmount-currentAmount where skuid='xxx' and leavedAmount>=currentAmount

此SQL采用了类似乐观锁的方式实现了原子性。在where后面判断剩余数量大于等于需要的数量,才能成功,否则失败。

扣减完成之后,需要记录流水数据。每一次扣减的时候,都需要外部用户传入一个uuid作为流水编号,此编号是全局唯一的。用户在扣减时传入唯一的编号有两个作用:

  1. 当用户归还数量时,需要带回此编码,用来标识此次返还属于历史上的哪次扣减。
  2. 进行幂等性控制。当用户调用扣减接口出现超时时,因为用户不知道是否成功,用户可以采用此编号进行重试或反查。在重试时,使用此编号进行标识防重

当用户只购买某个商品一个的时候,如果校验时剩余库存有8个,此时校验通过。但在后续的实际扣减时,因为其他用户也在并发的扣减,可能会出现幻读,此时用户实际去扣减时不足一个,导致失败。这种场景会导致多一次数据库查询,降低整体的扣减性能。这时候可以对MySQL架构进行升级

MySQL架构升级

多一次查询,就会增加数据库的压力,同时对整体性能也有一定的影响。此外,对外提供的查询库存数量的接口也会对数据库产生压力,同时读的请求要远大于写。

根据业务场景分析,读库存的请求一般是顾客浏览商品时产生,而调用扣减库存的请求基本上是用户购买时才触发。用户购买请求的业务价值比读请求会更大,因此对于写需要重点保障。针对上述的问题,可以对MySQL整体架构进行升级

image.png
整体的升级策略采用读写分离的方式,另外主从复制直接使用MySQL等数据库已有的功能,改动上非常小,只要在扣减服务里配置两个数据源。当客户查询剩余库存,扣减服务中的前置校验时,读取从数据库即可。而真正的数据扣减还是使用主数据库。

读写分离之后,根据二八原则,80% 的均为读流量,主库的压力降低了 80%。但采用了读写分离也会导致读取的数据不准确的问题,不过库存数量本身就在实时变化,短暂的差异业务上是可以容忍的,最终的实际扣减会保证数据的准确性。

在上面基础上,还可以升级,增加缓存

image.png

纯数据库的方案虽然可以避免超卖和少卖的情况,但是并发量实在很低,性能不是很乐观。所以这里再进行升级

第二种方案:缓存实现扣减

image.png
这和前面的扣减库存其实是一样的。但是此时扣减服务依赖的是Redis而不是数据库了。

这里针对Redis的hash结构不支持多个key的批量操作问题,我们可以采用Redis+lua脚本来实现批量扣减单线程请求。

升级成纯Redis实现扣减也会有问题

  1. Redis挂了,如果还没有执行到扣减Redis里面库存的操作挂了,只需要返回给客户端失败即可。如果已经执行到Redis扣减库存之后挂了。那这时候就需要有一个对账程序。通过对比Redis与数据库中的数据是否一致,并结合扣减服务的日志。当发现数据不一致同时日志记录扣减失败时,可以将数据库比Redis多的库存数据在Redis进行加回。
  2. Redis扣减完成,异步刷新数据库失败了。此时Redis里面的数据是准的,数据库的库存是多的。在结合扣减服务的日志确定是Redis扣减成功到但异步记录数据失败后,可以将数据库比Redis多的库存数据在数据库中进行扣减。

虽然使用纯Redis方案可以提高并发量,但是因为Redis不具备事务特性,极端情况下会存在Redis的数据无法回滚,导致出现少卖的情况。也可能发生异步写库失败,导致多扣的数据再也无法找回的情况。

第三种方案:数据库+缓存

顺序写的性能更好

在向磁盘进行数据操作时,向文件末尾不断追加写入的性能要远大于随机修改的性能。因为对于传统的机械硬盘来说,每一次的随机更新都需要机械键盘的磁头在硬盘的盘面上进行寻址,再去更新目标数据,这种方式十分消耗性能。而向文件末尾追加写入,每一次的写入只需要磁头一次寻址,将磁头定位到文件末尾即可,后续的顺序写入不断追加即可。

对于固态硬盘来说,虽然避免了磁头移动,但依然存在一定的寻址过程。此外,对文件内容的随机更新和数据库的表更新比较类似,都存在加锁带来的性能消耗。

数据库同样是插入要比更新的性能好。对于数据库的更新,为了保证对同一条数据并发更新的一致性,会在更新时增加锁,但加锁是十分消耗性能的。此外,对于没有索引的更新条件,要想找到需要更新的那条数据,需要遍历整张表,时间复杂度为 O(N)。而插入只在末尾进行追加,性能非常好。

顺序写的架构

通过上面的理论就可以得出一个兼具性能和高可靠的扣减架构

image.png
上述的架构和纯缓存的架构区别在于,写入数据库不是异步写入,而是在扣减的时候同步写入。同步写入数据库使用的是insert操作,就是顺序写,而不是update做数据库数量的修改,所以,性能会更好。

insert 的数据库称为任务库,它只存储每次扣减的原始数据,而不做真实扣减(即不进行 update)。它的表结构大致如下:

1
2
3
4
mysql复制代码create table task{
id bigint not null comment "任务顺序编号",
task_id bigint not null
}

任务表里存储的内容格式可以为 JSON、XML 等结构化的数据。以 JSON 为例,数据内容大致可以如下:

1
2
3
4
5
6
json复制代码{
"扣减号":uuid,
"skuid1":"数量",
"skuid2":"数量",
"xxxx":"xxxx"
}

这里我们肯定是还有一个记录业务数据的库,这里存储的是真正的扣减名企和SKU的汇总数据。对于另一个库里面的数据,只需要通过这个表进行异步同步就好了。

扣减流程

image.png

这里和纯缓存的区别在于增加了事务开启与回滚的步骤,以及同步的数据库写入流程

任务库里存储的是纯文本的 JSON 数据,无法被直接使用。需要将其中的数据转储至实际的业务库里。业务库里会存储两类数据,一类是每次扣减的流水数据,它与任务表里的数据区别在于它是结构化,而不是 JSON 文本的大字段内容。另外一类是汇总数据,即每一个 SKU 当前总共有多少量,当前还剩余多少量(即从任务库同步时需要进行扣减的),表结构大致如下:

1
2
3
4
5
6
mysql复制代码create table 流水表{
id bigint not null,
uuid bigint not null comment '扣减编号',
sku_id bigint not null comment '商品编号',
num int not null comment '当次扣减的数量'
}comment '扣减流水表'

商品的实时数据汇总表,结构如下:

1
2
3
4
5
6
mysql复制代码create table 汇总表{
id bitint not null,
sku_id unsigned bigint not null comment '商品编号',
total_num unsigned int not null comment '总数量',
leaved_num unsigned int not null comment '当前剩余的商品数量'
}comment '记录表'

在整体的流程上,还是复用了上一讲纯缓存的架构流程。当新加入一个商品,或者对已有商品进行补货时,对应的新增商品数量都会通过 Binlog 同步至缓存里。在扣减时,依然以缓存中的数量为准

image.png

本文转载自: 掘金

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

Dubbo 30 服务注册

发表于 2021-08-12

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

首先分享之前的所有文章 , 欢迎点赞收藏转发三连下次一定 >>>> 😜😜😜

文章合集 : 🎁 juejin.cn/post/694164…

Github : 👉 github.com/black-ant

CASE 备份 : 👉 gitee.com/antblack/ca…

一 . 前言

服务发现的作用主要范围包括向注册中心注册服务 , 以及服务的发现 (Feign 的负载均衡是在 Client 端做的 , 猜测Dubbo负载均衡也是)

Dubbo 提供多种注册中心 , 常见的有 Nacos , Zookeeper 和 Redis , 此篇也只看看3种

二 . 服务发现流程

2.1 服务发现的调用逻辑

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码// 服务发现的起点还是 BootStrap , 由 Listener 发起流程

C- DubboBootstrapApplicationListener # onContextRefreshedEvent
C- DubboBootstrap # start : 开始初始化
C- DubboBootstrap # exportServices : 注册 DubboService
C- ServiceConfig # export : 调用init 执行初始化 , 校验 config , 设置 serviceMetadata 元数据
C- ServiceConfig # doExport : 没太多逻辑 , 下层调用而已
C- ServiceConfig # doExportUrls : ServiceRepository 进行保存和下层调用
C- ServiceConfig # doExportUrlsFor1Protocol
C- ProtocolFilterWrapper # export
C- ProtocolListenerWrapper # export
C- RegistryProtocol # export : 发起注册逻辑

Zookeeper Service 详情

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
json复制代码{
"name": "dubbo-demo-annotation-provider",
"id": "10.10.100.140:20880",
"address": "10.10.100.140",
"port": 20880,
"sslPort": null,
"payload": {
"@class": "org.apache.dubbo.registry.zookeeper.ZookeeperInstance",
"id": null,
"name": "dubbo-demo-annotation-provider",
"metadata": {
"REGISTRY_CLUSTER": "registryConfig",
"anyhost": "true",
"application": "dubbo-demo-annotation-provider",
"deprecated": "false",
"dubbo": "2.0.2",
"dubbo.endpoints": "[{\"port\":20880,\"protocol\":\"dubbo\"}]",
"dubbo.metadata-service.url-params": "{\"version\":\"1.0.0\",\"dubbo\":\"2.0.2\",\"port\":\"20880\",\"protocol\":\"dubbo\"}",
"dubbo.metadata.revision": "3c1be7b82505c5da23a5bc1cd1211417",
"dubbo.metadata.storage-type": "local",
"dynamic": "true",
"generic": "false",
"interface": "org.apache.dubbo.demo.DemoService",
"metadata-type": "remote",
"methods": "sayHello,sayHelloAsync",
"pid": "21576",
"release": "",
"side": "provider",
"timestamp": "1626856228483"
}
},
"registrationTimeUTC": 1626856236245,
"serviceType": "DYNAMIC",
"uriSpec": null
}

Zookeeper 中数据情况

Dubbo-zookeeper-service.png

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
JAVA复制代码C- DubboBootstrap # start
C- DubboBootstrap # exportServices : start 逻辑中发起 export 处理

private void exportServices() {
// Step 1 : configManager.getServices() 获取 Service 列表 -> PRO00001
for (ServiceConfigBase sc : configManager.getServices()) {

// 获取当前 ServiceConfig , 并且为其设置容器
ServiceConfig<?> serviceConfig = (ServiceConfig<?>) sc;
serviceConfig.setBootstrap(this);
if (!serviceConfig.isRefreshed()) {
// 需要刷新时对其进行刷新
serviceConfig.refresh();
}

// 需要异步处理 -> export-async
if (sc.shouldExportAsync()) {

// 明显看到这里使用 ScheduledExecutorService + future 发起了异步调用
ExecutorService executor = executorRepository.getExportReferExecutor();
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
try {
if (!sc.isExported()) {
sc.export();
exportedServices.add(sc);
}
} catch (Throwable t) {
logger.error("export async catch error : " + t.getMessage(), t);
}
}, executor);

asyncExportingFutures.add(future);
} else {
// 这里逻辑都是一样的 , 没有导出过的进行导出 ,同时添加到对应集合中
if (!sc.isExported()) {
sc.export();
exportedServices.add(sc);
}
}
}
}


// PRO00001 : configManager.getServices 列表
<dubbo:service path="org.apache.dubbo.demo.DemoService" ref="org.apache.dubbo.demo.provider.DemoServiceImpl@60990e5c" generic="false" deprecated="false" dynamic="true" id="ServiceBean:org.apache.dubbo.demo.DemoService" />

补充 configManager

该方法从configsCache 中获取对应的参数 ,此处取得是 service

ConfigsCache.png

添加的流程 :

  • C- AbstractConfig # addIntoConfigManager -> org.apache.dubbo.config.spring.ServiceBean
  • C- ConfigManager # addConfig : 添加 Config , 用于前文循环时使用

可以看到 , 第一步时使用的就是 serverBean , 这个Bean 在扫描时创建 , 详见 -> Dubbo 3.0 : DubboService 的扫描

2.3 解析出 Service

中间没有太多东西 , 主要调用流程为 :

  • C- ServiceConfig # export :
  • C- ServiceConfig # doExport :
    • C- ServiceConfig # doExportUrls :
    • C- ServiceConfig # exported :
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
java复制代码// 在此阶段 , 构建出 Registry URL , 用于 Zookeeper 注册
private void doExportUrls() {

// PRO0001 :
ServiceRepository repository = ApplicationModel.getServiceRepository();
// PRO0002 :
ServiceDescriptor serviceDescriptor = repository.registerService(getInterfaceClass());
repository.registerProvider(
getUniqueServiceName(),
ref,
serviceDescriptor,
this,
serviceMetadata
);

// PRO0003 :
List<URL> registryURLs = ConfigValidationUtils.loadRegistries(this, true);

// PRO0004 :
for (ProtocolConfig protocolConfig : protocols) {
// org.apache.dubbo.demo.DemoService
String pathKey = URL.buildKey(getContextPath(protocolConfig)
.map(p -> p + "/" + path)
.orElse(path), group, version);
// 如果用户指定了路径,则再次注册服务,将其映射到路径
repository.registerService(pathKey, interfaceClass);
doExportUrlsFor1Protocol(protocolConfig, registryURLs);
}
}

// PRO0001 : ServiceRepository 结构
{
"services": {
"serviceName": "org.apache.dubbo.rpc.service.EchoService",
"serviceName": "org.apache.dubbo.rpc.service.GenericService",
"serviceName": "org.apache.dubbo.monitor.MetricsService",
"serviceName": "org.apache.dubbo.monitor.MonitorService",
"serviceName": "org.apache.dubbo.demo.DemoService",

},
"providers": {
"org.apache.dubbo.demo.DemoService": "ProviderModel"
},
"providersWithoutGroup": "org.apache.dubbo.demo.DemoService:0.0.0"
}




// PRO0003 : 构建 RegistryURL
service-discovery-registry://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?REGISTRY_CLUSTER=registryConfig&application=dubbo-demo-annotation-provider&dubbo=2.0.2&pid=28256&registry=zookeeper&timestamp=1626858537404

registry://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?REGISTRY_CLUSTER=registryConfig&application=dubbo-demo-annotation-provider&dubbo=2.0.2&pid=28256&registry=zookeeper&timestamp=1626858537404

2.4 执行主流程

其中主步骤分为 3 步 :

  • Step 1 : 属性准备
  • Step 2 : 获取 host port , 构建 url
  • Step 3 : 构建 Invoke , export 导出
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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
java复制代码private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) {
String name = protocolConfig.getName();
if (StringUtils.isEmpty(name)) {
name = DUBBO;
}

// Step 1 : 属性准备
Map<String, String> map = new HashMap<String, String>();\
// String PROVIDER_SIDE = "provider";
// String SIDE_KEY = "side";
map.put(SIDE_KEY, PROVIDER_SIDE);

// 此环节添加属性
ServiceConfig.appendRuntimeParameters(map);
AbstractConfig.appendParameters(map, getMetrics());
AbstractConfig.appendParameters(map, getApplication());
AbstractConfig.appendParameters(map, getModule());
// remove 'default.' prefix for configs from ProviderConfig
// appendParameters(map, provider, Constants.DEFAULT_KEY);
AbstractConfig.appendParameters(map, provider);
AbstractConfig.appendParameters(map, protocolConfig);
AbstractConfig.appendParameters(map, this);

MetadataReportConfig metadataReportConfig = getMetadataReportConfig();
if (metadataReportConfig != null && metadataReportConfig.isValid()) {
map.putIfAbsent(METADATA_KEY, REMOTE_METADATA_STORAGE_TYPE);
}
if (CollectionUtils.isNotEmpty(getMethods())) {
//TODO Improve method config processing
//... 此处如果 dubbo:method 进行了方法级处理 , 此处会深入处理 , 同样是添加 method 处理
}

if (ProtocolUtils.isGeneric(generic)) {
map.put(GENERIC_KEY, generic);
map.put(METHODS_KEY, ANY_VALUE);
} else {
String revision = Version.getVersion(interfaceClass, version);
if (revision != null && revision.length() > 0) {
map.put(REVISION_KEY, revision);
}

String[] methods = Wrapper.getWrapper(interfaceClass).getMethodNames();
// ["sayHello","sayHelloAsync"]
if (methods.length == 0) {
// 省略为空逻辑
} else {
map.put(METHODS_KEY, StringUtils.join(new HashSet<String>(Arrays.asList(methods)), ","));
}
}

// 提供程序配置的令牌值用于将值分配给ServiceConfig#令牌
if (ConfigUtils.isEmpty(token) && provider != null) {
token = provider.getToken();
}

if (!ConfigUtils.isEmpty(token)) {
if (ConfigUtils.isDefault(token)) {
map.put(TOKEN_KEY, UUID.randomUUID().toString());
} else {
map.put(TOKEN_KEY, token);
}
}
// 讲所有的 map 放入 元数据中
serviceMetadata.getAttachments().putAll(map);

// Step 2 : 获取 host port , 构建 url
String host = findConfigedHosts(protocolConfig, registryURLs, map);
Integer port = findConfigedPorts(protocolConfig, name, map);
// PRO24001 -> url
URL url = new ServiceConfigURL(name, null, null, host, port, getContextPath(protocolConfig).map(p -> p + "/" + path).orElse(path), map);

// 自定义Configurator以附加额外参数
if (ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class)
.hasExtension(url.getProtocol())) {
url = ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class)
.getExtension(url.getProtocol()).getConfigurator(url).configure(url);
}

String scope = url.getParameter(SCOPE_KEY);
// don't export when none is configured
if (!SCOPE_NONE.equalsIgnoreCase(scope)) {

// 如果配置不是远程的,则导出到本地
if (!SCOPE_REMOTE.equalsIgnoreCase(scope)) {
exportLocal(url);
}
// 如果配置不是本地的,则导出到远程
if (!SCOPE_LOCAL.equalsIgnoreCase(scope)) {
if (CollectionUtils.isNotEmpty(registryURLs)) {
// PIC24002 -> registryURLs
for (URL registryURL : registryURLs) {
//if protocol is only injvm ,not register
if (LOCAL_PROTOCOL.equalsIgnoreCase(url.getProtocol())) {
continue;
}
url = url.addParameterIfAbsent(DYNAMIC_KEY, registryURL.getParameter(DYNAMIC_KEY));
URL monitorUrl = ConfigValidationUtils.loadMonitor(this, registryURL);
if (monitorUrl != null) {
url = url.putAttribute(MONITOR_KEY, monitorUrl);
}

// For providers, this is used to enable custom proxy to generate invoker
String proxy = url.getParameter(PROXY_KEY);
if (StringUtils.isNotEmpty(proxy)) {
registryURL = registryURL.addParameter(PROXY_KEY, proxy);
}

// Step 3 : 构建 Invoke , export 导出
Invoker<?> invoker = PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass, registryURL.putAttribute(EXPORT_KEY, url));
DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
// -> 2.5 Protocol 注册
Exporter<?> exporter = PROTOCOL.export(wrapperInvoker);
exporters.add(exporter);
}
} else {
if (MetadataService.class.getName().equals(url.getServiceInterface())) {
MetadataUtils.saveMetadataURL(url);
}
Invoker<?> invoker = PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass, url);
DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);

Exporter<?> exporter = PROTOCOL.export(wrapperInvoker);
exporters.add(exporter);
}

MetadataUtils.publishServiceDefinition(url);
}
}
this.urls.add(url);
}

PRO24001 案例 : providerUrl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码dubbo://192.168.181.2:20880/org.apache.dubbo.demo.DemoService
?anyhost=true
&application=dubbo-demo-annotation-provider
&bind.ip=192.168.181.2
&bind.port=20880
&deprecated=false
&dubbo=2.0.2
&dynamic=true
&generic=false
&interface=org.apache.dubbo.demo.DemoService
&metadata-type=remote
&methods=sayHello,sayHelloAsync
&pid=17324
&release=
&side=provider
&timestamp=1628690334012

PRO24002 案例 : registryUrl

1
2
3
4
5
6
7
java复制代码service-discovery-registry://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService
?REGISTRY_CLUSTER=registryConfig
&application=dubbo-demo-annotation-provider
&dubbo=2.0.2
&pid=17324
&registry=zookeeper
&timestamp=1628690331733

PIC24002
image.png

2.5 传入代理类

在这一步构建了 url ,同时生成了 ServiceDiscoveryRegistry

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复制代码C- RegistryProtocol
public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {

// service-discovery-registry://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService....
URL registryUrl = getRegistryUrl(originInvoker);
// dubbo://192.168.181.2:20880/org.apache.dubbo.demo.DemoService?anyhost=true....
URL providerUrl = getProviderUrl(originInvoker);

// PRO0003 : URL 参数
final URL overrideSubscribeUrl = getSubscribedOverrideUrl(providerUrl);
final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker);
overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);

// 重写 URL
providerUrl = overrideUrlWithConfig(providerUrl, overrideSubscribeListener);
//export invoker
final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker, providerUrl);

// Step 3 : 通过 Registry Factory 获取使用的 Registry , 例如 : ZookeeperRegistry -> 3.1
final Registry registry = getRegistry(registryUrl);
// Step 4 : 构建注册对象 -> PRO0003
final URL registeredProviderUrl = getUrlToRegistry(providerUrl, registryUrl);

// Step 5 : 注册发布服务
boolean register = providerUrl.getParameter(REGISTER_KEY, true);
if (register) **{**
// -> 2.5.2 Registry 的管理
register(registry, registeredProviderUrl);
}

// 在提供程序模型上注册声明的url
registerStatedUrl(registryUrl, registeredProviderUrl, register);


exporter.setRegisterUrl(registeredProviderUrl);
exporter.setSubscribeUrl(overrideSubscribeUrl);

// Deprecated! Subscribe to override rules in 2.6.x or before.
registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);

notifyExport(exporter);
//Ensure that a new exporter instance is returned every time export
return new DestroyableExporter<>(exporter);
}


// PRO0003 : URL 参数
provider://192.168.181.2:20880/org.apache.dubbo.demo.DemoService
?anyhost=true
&application=dubbo-demo-annotation-provider
&bind.ip=192.168.181.2
&bind.port=20880
&category=configurators
&check=false
&deprecated=false
&dubbo=2.0.2
&dynamic=true
&generic=false
&interface=org.apache.dubbo.demo.DemoService
&metadata-type=remote
&methods=sayHello,sayHelloAsync
&pid=17324
&release=
&side=provider
&timestamp=1628690334012

Registry 可以分为以下几个部分 :

  • 由 getRegistry 发起获取 Registry
  • createRegistry 发起创建逻辑
  • RegistryProtocol 调用 doRegister 执行 Register

三 . Registry 的创建

共用流程

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
java复制代码// Step 1 : 获取 Registry
C- RegistryFactoryWrapper
public Registry getRegistry(URL url) {
return new ListenerRegistryWrapper(registryFactory.getRegistry(url),
Collections.unmodifiableList(ExtensionLoader.getExtensionLoader(RegistryServiceListener.class)
.getActivateExtension(url, "registry.listeners")));
}

// Step 2 : 调用工厂创建 registry
C- AbstractRegistryFactory
public Registry getRegistry(URL url) {

Registry defaultNopRegistry = getDefaultNopRegistryIfDestroyed();
if (null != defaultNopRegistry) {
return defaultNopRegistry;
}

url = URLBuilder.from(url)
.setPath(RegistryService.class.getName())
.addParameter(INTERFACE_KEY, RegistryService.class.getName())
.removeParameters(EXPORT_KEY, REFER_KEY, TIMESTAMP_KEY)
.build();
String key = createRegistryCacheKey(url);
// 上锁
LOCK.lock();
try {

//..........

// Map<String, Registry> REGISTRIES = new HashMap<>();
Registry registry = REGISTRIES.get(key);
if (registry != null) {
return registry;
}
//通过 spi/ioc 的方式获取 Registry -> org.apache.dubbo.registry.client.ServiceDiscoveryRegistry
registry = createRegistry(url);

// 放入缓存
REGISTRIES.put(key, registry);
return registry;
} finally {
// Release the lock
LOCK.unlock();
}
}

3.1 创建 ServiceDiscoveryRegistry

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
java复制代码// Step 3 : ServiceDiscoveryRegistryFactory 
protected Registry createRegistry(URL url) {
if (UrlUtils.hasServiceDiscoveryRegistryProtocol(url)) {
// 此处拿到的为 zookeeper
String protocol = url.getParameter(REGISTRY_KEY, DEFAULT_REGISTRY);
// zookeeper://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?REGISTRY_CLUSTER=registryConfig&application=dubbo-demo-annotation-provider&dubbo=2.0.2&interface=org.apache.dubbo.registry.RegistryService&pid=2816
url = url.setProtocol(protocol).removeParameter(REGISTRY_KEY);
}

return new ServiceDiscoveryRegistry(url);
}

public ServiceDiscoveryRegistry(URL registryURL) {
this.registryURL = registryURL;
this.serviceDiscovery = createServiceDiscovery(registryURL);
this.writableMetadataService = WritableMetadataService.getDefaultExtension();
}

// 下次调用 ,
protected ServiceDiscovery createServiceDiscovery(URL registryURL) {
ServiceDiscovery serviceDiscovery = getServiceDiscovery(registryURL);
execute(() -> {
// 初始化操作
serviceDiscovery.initialize(registryURL.addParameter(INTERFACE_KEY, ServiceDiscovery.class.getName())
.removeParameter(REGISTRY_TYPE_KEY));
});
return serviceDiscovery;
}

static WritableMetadataService getDefaultExtension() {
return getExtensionLoader(WritableMetadataService.class).getDefaultExtension();
}

3.2 创建 ZookeeperRegistry

Zookeeper 只是 Dubbo 支持的注册中心之一 , 以其为例看一下创建和使用流程 :

这里有一个核心点 , ZookeeperRegistry 的创建是在 , 它创建时会被包裹为一个 ListenerRegistryWrapper 对象

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
JAVA复制代码C- ZookeeperRegistryFactory
public Registry createRegistry(URL url) {
return new ZookeeperRegistry(url, zookeeperTransporter);
}

// 创建 ZookeeperRegistry
public ZookeeperRegistry(URL url, ZookeeperTransporter zookeeperTransporter) {
super(url);
if (url.isAnyHost()) {
throw new IllegalStateException("registry address == null");
}
String group = url.getGroup(DEFAULT_ROOT);
if (!group.startsWith(PATH_SEPARATOR)) {
group = PATH_SEPARATOR + group;
}
this.root = group;
zkClient = zookeeperTransporter.connect(url);
zkClient.addStateListener((state) -> {
if (state == StateListener.RECONNECTED) {
ZookeeperRegistry.this.fetchLatestAddresses();
} else if (state == StateListener.NEW_SESSION_CREATED) {
try {
ZookeeperRegistry.this.recover();
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
} else if (state == StateListener.SESSION_LOST) {

} else if (state == StateListener.SUSPENDED) {

} else if (state == StateListener.CONNECTED) {

}
});
}


C- ExtensionLoader
private T createExtension(String name, boolean wrap) {
Class<?> clazz = getExtensionClasses().get(name);
if (clazz == null || unacceptableExceptions.contains(name)) {
throw findException(name);
}
try {
T instance = (T) EXTENSION_INSTANCES.get(clazz);
if (instance == null) {
EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.getDeclaredConstructor().newInstance());
instance = (T) EXTENSION_INSTANCES.get(clazz);
}
injectExtension(instance);


if (wrap) {

List<Class<?>> wrapperClassesList = new ArrayList<>();
if (cachedWrapperClasses != null) {
wrapperClassesList.addAll(cachedWrapperClasses);
wrapperClassesList.sort(WrapperComparator.COMPARATOR);
Collections.reverse(wrapperClassesList);
}

if (CollectionUtils.isNotEmpty(wrapperClassesList)) {
for (Class<?> wrapperClass : wrapperClassesList) {
Wrapper wrapper = wrapperClass.getAnnotation(Wrapper.class);
if (wrapper == null
|| (ArrayUtils.contains(wrapper.matches(), name) && !ArrayUtils.contains(wrapper.mismatches(), name))) {
instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
}
}
}
}

initExtension(instance);
return instance;
} catch (Throwable t) {
throw new IllegalStateException("Extension instance (name: " + name + ", class: " +
type + ") couldn't be instantiated: " + t.getMessage(), t);
}
}

补充 : ListenerRegistryWrapper
可以看到 , 这里 ListenerRegistryWrapper 中包含了一个 Registry 对象

1
2
3
4
5
6
7
java复制代码public class ListenerRegistryWrapper implements Registry {
private static final Logger logger = LoggerFactory.getLogger(ListenerRegistryWrapper.class);

private final Registry registry;
private final List<RegistryServiceListener> listeners;

}

四 . Registry 的调用

Registry 在 RegistryProtocol # register 进行注册 , 会调用父类 FailbackRegistry , 最终调用各种的实现类

FailbackRegistry 通用处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码// 此处以 FailbackRegistry 为例 
public void register(URL url) {
if (!acceptable(url)) {
return;
}
super.register(url);
removeFailedRegistered(url);
removeFailedUnregistered(url);
try {
// Sending a registration request to the server side
doRegister(url);
} catch (Exception e) {
// 省略部分异常处理逻辑
// Record a failed registration request to a failed list, retry regularly
addFailedRegistered(url);
}
}

System-AbstractRegistry.png

4.1 ServiceDiscoveryRegistry 注册

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
JAVA复制代码// Step 1 : 发起 Registry
C- RegistryProtocol
private void register(Registry registry, URL registeredProviderUrl) {
registry.register(registeredProviderUrl);
}


C- ServiceDiscoveryRegistry
public final void register(URL url) {
if (!shouldRegister(url)) { // Should Not Register
return;
}
doRegister(url);
//--> url = addRegistryClusterKey(url); 其中调用 addRegistryClusterKey
}

// Step 2 : 获取 RegistryClusterKey
private URL addRegistryClusterKey(URL url) {
String registryCluster = serviceDiscovery.getUrl().getParameter(REGISTRY_CLUSTER_KEY);
if (registryCluster != null && url.getParameter(REGISTRY_CLUSTER_KEY) == null) {
url = url.addParameter(REGISTRY_CLUSTER_KEY, registryCluster);
}
// dubbo://192.168.181.2:20880/org.apache.dubbo.demo.DemoService?REGISTRY_CLUSTER=registryConfig&anyhost=true&application=dubbo-demo-annotation-provider&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.demo.DemoService&metadata-type=remote&methods=sayHello,sayHelloAsync&pid=13200&release=&side=provider&timestamp=1626146555672
return url;
}

// Step 3 : doRegister 进行注册
public void doRegister(URL url) {
url = addRegistryClusterKey(url);
// 其中主要是对 Map 进行操作
// ConcurrentNavigableMap<String, SortedSet<URL>> exportedServiceURLs = new ConcurrentSkipListMap<>();
if (writableMetadataService.exportURL(url)) {

} else {

}
}

4.2 ZookeeperRegistry 注册

1
2
3
4
5
6
7
java复制代码public void doRegister(URL url) {
try {
zkClient.create(toUrlPath(url), url.getParameter(DYNAMIC_KEY, true));
} catch (Throwable e) {
throw new RpcException("Failed to register " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e);
}
}

五 . 补充深入

5.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
java复制代码// Dubbo 提供了 Zookeeper , Nacos 等多个服务注册器 , 这里来看一下此处是如何选择的 , 此处以 Nacos 为例

// 环节一 : 判断是否支持 Nacos ExtensionLoader
C- DubboBootstrap
private boolean isUsedRegistryAsCenter(RegistryConfig registryConfig, Supplier<Boolean> usedRegistryAsCenter,
String centerType,
Class<?> extensionClass) {
final boolean supported;

Boolean configuredValue = usedRegistryAsCenter.get();
if (configuredValue != null) { // If configured, take its value.
supported = configuredValue.booleanValue();
} else { // Or check the extension existence
String protocol = registryConfig.getProtocol();
supported = supportsExtension(extensionClass, protocol);
}


return supported;
}

C- DubboBootstrap
private boolean supportsExtension(Class<?> extensionClass, String name) {
if (isNotEmpty(name)) {
ExtensionLoader extensionLoader = getExtensionLoader(extensionClass);
return extensionLoader.hasExtension(name);
}
return false;
}

// 环节二 : 获取 ExtensionLoader

Nacos 请求逻辑

1
2
3
4
5
6
7
8
9
10
JAVA复制代码// 调用逻辑
C- DubboBootstrapApplicationListener # onApplicationContextEvent
C- DubboBootstrapApplicationListener # onContextRefreshedEvent
C- DubboBootstrap # start
C- DubboBootstrap # exportServices
C- ServiceConfig # export + doExport + doExportUrlsFor1Protocol : 从配置中获取URL
C- RegistryProtocol # export
C- NacosNamingService # getAllInstances
C- NamingGrpcClientProxy # subscribe
C- NamingGrpcClientProxy # requestToServer

Zookeeper 请求逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码// 调用逻辑
C- CuratorZookeeperClient.<init>
C- CuratorZookeeperTransporter.createZookeeperClient
C- AbstractZookeeperTransporter.connect
C- ZookeeperTransporter$Adaptive.connect
C- ZookeeperDynamicConfiguration.<init>
C- ZookeeperDynamicConfigurationFactory.createDynamicConfiguration
C- AbstractDynamicConfigurationFactory.getDynamicConfiguration
C- DynamicConfiguration.getDynamicConfiguration
C- DubboBootstrap.prepareEnvironment
C- DubboBootstrap.startConfigCenter
C- DubboBootstrap.initialize
C- DubboBootstrap.start
C- DubboBootstrapApplicationListener.onContextRefreshedEvent
C- DubboBootstrapApplicationListener.onApplicationContextEvent

总结 :

ServiceDiscoveryRegistry 这一块没深入太多 , 后面有时间再看看 , 下面来看一下完整的流程图

Dubbo-Registry-Service.png

本文转载自: 掘金

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

Python爬虫鲁迅先生《经典语录》保存到Excel表格(附

发表于 2021-08-12

这是我参与8月更文挑战的第5天

Python爬虫鲁迅先生《经典语录》保存到Excel表格(附源码)

前言

今天用Python 爬取鲁迅先生《经典语录》,直接开整~

代码运行效果展示

在这里插入图片描述

开发工具

Python版本: 3.6.4

相关模块

requests

lxml

pandas

以及Python自带的模块

思路分析

1、获取数据

图片

通过“好句子迷”网站,获取网页。

1
python复制代码http://www.shuoshuodaitupian.com/writer/128_1

利用request模块,通过URL链接,获取html网页,下一步进行网页分析。

其中,URL只有最后一部分发生改变(1-10 :代表第1页–第10页的全部内容)

1
2
3
4
5
6
7
8
python复制代码# 1、获取数据
headers = {"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " \
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36",
}

for i in range(0, 9):
url = "http://www.shuoshuodaitupian.com/writer/128_" + str(i + 1) # 1-10页
result = requests.get(url, headers=headers).content.decode()

2、解析数据

通过Xpath语句解析,分别获取句子的内容、来源和评分等,如图所示。

把获取的每一组数据,放在一个字典里,再把字典追加到一个列表中。

图片

源码:

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
python复制代码# 2、解析数据
html = etree.HTML(result)
div_list = html.xpath('//div[@class="item statistic_item"]')
div_list = div_list[1:-1]

for div in div_list:
# 遍历每一条信息

item = {}

# ./ 注意从当前节点,向下获取
item['content'] = div.xpath('./a/text()')[0]
item['source'] = div.xpath('./div[@class="author_zuopin"]/text()')[0]
item['score'] = div.xpath('.//a[@class="infobox zan like "]/span/text()')[0]

item_list.append(item)

print("正在爬取第{}页".format(i + 1))
time.sleep(0.1)

保存数据:

把上述获取的数据放到一个列表中后,可通过pandas模块把数据类型转变为DataFrame,进而可以轻松地保存到excel文件中。

为防止中文乱码,注意编码格式。

3、保存数据

1
2
python复制代码df = pd.DataFrame(item_list) # 把数据存成csv文件
df.to_csv('鲁迅经典语录.csv', encoding='utf_8_sig') # 保证不乱码

按照评分进行排序之后的结果,如下图。

图片

如果想生成多条个人信息,可以利用for循环,把每个字典在添加到列表里,导出一个DataFrame

文章到这里就结束了,感谢你的观看,Python数据分析系列,下篇文章分享Python爬虫豆瓣话题下的短评数据分析

为了感谢读者们,我想把我最近收藏的一些编程干货分享给大家,回馈每一个读者,希望能帮到你们。

干货主要有:

① 2000多本Python电子书(主流和经典的书籍应该都有了)

② Python标准库资料(最全中文版)

③ 项目源码(四五十个有趣且经典的练手项目及源码)

④ Python基础入门、爬虫、web开发、大数据分析方面的视频(适合小白学习)

⑤ Python学习路线图(告别不入流的学习)

All done~详见个人简介获取完整源代码。。

往期回顾

Python实现“假”数据

本文转载自: 掘金

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

spring boot 实现nacos监听自定义内容 前言

发表于 2021-08-12

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

前言

不知道大家平常有没有遇到一个nacos的麻烦点,
spring boot、cloud集成nacos的时候是只能properties\yml格式的,此时如果想要监听一个很大的json文件时没有办法的,所以只能自己动手写

昨天写了一篇nacos配置读取、监听的源码笔记,基于这个修改

思路

自定义监听

nacos配置读取、监听核心的类其实就是
NacosContextRefresher

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
java复制代码// 注册监听器
private void registerNacosListener(final String group, final String dataId) {

Listener listener = listenerMap.computeIfAbsent(dataId, i -> new Listener() {
@Override
public void receiveConfigInfo(String configInfo) {
refreshCountIncrement();
String md5 = "";
if (!StringUtils.isEmpty(configInfo)) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md5 = new BigInteger(1, md.digest(configInfo.getBytes("UTF-8")))
.toString(16);
}
catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
log.warn("[Nacos] unable to get md5 for dataId: " + dataId, e);
}
}
refreshHistory.add(dataId, md5);
applicationContext.publishEvent(
new RefreshEvent(this, null, "Refresh Nacos config"));
if (log.isDebugEnabled()) {
log.debug("Refresh Nacos config group " + group + ",dataId" + dataId);
}
}

@Override
public Executor getExecutor() {
return null;
}
});

try {
configService.addListener(dataId, group, listener);
}
catch (NacosException e) {
e.printStackTrace();
}
}

但是呢这个方法是private的,所以我们只能自己重写一个类,不能继承

另外RefreshEvent这个事件不适合我们场景,所以也得自定义一个

同理事件接收器也是

代码

配置修改事件类

1
2
3
4
5
6
7
8
9
10
11
java复制代码public class ConfigChangeEvent extends ApplicationEvent {
String dataId;

String content;

public ConfigChangeEvent(Object source, String dataId, String content) {
super(source);
this.dataId = dataId;
this.content = content;
}
}

配置修改事件监听

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码
@Slf4j
@Component
public class ConfigEventListener implements ApplicationListener<ConfigChangeEvent> {

JSONObject jsonObject;

@Override
public void onApplicationEvent(ConfigChangeEvent event) {
Object source = event.getSource();
jsonObject = JSON.parseObject(event.content);
log.info("收到事件推送 ===> {}", source.toString());
}
}

注册监听器类

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
java复制代码@Slf4j
@Component
public class CusNacosConfigRef implements ApplicationListener<ApplicationReadyEvent>, ApplicationContextAware {

private static final AtomicLong REFRESH_COUNT = new AtomicLong(0);

private final NacosRefreshHistory refreshHistory;

private final ConfigService configService;

private ApplicationContext applicationContext;

private AtomicBoolean ready = new AtomicBoolean(false);

private Map<String, Listener> listenerMap = new ConcurrentHashMap<>(16);

public CusNacosConfigRef(NacosConfigProperties nacosConfigProperties) {
this.refreshHistory = new NacosRefreshHistory();
this.configService = nacosConfigProperties.configServiceInstance();

}

@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
// many Spring context
if (this.ready.compareAndSet(false, true)) {
this.registerNacosListenersForApplications();
}
}

@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}

private void registerNacosListenersForApplications() {
// 这个地方需要优化,改成配置形式,
String dataId = "boot-demo.json";
registerNacosListener("DEFAULT_GROUP", dataId);
}

private void registerNacosListener(final String group, final String dataId) {

Listener listener = listenerMap.computeIfAbsent(dataId, i -> new Listener() {
@Override
public void receiveConfigInfo(String configInfo) {
refreshCountIncrement();
String md5 = "";
if (!StringUtils.isEmpty(configInfo)) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md5 = new BigInteger(1, md.digest(configInfo.getBytes("UTF-8")))
.toString(16);
}
catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
log.warn("[Nacos] unable to get md5 for dataId: " + dataId, e);
}
}
refreshHistory.add(dataId, md5);
// 推送配置修改事件
applicationContext.publishEvent(new ConfigChangeEvent(this, dataId, configInfo));
if (log.isDebugEnabled()) {
log.debug("Refresh Nacos config group " + group + ",dataId" + dataId);
}
}

@Override
public Executor getExecutor() {
return null;
}
});

try {
configService.addListener(dataId, group, listener);
}
catch (NacosException e) {
e.printStackTrace();
}
}

public static long getRefreshCount() {
return REFRESH_COUNT.get();
}

public static void refreshCountIncrement() {
REFRESH_COUNT.incrementAndGet();
}

}

本文转载自: 掘金

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

Prometheus监控的4个黄金指标

发表于 2021-08-12

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

prometheus 4个黄金指标

Prometheus简介部分介绍监控的基本目标,首先是及时发现问题其次是要能够快速对问题进行定位。对于传统监控解决方案而言,用户看到的依然是一个黑盒,用户无法真正了解系统的真正的运行状态。因此Prometheus鼓励用户监控所有的东西。

Four Golden Signals是Google针对大量分布式监控的经验总结,4个黄金指标可以在服务级别帮助衡量终端用户体验、服务中断、业务影响等层面的问题。主要关注与以下四种类型的指标:延迟,通讯量,错误以及饱和度:

  • 延迟:服务请求所需时间。

记录用户所有请求所需的时间,重点是要区分成功请求的延迟时间和失败请求的延迟时间。 例如在数据库或者其他关键祸端服务异常触发HTTP 500的情况下,用户也可能会很快得到请求失败的响应内容,如果不加区分计算这些请求的延迟,可能导致计算结果与实际结果产生巨大的差异。除此以外,在微服务中通常提倡“快速失败”,开发人员需要特别注意这些延迟较大的错误,因为这些缓慢的错误会明显影响系统的性能,因此追踪这些错误的延迟也是非常重要的。

  • 通讯量:监控当前系统的流量,用于衡量服务的容量需求。

流量对于不同类型的系统而言可能代表不同的含义。例如,在HTTP REST API中, 流量通常是每秒HTTP请求数;

  • 错误:监控当前系统所有发生的错误请求,衡量当前系统错误发生的速率。

对于失败而言有些是显式的(比如, HTTP 500错误),而有些是隐式(比如,HTTP响应200,但实际业务流程依然是失败的)。

对于一些显式的错误如HTTP 500可以通过在负载均衡器(如Nginx)上进行捕获,而对于一些系统内部的异常,则可能需要直接从服务中添加钩子统计并进行获取。

  • 饱和度:衡量当前服务的饱和度。

主要强调最能影响服务状态的受限制的资源。 例如,如果系统主要受内存影响,那就主要关注系统的内存状态,如果系统主要受限与磁盘I/O,那就主要观测磁盘I/O的状态。因为通常情况下,当这些资源达到饱和后,服务的性能会明显下降。同时还可以利用饱和度对系统做出预测,比如,“磁盘是否可能在4个小时候就满了”。

spring cloud 微服务的4个黄金指标

QPS

1
sh复制代码sum(rate(http_server_requests_seconds_count{application="$application", instance="$instance"}[1m]))

错误数

统计状态码5xx

1
sh复制代码sum(rate(http_server_requests_seconds_count{application="$application", instance="$instance", status=~"5.."}[1m]))

延迟Duration

1
sh复制代码sum(rate(http_server_requests_seconds_sum{application="$application", instance="$instance", status!~"5.."}[1m]))/sum(rate(http_server_requests_seconds_count{application="$application", instance="$instance", status!~"5.."}[1m]))

饱和度

从线程数进行监控 tomcat/jetty

1
2
3
4
5
6
7
8
sh复制代码// tomcat 
A: tomcat_threads_busy_threads{application="$application", instance="$instance"}
B: tomcat_threads_current_threads{application="$application", instance="$instance"}
C: tomcat_threads_config_max_threads{application="$application", instance="$instance"}
// jetty
D: jetty_threads_busy{application="$application", instance="$instance"}
E: jetty_threads_current{application="$application", instance="$instance"}
F: jetty_threads_config_max{application="$application", instance="$instance"}

监控图
image081201.png

本文转载自: 掘金

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

基于javaweb(springboot)汽车配件管理系统设

发表于 2021-08-12

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

主要功能:

系统主要设计采用Java语言开发、采用springboot为后台框架、数据库框架采用mybatis、前端采用jquery、layui框架等

主要模块设计如下:

1.用户模块管理:用户登录、用户的查询、添加、删除操作、

2.菜单模块管理:页面菜单的展示、添加操作、修改操作、删除操作、

3.角色模块管理:用户角色列表的查看、删除等操作、

4.日志管理:日志信息的查看、添加、删除、

5.汽车配件类型管理:汽车配件类型管理列表查看、添加、修改、删除

6.汽车配件模块管理:汽车配件查询、添加、删除、修改、

7.通知公告模块管理:通知公告查询、添加、修改、删除操作

8.注销退出登录管理

系统开发背景

目前汽车配件销售企业大多数在其连锁店的管理还是手工进行,随着汽车配件行业的迅速发展,手工管理的种种弊端暴露无疑,给销售企业的发展带来了不必要的麻烦。为了规范企业内部管理,提高企业业务管理水平,更好的为客户服务,应采用计算机来管理汽车配件的进销存业务。汽车配件管理系统的目的是为企业提供一个计算机化的管理平台,实践企业内部科学有效的管理,促进企业管理信息化,规范化,将能使管理人员从繁琐的杂务工作中解脱出来,真正从事管理工作。本系统实在 vf 理论学习的基础上, 在老师的帮助下, 针对具体项目进行了详细
的设计,在设计与实现过程中,结合源代码具体的描述出整个项目的开发过程。

给大家截一部分效果图吧

登录:

首页主要功能:

配件类型管理:

具体配件管理:

通知公告管理:

(3)总结

经过一个礼拜的设计和开发, 汽车配件管理系统基本开发完毕。 其功能基本符合需求,能够完成汽车配件的查询、修改、添加等工作。用户也可较方便进行数据备份和恢复、数据删除。对于数据的一致性的问题也通过程序进行了有效的解决。但是由于设计时间较短,所以该系统还有许多不尽如人意的地方,比如用户界面不够美观,出错处理不够等多方面问题。这些都有待进一步改善。本次课程设计,是在老师的指导下,借助 Visual Foxpro 相关案例,运用老师所授的课程中的理论知识, 经过长时间的酝酿和修改,最终完善而成。在老师的悉心指导下,在同学们的热情帮助下,我对java相关知识有了更深一层的理解,除自己翻阅资料外我门还在一起讨论,有好多的好的思想,但是我无法把他表达出来,我对 VF运用还不熟练,我想通过自己的努力在以后作的更好。只学习了半年的课程,对许多东西理解还是不够深入,运用不够熟练,以及存在一些问题,敬请谅解

相关系统设计实现推荐:

好了,今天就到这儿吧,小伙伴们点赞、收藏、评论,一键三连走起呀,下期见~~

本文转载自: 掘金

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

优秀开源软件的类,都是怎么命名的?

发表于 2021-08-12

⚠️本文为掘金社区首发签约文章,未获授权禁止转载

日常编码中,代码的命名是个大的学问。能快速的看懂开源软件的代码结构和意图,也是一项必备的能力。那它们有什么规律呢?

Java项目的代码结构,能够体现它的设计理念。Java采用长命名的方式来规范类的命名,能够自己表达它的主要意图。配合高级的IDE,可以减少编码人员的记忆负担,靠模糊的匹配就能找到自己所需要的资源。

为了让大家更好的理解命名的套路,我借鉴了最流行的Java接开源软件(spring系列,netty,libgdx,guava,logback等等),总结了10类常见的类命名。大多数是以后缀形式存在的,也有不少可以组合使用,用来表达多重的意义。

image-20210810135355862.png

这些单词很简单,但可以让你的类命名看起来更加清爽和专业。接下来,我将带大家游览一遍。为了方便理解,每种类型,我都配备了相应的 示例。

管理类命名

写代码,少不了对统一资源的管理,清晰的启动过程可以有效的组织代码。为了让程序运行起来,少不了各种资源的注册、调度,少不了公共集合资源的管理。

Bootstrap,Starter

一般作为程序启动器使用,或者作为启动器的基类。通俗来说,可以认为是main函数的入口。

1
2
3
4
bash复制代码AbstractBootstrap
ServerBootstrap
MacosXApplicationStarter
DNSTaskStarter

Processor

某一类功能的处理器,用来表示某个处理过程,是一系列代码片段的集合。如果你不知道一些顺序类的代码怎么命名,就可以使用它,显得高大上一些。

1
2
3
bash复制代码CompoundProcessor
BinaryComparisonProcessor
DefaultDefaultValueProcessor

Manager

对有生命状态的对象进行管理,通常作为某一类资源的管理入口。

1
2
3
bash复制代码AccountManager
DevicePolicyManager
TransactionManager

Holder

表示持有某个或者某类对象的引用,并可以对其进行统一管理。多见于不好回收的内存统一处理,或者一些全局集合容器的缓存。

1
2
3
bash复制代码QueryHolder
InstructionHolder
ViewHolder

Factory

毫无疑问,工厂模式的命名,耳熟能详。尤其是Spring中,多不胜数。

1
2
3
bash复制代码SessionFactory
ScriptEngineFactory
LiveCaptureFactory

Provider

Provider = Strategy + Factory Method。它更高级一些,把策略模式和方法工厂揉在了一块,让人用起来很顺手。Provider一般是接口或者抽象类,以便能够完成子实现。

1
2
3
bash复制代码AccountFeatureProvider
ApplicationFeatureProviderImpl
CollatorProvider

Registrar

注册并管理一系列资源。

1
2
3
bash复制代码ImportServiceRegistrar
IKryoRegistrar
PipelineOptionsRegistrar

Engine

一般是核心模块,用来处理一类功能。引擎是个非常高级的名词,一般的类是没有资格用它的。

1
2
3
bash复制代码ScriptEngine
DataQLScriptEngine
C2DEngine

Service

某个服务。太简单,不忍举例。范围太广,不要滥用哦。

1
2
3
bash复制代码IntegratorServiceImpl
ISelectionService
PersistenceService

Task

某个任务。通常是个runnable

1
2
3
bash复制代码WorkflowTask
FutureTask
ForkJoinTask

传播类命名

为了完成一些统计类或者全局类的功能,有些参数需要一传到底。传播类的对象就可以通过统一封装的方式进行传递,并在合适的地方进行拷贝或者更新。

Context

如果你的程序执行,有一些变量,需要从函数执行的入口开始,一直传到大量子函数执行完毕之后。这些变量或者集合,如果以参数的形式传递,将会让代码变得冗长无比。这个时候,你就可以把变量统一塞到Context里面,以单个对象的形式进行传递。

在Java中,由于ThreadLocal的存在,Context甚至可以不用在参数之间进行传递。

1
2
3
bash复制代码AppContext
ServletContext
ApplicationContext

Propagator

传播,繁殖。用来将context中传递的值进行复制,添加,清除,重置,检索,恢复等动作。通常,它会提供一个叫做propagate的方法,实现真正的变量管理。

1
2
3
bash复制代码TextMapPropagator
FilePropagator
TransactionPropagator

回调类命名

使用多核可以增加程序运行的效率,不可避免的引入异步化。我们需要有一定的手段,获取异步任务执行的结果,对任务执行过程中的关键点进行检查。回调类API可以通过监听、通知等形式,获取这些事件。

Handler,Callback,Trigger,Listener

callback通常是一个接口,用于响应某类消息,进行后续处理;Handler通常表示持有真正消息处理逻辑的对象,它是有状态的;tigger触发器代表某类事件的处理,属于Handler,通常不会出现在类的命名中;Listener的应用更加局限,通常在观察者模式中用来表示特定的含义。

1
2
3
4
bash复制代码ChannelHandler
SuccessCallback
CronTrigger
EventListener

Aware

Aware就是感知的意思,一般以该单词结尾的类,都实现了Aware接口。拿spring来说,Aware 的目的是为了让bean获取spring容器的服务。具体回调方法由子类实现,比如ApplicationContextAware。它有点回调的意思。

1
2
3
bash复制代码ApplicationContextAware
ApplicationStartupAware
ApplicationEventPublisherAware

监控类命名

现在的程序都比较复杂,运行状态监控已经成为居家必备之良品。监控数据的收集往往需要侵入到程序的边边角角,如何有效的与正常业务进行区分,是非常有必要的。

Metric

表示监控数据。不要用Monitor了,比较丑。

1
2
3
bash复制代码TimelineMetric
HistogramMetric
Metric

Estimator

估计,统计。用于计算某一类统计数值的计算器。

1
2
3
bash复制代码ConditionalDensityEstimator
FixedFrameRateEstimator
NestableLoadProfileEstimator

Accumulator

累加器的意思。用来缓存累加的中间计算结果,并提供读取通道。

1
2
3
bash复制代码AbstractAccumulator
StatsAccumulator
TopFrequencyAccumulator

Tracker

一般用于记录日志或者监控值,通常用于apm中。

1
2
3
bash复制代码VelocityTracker
RocketTracker
MediaTracker

内存管理类命名

如果你的应用用到了自定义的内存管理,那么下面这些名词是绕不开的。比如Netty,就实现了自己的内存管理机制。

Allocator

与存储相关,通常表示内存分配器或者管理器。如果你得程序需要申请有规律得大块内存,allocator是你得不二选择。

1
2
3
bash复制代码AbstractByteBufAllocator
ArrayAllocator
RecyclingIntBlockAllocator

Chunk

表示一块内存。如果你想要对一类存储资源进行抽象,并统一管理,可以采用它。

1
2
3
bash复制代码EncryptedChunk
ChunkFactory
MultiChunk

Arena

英文是舞台、竞技场的意思。由于Linux把它用在内存管理上发扬光大,它普遍用于各种存储资源的申请、释放与管理。为不同规格的存储chunk提供舞台,好像也是非常形象的表示。

关键是,这个词很美,作为后缀让类名显得很漂亮。

1
2
3
bash复制代码BookingArena
StandaloneArena
PoolArena

Pool

表示池子。内存池,线程池,连接池,池池可用。

1
2
3
bash复制代码ConnectionPool
ObjectPool
MemoryPool

过滤检测类命名

程序收到的事件和信息是非常多的,有些是合法的,有些需要过滤扔掉。根据不同的使用范围和功能性差别,过滤操作也有多种形式。你会在框架类代码中发现大量这样的名词。

Pipeline,Chain

一般用在责任链模式中。Netty,Spring MVC,Tomcat等都有大量应用。通过将某个处理过程加入到责任链的某个位置中,就可以接收前面处理过程的结果,强制添加或者改变某些功能。就像Linux的管道操作一样,最终构造出想要的结果。

1
2
3
4
bash复制代码Pipeline
ChildPipeline
DefaultResourceTransformerChain
FilterChain

Filter

过滤器,用来筛选某些满足条件的数据集,或者在满足某些条件的时候执行一部分逻辑。如果和责任链连接起来,则通常能够实现多级的过滤。

1
2
3
bash复制代码FilenameFilter
AfterFirstEventTimeFilter
ScanFilter

Interceptor

拦截器,其实和Filter差不多。不过在Tomcat中,Interceptor可以拿到controller对象,但filter不行。拦截器是被包裹在过滤器中。

1
bash复制代码HttpRequestInterceptor

Evaluator

英文里是评估器的意思。可用于判断某些条件是否成立,一般内部方法evaluate会返回bool类型。比如你传递进去一个非常复杂的对象,或者字符串,进行正确与否的判断。

1
2
3
bash复制代码ScriptEvaluator
SubtractionExpressionEvaluator
StreamEvaluator

Detector

探测器。用来管理一系列探测性事件,并在发生的时候能够进行捕获和响应。比如Android的手势检测,温度检测等。

1
2
3
bash复制代码FileHandlerReloadingDetector
TransformGestureDetector
ScaleGestureDetector

结构类命名

除了基本的数据结构,如数组、链表、队列、栈等,其他更高一层的常见抽象类,能够大量减少大家的交流,并能封装常见的变化。

Cache

这个没啥好说的,就是缓存。大块的缓存。常见的缓存算法有LRU、LFU、FIFO等。

1
2
bash复制代码LoadingCache
EhCacheCache

Buffer

buffer是缓冲,不同于缓存,它一般用在数据写入阶段。

1
2
3
bash复制代码ByteBuffer
RingBuffer
DirectByteBuffer

Composite

将相似的组件进行组合,并以相同的接口或者功能进行暴露,使用者不知道这到底是一个组合体还是其他个体。

1
2
3
bash复制代码CompositeData
CompositeMap
ScrolledComposite

Wrapper

用来包装某个对象,做一些额外的处理,以便增加或者去掉某些功能。

1
2
3
bash复制代码IsoBufferWrapper
ResponseWrapper
MavenWrapperDownloader

Option, Param,Attribute

用来表示配置信息。说实话,它和Properties的区别并不大,但由于Option通常是一个类,所以功能可以扩展的更强大一些。它通常比Config的级别更小,关注的也是单个属性的值。Param一般是作为参数存在,对象生成的速度要快一些。

1
2
3
4
bash复制代码SpecificationOption
SelectOption
AlarmParam
ModelParam

Tuple

元组的概念。由于Java中缺乏元组结构,我们通常会自定义这样的类。

1
2
bash复制代码Tuple2
Tuple3

Aggregator

聚合器,可以做一些聚合计算。比如分库分表中的sum,max,min等聚合函数的汇集。

1
2
3
bash复制代码BigDecimalMaxAggregator
PipelineAggregator
TotalAggregator

Iterator

迭代器。可以实现Java的迭代器接口,也可以有自己的迭代方式。在数据集很大的时候,需要进行深度遍历,迭代器可以说是必备的。使用迭代器还可以在迭代过程中安全的删除某些元素。

1
2
bash复制代码BreakIterator
StringCharacterIterator

Batch

某些可以批量执行的请求或者对象。

1
2
bash复制代码SavedObjectBatch
BatchRequest

Limiter

限流器,使用漏桶算法或者令牌桶来完成平滑的限流。

1
2
3
bash复制代码DefaultTimepointLimiter
RateLimiter
TimeBasedLimiter

常见设计模式命名

设计模式是名词的重灾区,这里只列出最常使用的几个。

Strategy

将抽象部分与它的实现部分分离,使它们都可以独立地变化。策略模式。相同接口,不同实现类,同一方法结果不同,实现策略不同。比如一个配置文件,是放在xml里,还是放在json文件里,都可以使用不同的provider去命名。

1
2
3
bash复制代码RemoteAddressStrategy
StrategyRegistration
AppStrategy

Adapter

将一个类的接口转换为客户希望的另一个接口,Adapter模式使得原本由于接口不兼容而不能一起工作的那些类一起工作。

不过,相对于传统的适配器进行api转接,如果你的某个Handler里面方法特别的多,可以使用Adapter实现一些默认的方法进行0适配。那么其他类使用的时候,只需要继承Adapter,然后重写他想要重写的方法就可以了。这也是Adapter的常见用法。

1
2
3
bash复制代码ExtendedPropertiesAdapter
ArrayObjectAdapter
CardGridCursorAdapter

Action,Command

将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。

用来表示一系列动作指令,用来实现命令模式,封装一系列动作或者功能。Action一般用在UI操作上,后端框架可以无差别的使用。

在DDD的概念中,CQRS的Command的C,既为Command。

1
2
bash复制代码DeleteAction
BoardCommand

Event

表示一系列事件。一般的,在语义上,Action,Command等,来自于主动触发;Event来自于被动触发。

1
2
bash复制代码ObservesProtectedEvent
KeyEvent

Delegate

代理或者委托模式。委托模式是将一件属于委托者做的事情,交给另外一个被委托者来处理。

1
2
bash复制代码LayoutlibDelegate
FragmentDelegate

Builder

将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

构建者模式的标准命名。比如StringBuilder。当然StringBuffer是个另类。这也说明了,规则是人定的,人也可以破坏。

1
2
bash复制代码JsonBuilder
RequestBuilder

Template

模板方法类的命名。定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

1
bash复制代码JDBCTemplate

Proxy

代理模式。为其他对象提供一种代理以控制对这个对象的访问。

1
2
bash复制代码ProxyFactory 
SlowQueryProxy

解析类命名

写代码要涉及到大量的字符串解析、日期解析、对象转换等。根据语义和使用场合的区别,它们也分为多种。

Converter,Resolver

转换和解析。一般用于不同对象之间的格式转换,把一类对象转换成另一类。注意它们语义上的区别,一般特别复杂的转换或者有加载过程的需求,可以使用Resolver。

1
2
3
4
bash复制代码DataSetToListConverter
LayoutCommandLineConverter
InitRefResolver
MustacheViewResolver

Parser

用来表示非常复杂的解析器,比如解析DSL。

1
2
bash复制代码SQLParser
JSONParser

Customizer

用来表示对某个对象进行特别的配置。由于这些配置过程特别的复杂,值得单独提取出来进行自定义设置。

1
2
bash复制代码ContextCustomizer
DeviceFieldCustomizer

Formatter

格式化类。主要用于字符串、数字或者日期的格式化处理工作。

1
2
bash复制代码DateFormatter
StringFormatter

网络类命名

网络编程的同学,永远绕不过去的几个名词。

Packet

通常用于网络编程中的数据包。

1
2
bash复制代码DhcpPacket
PacketBuffer

Protocol

同样用户网络编程中,用来表示某个协议。

1
2
bash复制代码RedisProtocol
HttpProtocol

Encoder、Decoder、Codec

编码解码器

1
2
3
bash复制代码RedisEncoder
RedisDecoder
RedisCodec

Request,Response

一般用于网络请求的进和出。如果你用在非网络请求的方法上,会显得很怪异。

CRUD命名

这个就有意思多了,统一的Controller,Service,Repository,没什么好说的。但你一旦用了DDD,那就得按照DDD那一套的命名来。

由于DDD不属于通用编程范畴,它的名词就不多做介绍了。

其他

Util,Helper

都表示工具类,Util一般是无状态的,Helper以便需要创建实例才能使用。但是一般没有使用Tool作为后缀的。

1
2
3
bash复制代码HttpUtil
TestKeyFieldHelper
CreationHelper

Mode,Type

看到mode这个后缀,就能猜到这个类大概率是枚举。它通常把常见的可能性都列到枚举类里面,其他地方就可以引用这个Mode。

1
2
3
bash复制代码OperationMode
BridgeMode
ActionType

Invoker,Invocation

invoker是一类接口,通常会以反射或者触发的方式,执行一些具体的业务逻辑。通过抽象出invoke方法,可以在invoke执行之前对入参进行记录或者处理;在invoke执行之后对结果和异常进行处理,是AOP中常见的操作方式。

1
2
3
bash复制代码MethodInvoker
Invoker
ConstructorInvocation

Initializer

如果你的应用程序,需要经过大量的初始化操作才能启动,那就需要把它独立出来,专门处理初始化动作。

1
2
bash复制代码MultiBackgroundInitialize
ApplicationContextInitializer

Feture,Promise

它们都是用在多线程之间的,进行数据传递。

Feture相当于一个占位符,代表一个操作将来的结果。一般通过get可以直接阻塞得到结果,或者让它异步执行然后通过callback回调结果。

但如果回调中嵌入了回调呢?如果层次很深,就是回调地狱。Java中的CompletableFuture其实就是Promise,用来解决回调地狱问题。Promise是为了让代码变得优美而存在的。

Selector

根据一系列条件,获得相应的同类资源。它比较像Factory,但只处理单项资源。

1
2
bash复制代码X509CertSelector
NodeSelector

Reporter

用来汇报某些执行结果。

1
2
bash复制代码ExtentHtmlReporter
MetricReporter

Constants

一般用于常量列表。

Accessor

封装了一系列get和set方法的类。像lombok就有Accessors注解,生成这些方法。但Accessor类一般是要通过计算来完成get和set,而不是直接操作变量。这适合比较复杂的对象存取服务。

1
2
bash复制代码ComponentAccessor
StompHeaderAccessor

Generator

生成器,一般用于生成代码,生成id等。

1
2
bash复制代码CodeGenerator
CipherKeyGenerator

End

写代码,看源码,怎么少得了意会和神通?代码要带感,命名也风骚。命名起的好,代码会看起来很爽,大家也都喜欢。

说不清楚的事情,给一段代码,咱就能懂!就是这么神奇!

其实,写专业牛b的代码,并不需要了解太多的英文单词,大多数时候用不着英文4级这么了不起的水平。只需要有限的单词,就能玩出代码界好莱坞的感觉。

看完本文之后,翻一翻开源软件的代码们,看看是不是这个理?

上面这些命名,高频率存在于各种框架中。你要是搞懂了这些名词,阅读大部分源代码可以说是一点障碍都没有了。在同一个场景下,优先使用这些名词,已经是大家心照不宣的规范。

有很多名词来自于设计模式,但又在特定场合使用了比较特殊的单词,比如Provider,大家仔细感受下其中的区别就可以了。

命名是编码中非常重要的一环,希望大家找到其中的规律,让你的代码功能上强大,颜值上好看;祝大家的薪资水涨船高,配得上你的这份专业和工匠精神。

⚠️本文为掘金社区首发签约文章,未获授权禁止转载

本文转载自: 掘金

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

Java集合(一)—— Collection

发表于 2021-08-12

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

1、集合的引入

数组、集合是对多个数据进行存储操作的,简称容器。

在引入集合使用我们更多的使用的是数组。

1.1 数组的特点

  1. 数组一旦指定了长度,那么长度就被确定了,不可以更改了;
  2. 数组一旦声明了类型以后,数组中只能存放这个类型的数组,数组中只能存放同一种类型的数据。

1.2 数组的缺点

  1. 数组一旦指定了长度,那么长度就被确定了,不可以更改;
  2. 删除,增加元素,效率低;
  3. 数组中实际元素的数量是没有办法获取的,没有提供对应的方法或者属性来获取;
  4. 数组存储:有序,可重复;对于无序的,不可重复的数组不能满足要求

集合的引入正是为了解决了以上的缺点。

分类

集合主要包括两种 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。

参考博客

www.cyc2018.xyz/Java/Java%2…

snailclimb.gitee.io/javaguide/#…

2、Collection

image-20210813090833980

2.1 List

存储的元素是有序的、可重复的

  • ArrayList:Object[] 数组;
  • Vector:Object[] 数组,线程安全;
  • LinkedList:双向链表(JDK1.6 之前为循环链表,JDK1.7 取消了循环)

2.1.1 说说它们的区别?从何说?

从何说:从底层组成,数据结构的特性说

2.1.2 ArrayList 和 LinkedList 的区别

  1. 底层数据结构:ArrayList底层使用的是Object数组;LinkedList 底层使用的是双向链表 数据结构(JDK 1.6 之前为双向循环链表,JDK 1.7 后取消了循环);
  2. 效率:ArrayList 查找快(支持快速随机访问)、插入删除慢;LinkedList 查找慢、插入删除快;
  3. 内存空间占用:ArrayList 的空间浪费主要体现,在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费主要在它的每一个元素都需要消耗比 ArrayList更多的空间,存放直接前驱和直接后继以及数据本身。
  4. 是否保证线程安全: ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;

2.1.3 ArrayList 的扩容机制

从添加元素开始,判断是否扩容,默认扩容至原容器的 1.5倍左右(oldCapacity为奇数时,进行 oldCapacity >> 1)

以无参构造的ArrayList为例分析

  1. ArrayList 的成员变量和三个构造函数
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
arduino复制代码 public class ArrayList<E> extends AbstractList<E>
         implements List<E>, RandomAccess, Cloneable, java.io.Serializable
 {
     private static final long serialVersionUID = 8683452581122892189L;
 /**
  * 默认初始容器大小:10
  */
     private static final int DEFAULT_CAPACITY = 10;
 /**
  * 用于表示空数组的共享数组实例
  */
     private static final Object[] EMPTY_ELEMENTDATA = {};
 /**
  * 用于默认大小的空实例的共享空数组实例。
  * 我们将其与EMPTY_ELEMENTDATA区别开来,以便了解在添加第一个元素时应该膨胀多少。
  */
     private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
 /**
  * 保存ArrayList数据的数组
  */
     transient Object[] elementData; // non-private to simplify nested class access
 ​
 /**
  * ArrayList 所包含的元素个数
  * 和 elementData.length 区分开 后者表示的是当前 elementData 的总容量
  */
     private int size;
 ​
 /**
   * 默认无参构造函数,创建一个具有标识的空数组实例
   * 对,使用无参构造函数创建实例后,这时容器仍是一个空数组
   * 在第一次添加元素时,容量扩容为10
   */
     public ArrayList() {
         this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
     
 /**
   * 带初始容量参数的构造函数。(用户自己指定容量)
   */
     public ArrayList(int initialCapacity) {
         if (initialCapacity > 0) {
             this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
             this.elementData = EMPTY_ELEMENTDATA;
        } else {
             throw new IllegalArgumentException("Illegal Capacity: "+
                                                initialCapacity);
        }
    }
 ​
 /**
   * 构造包含指定collection元素的列表,这些元素利用该集合的迭代器按顺序返回
   * 如果指定的集合为null,throws NullPointerException。
   */    
     public ArrayList(Collection<? extends E> c) {
         elementData = c.toArray();
         if ((size = elementData.length) != 0) {
             // c.toArray might (incorrectly) not return Object[] (see 6260652)
             if (elementData.getClass() != Object[].class)
                 elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
             // replace with empty array.
             this.elementData = EMPTY_ELEMENTDATA;
        }
    }
 }
  1. ArrayList 的 add() 方法
1
2
3
4
5
6
7
8
9
10
arduino复制代码 /**
 * 将指定的元素附加到列表的末尾。
 */
 public boolean add(E e) {
     // 添加元素之前执行方法
     ensureCapacityInternal(size + 1);  // Increments modCount!!
     // ArrayList 添加元素的本质就是给数组赋值
     elementData[size++] = e;
     return true;
 }
  1. ensureCapacityInternal(int minCapacity) 方法

minCapacity = size + 1,意思就是新增元素时,该容器至少的容量大小是多于当前元素个数的 1 个

1
2
3
4
5
6
7
8
9
10
11
12
13
typescript复制代码 // 第一次调用时,此时 elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA,
 // 返回 DEFAULT_CAPACITY = 10
 private static int calculateCapacity(Object[] elementData, int minCapacity) {
     if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
         return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
     return minCapacity;
 }
 ​
 // 此时的 minCapacity 是 size+1,也就是 当前元素的个数 + 1 = 1
 private void ensureCapacityInternal(int minCapacity) {
     ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
 }
  1. ensureExplicitCapacity(int minCapacity) 方法
1
2
3
4
5
6
7
8
scss复制代码 // 第一次调用时,此时 minCapacity = 10,当前 elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA,长度为0
 private void ensureExplicitCapacity(int minCapacity) {
     modCount++;
 ​
     // overflow-conscious code
     if (minCapacity - elementData.length > 0)
         grow(minCapacity);
 }
* 当我们要 add 进第 1 个元素到 ArrayList 时,elementData.length 为 0 (因为还是一个空的 list),因为执行了 `ensureCapacityInternal()` 方法 ,所以 minCapacity 此时为 10。此时,`minCapacity - elementData.length > 0`成立,所以会进入 `grow(minCapacity)` 方法。
* 当 add 第 2 个元素时,minCapacity 为 2,此时 e lementData.length(容量)在添加第一个元素后扩容成 10 了。此时,`minCapacity - elementData.length > 0` 不成立,所以不会进入 (执行)`grow(minCapacity)` 方法。
* 添加第 3、4···到第 10 个元素时,依然不会执行 grow 方法,数组容量都为 10。
  1. grow(int minCapacity)方法
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
arduino复制代码 /**
  * 数组能分配的最大大小
  */
     private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
 /**
  * ArrayList 扩容方法
  */
 private void grow(int minCapacity) {
     // 原容器的大小
     int oldCapacity = elementData.length;
     // 扩容,新容器的大小 = 原容器的1.5倍。
     // 使用位运算的速度远快于整除运算
     int newCapacity = oldCapacity + (oldCapacity >> 1);
     // 主要用在当前容器容量为0时
     if (newCapacity - minCapacity < 0)
         newCapacity = minCapacity;
     // 当容器容量大于MAX_ARRAY_SIZE时
     if (newCapacity - MAX_ARRAY_SIZE > 0)
         newCapacity = hugeCapacity(minCapacity);
     // minCapacity is usually close to size, so this is a win:
     elementData = Arrays.copyOf(elementData, newCapacity);
 }
 /**
  * 当 minCapacity 大于 MAX_ARRAY_SIZE 时,则返回 Integer.MAX_VALUE
  */
 private static int hugeCapacity(int minCapacity) {
     if (minCapacity < 0) // overflow
         throw new OutOfMemoryError();
     return (minCapacity > MAX_ARRAY_SIZE) ?
         Integer.MAX_VALUE :
     MAX_ARRAY_SIZE;
 }
* 当 add 第 1 个元素时,oldCapacity 为 0,经比较后第一个 if 判断成立,newCapacity = minCapacity(为 10)。但是第二个 if 判断不会成立,即 newCapacity 不比 MAX\_ARRAY\_SIZE 大,则不会进入 `hugeCapacity` 方法。数组容量为 10,add 方法中 return true,size 增为 1。
* 当 add 第 11 个元素进入 grow 方法时,newCapacity 为 15,比 minCapacity(为 11)大,第一个 if 判断不成立。新容量没有大于数组最大 size,不会进入 hugeCapacity 方法。数组容量扩为 15,add 方法中 return true,size 增为 11。
* 以此类推······

2.1.4 System.arraycopy 和 Arrays.copyOf 的区别

  • System.arraycopy()将一个数组复制到目标数组中,目标数组长度必须足够,否则报错 ArrayIndexOutOfBoundsException
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
csharp复制代码 // src:源数组;srcPos:源数组起始下标;dest:目标数组;destPos:目标数组起始下标;length:要复制的源数组的长度
 System.arraycopy(Object src,int  srcPos,Object dest,int destPos,int length)
     
 public static void test2(){
     System.out.println("==== System.arraycopy ====");
     int[] arr1 = {2,4,2,34,12,4};
     int[] arr2 = new int[10];
     arr2[0] = 7;
     arr2[1] = 8;
     arr2[2] = 9;
     System.arraycopy(arr1,0,arr2,1,arr1.length);
     System.out.println(Arrays.toString(arr2));
 }
 // 复制后的元素,原数组与目标数组重叠的下标,源数组的元素替换目标数组的元素
 ==== System.arraycopy ====
 [7, 2, 4, 2, 34, 12, 4, 0, 0, 0]
  • Arrays.copyOf 返回一个包含源数组所有元素的新数组,长度自定义
1
2
3
4
5
6
7
8
9
10
11
csharp复制代码 public static int[] copyOf(int[] original, int newLength) 
 ​
 public static void test3(){
 System.out.println("==== Arrays.copyOf ====");
         int[] arr3 = {11,22,33,44,55};
         int[] arr4 = Arrays.copyOf(arr3,10);
         System.out.println(Arrays.toString(arr4));
 }
 ​
 ==== Arrays.copyOf ====
 [11, 22, 33, 44, 55, 0, 0, 0, 0, 0]
  • System.arraycopy() 需要原数组和目标数组 ,将原数组拷贝到自定义的数组中,而且可以选择原数组的拷贝起点、长度、放在目标数组的位置;
  • Array.copyOf() 是系统自动在内部新建了一个数组,并返回该数组。(源码内部调用的仍是 System.arraycopy() )

2.2 Set

存储的元素是无序的、不可重复的

  • HashSet:(无序、唯一)基于 HashMap 实现的,底层采用 HashMap 来保存元素,线程不安全,可以存储 null 值;
  • LinkedHashSet:LinkedHashSet 是 HashSet 的子类,并且其内部是通过 LinkedHashMap 来实现的,能够按添加顺序遍历;
  • TreeSet:(有序、唯一)红黑树(自平衡的排序二叉树)

2.3 Comparable 和 Comparator 区别

  • 它们都是接口,都必须实现并重写相应方法才能使用,目的是为了 自定义排序;
  • Comparable 接口是排序的元素(一般为自定义类)实现的,并重写compareTo(T t) 方法,在方法中自定义排序方式;
+ `compareTo(T t)` 返回的是 `int` 类型,一般记忆为:`this`的值 大于 参数的值,return 1;`this`的值 小于 参数的值,return -1;**升序**
+ `Integer`、`String` 等类都实现了 `Comparable` 接口;
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
typescript复制代码 public class Person implements Comparable<Person>{
     private Integer num; // 注意这里类型是 Integer ,后面会用到
     private String name;
     private int age;
 ​
     // get、set方法省略
  setter()/getter()...
 ​
     public Person() {
    }      
     public Person(String name, int age) {
         this.name = name;
         this.age = age;
    }
     public Person(Integer num, String name, int age) {
         this.num = num;
         this.name = name;
         this.age = age;
    }
     // 年龄升序
     @Override
     public int compareTo(Person p) {
         if(this.age > p.getAge()){
             return 1;
        } else if(this.age < p.getAge()){
             return -1;
        }
         return 0;
    }
     @Override
     public boolean equals(Object o) {
  ...
    }
     @Override
     public int hashCode() {
        ...
    }
     @Override
     public String toString() {
        ...
    }
 }
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
csharp复制代码 /**
  * 要排序的类 Person 实现了 Comparable 接口,
  * 并重写了 Comparator 方法,按年龄升序
  */
     public static void comparableTest(){
 System.out.println("+++++++ comparableTest ++++++++");
 System.out.println("============ list ==========");
         List<Person> personList = new ArrayList<>();
         personList.add(new Person(3,"小贱",22));
         personList.add(new Person(8,"小红",5));
         personList.add(new Person(7,"小华",15));
         personList.add(new Person(9,"小明",20));
 ​
         Collections.sort(personList);
         Iterator iterator = personList.iterator();
         while(iterator.hasNext()){
             System.out.println(iterator.next());
        }
         
 System.out.println("============ set ==========");
         TreeSet<Person> personTreeSet = new TreeSet<>();
         personTreeSet.add(new Person("小贱",22));
         personTreeSet.add(new Person("小红",5));
         personTreeSet.add(new Person("小华",15));
         personTreeSet.add(new Person("小明",20));
 ​
         Iterator iterator1 = personTreeSet.iterator();
         while(iterator1.hasNext()){
             System.out.println(iterator1.next());
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码 ============= 输出 ============
 +++++++ comparableTest ++++++++
 ============ list ==========
 Person{num=8, name='小红', age=5}
 Person{num=7, name='小华', age=15}
 Person{num=9, name='小明', age=20}
 Person{num=3, name='小贱', age=22}
 ============ set ==========
 Person{num=null, name='小红', age=5}
 Person{num=null, name='小华', age=15}
 Person{num=null, name='小明', age=20}
 Person{num=null, name='小贱', age=22}
  • Comparator 接口需自定义实现类,或在排序时使用匿名类实现,并重写 compare(T t1,T t2) 方法
+ `compare(T t1,T t2)` 返回的是 `int` 类型,一般记忆为: `p1`的值 大于 `p2`的值,return 1;`p1`的值 小于 `p2`的值,return -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
csharp复制代码 /**
  * 排序是实现 Comparator 接口,自定义排序
  */
     public static void comparatorTest(){
 System.out.println();
 System.out.println("+++++++ comparatorTest ++++++++");
         List<Person> personList = new ArrayList<>();
         personList.add(new Person(3,"小贱",22));
         personList.add(new Person(8,"小红",5));
         personList.add(new Person(7,"小华",15));
         personList.add(new Person(9,"小明",20));
 ​
         // 定制排序
         Collections.sort(personList, new Comparator<Person>() {
             @Override
             public int compare(Person p1, Person p2) {
                 if(p1.getNum() > p2.getNum()){
                     return -1;
                } else if(p1.getNum() < p2.getNum()){
                     return 1;
                }
                 return 0;
                 // 因为 num 定义为了 Integer 类型,这里也可以使用 Integer 定义的 compareTo 方法
                 // return p1.getNum().compareTo(p2.getNum()); // 默认升序
            }
        });
         Iterator iterator = personList.iterator();
         while(iterator.hasNext()){
             System.out.println(iterator.next());
        }
    }
1
2
3
4
5
6
ini复制代码 ========= 输出 =============
 +++++++ comparatorTest ++++++++
 Person{num=9, name='小明', age=20}
 Person{num=8, name='小红', age=5}
 Person{num=7, name='小华', age=15}
 Person{num=3, name='小贱', age=22}

本文转载自: 掘金

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

JWT实现原理

发表于 2021-08-12

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

JWT简介

Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

JWT 的原理

图片.png

  • JWT 认证流程:
    • 用户输入用户名/密码登录,服务端认证成功后,会返回给客户端一个 JWT
      • 客户端将 token 保存到本地(通常使用 localstorage,也可以使用 cookie)
      • 当用户希望访问一个受保护的路由或者资源的时候,需要请求头的 Authorization 字段中使用Bearer 模式添加 JWT,其内容看起来是下面这样
1
makefile复制代码Authorization: Bearer <token>
  • 服务端的保护路由将会检查请求头 Authorization 中的 JWT 信息,如果合法,则允许用户的行为
  • 因为 JWT 是自包含的(内部包含了一些会话信息),因此减少了需要查询数据库的需要
  • 因为 JWT 并不使用 Cookie 的,所以你可以使用任何域名提供你的 API 服务而不需要担心跨域资源共享问题(CORS)
  • 因为用户的状态不再存储在服务端的内存中,所以这是一种无状态的认证机制

JWT的构成

第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),第三部分是签证(signature).

header

jwt的头部承载两部分信息:

  • 声明类型,这里是jwt
  • 声明加密的算法 通常直接使用 HMAC SHA256

完整的头部就像下面这样的JSON:

1
2
3
4
arduino复制代码{
'typ': 'JWT',
'alg': 'HS256'
}

然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分

1
复制代码eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

playload

载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分

  • 标准中注册的声明
  • 公共的声明
  • 私有的声明

标准中注册的声明 (建议但不强制使用) :

  • iss: jwt签发者
  • sub: jwt所面向的用户
  • aud: 接收jwt的一方
  • exp: jwt的过期时间,这个过期时间必须要大于签发时间
  • nbf: 定义在什么时间之前,该jwt都是不可用的.
  • iat: jwt的签发时间
  • jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

公共的声明 :

公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.

私有的声明 :

私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。

定义一个payload:

1
2
3
4
5
json复制代码{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}

然后将其进行base64加密,得到Jwt的第二部分。

1
复制代码eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

signature

jwt的第三部分是一个签证信息,这个签证信息由三部分组成:

  • header (base64后的)
  • payload (base64后的)
  • secret

这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。

1
复制代码  eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

Token 和 JWT 的区别

相同:

  • 都是访问资源的令牌
  • 都可以记录用户的信息
  • 都是使服务端无状态化
  • 都是只有验证成功后,客户端才能访问服务端上受保护的资源

区别:

  • Token:服务端验证客户端发送过来的 Token 时,还需要查询数据库获取用户信息,然后验证 Token 是否有效。
  • JWT:将 Token 和 Payload 加密后存储于客户端,服务端只需要使用密钥解密进行校验(校验也是 JWT 自己实现的)即可,不需要查询或者减少查询数据库,因为 JWT 自包含了用户信息和加密的数据。

本文转载自: 掘金

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

编译原理之词法分析Lex 简介 常规表达式,正则表达式,re

发表于 2021-08-11

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

简介

Lex代表Lexical Analyzar,是一种生成扫描器的工具,可以识别文本中的词汇模式。 Lex与C是强耦合的,一个.l文件通过公用的LEX程序来传递,并生成c的输出文件。这些文件被编译为词法分析器的可执行版本。 lex把每个扫描出来的单词叫统统叫做token,token可以有很多类。对比自然语言的话,英语中的每个单词都是token,token有很多类,比如non(名词)就是一个类token,apple就是属于这个类型的一个具体token。对于某个编程语言来说,token的个数是很有限的,不像英语这种自然语言中有几十万个单词。 lex工具会帮我们生成一个yylex函数,yacc通过调用这个函数来得知拿到的token是什么类型的,但是token的类型是在yacc中定义的。 lex的输入文件一般会被命名成 .l文件,通过lex XX.l 我们得到输出的文件是lex.yy.c

常规表达式,正则表达式,re

用 Lex 定义常规表达式

字符 含义
A-Z,a-z,0-9 构成了部分模式的字符和数字
. 匹配任意字符,除了\n。
- 用来指定范围
[] 一个字符集合,匹配括号中的任意字符如果第一个字符是^那么它表示否定模式。
* 匹配0个或多个上述模式
+ 匹配1个或多个上述模式
? 匹配0个或一个上述模式
$ 作为模式的最后一个字符匹配一行的结尾
{x,y} 指出一个模式可能出现的次数,例如: A{1,3} 表示 A 可能出现1次到3次。
用来转义元字符
用来表示否定
“<一些符号>” 字符的字面含义。元字符具有。
/ 向前匹配。如果在匹配的模版中的“/”后跟有后续表达式,只匹配模版中“/”前 面的部分。如:如果输入 A01,那么在模版 A0/1 中的 A0 是匹配的。
() 将一系列正则表达式分组

举例

表达式 含义
joke[rs] 匹配 jokes 或 joker。
A{1,2}shis+ 匹配 AAshis, Ashis, AAshiss, Ashiss,Ashisss,…
(A[b-e])+ 匹配在 A 出现位置后跟随的从 b 到 e 的所有字符中的 0 个或 1个。

正则式

number [0-9]+

id [A-Za-z]+[A-Za-z0-9_]*

Lex编程

  1. 以Lex可以理解的格式指定模式相关是动作。
  2. 在这一文件上运行Lex,生成扫描器的C代码。
  3. 编译链接C代码生成可执行的扫描器。

一个 Lex 程序分为三个段:第一段是 C 和 Lex 的全局声明,第二段包括模式(C 代码),第三段是补充的 C 函数。 例如, 第三段中一般都有 main() 函数。这些段以%%来分界。

1
2
3
4
5
css复制代码        Definition section
      %%
      Rules section
      %%
      C code section
  • Definition Section 这块可以放C语言的各种各种include,define等声明语句,但是要用%{ %}括起来。 .l文件,可以放预定义的正则表达式:minus “-“ 还要放token的定义,方法是:代号 正则表达式。然后到了Rules Section就可以通过{符号} 来引用正则表达式
1
2
3
4
5
6
7
8
9
ini复制代码        %{
      int wordCount = 0;
      %}
      chars [A-za-z_'."]
      numbers ([0-9])+
      delim [" "\n\t]
      whitespace {delim}+
      words {chars}+
      %%
  • Rules section .l文件在这里放置的rules就是每个正则表达式要对应的动作,一般是返回一个token 这里的动作都是用{}扩起来的,用C语言来描述,这些代码可以做你任何想要做的事情
1
2
3
4
5
6
7
scss复制代码        {words} { wordCount++; /*
      increase the word count by one*/ }
      {whitespace} { /* do
      nothing*/ }
      {numbers} { /* one may
      want to add some processing here*/ }
      %%
  • C code Section main函数
1
2
3
4
5
6
7
8
9
10
11
12
13
csharp复制代码        void main()
      {
           yylex(); //这一函数开始分析。 它由 Lex 自动生成。
           printf(" No of words: %d\n", wordCount);
      }
       /*这一函数在文件(或输入)的末尾调用。 如果函数的返回值是1,就停止解析。
        因此它可以用来解析多个文件。 代码可以写在第三段,这就能够解析多个文件。
        方法是使用 yyin 文件指针指向不同的文件,直到所有的文件都被解析。
        最后,yywrap() 可以返回 1 来表示解析的结束。*/
       int yywrap()
      {
           return 1;
      }

编译运行

运行环境

ubuntu15.10 lex/flex 本机预安装了flex,使用lex命令和flex命令相同 gcc

编译

1
2
3
bash复制代码lex file.l                      #生成lex.yy.c
gcc lex.yy.c -o yourname -ll   #编译生成可执行程序 -ll 用来链接lex库
./youname < xxx.c               #运行程序,将输入流重定向为要分析的文件

本文转载自: 掘金

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

1…567568569…956

开发者博客

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