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

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


  • 首页

  • 归档

  • 搜索

【Spring Boot 快速入门】十六、Spring Bo

发表于 2021-11-07

「这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战」

前言

  大家周末愉快。昨夜今晨被两件事刷屏了,一个是北京(北方)很多地区都下雪了,第一场雪;还有一个是EDG获得《英雄联盟》S11总决赛冠军。祝贺EDG,瑞雪兆丰年。

  相信很多小伙伴在开发中,经常遇到某些参数的定义,这一类参数基本上是固定不变的,如果没有设计数据库中去配置,那么很多都是写到项目中的,这一节就和大家聊聊在项目中经常使用的常量定义方式。

常量

  常量是指在我们的项目中的整个运行过程中值保持不变的一个参数。

类中定义常量

  常量不同于变量,常量在项目运行中是不可以修改的,而变量是可以程序运行中计算赋值修改的量,因此常量使用静态static 和最后final修饰。在Java开发语言使用final关键字来定义一个常量,其中常量有三种类型:静态常量、成员常量和局部常量。
  如下图是一个定义的静态常量,一般语法格式是

1
2
3
js复制代码public class 类名 {
public static final 类型 静态常量名称 = 静态常量值;
}

通过一个类定义静态常量的简单示例如下:

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
js复制代码public class Constants {
/**
* 稀土
*/
public static final String XITU = "稀土";

/**
* 掘金
*/
public static final String JUEJIN = "掘金";

/**
* 代码不止
*/
public static final String DAIMABUZHI = "代码不止";

/**
* 掘金不停
*/
public static final String JUEJINBUTING = "掘金不停";

/**
* 掘金网站
*/
public static final String JUEJIN_URL = "https://juejin.cn/";

图片.png
  使用方法很简单,在项目中,直接写类名称+静态常量名称即可,示例如下:

1
2
3
js复制代码  public static void main(String[] args) {
System.out.println(Constants.XITU+Constants.JUEJIN+Constants.DAIMABUZHI+Constants.JUEJINBUTING+Constants.JUEJIN_URL);
}

  运行之后输出如下:

图片.png
  当然只是一个简单的示例,方便大家理解使用,其他高级的基本上都是基于这种形式的演变而来的。

接口定义常量

  接口(是指对协定进行定义的引用类型,这是百度百科给出的一种介绍。在我们的代码中使用interface关键字定义的一类对象,就是接口。接口可以用来实现。项目中也经常使用接口来设置一些常量值。基本格式为:

1
2
3
4
5
6
7
8
js复制代码public interface 接口对象的名称 {

/**
* 常量的名称
*/
类型 常量的名称 = 常量值;

}

如下代码就是一个示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
js复制代码public interface InterfaceinConstants {

/**
* JUEJIN_QUEUE
*/
String JUEJIN_QUEUE = "JUEJIN_KEY";

/**
* JUEJIN_KEY
*/
String JUEJIN_KEY = "JUEJIN_KEY";

/**
* JUEJIN_EXCHANGE
*/
String JUEJIN_EXCHANGE = "JUEJIN_EXCHANGE";
}

图片.png
使用方法与第一种方式类似,直接调用即可。

1
2
3
4
js复制代码 public static void main(String[] args) {
System.out.println(Constants.XITU+Constants.JUEJIN+Constants.DAIMABUZHI+Constants.JUEJINBUTING+Constants.JUEJIN_URL);
System.out.println(InterfaceinConstants.JUEJIN_EXCHANGE+InterfaceinConstants.JUEJIN_KEY+InterfaceinConstants.JUEJIN_QUEUE);
}

输出结果如下:

图片.png

枚举定义常量

  在我们的程序中被enum关健字修饰的类型就是枚举类型。枚举类型可以经有限的所有的常量几种起来,进行统一的管理和使用,枚举除了不能被继承外可以看成是普通的类即可,它拥有自己的接口、普通方法、静态方法、抽象方法、构造方法等,当成普通的类使用即可。

  一般枚举定义常量的格式如下:

1
2
3
4
js复制代码public enum 枚举名称 {
属性名称(值1,值2,值3...值N)
;
}

  如下是一个简单的示例值

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
js复制代码public enum PushStateEnum {
PUSH_NO(0,"未推送"),
PUSH_FAIL(1,"失败"),
PUSH_YES(2,"成功")
;

private Integer code;

@JsonValue
private String desc;

public static String getDescByValue(int value){
for (PushStateEnum item : PushStateEnum.values()) {
if(item.getCode()==value){
return item.getDesc();
}
}
return null ;
}

public static PushStateEnum getEnum(int value){
for (PushStateEnum item : PushStateEnum.values()) {
if(item.getCode()==value){
return item ;
}
}
return null ;
}
}

  使用方式与上面几个示例类似,直接输出即可。

1
2
3
4
5
6
7
8
js复制代码 public static void main(String[] args) {
System.out.println(PushStateEnum.PUSH_NO.getDesc());
System.out.println(PushStateEnum.PUSH_NO.getCode());
System.out.println(PushStateEnum.PUSH_FAIL.getDesc());
System.out.println(PushStateEnum.PUSH_FAIL.getCode());
System.out.println(PushStateEnum.PUSH_YES.getDesc());
System.out.println(PushStateEnum.PUSH_YES.getCode());
}

  运行结果如下:

图片.png
  使用枚举定义静态常量的好处是,同一个属性可以定义多个常量值,是上面种方式不具有的。

结语

  好了,以上就是项目中经常使用的静态常量定义的几种方式,大家可以根据自己的需要去选择合适的方式定义常量。其中枚举定义的场景多用于:状态码、类型码、错误码等情况。

  感谢您的阅读,希望您喜欢,如对您有帮助,欢迎点赞收藏。如有不足之处,欢迎评论指正。下次见。

  作者介绍:【小阿杰】一个爱鼓捣的程序猿,JAVA开发者和爱好者。公众号【Java全栈架构师】维护者,欢迎关注阅读交流。

本文转载自: 掘金

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

《RabbitMQ》 解决消息延迟和堆积问题

发表于 2021-11-07

大家好,我是小菜。
一个希望能够成为 吹着牛X谈架构 的男人!如果你也想成为我想成为的人,不然点个关注做个伴,让小菜不再孤单!

本文主要介绍 RabbitMQ的常见问题

如有需要,可以参考

如有帮助,不忘 点赞 ❥

微信公众号已开启,小菜良记,没关注的同学们记得关注哦!

  • 消息可靠性问题:如何确保发送的消息至少被消费一次?
  • 延迟消息问题:如何实现消息的延迟投递?
  • 消息堆积问题:如何解决数百万级以上消息堆积,无法及时消费问题?

我们在上篇已经说明了如何解决消息丢失的问题,也就是保证了消息的可靠性,那么其余两个问题同样重要,这篇我们将讲述其余两个问题的解决方式~!

消息丢失解决方案:《RabbitMQ》 | 消息丢失也就这么回事

一、延迟消息

延迟消息 字面意思就是让延迟接收消息,那么如何能让消息延迟到达?这就是我们要思考解决的问题,在了解延迟队列之前我们需要先明白 RabbitMQ 中的两个概念

  • 死信交换机
  • TTL

1)死信交换机

死信(dead letter),也就是废弃已死亡的消息,那什么情况下一个普通的消息能够成为死信?需要符合以下三个条件:

  1. 消费者使用 basic.reject 或 basic.nack 声明消费失败,并将消息的 requeue 参数设置为 false
  2. 消息是一个过期消息,超时后无人消费
  3. 要投递的队列消息堆积满了,最早的消息就会成为死信

而 死信交换机 便是 死信 的归属。

如果一个队列配置了 dead-letter-exchange 属性,指定了一个交换机,那么队列中的死信就会投递到这个交换机中,而这个交换机就称为 死信交换机 - DLX(Dead Letter Exchange )

步骤:当生产者正常投递到队列(simple.queue)中,如果消费者从队列(simple.queue) 消费消息却声明了 reject,那并且队列绑定了死信交换机(dl.queue),那么这个时候成为死信的消息就会投递到这个死信队列(dl.queue)中。

死信投递过程

从正常队列 --> 死信队列 的过程,我们必须声明两个关键信息

  • 死信交换机的名称
  • 死信交换机与死信队列绑定的路由key

而这两个信息也是我们投递消息的基础配置。

接下来我们简单模拟一下 条件1 所产生的场景

1、首先声明一个死信交换机和死信队列

我们这边是使用简单的注解方式直接生成

生成死信交换机和死信队列

通过 RabbitMQ 控制台界面可以看出已经成功生成

2、声明正常使用交换机与队列

然后这个时候我们就可以创建一个正常使用的交换机与队列,并指明死信交换机

同样可以通过控制台查看创建状态

其中是否有声明死信交换机我们可以通过队列的 DLX 和 DLK 标志判断

3、模拟拒收

然后我们现在通过代码模拟客户端拒绝消息的场景

1)消息发送

2)消息接收

查看控制台,结果如下:

1
2
java复制代码2021-11-06 23:56:52.095  INFO 2112 --- [ntContainer#0-1] c.l.m.c.listener.SpringRabbitListener    : 正常业务交换机 | 接收到的消息 : [hello]
2021-11-06 23:56:52.118 INFO 2112 --- [ntContainer#1-1] c.l.m.c.listener.SpringRabbitListener : 死信交换机 | 接收到的消息 : hello

这说明我们死信交换机已经成功发挥作用

2)TTL

以上我们已经成功认识到了 死信交换机 的使用,但是这与我们一开始说的 延迟队列 似乎并没有太大关系,莫急接下来说到的 TTL(Time-To-Live) 就是用来处理延迟消息的!

在 TTL 的概念中,如果一个队列中的消息 TTL 结束后仍未被消费,那么这个消息就会自动变为死信,而 TTL 超时情况分为两种:

  1. 消息所在的队列设置了存活时间
  2. 消息本身设置了存活时间

我们同样进行上述 条件2 的模拟场景

1、声明死信交换机与死信队列(上述已完成)
2、声明延迟队列并指定死信交换机

同样控制台查看创建结果,并且我们发现不止有 DLX 和 DLK 标志,还多了个 TTL ,说明该队列是延迟队列

3、模拟消费超时情况

我们往延迟队列中发送一条消息,并且没有消费者进行消费,等待 1 分钟后查看是否能进入 死信队列 中

我们已经发送了一条消息到延迟队列并且一分钟后也成功在控制台发现了这条信息已经进入到了死信交换机

1
java复制代码2021-11-07 00:01:30.854  INFO 32752 --- [ntContainer#1-1] c.l.m.c.listener.SpringRabbitListener    : 死信交换机 | 接收到的消息 : test ttl-message

以上是配置了队列超时时间,消息本身自然也能配置超时时间,当 消息 和 队列 都存在超时时间时,那么就以最短的 TTL 为准,消息的超时配置如下:

如上图所示,我们可以利用 Message 这个类来传递消息信息,并设置上超时时间,我们设置的是 5000 ms,等待发送成功后,控制台过5000 ms 也成功打印了死信交换机消费的消息:

1
java复制代码2021-11-07 00:03:09.048  INFO 39996 --- [ntContainer#1-1] c.l.m.c.listener.SpringRabbitListener    : 死信交换机 | 接收到的消息 : this is a ttl message

3)延迟队列

我们上述是使用 死信交换机 来间接实现 延迟队列 的效果,但实际在 RabbitMQ 不必如此麻烦,RabbitMQ 已经为我们封装好了插件,我们只需要下载安装即可~

RabbitMQ 插件下载地址

我们进入地址可以发现有许多插件,搜索 delay 关键字找到我们需要的插件进行下载

下载完后直接上传到 RabbitMQ 的插件目录 - plugins,小菜这边是使用 docker 临时安装测试的,所以已经将该插件目录挂载出来了:

1
shell复制代码docker run -itd --name rabbitmq -v plugins:/plugins -p 15672:15672 -p 5672:5672 rabbitmq:management

因此我这边直接将插件上传到容器中的 plugins 目录即可~

然后进入到容器中执行以下命令进行插件开启

1
shell复制代码rabbitmq-plugins enable rabbitmq_delayed_message_exchange

并且我们在控制台创建交换机的时候可以看到 type 类型多了个选项

成功执行到这步就说明已经开启了 RabbitMQ 的延迟队列功能

那接下来我们就可以来使用 DelayExchange,首先我们需要了解代码的方式创建延迟交换机:

方式1

方式2

当我们万事具备之后就可以来发送消息了

在发送消息的时候,消息头中一定要携带上 x-delay 参数,指定上延迟时间

通过这样配置之后,我们可以在控制台看到,经过10秒后 delay.queue 才收到对应消息,然后被对应消费者消费

3)总结

我们上面从 死信交换机 到 TTL 到 延迟队列,一步步认识了如何实现延迟消息的功能,然后我们进行一个小小的总结:

问题1:什么样的消息会成为死信?
  1. 消息被消费者 reject 或返回 nack
  2. 消息超时未及时消费
  3. 消息队列满了
问题2:消息超时的方式
  1. 给队列设置 TTL 属性
  2. 给消息设置 TTL 属性
问题3:如何使用延迟队列
  1. 下载并启用 RabbitMQ 延迟队列插件
  2. 声明一个交换机,并将 delayed 属性设置为 true
  3. 发送消息时,添加 x-delay 头,值为超时时间
问题4:延迟队列的使用场景
  1. 延迟发送短信通知
  2. 订单自动取消
  3. 库存自动回滚

二、惰性队列

讲完延迟队列,我们继续来认识惰性队列

讲惰性队列之前,我们先抛出一个问题~

RabbitMQ 如何解决消息堆积问题

什么情况下会出现消息堆积问题?

  1. 当生产者生产速度远远消费者消费速度
  2. 当消费者宕机没有及时重启

那么如何解决这个问题?通常思路如下:

  1. 在消费者机器重启后,增加更多的消费者进行处理
  2. 在消费者处理逻辑内部开辟线程池,利用多线程的方式提高处理速度
  3. 扩大队列的容量,提高堆积上限

这几个方式从理论上来说解决消息堆积问题也是没有问题的,但是处理方式不够优雅甚至不够灵活~ 那么除了以上的几种解决方式,我们可以利用 RabbitMQ 中自带的一种队列类型 – 惰性队列

什么是惰性队列?我们认识一下惰性队列的几个特性:

  • 接收到消息后直接存入磁盘而非内存
  • 消费者要消费消息时才会从磁盘中读取并加载到内存中
  • 它支持百万级消息的存储

说到底,就是利用磁盘的缓冲机制,而这种机制的缺点就是消息的时效性会降低,性能受限于磁盘的IO,认识特性和缺点之后,我们便来看看如何创建惰性队列

方式1

方式2

方式3

该方式是直接基于命令行修改将一个正在运行中的队列修改为惰性队列

1
shell复制代码rabbitmqctl set_policy Lazy "^lazy-queue$" '{"queue-mode":"lazy"}' --apply-to queues

其中几个命令参数含义如下:

  • rabbitmqctl:命令行工具
  • set_policy:添加一个策略
  • Lazy:策略名称,可以自定义
  • ^lazy-queue$:用正则表达式匹配队列的名称
  • ‘{“queue-mode”:”lazy”}’:设置队列为 lazy 模式
  • –apply-to queues:策略的作用对象,是所有的队列

这种惰性队列的方式尽管缺点是消息时效性会降低,但是在某些场景下也不是不能接受,何况它的优点同样明显:

  • 基于磁盘存储,消息上限高
  • 没有间歇性的 page-out,性能稳定

到这里,我们就已经讲述了 RabbitMQ 的常见问题,对于我们来说,普通的开发场景可能比较少遇到这些问题,但是没遇到不等于没有,所以我们还是需要多认识来防患于未然!

不要空谈,不要贪懒,和小菜一起做个吹着牛X做架构的程序猿吧~点个关注做个伴,让小菜不再孤单。咱们下文见!

看完不赞,都是坏蛋

今天的你多努力一点,明天的你就能少说一句求人的话!

我是小菜,一个和你一起变强的男人。 💋

微信公众号已开启,小菜良记,没关注的同学们记得关注哦!

本文转载自: 掘金

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

MVI 架构封装:快速优雅地实现网络请求

发表于 2021-11-07

前言

网络请求可以说是Android开发中最常见的需求之一,基本上每个页面都需要发起几个网络请求。

因此大家通常都会对网络请求进行一定的封装,解决模板代码过多,重复代码,异常捕获等一些问题

前面我们介绍了MVI架构的主要原理与更佳实践

MVVM 进阶版:MVI 架构了解一下~

MVI 架构更佳实践:支持 LiveData 属性监听

我们这次一起来看下MVI架构下如何对网络请求进行封装,以及相对于MVVM架构有什么优势

本文主要包括以下内容

  1. MVVM架构下的网络请求封装与问题
  2. MVI架构下封装网络请求
  3. MVI架构与Flow结合实现网络请求

MVVM架构下的网络请求封装与问题

相信大家都看过不少MVVM架构下的网络请求封装,一般是这样写的

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
kotlin复制代码# MainViewModel
class MainViewModel {
private val _userLiveData = MutableStateLiveData<User?>()
val userLiveData : StateLiveData<User?> = _userLiveData
fun login(username: String, password: String) {
viewModelScope.launch {
_userLiveData.value = repository.login(username, password)
}
}
}

class MainActivity : AppCompatActivity() {
fun initViewModel(){
// 请求网络
mViewModel.login("username", "password")
// 注册监听
mViewModel.userLiveData.observeState(this) {
onLoading {
showLoading()
}
onSuccess {data ->
mBinding.tvContent.text = data.toString()
}
onError {
dismissLoading()
}
}
}
}

如上所示,就是最常见的MVVM架构下网络请求封装,主要思路如是

  1. 添加一个StateLiveData,一个LiveData支持多种状态,例如加载中,加载成功,加载失败等
  2. 在页面中监听StateLiveData,在页面中处理onLoading,onSuccess,onError等逻辑

这种封装的本质其实就是将请求的回调逻辑处理迁移到View层了

这其实并不是我们想要的,我们的理想状况应该是逻辑尽量放在ViewModel中,View层只需要监听ViewModel层并更新UI

既然这种封装其实违背了不在View层写逻辑的原则,那么为什么还有那么多人用呢?

本质上是因为ViewModel层与View层的通信成本比较高

想象一下,如果我们不使用StateLiveData,针对每个请求就需要新建一个LiveData来表示请求状态,如果成功或失败后需要弹Toast或者Dialog,或者页面中有多个请求,就需要定义更多的LiveData,
同时为了保证对外暴露的LiveData不可变,每个状态都需要定义两遍LiveData

这就是为什么这种封装其实违背了不在View层写逻辑但仍然流行的原因,因为在MVVM架构中每处理一种状态,就需要添加两个LiveData,成本较高,大多数人并不愿意支付这个成本

而MVI架构正解决了这个问题

MVI架构下封装网络请求

之前已经介绍过了MVI架构,MVI架构使用方面我们就不再多说,我们直接来看下MVI架构下怎么发起一个简单网络请求

简单的网络请求

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
kotlin复制代码class NetworkViewModel : ViewModel() {
/**
* 页面请求,通常包括刷新页面loading状态等
*/
private fun pageRequest() {
viewModelScope.rxLaunch<String> {
onRequest = {
_viewStates.setState { copy(pageStatus = PageStatus.Loading) }
delay(2000)
"页面请求成功"
}
onSuccess = {
_viewStates.setState { copy(content = it, pageStatus = PageStatus.Success) }
_viewEvents.setEvent(NetworkViewEvent.ShowToast("请求成功"))
}
onError = {
_viewStates.setState { copy(pageStatus = PageStatus.Error(it)) }
}
}
}
}

# Activity层
class MainActivity : AppCompatActivity() {
private fun initViewModel() {
viewModel.viewStates.let { state ->
//监听网络请求状态
state.observeState(this, NetworkViewState::pageStatus) {
when (it) {
is PageStatus.Success -> state_layout.showContent()
is PageStatus.Loading -> state_layout.showLoading()
is PageStatus.Error -> state_layout.showError()
}
}
//监听页面数据
state.observeState(this, NetworkViewState::content) {
tv_content.text = it
}
}
//监听一次性事件,如Toast,ShowDialog等
viewModel.viewEvents.observe(this) {
when (it) {
is NetworkViewEvent.ShowToast -> toast(it.message)
is NetworkViewEvent.ShowLoadingDialog -> showLoadingDialog()
is NetworkViewEvent.DismissLoadingDialog -> dismissLoadingDialog()
}
}
}
}

如上,代码很简单

  1. 页面的所有状态都存储在NetworkViewState中,后面如果需要添加状态不需要添加LiveData,添加属性即可,NetworkViewEvent中存储了所有一次事件,同理
  2. ViewModel中发起网络请求并监听网络请求回调,其中viewModelScope.rxLaunch是我们自定义的扩展方法,后面会再介绍
  3. ViewModel中在请求的onRequest,onSuccess,onError时会通过_viewStates更新页面,通过_viewEvents添加一次性事件,如Toast
  4. View层只需要监听ViewState与ViewEvent并更新UI,页面的逻辑全都在ViewModel中写

通过使用MVI架构,所有的逻辑都在ViewModel中处理,同时添加新状态时不需要添加LiveData,降低了View与ViewModel的通信成本,解决了MVVM架构下的一些问题

局部网络请求

我们页面中通常会有一些局部网络请求,例如点赞,收藏等,这些网络请求不需要刷新整个页面,只需要处理单个View的状态或者弹出Toast

下面我们来看下MVI架构下是如何实现的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
kotlin复制代码    /**
* 页面局部请求,例如点赞收藏等,通常需要弹dialog或toast
*/
private fun partRequest() {
viewModelScope.rxLaunch<String> {
onRequest = {
_viewEvents.setEvent(NetworkViewEvent.ShowLoadingDialog)
delay(2000)
"点赞成功"
}
onSuccess = {
_viewEvents.setEvent(NetworkViewEvent.DismissLoadingDialog)
_viewEvents.setEvent(NetworkViewEvent.ShowToast(it))
_viewStates.setState { copy(content = it) }
}
onError = {
_viewEvents.setEvent(NetworkViewEvent.DismissLoadingDialog)
}
}
}

如上,针对局部网络请求,我们也是通过_viewStates与_viewEvents更新UI,并不需要添加额外的LiveData,使用起来比较方便

多数据源请求

页面中通常也会有一些多数据源的请求,我们可以利用协程的async操作符处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
kotlin复制代码    /**
* 多数据源请求
*/
private fun multiSourceRequest() {
viewModelScope.rxLaunch<String> {
onRequest = {
_viewEvents.setEvent(NetworkViewEvent.ShowLoadingDialog)
coroutineScope {
val source1 = async { source1() }
val source2 = async { source2() }
val result = source1.await() + "," + source2.await()
result
}
}
onSuccess = {
_viewEvents.setEvent(NetworkViewEvent.DismissLoadingDialog)
_viewEvents.setEvent(NetworkViewEvent.ShowToast(it))
_viewStates.setState { copy(content = it) }
}
onError = {
_viewEvents.setEvent(NetworkViewEvent.DismissLoadingDialog)
}
}
}

异常处理

我们的APP中通常需要一些通用的异常处理,我们可以封装在rxLaunch扩展方法中

1
2
3
4
5
6
7
8
9
10
11
12
kotlin复制代码class CoroutineScopeHelper<T>(private val coroutineScope: CoroutineScope) {
fun rxLaunch(init: LaunchBuilder<T>.() -> Unit): Job {
val result = LaunchBuilder<T>().apply(init)
val handler = NetworkExceptionHandler {
result.onError?.invoke(it)
}
return coroutineScope.launch(handler) {
val res: T = result.onRequest()
result.onSuccess?.invoke(res)
}
}
}

如上:

  1. rxLaunch就是我们定义的扩展方法,本质就是将协程转化为类RxJava的回调
  2. 通用的异常处理可写在自定义的NetworkExceptionHandler中,如果请求错误则会自动处理
  3. 处理后的异常将传递到onError中,供我们进一步处理

MVI架构与Flow结合实现网络请求

我们上面通过自定义扩展函数实现了rxLaunch,其实是将协程转化为类RXJava的写法,但其实kotin协程已经有了自己的RXJava : Flow

我们完全可以利用Flow来实现同样的功能,不需要自己自定义

简单的网络请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
kotlin复制代码    /**
* 页面请求,通常包括刷新页面loading状态等
*/
private fun pageRequest() {
viewModelScope.launch {
flow {
delay(2000)
emit("页面请求成功")
}.onStart {
_viewStates.setState { copy(pageStatus = PageStatus.Loading) }
}.onEach {
_viewStates.setState { copy(content = it, pageStatus = PageStatus.Success) }
_viewEvents.setEvent(NetworkViewEvent.ShowToast(it))
}.commonCatch {
_viewStates.setState { copy(pageStatus = PageStatus.Error(it)) }
}.collect()
}
}
  1. 在flow中发起网络请求并将结果通过emit回调
  2. onStart是请求的开始,这里触发Activity中的showLoading
  3. 在onEach中获取flow中emit的结果,即成功回调,在这里更新请求状态与页面数据
  4. 在commonCatch中捕获异常
  5. 局部的网络请求与这里类似,并且不需要添加额外的LiveData,这里就不缀述了

多数据源网络请求

Flow中提供了多个操作符,可以将多个Flow的结果组合起来

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
kotlin复制代码    /**
* 多数据源请求
*/
private fun multiSourceRequest() {
viewModelScope.launch {
val flow1 = flow {
delay(1000)
emit("数据源1")
}
val flow2 = flow {
delay(2000)
emit("数据源2")
}
flow1.zip(flow2) { a, b ->
"$a,$b"
}.onStart {
_viewEvents.setEvent(NetworkViewEvent.ShowLoadingDialog)
}.onEach {
_viewEvents.setEvent(NetworkViewEvent.DismissLoadingDialog)
_viewEvents.setEvent(NetworkViewEvent.ShowToast(it))
_viewStates.setState { copy(content = it) }
}.commonCatch {
_viewEvents.setEvent(NetworkViewEvent.DismissLoadingDialog)
}.collect()
}
}

如上,我们通过zip操作符组合两个Flow,它将合并两个Flow的结果并回调,我们在onEach中将得到数据源1,数据源2

异常处理

跟上面一样,有时我们需要配置一些能用的异常处理,可以看到,我们在上面调用了commonCatch,这其实也是我们自定义的一个扩展函数

1
2
3
4
5
6
7
8
9
10
kotlin复制代码fun <T> Flow<T>.commonCatch(action: suspend FlowCollector<T>.(cause: Throwable) -> Unit): Flow<T> {
return this.catch {
if (it is UnknownHostException || it is SocketTimeoutException) {
MyApp.get().toast("发生网络错误,请稍后重试")
} else {
MyApp.get().toast("请求失败,请重试")
}
action(it)
}
}

如上所示,其实是对Flow.catch的一个封装,读者可以根据自己的需求封装处理

关于Repository

可以看到,我上面都没有使用到Repository,都是直接在ViewModel层中处理

平常在项目开发中也可以发现,一般的页面并没有写Repository的需要,直接在ViewModel中处理即可

但如果数据获取比较复杂,比如同时从网络与本地数据获取,或者需要复用网络请求等时,也可以添加一个Repository

我们可以通过Repository获取数据后,再通过_viewState更新页面状态,如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
kotlin复制代码    private fun fetchNews() {
viewModelScope.launch {
flow {
emit(repository.getMockApiResponse())
}.onStart {
_viewStates.setState { copy(fetchStatus = FetchStatus.Fetching) }
}.onEach {
_viewStates.setState { copy(fetchStatus = FetchStatus.Fetched, newsList = it.data)}
}.commonCatch {
_viewStates.setState { copy(fetchStatus = FetchStatus.Fetched) }
}.collect()
}
}

总结

在MVVM架构下一般使用StateLiveData来进行网络架构封装,并在View层监听回调,这种封装方式的问题在于将网络请求回调处理逻辑转移到了View层,违背了尽量不在View层写逻辑的原则

但这种写法流行的原因在于MVVM架构下View与ViewModel交互成本较高,如果每个请求的回调都在ViewModel中处理,则需要定义很多LiveData,这是很多人不愿意做的

而MVI架构解决了这个问题,将页面所有状态放在一个ViewState中,对外也只需要暴露一个LiveData

MVI配合Flow或者自定义扩展函数,可以将页面逻辑全部放在ViewModel中,View层只需要监听LiveData的属性并刷新UI即可

当页面需要添加状态时,只需要给ViewState添加一个属性而不是添加两个LiveData,降低了View与ViewModel的交互成本

如果你也觉得在View层监听网络请求回调不是一个很好的设计的话,那么可以尝试使用一下MVI架构

如果本文对你有所帮助,欢迎点赞关注Star~

项目地址

本文所有代码可见:github.com/shenzhen201…

本文转载自: 掘金

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

Python 时间操作之zoneinfo模块 复习回顾 1

发表于 2021-11-07

这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战

复习回顾

我们都知道量度两个时刻之间的间隔长短的物理量叫做“时间”,时间是伴随的空间变化而变化的。众所周知,地球是围绕的太阳转动的。地球公转一周是一年,地球自转一周是一天。

由于地球是自西向东转的,东边区域比西方总是先看到太阳,因此划分出时区概念。

image.png

  • 什么是时区?

+ 时区是指同一个时间制的区域
+ 全球共划分24个时区(-12,12)
+ 以本初子午线为基准,从零时区向东划分东一区至东十二区;向西划分西一至四二区
  • Python 关于时间操作

我们前面已经学习相关的时间日期操作的模块

+ 重点关注系统日志文件的时间戳的[time 模块](https://dev.newban.cn/7024474853809848356)
+ 对时间日期进行更加人性化处理的[datetime模块](https://dev.newban.cn/7024792501047263269)
+ 打印出所有年或者月份的日历的[calendar模块](https://dev.newban.cn/7026680117770321950)

那么,关于时区处理相关操作的,Python 有没有相关的模块提供?

在Python 3.9之前,没有独立的模块对时区进行处理的模块,而是使用datetime模块中timezone类来实现的

在Python 3.9,提供了zoneinfo独立的模块支持IANA时区的支持。

接下来我们详细来学习一下zoneinfo模块相关方法吧,Let’s go~

  1. zoneinfo 模块概述

zoneinfo 模块是Python 3.9版本推出一个可以实现时区的模块,该模块默认情况下是使用系统时区的数据。

zoneinfo 模块是基于datetime、time模块的类型zoneinfo类而设计的

查看Zoneinfo代码,可知道Zoneinfo类是tzinfo的一个子类

image.png

  • gmt、utc及本地时间

+ gmt:格林威治平时(也称格林威治时间),前世界标准时间
+ utc:协调世界时,又称现世界标准时间,由原子时间+世界时间两部分组成
+ 本地时间=gmt+时区偏移=utc+时区偏移
  • zoneinfo 模块特点

+ 支持IANA 时区查询,如时区查询代码Asia/Kolkata
+ 可以与datetime时间日期和日期算法上进行兼容,更加方便处理夏令时
+ 不提供时区数据,需要下载第三方库tzdata
  • zoneinfo 模块使用

zoneinfo 模块是Python 3.9版本支持的

+ 检查本地电脑Python版本更新到3.9
+ 在代码中只需要使用import进行导入即可
1
python复制代码import zoneinfo
  1. zoneinfo 相关方法

  • zoneinfo 模块提供相关方法

方法 作用
zoneinfo.avariable_timezones() 获取包含IANA时区所有效的key值
zoneinfo.reset_tzpath(to=None) 设置或者重置时区搜索路径

  • zoninfo 模块提供给属性

属性 作用
zoneinfo.TZPATH 时区搜索的路径只读序列

  • zoneinfo 模块提供的Zoneinfol类是datetime.tzinfo的抽象基类。
方法 作用
zoneinfo.Zoneinfo(key) 是datetime.tzinfo表示由字符串表示IANA时区

ps:Zoneinfo对象可以使用在datetime中传入给tzinfo


  • zoninfo.Zoneinfo类提供给方法

方法 作用
Zoneinfo.clear_cache(*,key) 清除zoneinfo方法缓存失效的
Zoneinfo.form_file(fobj,/,key) 构建一个zoneinfo从文件的对象返回字节
Zoneinfo.no_cache(key) 绕过构造函数缓存的备用构造函数

  • zoninfo.Zoneinfo类提供给属性

属性 作用
Zoneinfo.key 从IANA数据中只读attribute返回key传递给构造函数
  1. 小试牛刀

我们根据前面集几节的学习,来对zoneinfo 模块相关方法进行学习

  • zoneinfo.Zoneinfol类是datetime.tzinfo抽象基类,可以传入tzinfo
  • Zoneinfo对象可以结合datetime.datetime实例化对象传入tzinfo字段,获取具有时间,进行时间计算等
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
python复制代码import zoneinfo
from datetime import datetime,timedelta,timezone

tz = zoneinfo.ZoneInfo("Pacific/Kwajalein")

date_utc = datetime(2021,11,7,20,tzinfo=timezone.utc)

date_tz = date_utc.astimezone(tz)

date_tz_dst = date_tz+timedelta(hours=1)

print("utc时间:",date_utc)
print("tz时间:",date_tz)
print("tz_dst时间:",date_tz_dst)
print("时区简称:",date_utc.tzname())

image.png

总结

本期,我们对Python 3.9 新提供的zoneinfo模块相关使用进行学习,支持IANA时区查询,替代了以前必须要使用第三库pytz来转换时区代码。

以上是本期内容,欢迎大佬们点赞评论,下期见~

本文转载自: 掘金

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

约定俗成的Flask项目结构

发表于 2021-11-07

一.使用工厂函数创建程序实例

工厂(factory)是指创建其他对象的对象,通常是一个返回其他类的对象的函数或方法。程序实例在工厂函数中创建,这个函数返回程序实例app。按照惯例,这个函数被命名为create_app()或make_app()。我们把这个工厂函数称为程序工厂(Application Factory)——即“生产程序的工厂”,使用它可以在任何地方创建程序实例。

工厂函数提供了很大的灵活性。另外,借助工厂函数,我们还可以分离扩展的初始化操作。==在这个工厂函数中,我们会创建程序实例,为其加载配置,注册蓝本,执行扩展的初始化操作.最后返回程序实例==。

约定俗成:

项目根目录中(app.py同级)创建工厂文件factory.py

image.png

1
2
3
4
5
6
7
8
9
10
11
12
csharp复制代码from flask import Flask


def create_app():
app = Flask(__name__)
# 加载配置

# 注册蓝本

# 初始化扩展

return app

此时app.py调用create_app()获取程序实例app

1
2
3
4
5
6
7
8
9
10
11
python复制代码from factory import create_app

app = create_app()

@app.route('/')
def hello_world():
return 'Hello World!'


if __name__ == '__main__':
app.run()

二.使用类组织配置文件

在实际需求中,我们往往需要不同的配置组合。例如,开发用的配置,测试用的配置,生产环境用的配置.我们可以在单个配置文件中使用Python类来组织多个不同类别的配置。

约定俗成:

项目根目录中(app.py同级)创建配置文件settings.py

1
2
3
4
5
6
7
8
9
10
11
12
ruby复制代码# @FileName: settings
class BaseConfig(object):
# 基础配置

class DevelopmentConfig(BaseConfig):
# 开发环境配置

class TestConfig(BaseConfig):
# 测试环境配置

class ProductionConfig(BaseConfig):
# 生产环境(上线环境)配置

在配置文件的底部,我们创建了一个存储配置名称和对应配置类的字典config,用于在创建程序实例时通过配置名称来获取对应的配置类

工厂函数中加载配置

在创建程序实例后使用app.config.from_object()方法加载配置,传入配置类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
csharp复制代码//factory.py
from flask import Flask
import settings


def create_app():
app = Flask(__name__)
# 加载配置
app.config.from_object(settings.DevelopmentConfig)
# 注册蓝图

# 初始化扩展

return app

三.扩展类实例化和初始化分离

为了完成扩展的初始化操作,我们需要在实例化扩展类时传入程序实例。但使用工厂函数时,并没有一个创建好的程序实例可以导入。如果我们把实例化操作放到工厂函数中,那么我们就没有一个全局的扩展对象可以使用,比如表示数据库的db对象。

为了解决这个问题,大部分扩展都提供了一个init_app()方法来支持分离扩展的实例化和初始化操作。现在我们仍然像往常一样初始化扩展类,但是并不传入程序实例.这时扩展类实例化的工作可以抽离出来

约定俗成:

项目根目录中(app.py同级)创建用于扩展类实例化的文件extensions.py

以flask-sqlalchemy扩展为例

1
2
3
4
5
6
ini复制代码# @FileName: extendsions
from flask_sqlalchemy import SQLAlchemy

#扩展类实例化
db = SQLAlchemy()
#......

image.png

四.使用蓝图模块化程序

约定俗成:

创建blueprints目录用于存放各个蓝图,蓝图以包结构组织

image.png

1.创建蓝图

在每个蓝图包的init.py文件创建蓝图实例

1
2
3
4
5
6
7
8
python复制代码# @FileName: __init__.py

from flask import Blueprint
#从Flask导入Blueprint类,实例化这个类就获得了我们的蓝本对象。
user_bp = Blueprint("user",__name__)
#构造方法中的第一个参数是蓝本的名称;第二个参数是包或模块的名称,我们可以使用__name__变量
from . import login
#导入需要使用蓝图的视图文件,在下面导入解决循环导入报错

在蓝本对象的名称后添加一个_bp后缀.是为了更容易区分蓝本对象

2.注册蓝图

为了让蓝图发挥作用,我们需要把蓝本注册到程序实例上:

工厂函数中注册蓝图

image.png

3.使用蓝图

1
2
3
4
5
6
7
python复制代码//blueprints/user/login.py
from bluprints.user import user_bp


@user_bp.route('/')
def test():
return "hello bluprint"

五.构建NOSQL数据库连接模块

约定俗成:

项目根目录中(app.py同级)创建nosql数据库连接文件nosql_connection.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ini复制代码from pymongo import MongoClient
import redis
MONGODB_HOST = "106.14.107.75"
MONGODB_PORT = 27017
MONGODB_DATABASE = "mymongo"
REDIS_HOST = "106.14.107.75"
REDIS_PORT = 6379
REDIS_AUTH = "123456"

#mongodb 获取指定集合对象
def mongodbConnection(collectionName):
myclient = MongoClient(MONGODB_HOST, MONGODB_PORT)
mydb = myclient[MONGODB_DATABASE]
collection = mydb[collectionName]
return collection

#redis 获取指定数据库(0~15)连接对象
def redisConnection(databaseNum):
pool = redis.ConnectionPool(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_AUTH, db=databaseNum, decode_responses=True)
mycon = redis.Redis(connection_pool=pool)
return mycon

使用:

调用数据库连接方法,并传入相应参数,获取连接对象

1
2
3
4
ini复制代码collection = mongodbConnection("address")
#获得mongodb address集合操作对象
con = redisConnection(0)
#获得redis 0号数据库操作对象

六.集中式管理模型类

约定俗成:

项目根目录中(app.py同级)创建模型文件models.py

1
2
3
4
5
6
7
8
9
10
11
12
13
ini复制代码# @FileName: models
from extendsions import db


class User(db.Model):
__tablename__ = "user_flask"
id = db.Column(db.Integer,primary_key=True)
name = db.Column(db.String(20))
age = db.Column(db.Integer)
sex = db.Column(db.String(2))
love = db.Column(db.String(20))

#.......

模型迁移

将模型类导入app.py(重要),只需导入

image.png

七.模块化静态文件和模板文件

约定俗成:

以蓝图模块划分,各个蓝图拥有各自的静态文件与模板

image.png

完整项目结构

image.png

本文转载自: 掘金

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

冲刺大厂每日算法&面试题,动态规划21天——第十天 导读 2

发表于 2021-11-07

「这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战」

导读

在这里插入图片描述

肥友们为了更好的去帮助新同学适应算法和面试题,最近我们开始进行专项突击一步一步来。我们先来搞一下让大家最头疼的一类算法题,动态规划我们将进行为时21天的养成计划。还在等什么快来一起肥学进行动态规划21天挑战吧!!

21天动态规划入门

如果一个数列 至少有三个元素 ,并且任意两个相邻元素之差相同,则称该数列为等差数列。

例如,[1,3,5,7,9]、[7,7,7,7] 和 [3,-1,-5,-9] 都是等差数列。 给你一个整数数组 nums ,返回数组
nums 中所有为等差数组的 子数组 个数。

子数组 是数组中的一个连续序列。

1
2
3
4
5
java复制代码示例 1:

输入:nums = [1,2,3,4]
输出:3
解释:nums 中有三个子等差数组:[1, 2, 3]、[2, 3, 4] 和 [1,2,3,4] 自身。
1
2
3
4
java复制代码示例 2:

输入:nums = [1]
输出:0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码class Solution {
public int numberOfArithmeticSlices(int[] nums) {
int n = nums.length;
if (n == 1) {
return 0;
}

int d = nums[0] - nums[1], t = 0;
int ans = 0;
// 因为等差数列的长度至少为 3,所以可以从 i=2 开始枚举
for (int i = 2; i < n; ++i) {
if (nums[i - 1] - nums[i] == d) {
++t;
} else {
d = nums[i - 1] - nums[i];
t = 0;
}
ans += t;
}
return ans;
}
}

一条包含字母 A-Z 的消息通过以下映射进行了 编码 :

‘A’ -> 1 ‘B’ -> 2 … ‘Z’ -> 26 要 解码
已编码的消息,所有数字必须基于上述映射的方法,反向映射回字母(可能有多种方法)。例如,”11106” 可以映射为:

“AAJF” ,将消息分组为 (1 1 10 6) “KJF” ,将消息分组为 (11 10 6) 注意,消息不能分组为 (1 11
06) ,因为 “06” 不能映射为 “F” ,这是由于 “6” 和 “06” 在映射中并不等价。

给你一个只含数字的 非空 字符串 s ,请计算并返回 解码 方法的 总数 。

题目数据保证答案肯定是一个 32 位 的整数。

1
2
3
4
5
java复制代码示例 1:

输入:s = "12"
输出:2
解释:它可以解码为 "AB"(1 2)或者 "L"(12)。
1
2
3
4
5
java复制代码示例 2:

输入:s = "226"
输出:3
解释:它可以解码为 "BZ" (2 26), "VF" (22 6), 或者 "BBF" (2 2 6) 。
1
2
3
4
5
6
7
java复制代码示例 3:

输入:s = "0"
输出:0
解释:没有字符映射到以 0 开头的数字。
含有 0 的有效映射是 'J' -> "10" 和 'T'-> "20" 。
由于没有字符,因此没有有效的方法对此进行解码,因为所有数字都需要映射。
1
2
3
4
5
java复制代码示例 4:

输入:s = "06"
输出:0
解释:"06" 不能映射到 "F" ,因为字符串含有前导 0("6" 和 "06" 在映射中并不等价)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码class Solution {
public int numDecodings(String s) {
int n = s.length();
int[] f = new int[n + 1];
f[0] = 1;
for (int i = 1; i <= n; ++i) {
if (s.charAt(i - 1) != '0') {
f[i] += f[i - 1];
}
if (i > 1 && s.charAt(i - 2) != '0' && ((s.charAt(i - 2) - '0') * 10 + (s.charAt(i - 1) - '0') <= 26)) {
f[i] += f[i - 2];
}
}
return f[n];
}
}

作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/decode-ways/solution/jie-ma-fang-fa-by-leetcode-solution-p8np/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

面试题

继续讲Linux命令总结:

1
2
3
4
5
6
7
8
9
10
java复制代码七、系统和关机 (系统的关机、重启以及登出 )
shutdown -h now 关闭系统(1)
init 0 关闭系统(2)
telinit 0 关闭系统(3)
shutdown -h hours:minutes & 按预定时间关闭系统
shutdown -c 取消按预定时间关闭系统
shutdown -r now 重启(1)
reboot 重启(2)
logout 注销
time 测算一个命令(即程序)的执行时间

本文转载自: 掘金

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

当 Redis 启动时 在开始之前 当 Redis 启动时

发表于 2021-11-07

「这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战」

Redis 作为现在最受欢迎的的键值数据库,在我们后端工程师的系统开发中,扮演着非常重要的角色。它超高的读写性能、丰富的存储结构、高可用集群设计以及活跃的社区,无疑是开源内存数据库的首选。我们今天就来了解一下 Redis 是如何处理一条命令的? 它在代码中又用了哪些小心思来提升性能?


数据来源 db-engines.com
在开始之前
=====

在开始之前,确保我们已经对 Redis 的安装、运行、常用命令有了一定的了解,这样我们在尝试理解内部实现原理时才会有感性的认知。我们来复习一下吧:

  1. 通过 Docker 运行一个 Redis 容器并进入
1
2
3
4
shell复制代码$ docker pull redis:4
$ docker exec -p 6379:6379 -itd --name redis redis:4
# 进入容器
$ docker exec -it redis sh
  1. 通过redis-cli连接,执行一些命令
1
2
3
4
5
shell复制代码$ redis-cli
redis-cli(6379)> set name foo
OK
redis-cli(6379)> get name
"foo"

我们看一下,在上面的过程中 Redis 到底发生什么?
image
从外部视角,即客户端的视角来看,可以分解为以下几步

  1. Redis 服务启动起来
  2. 客户端与 Redis 建立连接
  3. 客户端发送set name foo命令,返回响应OK
  4. 客户端发送get name命令,返回响应foo

那么接下来,我们从 Redis 内部视角(基于 Redis 5.0 的源码进行分析)出发,来看看,当一条命令set name foo被 Redis 接收到时,它到底“一个人抗下了多少”?
image

当 Redis 启动时

当 Redis 启动时,首先会对服务器进行初始化流程,包含以下的步骤:

  1. 初始化配置(系统默认配置)
  2. 加载并解析配置文件(用户配置)
  3. 初始化服务内部变量
  4. 创建事件循环 eventLoop
  5. 创建 socket 并开始监听
  6. 创建文件事件与时间事件
  7. 开始事件循环

初始化配置

第一步操作的核心函数为initServerConfig(void),有两百多行,主要是对服务器的一些核心参数设置系统默认配置,比如:

核心参数 默认值
定时任务执行频率默认 10
监听端口号 6379
最大客户端数量 10000
客户端超时时间 0(永不超时)
数据库数目 16
此外,还会进行初始化常用命令的列表。

加载并解析配置文件

第二步操作的核心函数为void loadServerConfig(char *filename, char *options),参数中filename为配置文件的路径,options为命令行通过参数指定的配置信息,比如:

1
shell复制代码$ redis-server /config/redis/redis.conf -p 400

则/config/redis/redis.conf就会作为filename,-p 400作为options。
在解析配置文件时,会将整个配置文件内容加载到内存,通过\n进行分割,然后将对#开头的行进行跳过,它代表是注释。

初始化服务内部变量

第三步操作的核心函数是initServer(void),主要是初始化一些客户端链表、数据库、全局变量和共享变量等。
Redis 通过复用共享变量来减少频繁的内存分配,通过函数createSharedObjects(void)来实现,共享变量都被保存在全局结构体shared中,主要有:

  • 用于响应的字符串,比如:shared.ok、shared.err
  • 0~10000的整数,比如:shared.integers[1]

创建事件循环 eventLoop

第四步操作的核心函数是aeCreateEventLoop(int setsiee),创建事件循环eventLoop,即分配结构体所需内存,并初始化结构体各字段,并调用aeApiCreate函数初始化了 epoll 对应的结构体。

创建 socket 并开始监听

第五步操作的核心函数是listenToPort(int port, int *fds, int *count)
这个函数中使用了server变量,这是一个全局变量

1
2
3
c复制代码// server.c
/* Global vars */
struct redisServer server; /* Server global state */

这个变量的初始化正提到的步骤三里操作的。server.bindaddr存储了用户在配置文件中写的所有IP地址。第五步所做的就是遍历用户配置的IP地址,建立非阻塞的 socket,进行监听。

创建文件事件与时间事件

接下来是第六步操作是创建文件事件与时间事件,Redis 把 socket 读写事件抽象为了文件事件,即aeFileEvent,通过事件循环进行执行。因此需要对刚才监听的 socket 创建对应的文件事件。
创建文件事件的处理核心函数是

1
2
c复制代码int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,
aeFileProc *proc, void *clientData)

我理解的是,利用 epoll 的非阻塞特性,为每个事件绑定一个对应的处理函数,当事件发生时,调用对应的处理函数进行处理即可。比如:监听事件的处理函数为acceptTcpHandler,实现了 socket 连接请求的 accept ,以及客户端对象的创建。

时间事件由定时任务函数触发,核心处理函数是aeCreateTimeEvent,时间事件实际上只有一个,他通过链表连接多个定时任务。

开始事件循环

最后一步是开始事件循环,核心处理函数代码很少,我们来看一下

1
2
3
4
5
6
7
8
c复制代码void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
while (!eventLoop->stop) {
if (eventLoop->beforesleep != NULL)
eventLoop->beforesleep(eventLoop);
aeProcessEvents(eventLoop, AE_ALL_EVENTS|AE_CALL_AFTER_SLEEP);
}
}

注意到eventLoop->beforesleep函数,在每次事件循环前执行,它会执行一些不是很费时的操作,如:集群相关操作、过期键删除操作(这里可称为快速过期键删除)、向客户端返回命令回复等。之后就是aeProcessEvents函数的执行,它会阻塞等待一小会儿文件事件的响应,如果有结果了,就去执行对应的处理函数,然后再去执行一下时间事件。

总结

本篇讲解了当 Redis 启动时程序做了哪些动作:初始化配置和变量、创建事件循环、监听 socket、创建文件事件和时间事件等等。也了解到 Redis 的全局变量和共享变量的作用,在共享变量中初始化0~10000的整数这种操作,和之前学习 Python 内存管理时它的小整数池是一个思路(空间换是时间)。

本文转载自: 掘金

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

jvm——程序计数器 程序计数器定义

发表于 2021-11-07

「这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战」

程序计数器定义

程序计数器全称为 Program Counter Register (寄存器)

作用

用于记录下一条jvm指令的执行地址,分支、循环、跳转、异常、线程恢复等都依赖于计数器。

image.png

这是一段控制台输出的java代码,编译后各个阶段的情况,主要看二进制字节码部分。

当然这里的二进制字节码并不是大家所熟知的计算机的0和1,它并不能直接给到cpu执行,它需要通过解释器解释成一条机器码,机器码就可以交给cpu执行它了。
程序计数器的作用是记住下一条jvm指令的执行地址,从上面图中可以看出jvm指令前面都会有一个数字,可以把这个数字理解为一个指令对于的地址。

当这些指令被加载到虚拟机后执行顺序:

1. 先得到 0:getstatic 指令

2. 交给解释器

3. 解释器解释成机器码

4. 机器码交给cpu,cpu执行

与此同时它会把下一条指令的地址 3地址 放入我们的程序计数器,等第一条指令完全执行完后,解释器就会到程序计数器中取到下一条指令的地址,去找下一条指令找到后重复刚才的流程。

注意:

当解释器去程序计数器中取这个 3地址 的时候会把下一条指令 4地址 放入程序计数器中。

在物理上实现程序计数器是在一个叫做寄存器来实现的,寄存器可以说是cpu中读取速度最快的部分。

特点

线程私有

java程序支持多线程运行,多线程运行时cpu调度器组件会给线程分配一个时间片。

假如给线程一分配一个时间片,在时间片内它的代码没有执行完,它就会把县城一的状态执行一个暂存,切换到线程二中去,线程二执行完后又会切换到线程一。切换回去的时候会去程序计数器中拿到之前最后执行的代码地址,程序计数器绑定自己线程。

不会存在内存溢出。

在jvm规范中已经规定了程序计数器部分不会内存溢出

本文转载自: 掘金

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

mysql索引过长Specialed key was too

发表于 2021-11-07

「这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战」。

在创建要给表的时候遇到一个有意思的问题,提示Specified key was too long; max key length is 767 bytes,从描述上来看,是Key太长,超过了指定的 767字节限制

下面是产生问题的表结构

1
2
3
4
5
6
7
sql复制代码CREATE TABLE `test_table` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(1000) NOT NULL DEFAULT '',
`link` varchar(1000) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

我们可以看到,对于name,我们设置长度为1000可变字符,因为采用utf8mb4编码, 所以它的大小就变成了 1000 * 4 > 767

所以再不修改其他配置的前提下,varchar的长度大小应该是 767 / 4 = 191

有兴趣的同学可以测试下,分别指定name大小为191, 192时,是不是前面的可以创建表成功,后面的创建表失败,并提示错误Specified key was too long; max key length is 767 bytes

解决办法一

  • 使用innodb引擎
  • 启用innodb_large_prefix选项,修改约束扩展至3072字节
  • 重新创建数据库

my.cnf配置

1
2
3
4
sql复制代码set global innodb_large_prefix=on;
set global innodb_file_per_table=on;
set global innodb_file_format=BARRACUDA;
set global innodb_file_format_max=BARRACUDA;

上面这个3072字节的得出原因如下

我们知道InnoDB一个page的默认大小是16k。由于是Btree组织,要求叶子节点上一个page至少要包含两条记录(否则就退化链表了)。

所以一个记录最多不能超过8k。又由于InnoDB的聚簇索引结构,一个二级索引要包含主键索引,因此每个单个索引不能超过4k (极端情况,pk和某个二级索引都达到这个限制)。

由于需要预留和辅助空间,扣掉后不能超过3500,取个“整数”就是(1024*3)。

解决办法二

在创建表的时候,加上 row_format=DYNAMIC

1
2
3
4
5
6
7
sql复制代码CREATE TABLE `test_table` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL DEFAULT '',
`link` varchar(255) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 row_format=DYNAMIC;

这个参数的作用如下

MySQL 索引只支持767个字节,utf8mb4 每个字符占用4个字节,所以索引最大长度只能为191个字符,即varchar(191),若想要使用更大的字段,mysql需要设置成支持数据压缩,并且修改表属性 row_format ={DYNAMIC|COMPRESSED}

II. 其他

1. 一灰灰Blog: liuyueyi.github.io/hexblog

一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

2. 声明

尽信书则不如,已上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

  • 微博地址: 小灰灰Blog
  • QQ: 一灰灰/3302797840

本文转载自: 掘金

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

【分布式事务系列】Seata-file、db存储模式

发表于 2021-11-07

这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战

file存储模式

Server端存储模式(store.mode)有file、db两种,file存储模式无需改动,直接启动即可。

file存储模式为单机模式,全局模式会话信息持久化在本地文件${seata_home}\bin\sessionStore\root.data中,性能较高,启动命令如下:

1
css复制代码sh seata-server.sh -p 8091 -h 127.0.0.1 -m file
db存储模式

db存储模式为高可用模式,全局事务会话信息通过db共享,性能相对差一些,操作步骤如下:

  • 创建表结构,Seata全局事务会话信息由全局事务、分支事务、全局锁构成,对应表globaltable、branchtable、lock_table
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
sql复制代码drop table if exists `global_table`;
create table `global_table` (
`xid` varchar(128) not null,
`transaction_id` bigint,
`status` tinyint not null,
`application_id` varchar(32),
`transaction_service_group` varchar(32),
`transaction_name` varchar(128),
`timeout` int,
`begin_time` bigint,
`application_data` varchar(2000),
`gmt_create` datetime,
`gmt_modified` datetime,
primary key (`xid`),
key `idx_gmt_modified_status` (`gmt_modified`, `status`),
key `idx_transaction_id` (`transaction_id`)
);

-- the table to store BranchSession data
drop table if exists `branch_table`;
create table `branch_table` (
`branch_id` bigint not null,
`xid` varchar(128) not null,
`transaction_id` bigint ,
`resource_group_id` varchar(32),
`resource_id` varchar(256) ,
`lock_key` varchar(128) ,
`branch_type` varchar(8) ,
`status` tinyint,
`client_id` varchar(64),
`application_data` varchar(2000),
`gmt_create` datetime,
`gmt_modified` datetime,
primary key (`branch_id`),
key `idx_xid` (`xid`)
);

-- the table to store lock data
drop table if exists `lock_table`;
create table `lock_table` (
`row_key` varchar(128) not null,
`xid` varchar(96),
`transaction_id` long ,
`branch_id` long,
`resource_id` varchar(256) ,
`table_name` varchar(32) ,
`pk` varchar(36) ,
`gmt_create` datetime ,
`gmt_modified` datetime,
primary key(`row_key`)
);
  • 设置事务日志存储方式,进入${seata_home}/conf/file.conf,修改store.mode=‘db’
  • 修改数据库连接
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
conf复制代码 db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
datasource = "dbcp"
## mysql/oracle/postgresql/h2/oceanbase etc.
dbType = "mysql"
driverClassName = "com.mysql.jdbc.Driver"
## if using mysql to store the data, recommend add rewriteBatchedStatements=true in jdbc connection param
url = "jdbc:mysql://127.0.0.1:3306/seata?rewriteBatchedStatements=true"
user = "mysql"
password = "mysql"
minConn = 5
maxConn = 100
globalTable = "global_table"
branchTable = "branch_table"
lockTable = "lock_table"
queryLimit = 100
maxWait = 5000
}
  • 启动seata-server:
1
sh复制代码seata-server.sh -h 127.0.0.1 -p 8091 -m db -n 1

参数说明如下:

-h: 注册到注册中心的IP地址,Seata-Server把自己注册到注册中心,支持Nacos、Eureka、Redis、Zookeeper、Consul等。

-p:Server RPC 监听端口

-m:全局事务会话信息存储模式,包括file、db,,优先读取启动参数

-n:Server node,有多个Server时,需区分各自节点,用于生成不同区别的transactionId,来避免冲突。

本文转载自: 掘金

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

1…401402403…956

开发者博客

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