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

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


  • 首页

  • 归档

  • 搜索

了解Redis数据结构,一篇就够!!!

发表于 2024-03-07
大家好,我是石头~


作为一名Java程序猿,我们深知在构建一个高性能、高可用的系统中,选择合适的数据存储与缓存机制至关重要。


Redis,作为一款开源的内存数据结构存储系统,因其出色的性能、丰富的数据结构以及强大的功能,被广泛应用于缓存、消息队列、排行榜等场景。深入理解Redis的核心数据结构及其特性,是我们在面对不同业务场景时做出最佳技术决策的关键。


Redis为我们提供了5种数据类型,接下来,我们将逐一进行介绍。

字符串(String)

String是Redis中最基本的数据类型,它不仅支持存储常规的字符串和数字,还凭借其二进制安全性,能够存储各种形式的数据,包括图片、音频、视频以及序列化的对象等。


此外,Redis的String还是动态字符串(SDS),支持最大长度达512MB,这意味着它们会根据存储内容的大小自动调整自身的容量。这种动态调整的特性极大地提高了Redis的灵活性和效率,使得开发者无需关心字符串的大小问题,可以专注于业务逻辑的实现。

列表(List)

Redis的List是一个简单的字符串列表,按照插入顺序排序。你可以向列表的两端添加元素,也可以从两端获取元素。列表非常适合实现消息队列、栈等数据结构。

哈希(Hash)

Redis的Hash是一个键值对的集合,非常适合存储对象。每个哈希可以包含多个字段和字段值对,这些字段和字段值对都是字符串类型。哈希特别适用于存储用户信息、配置参数等。

集合(Set)

Redis的Set是一个无序的字符串集合,它不允许重复元素。集合提供了许多有用的操作,如并集、交集、差集等,非常适合用于成员关系运算、唯一性检查等场景。

有序集合(Sorted Set)

有序集合是Redis提供的一种数据结构,它类似于集合,但每个元素都会关联一个分数。有序集合中的元素按照分数从小到大排序。有序集合非常适合实现排行榜、范围查询等场景。

实践建议

  • 尽量避免使用大键和大值,以减少内存占用和网络传输开销
  • 使用合适的数据结构来存储数据,根据数据的特点选择最适合的数据结构
  • 使用有意义且具有唯一性的前缀命名空间,例如namespace:key,这样可以帮助避免不同业务间的键冲突
  • 避免使用特殊字符作为键名组成部分,尤其是空格、换行符和转义字符

本文转载自: 掘金

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

为什么现在连Date类都不建议使用了? 一、有什么问题吗ja

发表于 2024-03-07

本文已经授权【稀土掘金技术社区】官方公众号独家原创发布。

一、有什么问题吗java.util.Date?

image.png

java.util.Date(Date从现在开始)是一个糟糕的类型,这解释了为什么它的大部分内容在 Java 1.1 中被弃用(但不幸的是仍在使用)。

设计缺陷包括:

  • 它的名称具有误导性:它并不代表 a Date,而是代表时间的一个瞬间。所以它应该被称为Instant——正如它的java.time等价物一样。
  • 它是非最终的:这鼓励了对继承的不良使用,例如java.sql.Date(这意味着代表一个日期,并且由于具有相同的短名称而也令人困惑)
  • 它是可变的:日期/时间类型是自然值,可以通过不可变类型有效地建模。可变的事实Date(例如通过setTime方法)意味着勤奋的开发人员最终会在各处创建防御性副本。
  • 它在许多地方(包括)隐式使用系统本地时区,toString()这让许多开发人员感到困惑。有关此内容的更多信息,请参阅“什么是即时”部分
  • 它的月份编号是从 0 开始的,是从 C 语言复制的。这导致了很多很多相差一的错误。
  • 它的年份编号是基于 1900 年的,也是从 C 语言复制的。当然,当 Java 出现时,我们已经意识到这不利于可读性?
  • 它的方法命名不明确:getDate()返回月份中的某一天,并getDay()返回星期几。给这些更具描述性的名字有多难?
  • 对于是否支持闰秒含糊其辞:“秒由 0 到 61 之间的整数表示;值 60 和 61 仅在闰秒时出现,即使如此,也仅在实际正确跟踪闰秒的 Java 实现中出现。” 我强烈怀疑大多数开发人员(包括我自己)都做了很多假设,认为 for 的范围getSeconds()实际上在 0-59 范围内(含)。
  • 它的宽容没有明显的理由:“在所有情况下,为这些目的而对方法给出的论据不必落在指定的范围内; 例如,日期可以指定为 1 月 32 日,并被解释为 2 月 1 日。” 多久有用一次?

原文如下:为什么要避免使用Date类?

二、为啥要改?

我们要改的原因很简单,我们的代码缺陷扫描规则认为这是一个必须修改的缺陷,否则不给发布,不改不行,服了。

image.png

解决思路:避免使用java.util.Date与java.sql.Date类和其提供的API,考虑使用java.time.Instant类或java.time.LocalDateTime类及其提供的API替代。

三、怎么改?

只能说这种基础的类改起来牵一发动全身,需要从DO实体类看起,然后就是各种Converter,最后是DTO。由于我们还是微服务架构,业务服务依赖于基础服务的API,所以必须要一起改否则就会报错。这里就不细说修改流程了,主要说一下我们在改造的时候遇到的一些问题。

  1. 耐心比对数据库日期字段和DO的映射

(1)确定字段类型

首先你需要确定数据对象中的 Date 字段代表的是日期、时间还是时间戳。

  • 如果字段代表日期和时间,则可能需要使用 LocalDateTime。
  • 如果字段仅代表日期,则可能需要使用 LocalDate。
  • 如果字段仅代表时间,则可能需要使用 LocalTime。
  • 如果字段需要保存时间戳(带时区的),则可能需要使用 Instant 或 ZonedDateTime。

(2)更新数据对象类

更新数据对象类中的字段,把 Date 类型改为适当的 java.time 类型。

  1. 将DateUtil中的方法改造

(1)替换原来的new Date()和Calendar.getInstance().getTime()

原来的方式:

1
2
Java复制代码Date nowDate = new Date();
Date nowCalendarDate = Calendar.getInstance().getTime();

使用 java.time 改造后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Java复制代码// 使用Instant代表一个时间点,这与Date类似
Instant nowInstant = Instant.now();

// 如果需要用到具体的日期和时间(例如年、月、日、时、分、秒)
LocalDateTime nowLocalDateTime = LocalDateTime.now();

// 如果你需要和特定的时区交互,可以使用ZonedDateTime
ZonedDateTime nowZonedDateTime = ZonedDateTime.now();

// 如果你需要转换回java.util.Date,你可以这样做(假设你的代码其他部分还需要使用Date)
Date nowFromDateInstant = Date.from(nowInstant);

// 如果需要与java.sql.Timestamp交互
java.sql.Timestamp nowFromInstant = java.sql.Timestamp.from(nowInstant);

一些注意点:

  1. Instant 表示的是一个时间点,它是时区无关的,相当于旧的 Date 类。它通常用于表示时间戳。
  2. LocalDateTime 表示没有时区信息的日期和时间,它不能直接转换为时间戳,除非你将其与时区结合使用(例如通过 ZonedDateTime)。
  3. ZonedDateTime 包含时区信息的日期和时间,它更类似于 Calendar,因为 Calendar 也包含时区信息。
  4. 当你需要将 java.time 对象转换回 java.util.Date 对象时,可以使用 Date.from(Instant) 方法。这在你的代码需要与旧的API或库交互时非常有用。

(2)一些基础的方法改造

a. dateFormat

原来的方式

1
2
3
4
typescript复制代码public static String dateFormat(Date date, String dateFormat) {
SimpleDateFormat formatter = new SimpleDateFormat(dateFormat);
return formatter.format(date);
}

使用java.time改造后

1
2
3
4
java复制代码public static String dateFormat(LocalDateTime date, String dateFormat) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(dateFormat);
return date.format(formatter);
}

b. addSecond、addMinute、addHour、addDay、addMonth、addYear

原来的方式

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
java复制代码public static Date addSecond(Date date, int second) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(13, second);
return calendar.getTime();
}

public static Date addMinute(Date date, int minute) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(12, minute);
return calendar.getTime();
}

public static Date addHour(Date date, int hour) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(10, hour);
return calendar.getTime();
}

public static Date addDay(Date date, int day) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(5, day);
return calendar.getTime();
}

public static Date addMonth(Date date, int month) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(2, month);
return calendar.getTime();
}

public static Date addYear(Date date, int year) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(1, year);
return calendar.getTime();
}

使用java.time改造后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码public static LocalDateTime addSecond(LocalDateTime date, int second) {
return date.plusSeconds(second);
}

public static LocalDateTime addMinute(LocalDateTime date, int minute) {
return date.plusMinutes(minute);
}

public static LocalDateTime addHour(LocalDateTime date, int hour) {
return date.plusHours(hour);
}

public static LocalDateTime addDay(LocalDateTime date, int day) {
return date.plusDays(day);
}

public static LocalDateTime addMonth(LocalDateTime date, int month) {
return date.plusMonths(month);
}

public static LocalDateTime addYear(LocalDateTime date, int year) {
return date.plusYears(year);
}

c. dateToWeek

原来的方式

1
2
3
4
5
6
java复制代码public static final String[] WEEK_DAY_OF_CHINESE = new String[]{"周日", "周一", "周二", "周三", "周四", "周五", "周六"};
public static String dateToWeek(Date date) {
Calendar cal = Calendar.getInstance();
cal.setTime(date);
return WEEK_DAY_OF_CHINESE[cal.get(7) - 1];
}

使用java.time改造后

1
2
3
4
5
6
java复制代码public static final String[] WEEK_DAY_OF_CHINESE = new String[]{"周日", "周一", "周二", "周三", "周四", "周五", "周六"};

public static String dateToWeek(LocalDate date) {
DayOfWeek dayOfWeek = date.getDayOfWeek();
return WEEK_DAY_OF_CHINESE[dayOfWeek.getValue() % 7];
}

d. getStartOfDay和getEndOfDay

原来的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码public static Date getStartTimeOfDay(Date date) {
if (date == null) {
return null;
} else {
LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(date.getTime()), ZoneId.systemDefault());
LocalDateTime startOfDay = localDateTime.with(LocalTime.MIN);
return Date.from(startOfDay.atZone(ZoneId.systemDefault()).toInstant());
}
}

public static Date getEndTimeOfDay(Date date) {
if (date == null) {
return null;
} else {
LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(date.getTime()), ZoneId.systemDefault());
LocalDateTime endOfDay = localDateTime.with(LocalTime.MAX);
return Date.from(endOfDay.atZone(ZoneId.systemDefault()).toInstant());
}
}

使用java.time改造后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public static LocalDateTime getStartTimeOfDay(LocalDateTime date) {
if (date == null) {
return null;
} else {
// 获取一天的开始时间,即00:00
return date.toLocalDate().atStartOfDay();
}
}

public static LocalDateTime getEndTimeOfDay(LocalDateTime date) {
if (date == null) {
return null;
} else {
// 获取一天的结束时间,即23:59:59.999999999
return date.toLocalDate().atTime(LocalTime.MAX);
}
}

e. betweenStartAndEnd

原来的方式

1
2
3
4
5
6
7
8
9
java复制代码public static Boolean betweenStartAndEnd(Date nowTime, Date beginTime, Date endTime) {
Calendar date = Calendar.getInstance();
date.setTime(nowTime);
Calendar begin = Calendar.getInstance();
begin.setTime(beginTime);
Calendar end = Calendar.getInstance();
end.setTime(endTime);
return date.after(begin) && date.before(end);
}

使用java.time改造后

1
2
3
Java复制代码public static Boolean betweenStartAndEnd(Instant nowTime, Instant beginTime, Instant endTime) {
return nowTime.isAfter(beginTime) && nowTime.isBefore(endTime);
}

我这里就只列了一些,如果有缺失的可以自己补充,不会写的话直接问问ChatGPT,它最会干这事了。最后把这些修改后的方法替换一下就行了。

四、小结一下

这个改造难度不高,但是复杂度非常高,一个地方没改好,轻则接口报错,重则启动失败,非常耗费精力,真不想改。

文末小彩蛋,自己花一个星期做的小网站,放出来给大家看看,网址如下:http://47.120.49.119:8080

本文转载自: 掘金

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

Git 分支整合之道:Merge 与 Rebase 的理念碰

发表于 2024-03-07

引言

在使用 Git 进行版本控制时,分支合并是一个常见的操作,而 Git 提供了两种主要的分支合并方式:git merge 和 git rebase。本文将深入探讨这两种合并方式的原理、优缺点以及适用场景,以帮助开发者更好地理解和选择合适的合并策略。

区别

Git Merge

原理: git merge 是将两个分支的历史记录合并为一个新的合并提交。它会保留分支的分叉结构,并创建一个新的合并提交,将两个分支的更改整合在一起。

优点:

  1. 保留分叉结构: 清晰地显示分支之间的合并关系,有助于理解整个项目的开发流程。
  2. 不改变历史: 合并分支时不会改变提交历史,不会对其他开发人员造成困扰,保持代码库的稳定性。
  3. 较少的冲突: 相对于 rebase,合并分支时可能会遇到较少的冲突。

缺点:

  1. 提交历史较混乱: 可能会导致提交历史变得较为混乱,不够线性清晰。
  2. 增加额外的合并提交: 创建额外的合并提交,可能会增加一些无关的提交信息。

Git Rebase

原理: git rebase 是重新设置分支的基点,并将当前分支的提交逐个应用到目标分支上。它会创建更整洁、更线性的提交历史。

优点:

  1. 历史整洁: 创建更整洁、更线性的提交历史,避免了额外的合并提交。
  2. 易于理解: 直观的提交历史线性结构更易于理解和回顾。
  3. 更好的回退和调整: 可以更容易地进行提交调整、回退或修改。

缺点:

  1. 潜在的冲突: 可能会遇到分支冲突,需要手动解决冲突并继续 rebase 过程。
  2. 改变历史: 可能会改变提交历史,导致其他开发人员困惑,并可能导致代码库的不稳定性。

如何选择合适的合并方式?

  • 如果更注重提交历史的整洁性和线性结构,可以选择使用 rebase。
  • 如果更注重保留分支的分叉结构和稳定性,可以选择使用 merge。
  • 根据项目需求和团队工作流程灵活选择合适的合并方式。

通过对比分析 git merge 和 git rebase,我们可以更好地理解它们各自的优缺点,以及在不同场景下的适用性。选择合适的合并方式有助于保持代码库的清晰性和稳定性,提高团队的开发效率。

应用场景及实战技巧

Git Merge

Git 中的 merge 命令用于将一个分支的更改合并到另一个分支。这是一个常见的操作,特别是在团队协作开发中。下面是一些 git merge 的常见应用场景及实战技巧:

1. 合并特性分支到主分支
  • 场景说明: 当开发完成一个新功能或修复一个 bug 时,通常会在一个单独的特性分支上进行开发。一旦功能或修复完成,就可以将该特性分支合并到主分支(通常是 main 或 master 分支)中。
  • 实战技巧:
1
2
bash复制代码git checkout main
git merge feature-branch
2. 解决分支冲突
  • 场景说明: 当两个分支上对同一文件进行了不同的更改,或者对同一行代码进行了不同的修改时,会导致分支冲突。解决分支冲突后,才能成功进行合并操作。
  • 实战技巧:
1. 使用 `git status` 查看冲突文件。
2. 手动编辑冲突文件,解决冲突。
3. 使用 `git add` 将解决冲突后的文件标记为已解决。
4. 使用 `git merge --continue` 继续合并过程。
3. 合并远程分支
  • 场景说明: 当团队成员在远程仓库上创建了新的分支并进行了更改,您可能需要将这些更改合并到本地仓库中。
  • 实战技巧:
1
2
bash复制代码git fetch origin
git merge origin/remote-branch
4. 避免 Fast-Forward 合并
  • 场景说明: Fast-Forward 合并是指合并时直接将目标分支指针移动到源分支指针所指的提交。有时候,您可能希望保留分支历史,以便在后续审查时更容易理解项目的演变。
  • 实战技巧: 使用 --no-ff 选项执行普通合并,即使没有分支历史也会创建一个合并提交。
1
bash复制代码git merge --no-ff feature-branch
5. 合并多个分支
  • 场景说明: 有时候需要将多个分支的更改合并到一个分支中,这需要进行一次合并操作。
  • 实战技巧: 合并多个分支时,首先合并其中一个分支到目标分支,然后再合并其他分支。
1
2
bash复制代码git merge branch1
git merge branch2

Git Rebase

Git 中的 rebase 命令用于重新设置分支的基点,从而改变提交的历史记录。它与 merge 相比,可以创建更加整洁的提交历史,但也需要谨慎使用。以下是一些 git rebase 的常见应用场景及实战技巧:

1. 将当前分支的提交整合到目标分支中
  • 场景说明: 当您在当前分支上进行了一系列提交后,希望将这些提交整合到目标分支中时,可以使用 rebase。
  • 实战技巧:
1
2
bash复制代码git checkout target-branch
git rebase source-branch
2. 解决分支冲突
  • 场景说明: 在进行 rebase 过程中,可能会遇到分支冲突。解决冲突后,需要手动继续 rebase 进程。
  • 实战技巧:
1. 使用 `git status` 查看冲突文件。
2. 手动编辑冲突文件,解决冲突。
3. 使用 `git add` 将解决冲突后的文件标记为已解决。
4. 使用 `git rebase --continue` 继续 rebase 过程。
3. 保持分支历史的整洁性
  • 场景说明: 使用 rebase 可以创建更加整洁的提交历史,减少不必要的合并提交。
  • 实战技巧:
1
2
bash复制代码git checkout source-branch
git rebase target-branch
4. 合并多个提交
  • 场景说明: 有时候,可能希望将多个提交整合成一个提交,以便更好地组织提交历史。
  • 实战技巧: 在进行 rebase 过程中,可以选择将多个提交合并成一个提交。
1
bash复制代码git rebase -i HEAD~n

5. 避免创建额外的合并提交

  • 场景说明: 使用 rebase 可以避免创建额外的合并提交,保持提交历史的干净整洁。
  • 实战技巧: 在进行 rebase 过程中,使用 --no-ff 选项,禁用 Fast-Forward 合并。
1
bash复制代码git rebase --no-ff target-branch

6. 注意事项

  • 谨慎使用: 使用 rebase 会改变提交的历史记录,因此需要谨慎操作,避免对共享仓库造成影响。
  • 避免在共享分支上使用: 避免在共享分支(如 main 或 master)上使用 rebase,以免影响他人的开发工作。

总结

在选择 rebase 还是 merge 时,需要根据项目的需求和团队的工作流程来权衡优缺点。如果您更注重提交历史的整洁性和线性结构,可以选择使用 rebase;如果您更注重保留分支的分叉结构和稳定性,可以选择使用 merge。另外,也可以根据具体情况在两种合并方式之间灵活选择,以满足项目的实际需求。

写在最后

通过本文,读者可以更全面地了解 Git 分支合并的两种主要方式:Git Merge 和 Git Rebase,并学会如何根据项目需求选择适合的合并策略。

喜欢的话帮忙点个赞 + 关注吧,将持续更新 Git 相关的文章,还可以关注我的公众号 梁三石FE ,感谢您的关注~

本文转载自: 掘金

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

不太熟悉Git? 不妨看看这篇文章 前言 基本操作 远程仓库

发表于 2024-03-07

前言

Git是目前世界上最先进的分布式版本控制系统,由C语言进行开发

在2022年之前,Linux构建的方式是世界各地的志愿者把源代码文件通过diff的方式发送给Linus,然后由Linus本人通过手工方式合并代码

Linus痛恨的CVS和SVN都是集中式的版本控制系统,而Git是分布式的版本控制系统,这两者有何区别?

集中式:版本库是集中存放在中央服务器的,开发的时候用的都是开发机(本地机器),所以要先从中央服务器获取最新的版本,然后进行开发之后将代码推送给中央服务器。最大的问题是必须联网才能够进行工作,在网速慢的时候,提交大文件就会非常耗时。

分布式:分布式控制版本是没有“中央服务器”这个概念的,每个人的电脑上都是一个完整的版本库,这样在开发的时候就不需要联网,因为版本库在自己的电脑上。既然每个人的电脑上都有一个完整的版本库,那多个人如何协作呢?这时候就需要将各自对文件的修改推送给对方,就可以互相看到对方的修改了。和集中式版本控制系统相比,分布式版本控制的安全性会高很多。因为每个人的电脑里都有完整的版本库,某个人的版本库出问题了没有关系,而集中式版本控制只有一个版本库。通常分布式版本控制系统也有一台充当“中央服务器”的电脑,当然这个服务器的作用只是方便“交换修改”,没有它也一样能够干活,只是交换修改不方便。

Git与svn的区别

  1. 历史记录:Git更加轻量级,每次提交只记录变化,而SVN每次提交都会存储完整的文件
  2. 版本管理:Git更加灵活,允许分支和分支合并,而SVN只有主干
  3. 安全性:Git分布式存储,一个服务器挂掉不会影响其他服务器,而SVN单一服务器容易出现安全问题
  4. 开发流程:Git的开发流程更加快捷,可以快速的实现拉取、提交,而SVN开发流程繁琐
  5. 部署:Git无需安全客户端,支持跨平台,而SVN必须安装客户端才能使用
  6. 使用:Git更加简单,学习成本更低,而SVN略显复杂

基本操作

创建版本库

版本库:仓库repository,可以简单理解为一个目录,这个目录中的所有文件都可以被Git管理起来,每个文件的修改、删除,Git都能够进行跟踪,以便任何时刻都可以追踪历史,或者在将来某个时刻可以“还原”。

创建一个版本库很简单

//1.创建目录

mkdir learngit

cd learngit

pwd

//2.通过git init将这个目录变成Git可以管理的仓库

执行完git init命令之后,当前目录会多出一个.git目录

这个目录是git用来跟踪版本库的,默认是一个隐藏文件

//3. 将文件放到Git仓库中

vim readme.txt //随便写点什么

git add readme.txt //将readme.txt文件添加到仓库

git commit -m “this is a readme.txt”

为什么add和commit要分成两步呢?因为一次commit可以提交很多文件,而可以多次add不同的文件

状态查看

现在我们将readme.txt文件修改为如下内容

Git is a distributed version control system.

Git is free software.

可以通过git status 来查看结果

git status 告诉我们readme.txt文件已经被修改了,但是还没有准备提交的修改

怎么查看到底该了什么内容呢?

可以通过git diff命令来查看difference,显示的格式正是Unix通用的Diff格式

通过git add readme.txt进行提交

再通过git status查看当前状态

这是告诉我们,被提交的修改有readme.txt文件

  • 如果想要查看工作区的状态,使用Git status命令
  • 如果git status告诉你有文件被修改过,用git diff可以查看修改内容

回退

我们再次修改readme文件

以下是新内容

Git is a distributed version control system. Git is free software distributed under the GPL.

之后尝试提交

像这样,不断对文件进行修改,然后不断提交修改到版本库里,就类似于把git的状态存盘,每当文件修改到一定程度的时候,就可以『保存一个快照』,这个快找在Git中被称为commit,一旦把文件弄乱了或者误删了文件,可以从最近的一个commmit中恢复

git log能够帮助我们看到git的历史记录

显示的是从最近到最远的提交日志,如果嫌输出信息太多,可以加上--pretty=online参数

在Git中,用HEAD来表示当前版本,上一个版本为HEAD^,上上一个版本为HEAD^^,

我们可以通过git reset --hard HEAD^来将文件回退到上一个版本

在底层,Git在内部有一个指向当前版本的HEAD指针,当进行回退版本的时候,GIT仅仅是将HEAD的指向进行了改变

一个新的问题:如果这个时候我又想回到GPL的那个版本该怎么办呢?

可以看到这时候我们通过Git log进行日志查看,已经没有那个版本的记录了

我们可以通过git reflog 看到之前提交的id

通过git reset –hard参数+ID 进行回退

工作区与暂存区

Git与其他版本控制系统的一个不同之处就是有暂存区的概念

工作区

就是电脑里能够看到的目录,比如learngit文件夹

版本库

工作区有一个隐藏目录.git,这个不算是工作区,而是Git的版本库。Git的版本库里面有很多东西,其中最重要的就是称为stage(index)的暂存区,还有Git为我们自动创建的第一个分支master,以及指向master的一个指针叫做HEAD

我们将文件往Git版本库添加的时候,是分两步执行的

  • git add把文件添加进去,实际上就是把文件添加到暂存区
  • git commit提交更改,实际上就是把文件去的所有内容提交到当前分支

因为在创建GIT版本库的时候,GIT自动为我们创建了唯一一个分支master,所以git commit就是往master分支上提交更改

git commit之后

管理修改

每一次的修改都必须使用git add添加之后,才会放到暂存区,在git commit的时候才会提交到分支。如果修改了之后没有使用git add命令,那么是不会提交到分支中的

撤销修改

当我们将文件修改成以下内容,但是未提交的时候,git会进行以下提示

Git is free software distributed under the GPL. Git has a mutable index called stage. Git tracks changes of files. My stupid boss still prefers SVN.

可以使用git checkout – readme.txt 进行把readme.txt文件的工作区的修改全部撤销

切记,一定要添加–标签,不然这个命令会变成切换到另一个分支

这里的撤销有两种情况

  • readme.txt自修改之后还没有被放到缓存区中,现在,撤销修改就回到和版本库一模一样的状态
  • readme.txt已经添加到暂存区之后,又做了修改,撤回修改就回到添加到暂存区之后的状态

当然 现在推荐的命令是 git restore readme.txt

当已经把修改通过git add命令添加到暂存区之后,想要丢弃修改,分成两步

  • git reset HEAD
  • git checkout –file

删除文件

当我们在工作区下添加一个文件

vim test.txt

内容如下:

hello world.

然后通过git add test.txt 与 git commit -m “add test.txt”进行提交

这个时候我们把test.txt文件进行删除

再通过git status查看会发现

现在我们有两种选择

1.确定删除 使用git rm test.txt,然后git commit -m “remove test.txt”

2.删错了 使用版本库里的版本替换工作区的版本 git checkout – test.txt

远程仓库

添加远程仓库

git remote add origin git@xxx

git push //将本地库的内容推送到远程库上

-u 参数不但能够实现远程推送 还能够将本地分支与远端分支关联起来 之后的推送或者拉取就能够简化命令

git remote -v

查看远程仓库信息

git remote rm origin

删除远程仓库(解除本地和远程的绑定关系)

git clone克隆远端代码 ssh协议的速度>https

分支管理

HEAD是一个指针,指向的分支就是当前分支,在一开始的时候,master分支是一条线,Git使用master指向最新的提交,再用HEAD指向master,就能确定当前分支以及提交点

每次提交,master分支都会向前移动一步,这样随着不断提交,master分支的线也会越来越长

当我们创建了一个新的分支,其实也就是一个新的指针dev,指向的是master相同的提交,再把HEAD指向dev,表示当前分支在dev上

因为git的新建分支是创建一个指针以及修改HEAD的指向,而文件本身内容不变,所以速度很快

从现在开始,对工作区的修改和提交就是针对dev分支了,新提交之后dev指针向前移动,但是master指针不变

分支管理

git branch dev 创建dev分支

git checkout dev 切换到dev分支

添加一个内容为hello world的test文件

git add + git commit 提交成果

git checkout master 切换回主分支

git merge dev 将dev分支合并到当前分支

然后再通过git branch -d dev将dev分支进行删除

git推荐现在的分支操作使用switch命令

git switch -c 创建并切换到新分支

git switch master 创建到已有分支

冲突

git使用<<<<<<<,=======,>>>>>>>标记出不同分支的内容标记出不同的分支

更改冲突文件为想要的内容后提交

分支管理

在合并分支的时候,如果可能,git会使用fast forward模式,在这种模式下,删除分支之后就会丢掉分支信息

合并分支的时候,加上–no-ff参数就可以使用普通模式合并,合并后的历史有分支,能够看出来曾经做过合并

git merge --no-ff "merge dev" dev

bug

一般每个bug都会新建一个分支来修改,修复之后合并分支,然后将临时分支删除

git stash 将当前工作区进行存储

一是用git stash apply恢复,但是恢复后,stash内容并不删除,你需要用git stash drop来删除;

另一种方式是用git stash pop,恢复的同时把stash内容也删了:

因此,多人协作的工作模式通常是这样:

  1. 首先,可以试图用git push origin 推送自己的修改;
  2. 如果推送失败,则因为远程分支比你的本地更新,需要先用git pull试图合并;
  3. 如果合并有冲突,则解决冲突,并在本地提交;
  4. 没有冲突或者解决掉冲突后,再用git push origin 推送就能成功!

如果git pull提示no tracking information,则说明本地分支和远程分支的链接关系没有创建,用命令git branch –set-upstream-to origin/。

这就是多人协作的工作模式,一旦熟悉了,就非常简单。

标签

git tag 给commit打标签

git show 查看标签信息

git tag -a 查看所有标签

git tag -d 删除标签

git push origin 推送本地标签

github

1.fork一个仓库 然后clone仓库 因为没权限clone原仓库

2.开发 往自己的仓库推送

3.推送pr给原仓库

Git进阶操作

git pull

拉取成功但不更更新

这个因为本地有更改,和仓库对应不上,解决方式如下

1.git stash将本地修改存储起来

2.git pull //建议使用完整的git pull origin branchname

git pull内部执行原理

pull包含两个操作,fetch和merge

fetch: 将远程仓库拉取到本地仓库

merge:将本地仓库和分支进行merge

git pull的时候会向远端发送git-upload-pack请求,携带的是本地仓库commit的记录,如果一致则不需要拉取,不一样就将远端仓库拉下来

push之前pull的原因

git commit的时候,仓库并不会将本地和远程仓库代码进行比较,不会识别出代码是否存在冲突,必须进行pull命令之后,才会将本地代码和远程仓库的代码进行比较,如果二者的代码存在冲突,必须要解决冲突后重新commit push,如果不存在冲突,则pull的时候直接合并代码,不会将本地代码覆盖掉

git status中的untracked file

在Git中,未被跟踪的文件(untracked file)是指存在与Git管理的目录中,但是尚未被添加到Git版本控制中的文件。这些文件没有被Git追踪,因此它们不受Git版本控制的管理。

当使用git status命令查看git存储库的状态,一般日志文件就会是untracked file,因为没必要对日志进行追踪,日志也不会提交到代码库中

如果想把未被追踪的文件添加到Git版本控制中,可以使用git add命令将它们添加到Git的暂存区中,然后使用git commit提交到存储库中

git add提交了多余的文件,并且已经git commit了,怎么撤销

如果只是git add了,但是还没有git commit ,也就是说这些文件只是添加到了暂存区,还没有进行提交,那么可以通过git reset + filename,将文件从暂存区中删除,或者git reset直接将所有文件都从暂存区中撤销

第一步得撤销git commit

git revert sha值 撤销你的某一个提交

或者直接 git revert HEAD 撤销最近一次提交,并创建一个新的提交来记录这个撤销操作,这个操作可以用来修复一个错误的提交或者撤销一个不必要的提交

如果报错error:commit sha is a merge but no -m option was given

这是因为正在尝试通过git revert命令撤销一个合并提交,但是没有指定用于撤销的父提交。要解决这个问题,需要使用-m选项来指定父提交,该选项后面需要指定一个数字,标识用于撤销提交的父提交的编号

假设想撤销最近的一次合并提交,可以使用

git revert -m 1 HEAD

使用合并提交的第一个父提交来撤销该提交

git commit错分支了怎么办

1.git log命令查找刚刚提交的SHA值

2.git branch + git checkout 切换到你想提交的分支

3.git cherry-pick + sha 讲提交应用到当前分支

git revert后工作区代码消失

git reset –hard HEAD 该命令会将工作区和暂存区都重置为最新的提交,并清除所有未提交的修改。需要注意的是,这个命令会清除本地未提交的更改,因此在使用前请确认这些更改已经备份或者提交到了其他分支上。如果仍然无法恢复更改,可以使用git reflog命令查找之前的提交记录,使用git reset –hard +sha指向指定的提交

修改git commit提交信息

当我们cr被打回来的时候,就不需要重新git commit,而是直接git commit –amend修改之前的提交信息,这会被git认为是一次新的提交

1.git commit –amend 打开编辑器

2.修改提交信息

3.git push

查看git信息

1.git log 查看所有的提交历史记录 例如提交ID 作者 日期 提交说明等等,常用于查看Git仓库的历史提交记录以及对比和合并分支

2.git reflog 记录了Git仓库的每一次操作,包括分支和标签的创建、删除、移动等操作,这个命令可以用来找回已经删除的分支或者标签

git diff

1.git diff –cached 查看暂存区和最后一次提交之间的差异 也就是git add离上一次的更改

2.git diff filename 查看文件的更改

3.git diff 查看已经修改但是未暂存(没有add)的更改

工作区、暂存区、本地仓库、远程仓库

1.工作区:实际写代码的地方,电脑上的文件夹,里面放着我们的代码文件

2.暂存区:git提供的一个临时的存储取余,可以暂时存储我们修改过的文件,等待提交到本地仓库,对应的是某一个分支

3.本地仓库:存储代码版本历史记录的地方,可以看作是Git维护的一个数据库,存储了项目的所有历史版本

4.远程仓库:一个在网络上的Git仓库,通常由代码托管服务商提供,可以把本地仓库的代码推送到远程仓库中,也可以从远程仓库中拉取代码到本地仓库进行使用

基本工作流:通过git add将文件添加到暂存区,然后通过git commit提交到本地仓库,最后通过git push提交到远程仓库

为什么需要本地仓库:

  • 安全性:本地仓库可以在本地保存代码的历史版本,即使在远程仓库数据丢失或者被破坏的情况下,本地仓库中的代码仍然是安全的
  • 离线操作:没有网络的情况下,本地仓库允许开发人员继续对代码进行修改和提交
  • 提高效率:由于本地仓库不需要每次从远程仓库拉取代码,可以大大减少代码拉取的时间和网络带宽

git工作区修改后切换到新分支,保存修改

如果在工作区有未提交的修改,并且切换到新分支,那么这些修改是不会被保存的,因为一个暂存区对应的是一个分支

这个时候我们就需要

1.git stash //将工作区的修改保存到一个临时区

2.git checkout

3.git stash pop 或者git stash apply

4.如果有冲突的话,处理对应的文件

1
2
3
4
5
markdown复制代码<<<<<<< HEAD
// 这里是当前分支的修改内容
=======
// 这里是合并分支的修改内容
>>>>>>> merge-branch

二者区别

apply从stash中恢复最近的一次修改,但不会将这些修改移出

pop从stash中恢复最近的一次修改,同时将修改弹出

git merge和git rebase的区别

merge 是合并的意思

rebase 是复位基底的意思

推荐是使用git rebase 因为rebase的代码历史非常清晰

比如有一个master分支,同时6个人进行开发,需要创建六个单独的个人分支,然后使用merge的话就会有六个branch和主分支交织在一起,也就是master的commit历史是网状的.master是创建一个新的结点,然后将两个分支的历史联系在一起

而rebase会把提交移动到master的最前面,形成一条线

结语

本文泛谈Git的历史变动,各种基础以及高级操作和相关命令,以及他们的原理,相信大家能够有所收获。

创作不易,如果有收获欢迎点赞、评论、收藏,您的支持就是我最大的动力。

本文转载自: 掘金

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

线程池优化实践

发表于 2024-03-07

省流:通过线程池优化查询的业务逻辑,提高接口的效率,但需注意线程池的参数配置(阻塞队列大小,拒绝策略),避免带来其他的问题

问题背景:我们在使用系统的过程中,会遇到一些导出数据的场景,比如导出某个人最近一天的所有会议记录,以及会议的参会人信息

查询会议记录的业务逻辑

第一步 先根据用户查询查询用户的会议列

第二步 根据第一步查询出来的会议列表的会议信息,去循环查询每个会议的与会人信息

第三步 组合结果返回

查询会议记录业务流程图.png

业务代码(注:代码为模拟代码,并不涉及真实生产的代码)

image.png

并发为5时,请求耗时为平均在2000ms左右

image.png

并发50时,请求耗时为平均3000+ms左右
image.png

并发500时,请求耗时为平均4000+ms左右
image.png

随着并发量的上涨,查询数据的耗时越来越长,且并发为5时就已经达到2000ms左右了,耗时巨长

通过观察业务逻辑,循环查询每个会议的与会人信息的时候,并没有逻辑上的关联,也就是说,查询每个会议的与会人信息可以看作一个独立的过程,我们可以考虑用单独的线程去执行这个查询,通过并发来提高整体的效率。
改进后的流程为
第一步 先根据用户查询查询用户的会议列表
第二步 根据第一步查询出来的会议列表的会议信息,把每个会议的与会人查询提交到线程池当中去执行
第三步 组合结果返回
查询会议记录业务流程图(改进).png

业务代码(注:代码为模拟代码,并不涉及真实生产的代码)

线程池参数

核心线程数为4(一般IO类型任务核心线程设为cpu * 2,计算类型任务时,核心线程设为cpu + 1,查询数据库或者访问第三方接口都数据IO类型的任务)

最大线程数设置为20

空闲线程存活时间 5分钟

任务队列大小 500

拒绝策略 直接拒绝

image.png

image.png

改进后
并发为5时,耗时为100+ms,速度得到质的提升

image.png

并发为50时,初期耗时为100ms,但是随着并发量的上升,后续的耗时逐渐开始变大,最大的时候为1000+ms,此时还出现阻塞队列中的任务,获取超时的问题,此时已经开始影响到了正常的业务,有部分的用户开始导出缺失的数据了

image.png
耗时增大
image.png
超时问题
image.png

并发为500时,也是同样的初期耗时较低,后续耗时变大,此时阻塞队列出现了大量超时的问题,大量用户已经开始导出空白的数据了,问题更加严重了

image.png

目前线程池出现的问题

问题一:后续耗时会变大?

问题二:阻塞队列已满,且出现大量队列中的任务超时问题,直接导致用户导出了空白的数据?

解决方案:

问题一:耗时开始增大是正常的现象,因为当流量变大的时候,服务处理能力到最高后无法提升了,必然会有开始变慢的问题,影响不大,可通过限流或者是提醒用户稍等重试的方式较低影响

问题二:
这个问题的本质是因为线程池参数设置得不合理

1、队列设置错误,该场景下,需要充分利用线程资源,将线程放入队列只会徒增等待的时间,导致等待队列里的任务全部超时抛出异常

2、拒绝策略设置错误,直接拒绝任务会抛出异常导致主流程中断,但此时主流程可能已经提交了部分任务,导致无效任务的提交

优化后参数

image.png
相比于之前,只修改了两个参数

1.把原来的任务队列设置为SynchronousQueue,SynchronousQueue 是一个特殊的队列,其最大容量是1。也就是说,任何一次插入操作都必须等待一个相应的删除操作,反之亦然。如果没有相应的操作正在进行,则该线程将被阻塞,也就是说当线程池达到最大线程后,就不在接受新的任务了,解决阻塞队列内的任务超时的问题

2.拒绝策略改为CallerRunsPolicy,当阻塞队列满了,由提交任务的线程去完成执行,原来的流程中,主线程提交完任务后就阻塞等待了,浪费了主线程的线程资源,可利用主线程的资源,当阻塞队列满了,让主线程来执行当前任务,充分利用主线程的资源

执行
并发量为5时,执行耗时为100ms左右,和原来参数表现基本一致

image.png

并发量为50时,执行耗时为100+ms左右,最高也只到150ms,且没有出现任何异常

image.png

并发量为500时,执行耗时为300+ms左右,最高也只到500+ms,且没有出现任何异常

image.png

本文转载自: 掘金

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

进电子厂了,感触颇多 故事 有感

发表于 2024-03-06

作者:三哥

个人网站:j3code.cn

本文已收录到语雀:www.yuque.com/j3code/me-p…

是的,真进电子厂了,但主人公不是我。

虽然我不是主人公,但是我经历的过程是和主人公一样的,真实而又无奈。真实是真真切切的经历了这一段过程,而无奈则是我进厂这段旅程所体会出来的。

如果你对我的这段经历感兴趣,可以耐心的往下看。

故事

这里我先定义一个主人公 A,然后他来我这边找工作,那么我作为在广州待了有些年头的靓仔(一坤年多点),肯定要出时间给他张罗张罗,所以就有了这一段难忘的经历(现在回想起来,我是不想再经历第二次了)。

在广州没有学历,能选择的工作其实并不多,无非就是进厂或者打零工当学徒。但奈何我也没有什么找工作的门道,所以只能帮着从某直聘软件上刷工作岗位了。只要是不要求经验,学历的统统都是我备选岗位中的一个。在这里就不得不说在某直聘软件上,刷这种普工、配/拣/打包等岗位一刷一大堆,而且还自动找上门来,不像我现在干的程序员行业,要么进人才库要么人才库都不配进,只能石沉大海。

在结合 A 自身的情况和交通出行方便的情况下,我们决定接受进厂工作。因为他们发的待遇信息确实很吸引人,如:小时工 30/小时,多劳多的;正式工底薪加提成综合薪资7-8k;还包吃包住。你看,这怎么能不吸引人,怎么能不吸引一个以前只能拿 2-3k 工资不包吃不包住的主人公 A 呢!当然,我当时也觉得非常可以,以至于不假思索的和主人公 A 达成了统一战线。

那么,这次的难忘之旅就由此展开了。

先是早上按照他们微信上聊的,带好行李,身份证去一个交通非常方便的地铁口面试。那么我在想去面个试带行李干啥,我放在住的地方也挺好的,等面试通过了,安排住宿的地方了,我在将行李搬过去,这不简单轻松嘛。所以我和主人公 A ,就一致决定没有带行李过去,直接人 + 身份证过去面试,然而奇怪的点就来了。当我们到达面试的地方时,见到好多外来务工的人,这一幕让我有点触动。都是大包小包,蛇皮袋+黑行李箱,然而我两却空空如也。这时候我还没意识到问题的严重性,就直接过去问怎么面试,而那个穿蓝色工作服的人只是抬头撇了我一眼,看我们没有行李,直接抬手挥了挥,说没行李的不收。我说,为什么,他也不搭理我,直接说别妨碍后面的人面试(其实后面也没人),不收就是不收。

在这里我已经忍不了了,先说一下此刻我的内心:diss 他上下 18 代无数遍了,后面细讲,咱们先看故事。

其实我当时是想直接怼他,但为了照顾主人公 A 的情况,我忍了,重新回家去拿了一次行李,然后再次去面试的地方找工作了,但这次不是上一个地点,而是我另一个备选面试(也是需要行李,还是行李)。

两个人拖着个行李,酿酿跄跄的来到了下一个面试点,还是和上一个面试点一样的流程,但这次不同的是我带了行李,ok,他接收了,说等会安排面试。其实我以为这就完了,但是并没有,这才是开始的第一步。

等了一会之后,这个面试点就陆陆续续的来了二三十个人,也还是和我上次看到的人群一样,大包小包,蛇皮袋+黑色行李箱。此时我内心就不仅感慨,为了个工作也是不容易。等了差不多十几分钟就有个嗓门大的人喊,要面试的人跟我来,带上行李,没行李的不要来。就这样,我和主人公 A 被带到了一个不是很大的大厅,里面差不多容纳了几百人,此时我又被触动了(是的,以我目前的认知,我这段经历会被触动很多次),感觉进了传销一样。

从进入那个大厅以后,我就觉得非常无力,这种无力感你只有亲自体会了才知道。全是人,但就是没有工作人员,只知道等着叫号,其余时间找个地方坐着。当时就想,走也不是来都来了,留也不是和印象中的面试太不一样了。所以就在这种内心纠结中,徘徊了几个小时,是的你没看错,就是几个小时(起码两小时)。

这期间其实还有套路,我怕文章会写的很长,就忽略了,如果你们想听,以后多关注我 B 站直播,我会时不时的讲出来。

中午的时候,终于有个人扯着嗓门喊话,让我们安静下来,听他介绍后续的招聘流程。他的话不是很精炼,但是足够洗脑,因为他会在合适的时机给这些找工作的人透露,在该公司上班工资比的过广州大多数CBD上班的白领。只要这话时不时的出现,人群就会时不时的骚动几下,其实我能明白这些务工人拼搏一年不就是为了多赚点吗?听到高薪,眼睛发光是在正常不过了。但是,我就是看不惯这些中介公司层层剥利,压榨忽悠这些信息闭塞的外来务工人员(我这是事后才知道,不是工厂直招,而是中介)。

反正一番讲话之后,我们被分成了两批人,一批工资高点,但是上班地点异常的偏僻,另一批就是工资地点,但好在是市里,交通方便。

但我个人觉得,工作高低也是骗人的,说的高工资,去的人就多,他们抽的利也就多。

而我选择工资低的这类工作也就是看中他交通方便,能时常照顾一下,帮点忙啥的。

但,你以为这就完了嘛,并没有。之后的这些时间,我被他们用大巴车从集合点、拉到电子厂、再由电子厂拉到体检中心、最后拉到安排宿舍大楼。每一个地点的具体事宜如下:

  1. 集合点:忽悠办电话卡,打印身份证,蓝底照片制作,几百号人分成高薪资与低薪资两批人(但都声称比 CBD 白领薪资高)。
  2. 电子厂:面试,很奇葩的面试,就是看你的面相 + 基本信息,全称一句话都不用说。
  3. 体检中心:这个就是单纯体检,不过体检地点非常远,非常远,一看就是和这种中介公司有绑定关系,还是抽利。
  4. 宿舍大楼:安排宿舍,如蜂窝煤一样的格子间,超密集。

这期间,我们还没工作,杂七杂八的费用就被他们收去了 200 块(我们没有办卡,如果办卡了估计更多)。而且以上的时间线,每一个点都是非常熬人的,我一个正常男子都觉得非常累非常累,真不知道那些年纪小的、年纪大的、女生是如何坚持下来的。

熬人是因为,我们是一批人一批人的过,要等到这一批人的流程走完了才能走下一个流程,而且有些流程只有特定的时间点才开始,来早了,你就给我等着,到点才开始。

最后,当你分配到一张上下铺的床位后,你的这一趟流程就算是彻底走完了,时间也应该是来到了晚上六七点,所以就为了一个进厂工作的机会,从早上九点折腾到晚上六七点,真 TM 累。

以上就是我今天一天所经历的事情,现在想想,如果是你,你会为了进厂而这样折腾吗?

有感

不经历一遍,永远是不能感同身受。

上面的故事你们看了,也就是看了,可能内心都不会有一点波澜,但这个没关系,如果条件允许我希望看到这篇文章的人,永远都不要有这种经历。

首先,我来对这种中介第三方公司招人的方面,来说说我的看法:

你们要先分别什么是工厂直招还是第三方中介招聘,如果是从某直聘上看到的岗位应该是会写“派遣”或者“代招”。当然,也有不写的,那么这就需要后续的判断了“没有行李不接待”。是的,就是这个,我现在才反应过来,如果你去面试,不带行李,他们接待你,然后给你安排这一系列的流程,你中途觉得不靠谱,你是可以随时走人的,一点都不麻烦。但是如果你是大包小包 + 黑色行李箱的话,那么你就不可能那么方便的随时想走就走了,这个行李就是第三方中介拷住你的加锁。

你们也不要相信从某直聘上加的所谓人事的话,他们只有一句话是真的,那就是引你到面试的地点是真的,其余的都是假话,其余的都是假话。如果你们以后不幸碰到我上面的故事情节,千万不要对这种人抱有任何的感激,觉得他们真好,为了给我一个工作机会,解答我的各种问题。醒醒吧!你的真诚/感激之情真有可能是喂了狗(狗子对不起,不应该拿你做对比),你带着真诚的心态去问你所不了解的各种问题,他们只会用高薪,轻松等话术迷惑你。因为他们招到一个人,拿一个人的提成,你们只是他们的赚钱工具,才不会管你这个工作是否真高薪,真轻松,如果是为啥他们自己不上。

这种第三方公司做事效率是非常快,一天办理几百人的入职流程,为了就是让你们入职,他们有提成。如果等到第二天你们反应过来,后悔了,那么他们就没钱可赚了。效率快是好,但是整套流程下来,你们有没有发现,连午饭时间都不给人了(就怕你们溜走)。是的,我当时就是没吃午饭,硬撑着走完了一天。而且在后续我与同行的打工人交谈过程中了解到,有些人早饭都没吃,也就是一点都行没吃,来来回回折腾一整天,直到晚上才有时间去吃饭,而这个时候就不怕你走了,因为你的入职流程已经办完了,该交的钱也交了。

以上套路只是我经历过得一种,但肯定不止这一种,希望大家能提前辨别!

接着,我来说说这一天,我与同行人交谈过程中了解到的各种人生经历:

虽然我是程序员,但绝不是那种沉闷不爱社交的性格,相反我还是挺爱和人交谈的,爱听别人的故事也爱和别人分享我的故事。

我碰到的第一种人就是未满 18 岁的小孩,故事开始的时候我提过,前期我是被人带到一个大厅集合的,之后,未满 18 岁的小孩就被所谓的工作人员给筛选出来了。其实说是筛选出来,肯定也不是让他们走,只是安排他们去那种更偏僻、工资更低一点的、对年龄要求不严格的小厂工作。反正,只要有人头进入工厂,他们这种中介就有钱赚,只是从你身上赚多赚少而已。

看着这几个未满 18 岁就出来打工的小孩,内心多少都是有点不是滋味。毕竟这个时候他们应该是呆在校园,且正处于高中,正是读书改变命运的关键时刻。但,我想这种再次读书的机会,应该是不会在这几个小孩身上出现了。因为我没有机会和这几个小孩说话,所以不知道他们是什么原因走出校园,走出家庭,而选择步入社会。但是,希望他们几个往后能一帆风顺,改天换命。

第二种人,我也没有机会和他说话,我关注到他是因为一段对话:

招人的负责人说:看你年龄比较偏大,都 45 了?

务工大哥:是,确实大了点。

招人的负责人说:像你这种情况,年龄比较大,我可以招你进去,但是工厂要不要,就不能保证了,这个你要明白。

务工大哥:行,就过去试试吧!

招人的负责人说:好,等会跟着一起过去。

其实从这段对话中我们也能很快想到,工厂肯定是不想要那种年龄偏大的员工了,毕竟工作强度还是有的,万一出现什么身体情况,咋搞。而务工大哥,肯定也是不想失去这种工作机会嘛,毕竟对于他而言,能有高工资就行,其他的应该都是次要的。

看着这务工大哥珍惜工作机会的态度,我瞬间就醍醐灌顶。内心不禁在想,是什么样的生活压力或者其他原因,才能让这务工大哥情愿跟着一群小年轻这样奔波找工作。

因为没有机会和这位务工大哥搭上话,所以我也就内心想想罢了。

第三种人,我终于是和他们搭上话了,他们都很年轻。在和他们简单的交流过后,我了解到的情况大致如下:

年龄基本都是18、19岁左右,并且学历都不高,有初中、高中、中专等学历的。还有些是中途辍学,并没有完整的读完学历就出来工作的,而且我问他们不读书的原因,他们的回答基本都是自己不想读了,想出来找工作,有钱花。所以现在读书,倒不是家里穷读不起,而是很大一部分人,受不了社会上的一下诱惑,想要快点赚钱,好有能力买各种喜欢的东西,包括打游戏买装备(因为和他们聊天中,都是游戏不离手的状态)。

像他们这种,吃不了学习上的苦,就只能吃生活上的苦。以后或许他们还有选择的机会,毕竟还年轻,但是,我想他们应该在未来很长一段时间,都是游离在各种电子厂。因为感觉他们还是玩心重,而电子厂这种工作,多白了就是工作轻松,多劳多得,还包吃包住。等到某一时刻他们过腻了这种日子,应该能重新审视一下自己,未来的路,希望他们能早点醒悟。

第四种人,你可以理解他们是第三种人的升级版,就是他们一直没有醒悟,一直都是从事着电子厂这种工作。只是从厂 A,来到了他们认为工资,工作各方面还不错的厂 B 而已。你可以在他们身上看到全国各地奔波的痕迹:蛇皮袋、黑行李箱、竹席被褥、胡子拉碴、头发凌乱、烟味很重。我提到的这些,只是单纯的用文字写出来,而没有任何的其他意思。在和他们交谈的过程中,能很明显的看出一种无奈的心情出来,他们无非就是想要一个稳定,有点收入的工作。但是工厂就是这样,忙的时候招人,不忙的时候拼命的赶人。他们也不想走的,但工厂不忙,肯定是不发工资的,你这样呆在工厂没有任何的意义,所以只能又拿起行囊四处奔波。

所以,生活就是充满着未知的变化,我们能做的就是不断的提升自己,来面对这种变化。希望能看到这里的人,都不被生活所压迫,加油吧!

还有最后一种人,也是我最想不到的一种人。他们在没有进厂之前,手底下有十多号人,一个月光租金就可以花出去五六万。是的,我们暂且可以把他们定为老板被迫进厂的一类人。

说他们是老板,一点都没错。因为他们在没有进厂之前,确实是自己开店做老板的,只是因为 21 年疫情的时候,大家都闭门不出,实体经济很难再维持这种高房租的压力,所以才被迫关店,倒闭。我那时问他们,为什么不多坚持坚持,你看现在不就春暖花开了吗?他们只是面带苦涩的说,多坚持一个月,就是五六万的流水出去,而且那会不知道什么时候是个头。并且他们不是没坚持,而是已经没有钱再可以坚持下去了,所以只能现倒闭,让自己缓一缓,释放一下压力。

我也确实从他们身上感受到了他们所说的压力,只出不进,五六万的流水,这谁能受得了。并且疫情过去的时候,也想过重新再来,但是已经没有钱可以再来了,那会欠的钱还没还清,已经没有资金重头再来了。

这一天的交谈中,我见过两个这种人,以前都是老板,也都是因为疫情原因,赔钱了,只能进厂缓一缓,让自己好受一点。但是他们毕竟是见过一些世面的人,你可以很明显的感受到他们和前面这几种人不一样,如装扮、说话和回答问题方式,并且他们也都很乐观,还说,等我在工厂缓过来之后,还是会继续再拼搏一番。你看看,这不就非常好嘛!在人生的这条道路上,如果你跌倒,说明你选择的是一条坎坷的路,只要你能站起来,继续走下去,我相信你的收获绝对能对得起你这一路的坎坷。

好了,洋洋洒洒五千多字,算是我对那天经历的一个交代。如果你看到这里,内心也有一些感触,说明你是一个感性的人,欢迎你在评论区留下你的足迹。

题外话:主人公 A 最后还是没去成!🤣🤣🤣

本文转载自: 掘金

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

前端太卷了,不玩了,写写nodejs全栈涨工资,赶紧学起来

发表于 2024-03-06

如果你感觉到累了,卷不动了,那就来看这篇文章吧,写写全栈,涨涨工资,吹吹牛皮!人生得意须尽欢,不要只想忙搬砖!

首先聊下node.js的优缺点和应用场景

Node.js的优点和应用场景

Node.js作为后端开发的选择具有许多优点,以下是其中一些:

  1. 高性能: Node.js采用了事件驱动、非阻塞I/O模型,使得它能够处理大量并发请求而不会阻塞线程,从而具有出色的性能表现。
  2. 轻量级和高效: Node.js的设计简洁而轻量,启动速度快,内存占用低,适合构建轻量级、高效的应用程序。
  3. JavaScript全栈: 使用Node.js,开发者可以使用同一种语言(JavaScript)进行前后端开发,简化了开发人员的学习成本和代码维护成本。
  4. 丰富的生态系统: Node.js拥有丰富的第三方模块和库,可以轻松集成各种功能和服务,提高开发效率。
  5. 可扩展性: Node.js具有良好的可扩展性,可以通过添加更多的服务器实例来横向扩展应用程序,满足不断增长的用户需求。
  6. 实时应用: 由于Node.js对于事件驱动和非阻塞I/O的支持,它非常适合构建实时应用,如即时通讯、在线游戏、实时分析等。
  7. 微服务架构: Node.js可以作为微服务架构中的一个服务组件,通过轻量级的设计和快速的响应能力,使得微服务之间的通信更加高效。
  8. 数据流处理: Node.js适合处理大量的数据流,例如文件操作、网络流量分析等,可以通过流式处理来有效地管理和处理数据。

应用场景包括但不限于:

  1. Web应用程序: 开发基于Node.js的Web应用程序,如社交网络、电子商务平台、博客、内容管理系统等。
  2. API服务: 使用Node.js构建RESTful API服务,为移动应用、前端应用提供数据接口。
  3. 实时应用: 构建实时应用程序,如聊天应用、在线游戏、实时地图等,利用Node.js的事件驱动和非阻塞I/O模型实现高效的实时通讯。
  4. 数据流处理: 使用Node.js处理大量的数据流,例如日志处理、实时监控、数据分析等。
  5. 微服务架构: 将Node.js作为微服务架构中的一个服务组件,通过轻量级的设计和快速的响应能力,实现服务之间的高效通信。

总的来说,Node.js作为一种高性能、轻量级的后端开发工具,适用于各种类型的应用场景,尤其在需要处理大量并发请求和实时通讯的应用中表现突出。

node.js的局限性

尽管Node.js在许多方面都表现出色,但它也有一些局限性和适用场景的限制。以下是一些Node.js的局限性:

  1. 单线程阻塞: 虽然Node.js采用了非阻塞I/O的模型,但在处理CPU密集型任务时,单线程的特性可能导致性能瓶颈。由于Node.js是单线程的,处理阻塞操作(如大量计算或长时间的同步操作)会影响整个应用程序的响应性。
  2. 回调地狱(Callback Hell): 在复杂的异步操作中,嵌套的回调函数可能导致代码难以理解和维护,这被称为“回调地狱”问题。虽然可以使用Promise、async/await等来缓解这个问题,但在某些情况下仍可能存在。
  3. 相对较小的标准库: Node.js的标准库相对较小,相比于其他后端语言,需要依赖第三方模块来实现一些常见的功能。这可能需要花费额外的时间来选择、学习和整合这些模块。
  4. 不适合密集型计算: 由于Node.js是单线程的,不适合用于处理大量的计算密集型任务。如果应用程序主要依赖于大量的数学计算或复杂的数据处理,其他多线程的语言可能更合适。
  5. Callback错误处理: 在回调模式下,错误处理可能变得繁琐,需要在每个回调中检查错误对象。这使得开发者需要更加小心地处理错误,以确保它们不会被忽略。
  6. 相对较新的技术栈: 相较于一些传统的后端技术栈,Node.js是相对较新的技术,一些企业可能仍然更倾向于使用更成熟的技术。
  7. 不适合长连接: 对于长连接的应用场景,如传统的即时通讯(IM)系统,Node.js的单线程模型可能不是最佳选择,因为它会导致长时间占用一个线程。

尽管有这些局限性,但Node.js在许多应用场景下仍然是一个强大且高效的工具。选择使用Node.js还是其他后端技术应该根据具体项目的需求、团队的技术栈和开发者的经验来做出。

node.js常用的几种主流框架

Node.js是一个非常灵活的JavaScript运行时环境,它可以用于构建各种类型的应用程序,从简单的命令行工具到大型的网络应用程序。以下是一些常用的Node.js框架:

  1. Express.js:Express.js是Node.js最流行的Web应用程序框架之一,它提供了一组强大的功能,使得构建Web应用变得更加简单和快速。Express.js具有路由、中间件、模板引擎等功能,可以满足大多数Web应用的需求。
  2. Koa.js:Koa.js是由Express.js原班人马打造的下一代Node.js Web框架,它使用了ES6的新特性,如async/await,使得编写异步代码更加简洁。Koa.js更加轻量级和灵活,它提供了更强大的中间件功能,可以更方便地实现定制化的功能。
  3. Nest.js:Nest.js是一个用于构建高效、可扩展的服务器端应用程序的渐进式Node.js框架。它基于Express.js,但引入了许多现代化的概念,如依赖注入、模块化、类型检查等,使得构建复杂应用变得更加简单。
  4. Hapi.js:Hapi.js是一个专注于提供配置简单、可测试性强的Web服务器框架。它提供了一系列的插件,可以轻松地扩展其功能,同时具有强大的路由、验证、缓存等功能,适用于构建大型和高可靠性的Web应用程序。
  5. Meteor.js:Meteor.js是一个全栈JavaScript框架,它可以同时构建客户端和服务器端的应用程序。Meteor.js提供了一整套的工具和库,包括数据库访问、实时数据同步、用户认证等功能,使得构建实时Web应用变得更加简单和快速。
  6. Sails.js:Sails.js是一个基于Express.js的MVC框架,它提供了类似于Ruby on Rails的开发体验,使得构建数据驱动的Web应用变得更加简单。Sails.js具有自动生成API、蓝图路由、数据关联等功能,适用于构建RESTful API和实时Web应用。

Express框架:实践与技术探索

1. Express框架简介:

Express是一个轻量级且灵活的Node.js Web应用程序框架,它提供了一组简洁而强大的工具,帮助开发者快速构建Web应用。Express的核心理念是中间件,通过中间件可以处理HTTP请求、响应以及应用程序的逻辑。


2. 基础搭建与路由:

在开始实践之前,首先需要搭建Express应用程序的基础结构。通过使用express-generator工具或手动创建package.json和app.js文件,可以快速启动一个Express项目。接下来,我们将学习如何定义路由以及如何处理HTTP请求和响应。

1
2
3
4
5
6
7
8
9
10
javascript复制代码const express = require('express');
const app = express();

app.get('/', (req, res) => {
res.send('Hello World!');
});

app.listen(3000, () => {
console.log('Express app listening on port 3000');
});

3. 中间件:

Express中间件是一个函数,它可以访问请求对象(req)、响应对象(res)以及应用程序的下一个中间件函数(通常命名为next)。中间件函数可以用来执行任何代码,修改请求和响应对象,以及终止请求-响应周期。

1
2
3
4
javascript复制代码app.use((req, res, next) => {
console.log('Time:', Date.now());
next();
});

4. 模板引擎与视图:

Express框架允许使用各种模板引擎来生成动态HTML内容。常用的模板引擎包括EJS、Pug和Handlebars。通过配置模板引擎,可以将动态数据嵌入到静态模板中,以生成最终的HTML页面。

1
javascript复制代码app.set('view engine', 'ejs');

5. 数据库集成与ORM:

在实际应用中,数据库是不可或缺的一部分。Express框架与各种数据库集成良好,可以通过ORM(对象关系映射)工具来简化数据库操作。常用的ORM工具包括Sequelize、Mongoose等,它们可以帮助开发者更轻松地进行数据模型定义、查询和操作。


6. RESTful API设计与实现:

Express框架非常适合构建RESTful API。通过定义不同的HTTP动词和路由,可以实现资源的创建、读取、更新和删除操作。此外,Express还提供了一系列中间件来处理请求体、响应格式等,使得构建API变得更加简单。

1
2
3
4
5
6
7
javascript复制代码app.get('/api/users', (req, res) => {
// 获取所有用户信息
});

app.post('/api/users', (req, res) => {
// 创建新用户
});

7. 实践案例:

为了更好地理解Express框架的实践,我们将以一个简单的博客应用为例。在这个应用中,我们可以拓展一下用户的注册、登录、文章的创建和展示等功能,并且结合数据库和RESTful API设计。在这个示例中,我们将使用MongoDB作为数据库,并使用Mongoose作为MongoDB的对象建模工具。首先,确保您已经安装了Node.js``和MongoDB,并创建了一个名为blogApp的文件夹来存放我们的项目。

  1. 首先,在项目文件夹中初始化npm,并安装Express、Mongoose和body-parser依赖:
1
2
bash复制代码npm init -y
npm install express mongoose body-parser
  1. 在项目文件夹中创建app.js文件,并编写以下代码:
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
javascript复制代码// 导入所需的模块
const express = require('express');
const bodyParser = require('body-parser');
const mongoose = require('mongoose');

// 连接MongoDB数据库
mongoose.connect('mongodb://localhost:27017/blog', { useNewUrlParser: true, useUnifiedTopology: true });
const db = mongoose.connection;

// 检测数据库连接状态
db.on('error', console.error.bind(console, 'MongoDB connection error:'));
db.once('open', function() {
console.log('Connected to MongoDB');
});

// 创建Express应用
const app = express();

// 使用body-parser中间件解析请求体
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

// 定义用户模型
const User = mongoose.model('User', new mongoose.Schema({
username: String,
password: String
}));

// 注册用户
app.post('/api/register', async (req, res) => {
try {
const { username, password } = req.body;
const user = await User.create({ username, password });
res.json({ success: true, message: 'User registered successfully', user });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});

// 用户登录
app.post('/api/login', async (req, res) => {
try {
const { username, password } = req.body;
const user = await User.findOne({ username, password });
if (user) {
res.json({ success: true, message: 'User logged in successfully', user });
} else {
res.status(401).json({ success: false, message: 'Invalid username or password' });
}
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});

// 启动Express服务器
const port = 3000;
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});

以上代码实现了用户注册和登录的功能,使用了MongoDB作为数据库存储用户信息,并提供了RESTful风格的API接口。

您可以通过以下命令启动服务器:

1
bash复制代码node app.js
  1. 接下来,我们添加文章模型和相关的路由来实现文章的创建和展示功能。在app.js文件中添加以下代码:
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
javascript复制代码// 定义文章模型
const Article = mongoose.model('Article', new mongoose.Schema({
title: String,
content: String,
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
}
}));

// 创建文章
app.post('/api/articles', async (req, res) => {
try {
const { title, content, author } = req.body;
const article = await Article.create({ title, content, author });
res.json({ success: true, message: 'Article created successfully', article });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});

// 获取所有文章
app.get('/api/articles', async (req, res) => {
try {
const articles = await Article.find().populate('author', 'username');
res.json({ success: true, articles });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});

以上代码实现了创建文章和获取所有文章的功能,每篇文章都与特定的作者相关联。

现在,您可以使用POST请求来创建新的用户和文章,使用GET请求来获取所有文章。例如:

  • 注册新用户:发送POST请求到/api/register,传递username和password字段。
  • 用户登录:发送POST请求到/api/login,传递username和password字段。
  • 创建新文章:发送POST请求到/api/articles,传递title、content和author字段(注意,author字段应该是已注册用户的ID)。
  • 获取所有文章:发送GET请求到/api/articles。

这个示例演示了如何使用Express框架结合MongoDB实现一个简单的博客应用,并提供了RESTful API接口。可以根据需求扩展和定制这个应用,例如添加用户身份验证、文章编辑和删除功能等。

看完后是不是觉得后端(CRUD)很简单,没错!就是这么简单!喜欢的小伙伴给个点赞加收藏,码字不易!

本文转载自: 掘金

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

在Jetpack Compose中使用useRequest轻

发表于 2024-03-06

写在前面

本文中提及的use开头的函数,都出自与我的 ComposeHooks 项目,它提供了一系列 React Hooks 风格的状态封装函数,可以帮你更好的使用 Compose,无需关系复杂的状态管理,专心于业务与UI组件。

这是系列文章的第已篇,全部文章:

  • 在Compose中使用useRequest轻松管理网络请求
  • 在Compose中使用状态提升?我提升个P…Provider
  • 在Compose中父组件如何调用子组件的函数?
  • 在Compose中方便的使用MVI思想?试试useReducer!

还记得: 使用 ahooks 中的 useRequest 轻松管理React中的网络请求 这篇文章么?

现在我将 ahooks 带到了 Compose 开发中!

在Compose项目中,你是如何进行网络请求,管理状态的?ViewModel?还是 LaunchedEffect 配合 State?

他们看起来都不够优雅,我们真的需要在 Compose 中创建这么多VM么?

还有更优雅的方式么?

来了解一下 junerver/ComposeHooks 吧,也许它会让你在 Compose 开发中更加得心应手,更多的专注于组件而非复杂的状态管理。

来试试 useRequest 吧

一句话发起请求:

1
kotlin复制代码val (data, loading, error, request) = useRequest(::fn.asSuspendNoopFn())

useRequest 的第一个参数是一个 suspend 函数,在组件初次加载时,会自动触发该函数执行。同时自动管理该函数的 loading , data , error 等状态。

手动触发/自动触发

useRequest 的第二个参数是配置选项,我们可以使用optionsOf轻松的自定义请求的相关配置,例如设置手动触发:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
kotlin复制代码// 需要导入 invoke函数
import xyz.junerver.compose.hooks.invoke

// 这是一个模拟网络请求的suspend函数
suspend fun fn(s1: String, s2: String): String {
delay(2_000)
return "response: result success+${Random.nextInt(20)}"
}

@Composable
fun TestRequestComponent() {
val (data, loading, error, request) = useRequest(
requestFn = ::fn.asSuspendNoopFn(), //普通的挂起函数需要转换成钩子的参数
optionsOf {
manual = true
})
Button(onClick = { request("1", "2") }) {
Text(text = "手动请求")
}
}

此时我们的请求不再自动执行,当需要发起请求时直接调用 request,并传递参数。

通过对 useRequest 返回值解构声明或得到的 request 函数是一个普通函数,我们可以直接在组件中使用,无需关心协程作用域的问题。

manual 参数的默认值是false,如果你需要自动请求,那么直接忽视即可。

错误重试

当我们的网络请求出错时,一般都需要进行错误重试的工作,在 ComposeHooks 中这个操作变得非常简单。

对上面的配置增加一行代码,配置最大重试次数:retryCount = 3,你的请求函数会在出错后自动重试。

1
2
3
4
5
kotlin复制代码val (data, loading, error, request) = useRequest(
requestFn = ::fn.asSuspendNoopFn(),
optionsOf {
retryCount = 3
})

轮询

同样只需要添加一行代码:pollingInterval = 2.seconds,现在你的请求将会每隔两秒自动发出

1
2
3
4
5
kotlin复制代码val (data, loading, error, request) = useRequest(
requestFn = ::fn.asSuspendNoopFn(),
optionsOf {
pollingInterval = 2.seconds
})

防抖、节流

只需要简单的配置 debounceOptions 与 throttleOptions 参数,即可为 request 函数增加防抖或节流的效果,非常简单方便。

1
2
3
4
5
6
7
8
kotlin复制代码val (data, loading, error, request) = useRequest(
requestFn = ::fn.asSuspendNoopFn(),
optionsOf {
manual = true
debounceOptions = optionsOf {
wait = 2.seconds //两秒防抖间隔
}
})

探索更多

项目开源地址:junerver/ComposeHooks

MavenCentral:hooks

1
kotlin复制代码implementation("xyz.junerver.compose:hooks:1.0.11")

欢迎使用、勘误、pr。

本文转载自: 掘金

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

程序员副业 一种前后端分离的赚钱模式

发表于 2024-03-06

必看大字最新消息重磅公众号首图(2) (1).jpg

本文首发于公众号:嘟爷创业日记 。 我已经坚持日更130天+,欢迎过来追剧~

今天认识了一些人,发现很多人赚钱的模式是只做前端,后端交给专业的人合作就行,赚自己擅长的钱,但是他们选择的是高客单价的产品,比如出国、移民、境外旅游,这些普通人接触不到,如果你能找到合适靠谱的合作伙伴,你专心去负责引流就行,成交了自然有佣金,这种大都是上千的佣金,一天赚1单那也挺舒服的。

这里的前后端不是指开发的前后端,而是负责做内容引流的算前端,负责交付变现加售后的算后端。

那你要做的无非就是提高内容质量,想要提高收入:

1:做矩阵,发的号多引流的量就大一些,把矩阵自动化,越省时间越好,内容一般发在小红书居多。

2:收徒,带人做这块,收徒费可以低一些,可以从每笔成交里面抽几个点即可。

这种生意模式其实很多大佬在弄,就赚两部分钱:

1:一是赚你学习的钱,这个一般不会很高。

2:二是赚钱长期成交的钱,你的后端一般是他们提供的,他们会从你每一笔成交里面赚一些钱,这样就形成了闭环,互惠互利,他们也愿意真心去教你,因为你赚的多,对方也赚的多。

所谓的搭台子就是这样,把生态搞起来,当然这个已经是后期玩法了,每个新人刚开始的时候大部分都是自己弄的。
我最近打算开重启一个撸毛的业务啦,手机买了几台在路上了,先弄5个号测试可行性,做项目特别是各大平台上的,需要多弄一些号备着,封号也要算在成本预算里面。

这个月开始生活规律要调整下了,打算以后晚上10点后就不接待客户了,最近经常晚上12点了,还有客户来咨询,之前怕不回信息这笔钱就赚不到,现在想想得不偿失,我以前微信昵称会带上【晚10点睡】这几个字,主要就是给用户看的,让他们知道我晚上10点后就睡觉了,不要再打扰我了,但是很多用户是没有边界感的,认为我付了款你就应该24小时随叫随到,这种人不要再做他们第二次生意哈哈。

我年初设定的今年计划是早上6点-12点,下午2-5点,这样一天就有9小时的工作时间,晚上就自由安排,看书混群出门散步都行,设想很美好,但是我执行起来一次都没成功,因为晚睡,所以无法早起。为这事我自责了自己好久,要想想办法早起看看,不然一直目前这种状态是不健康的。在家待了几个月都胖了几斤了。

本文转载自: 掘金

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

Flutter&Flame游戏实践#02 Trex-物理

发表于 2024-03-06

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!


Flutter&Flame 游戏开发系列前言:

该系列是 [张风捷特烈] 的 Flame 游戏开发教程。Flutter 作为 全平台 的 原生级 渲染框架,兼具 全端 跨平台和高性能的特点。目前官方对休闲游戏的宣传越来越多,以 Flame 游戏引擎为基础,Flutter 有游戏方向发展的前景。本系列教程旨在让更多的开发者了解 Flutter 游戏开发。

第一季:30 篇短文章,快速了解 Flame 基础。[已完结]

第二季:从休闲游戏实践,进阶 Flutter&Flame 游戏开发。

两季知识是独立存在的,第二季 不需要 第一季作为基础。本系列教程源码地址在 【toly1994328/toly_game】,系列文章列表可在《文章总集》 或 【github 项目首页】 查看。


一、认识物理运动

上一篇我们已经将图片资源展示到屏幕上,本篇将了解一下游戏中 帧的持续渲染 而产生动画效果。现在先抛开 trex , 来研究一下如何完成对物体运动的模拟。从中即可以掌握一些 Flame 的基础知识,同时这将有助于我们后续功能的实现。

物理运动是通过在编程中模拟物理学规律,让角色产生的运动效果:

一维 二维
228.gif 231.gif

1. 地板自定义绘制: GroundComponent

Flame 中 Component 的 render 回调 方法,其中可以回调 Canvas 对象,我们可以操作 Canvas 进行绘制,比如这里在 GroundComponent 中,通过路径 Path 操作,绘制一个带有斜线的地面:

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
dart复制代码---->[lib/world/01/heroes/ground_component.dart]----
class GroundComponent extends PositionComponent {
final double groundHeight = 12;

@override
void onGameResize(Vector2 size) {
super.onGameResize(size);
width = size.x;
y = size.y - groundHeight - 60;
}

@override
void render(Canvas canvas) {
super.render(canvas);
Paint paint = Paint()..style = PaintingStyle.stroke;
Path path = Path()..lineTo(width, 0);
double step = 15;
for (double i = 0; i < width; i += step) {
path
..moveTo(step + i, 0)
..relativeLineTo(-step, groundHeight);
}
canvas.drawPath(path, paint);
}
}

2. 放置物体:BoxComponent

这个方块通过 BoxComponent 构件维护,如下定义了一些描述尺寸和坐标的变量,将它放在地面上方。方块的绘制非常简单,通过 canvas.drawRect 一个矩形即可:

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
dart复制代码---->[lib/world/01/heroes/box_component.dart]----
class BoxComponent extends PositionComponent {
final double initX = 60; // 初始位置 x
final double initY = 60; // 初始位置 y
final double groundHeight = 12; // 地面高度
final Vector2 boxSize = Vector2(40, 40); // 方块尺寸

@override
void onGameResize(Vector2 size) {
super.onGameResize(size);
x = initX;
y = size.y - groundHeight - initY - boxSize.y;
}

Paint paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 2
..color = const Color(0xff133C9A);

@override
void render(Canvas canvas) {
super.render(canvas);
canvas.drawRect(Rect.fromLTRB(0, 0, boxSize.x, boxSize.y), paint);
}
}

3. 物理量的定义

在现实世界中,具有 速度 的物体随着 时间 流逝,不断产生 位移。物体匀速运动过程中,物理量有:

image.png

名称 变量 单位 介绍
初始位置 initX 逻辑像素 px 方块原始位置
位移 sX 逻辑像素 px 方块相比于原始位置的水平
速度 vX px/s 水平方向速度的大小
时间 t s 物体运动的时间
每帧偏移量 dX 逻辑像素 px 每帧时间内的偏移量

物体的位置 x 随时间的变化将满足:

1
2
3
dart复制代码dX = vX * t;
sX += dX;
x = initX + sX;

二、模拟匀速运动

游戏中,Flame 游戏引擎会一直进行渲染界面,即使界面是静止的,也会不停刷新,这称之为 游戏循环 GameLoop 。 一般的游戏引擎都是这种性质,因为游戏一般都是持续的界面交互,这种不停更新是合理的:

其优势在于数据变化后,不用考虑如何通知界面更新。因为会不断地自动触发更新渲染界面。

在 Flame 中,可以通过 update 回调来监听每帧触发的时机。其中 dt 是两帧之间的时间间隔,单位是秒(s)。 Flame 的游戏循环本质上是通过 Flutter 中的 Ticker ,所以一般来说每帧间的时间间隔是 16 ms 左右。这样游戏的每秒帧率可以达到 60fps 。

image.png


level 1 :匀速运动

想让方块水平移动,就是要在每帧之间让 位移 sx 根据 速度 vX 进行变化,然后将物体的 x 坐标 赋值为初始位置加上位移。这个逻辑在 update 回调方法中进行处理:

225.gif

1
2
3
4
5
6
7
8
9
10
11
dart复制代码---->[lib/world/02/heroes/box_component.dart]----
double vX = 100; // 水平速度
double sX = 0; // 水平总位移

@override
void update(double dt) {
super.update(dt);
double ds = vX * dt;
sX += ds;
x = initX + sX;
}

level 2 :运动范围的限定

上面的小块虽然可以运动,但由于速度会一直增加位移,导致方块滑到非常远的地方。现在想让给移动 增加边界,当运动到指定位置就停下。如下所示,背景在右侧 700 + 40 像素的位置画个墙:

226.gif

想在边界处停下,本质上就是让位移无法再增加,在 update 中添加处理逻辑:只需要在位移大于额定位移时,位移 sX 保持不变即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
dart复制代码---->[lib/world/03/heroes/box_component.dart]----
final double maxWallX = 700;

@override
void update(double dt) {
super.update(dt);
double ds = vX * dt;
sX += ds;
if (sX > maxWallX - initX) {
sX = maxWallX - initX;
}
x = initX + sX;
}

void run() {
vX = 100;
}

此时案例中碰到墙就无法恢复了,可以做个小优化。提供 run 方法,当点击屏幕时,重置位移并设置速度。这样就可以自己控制物体运动的时机。在游戏主类中的 onTapDown 回调中触发方法:

image.png

1
2
3
4
5
dart复制代码---->[lib/world/03/heroes/box_component.dart]----
void run() {
sX = 0;
vX = 100;
}

level 3 :速度的反弹

为了更好地理解 速度 和 位移,这里继续添加新的小功能。在起始位置也放置一个墙,让方块在达到左右边界时反向运动。这样就可以让其在边界中永远运动:

227.gif

处理逻辑也很简单,比如达到最大位移时, 将速度置为反向。这就表示在之后每帧之间的位移增量将是 负数。总偏移量 sX 累加时就会越来越小。所以 x 坐标变小,在界面上的效果就是向左运动。达到左边界时同理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
dart复制代码----[lib/world/04/heroes/box_component.dart]----
@override
void update(double dt) {
super.update(dt);
double ds = vX * dt;
sX += ds;
// 达到最大位移
if (sX > maxWallX - initX) {
sX = maxWallX - initX;
vX = -vX;
}
// 达到最小位移
if (sX < 0) {
sX = 0;
vX = -vX;
}
x = initX + sX;
}

三、模拟匀变速运动

在现实世界中,物体在力的作用下产生 加速度 ,使 速度 随着 时间 增加。 更快的速度会使在相同的时间内产生更大的 位移。 下图的运动中,速度每秒会增加 100 px/s 。在打点计时器的表现中,每 0.2 s 记录一下物体中心点的坐标,进行渲染。可以很明显地看出:

随着速度的增加,在同时间间隔内位移会增加

228.gif


1. 加速度的模拟

加速度的模拟代码实现起来是很简单的,只要定义一个 aX 变量表示水平方向的加速度,在每帧计算时通过 vX += aX * dt 对速度进行该变即可。

228.gif

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
dart复制代码----[lib/world/05/heroes/box_component.dart]----
// 略同...
double aX = 0; // 水平加速度
final double maxWallX = 700;

void run() {
sX = 0;
aX = 100;
vX = 150;
}

@override
void update(double dt) {
super.update(dt);
vX += aX * dt;
double ds = vX * dt;
// 略同...

2. 打点计时器

一个自由下落的物理,我们很难仅靠脑子想像,来定量地描述其中的物理学规律。所以需要额外的手段来 记录 物体运动过程详细的物理量变化。

相信大家对中学物理中的 打点计时器 都不陌生,它可以以固定的频率震动,在一条运动的纸带上留下墨点。这样就可以根据墨点位置计算唯一,推算运动学规律。

image.png

这是我非常喜欢的一个物理实验,它展现出人类记录自然、探索自然的智慧。这里简单实现一个打点计时器,它可以更直观地帮助我们理解运动过程。而不是仅限于表象: “哦,它动了” 。

打点计时器通过类 TickerTap 完成记录点的工作,其中 frequency 表示每秒打点的频率;_points 表示收录的墨点。定义一个 onTick 方法,记录时间以及校验是否达到间隔秒数;达到的话,收录到点集中:

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
dart复制代码---->[lib/world/06/heroes/dot_component.dart]----
class TickerTap {
/// 打点计数器的频率: 1s 打多少个点
double frequency = 0;

TickerTap({this.frequency = 5});

final List<Offset> _points = [];

List<Offset> get points => _points;

void clear() {
_points.clear();
}

double _totalTime = 0;
double _snapshot = 0;

void onTick(Offset point, double dt) {
_totalTime += dt;
if ((_totalTime - _snapshot) > 1 / frequency) {
_snapshot = _totalTime;
_points.add(point);
}
}
}

然后定义一个 DotComponent 构建负责渲染 TickerTap 记录的点集;在 render 回调方法中遍历绘制小圆:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
dart复制代码---->[lib/world/06/heroes/dot_component.dart]----
class DotComponent extends PositionComponent {
final TickerTap dotRecord;

DotComponent(this.dotRecord);

@override
void onGameResize(Vector2 size) {
y = size.y - 111;
super.onGameResize(size);
}

Paint paint = Paint()
..style = PaintingStyle.fill
..color = const Color(0xff133C9A);

@override
void render(Canvas canvas) {
for (Offset points in dotRecord.points) {
canvas.drawCircle(points, 4, paint);
}
}
}

物体的运动发生在 BoxComponent 中,打点计时器也需要在该类中展开工作。通过构造函数传入打点计时器,在 update 回调中触发 onTick 方法,通知打点计时器物体位置的变化。另外在左右边缘碰撞时,将点置空,避免点集的无限累积。

这样 BoxComponent 作为数据的生产者,DotComponent 作为数据的消费者,TickerTap 作为数据的维护者。这三个角色就为我们描绘出了如下的画画:

228.gif

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
dart复制代码---->[lib/world/06/heroes/box_component.dart]----
class BoxComponent extends PositionComponent {
final TickerTap dotRecord;
BoxComponent({required this.dotRecord});
// 略同...

@override
void update(double dt) {
// 略同...
if (sX > maxWallX - initX) {
sX = maxWallX - initX;
vX = -vX;
dotRecord.clear();
}
// 达到最小位移
if (sX < 0) {
sX = 0;
vX = -vX;
dotRecord.clear();
}
x = initX + sX;
dotRecord.onTick(Offset(x+boxSize.x/2 ,0+boxSize.y/2), dt);
}

三、二维的物理运动

上面只是对物体在水平方向上的一维运动模拟。当我们加入竖直方向上的速度和位移量之后,就可以让物体在竖直方向上产生运动效果。一维运动通过一条线段的两端限制移动区域; 那么二维运动需要一块 矩形面 限制物体运动区域。如下所示,在顶部建一面墙:

image.png


1. 竖直方向运动参数

如下,定义竖直 Y 方向上的总位移 sY 、竖直速度 vY、竖直加速度 aY 。竖直方向最大高度 maxWallY。

tips: 屏幕坐标系向上为负方向, 这里 maxWallY = -200 ;表示在上方 200 逻辑像素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
dart复制代码---->[lib/world/07/heroes/box_component.dart]----
double vX = 0; // 水平速度
double vY = 0; // 竖直速度
double sX = 0; // 水平总位移
double sY = 0; // 竖直总位移
double aX = 0; // 水平加速度
double aY = 0; // 竖直加速度

final double maxWallY = -200; // 竖直方向最大高度

void run() {
sX = 0;
aX = 100;
vX = 150;
vY = -60;
}

启动时为 vY 赋值为 -60 ,说明物体向上运动,每秒在竖直方向前进 60 逻辑像素;在 update 每帧回调时,处理到达边界的反向运动:

230.gif

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
dart复制代码@override
void update(double dt) {
super.update(dt);
vX += aX * dt;
vY += aY * dt;
sX += vX * dt;
sY += vY * dt;
// 达到最大位移
if (sX > maxWallX - initX) {
sX = maxWallX - initX;
vX = -vX;
}
// 达到最小位移
if (sX < 0) {
sX = 0;
vX = -vX;
}
// 达到顶部高度
if (sY < maxWallY + boxSize.y) {
sY = maxWallY + boxSize.y;
vY = -vY;
}
// 达到底部高度
if (sY > 0) {
sY = 0;
vY = -vY;
dotRecord.clear();
}
x = initX + sX;
y = initY + sY;
if (vX != 0 && vY != 0) {
dotRecord.onTick(Offset(x + boxSize.x / 2, sY + boxSize.y / 2), dt);
}
}

4. 平抛运动:认识构件的组合

我们知道,构件加入到 Game 场景(蓝框)中时,默认位置在屏幕的 左上角 。计算摆放完地面后,摆放方块时还需要根据地面的高度再计算一下。这是比较麻烦的,特别是当地面上有非常多的构件时,想要改变地面高度,其他地方也要同步修改。

image.png

我们希望以地面高度的区域(红色)为参考,让物体默认放在 地面左上角 。这样地面的位置改变可以连带其内容一起变化。Flame 中的 Component 本身也是一个树形结构,可以容纳若干的 Component。比如这里在 GroundComponent 中添加 BoxComponent 和 DotComponent,那么方块和点集将会以上面红框左上角为原点:

image.png

此时方块的位置不用根据屏幕尺寸进行计算,将会方便很多。只要定义方块相对于地面左上角的初始偏移量 initX 和 initY 即可;另外 Component 构件也可以通过 TapCallbacks 响应点击事件,这样只有 点击方块区域 才会触发事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
dart复制代码--->[lib/world/08/heroes/box_component.dart]----
class BoxComponent extends PositionComponent with TapCallbacks{

final double initX = 60; // 初始位置 x
late double initY = -boxSize.y; // 初始位置 y
final double initSY = -200; // 初始 y 偏移量位置
final double groundHeight = 12; // 地面高度
final Vector2 boxSize = Vector2(40, 40); // 方块尺寸

@override
void onGameResize(Vector2 size) {
super.onGameResize(size);
width = boxSize.x;
height = boxSize.y;
sY = initSY;
}

@override
void onTapDown(TapDownEvent event) {
run();
}

现在要实现如下的平抛运动,有了上面的基础,实现起来就比较容易了:

[1]. 物体在开始时已经具有位移: 将 sY 在开始赋值为 -200。

[2]. 平抛是一个具有 水平向右初速度 和 竖直加速度 的运动。

231.gif

1
2
3
4
5
6
7
8
ini复制代码--->[lib/world/08/heroes/box_component.dart]----
void run() {
sY = initSY;
sX = 0;
vX = 300;
vY = 0;
aY = 100;
}

这里在到达地面时,希望停下而不是继续反弹,只需要将速度和加速度置为 0 即可。这样我们就实现了平抛运动,另外斜抛运动就是在开始时,竖直方向 vY 非零,大家可以自己试验一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
dart复制代码--->[lib/world/08/heroes/box_component.dart]----
@override
void update(double dt) {
super.update(dt);
vX += aX * dt;
vY += aY * dt;
sX += vX * dt;
sY += vY * dt;
// 达到底部高度
if (sY > 0) {
sY = 0;
vX = 0;
vY = 0;
aY = 0;
}
x = initX + sX;
y = initY + sY;
dotRecord.onTick(Offset(x + boxSize.x / 2, y + boxSize.y / 2), dt);
}

3. 竖直上抛运动与跳跃

最后书归正传,来看一下 竖直上抛运动 。 一个物体有向上的速度,再施加向下的加速,就能模拟出跳起到降落的动作,而且运动的过程符合物理世界的特点:速度越来越慢,直到反方向越来越快下坠:

234.gif

1
2
3
4
5
6
ini复制代码--->[lib/world/09/heroes/box_component.dart]----
void run() {
aY = 100;
sY = 0;
vY = -200;
}

我们可以通过这个竖直上抛运动来让小恐龙跳跃,效果如下:

235.gif

代码实现如下,这里小恐龙只有向上跳跃的需求,使用只定义 vY、aY、sY 即可。另外在跳跃时对小恐龙的状态进行切换,限制跳跃中不能再跳跃;只有 jumping 状态才需要进行位移,落地时将状态置为 running 。这样不同时期就可以展示不同状态的精灵:

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
dart复制代码---->[lib/trex/03/heroes/player.dart]----
double vY = 0; // 竖直速度
double aY = 300; // 竖直加速度
double sY = 0; // 竖直位移

void jump() {
if (current == PlayerState.jumping) {
return;
}
vY = -280 ;
sY = 0;
current = PlayerState.jumping;
}

@override
void update(double dt) {
super.update(dt);
if (current == PlayerState.jumping) {
vY += aY * dt;
sY += vY * dt;
if (sY > 0) {
sY = 0;
current = PlayerState.running;
}
y = initY + sY;
}
}

到这里第二集就结束啦,本集研究了一下如何通过编程的手段模拟物理量的变化;从中也学习了 Component 的一些知识,比如通过 render 回调自定义绘制、update 回调监听每帧的更新。

image.png

本集相当于一个小插曲,让我们对 Flame 有更多的认知。下一集,将进入 Trex 游戏的核心逻辑。

本文转载自: 掘金

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

1…515253…956

开发者博客

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