2024年4月

大家好,我是蓝胖子,说起提高http的传输效率,很多人会开启http的Keep-Alive选项,这会http请求能够复用tcp连接,节省了握手的开销。但开启Keep-Alive真的没有问题吗?我们来细细分析下。

最大空闲时间造成请求失败

通常我们开启Keep-Alive后 ,服务端还会设置连接的最大空闲时间,这样能保证在没有请求发生时,及时释放连接,不会让过多的tcp连接白白占用机器资源。

问题就出现在服务端主动关闭空闲连接这个地方,试想一下这个场景,客户端复用了一个空闲连接发送http请求,但此时服务端正好检测到这个连接超过了配置的连接最大空闲时间,在请求到达前,提前关闭了空闲连接,这样就会导致客户端此次的请求失败。

过程如下图所示,

image.png

如何避免此类问题

上述问题在理论上的确是一直存在的,但是我们可以针对发送http请求的代码做一些加强,来尽量避免此类问题。来看看在Golang中,http client客户端是如何尽量做到安全的http重试的。

go http client 是如何做到安全重试请求的?

在golang中,在发送一次http请求后,如果发现请求失败,会通过
shouldRetryRequest
函数判断此次请求是否应该被重试,代码如下,

func (pc *persistConn) shouldRetryRequest(req *Request, err error) bool {  
    if http2isNoCachedConnError(err) {  
       // Issue 16582: if the user started a bunch of  
       // requests at once, they can all pick the same conn       // and violate the server's max concurrent streams.       // Instead, match the HTTP/1 behavior for now and dial       // again to get a new TCP connection, rather than failing       // this request.      
        return true  
    }  
    if err == errMissingHost {  
       // User error.  
       return false  
    }  
    if !pc.isReused() {  
       // This was a fresh connection. There's no reason the server  
       // should've hung up on us.       //       // Also, if we retried now, we could loop forever       // creating new connections and retrying if the server       // is just hanging up on us because it doesn't like       // our request (as opposed to sending an error).       
       return false  
    }  
    if _, ok := err.(nothingWrittenError); ok {  
       // We never wrote anything, so it's safe to retry, if there's no body or we  
       // can "rewind" the body with GetBody.      
        return req.outgoingLength() == 0 || req.GetBody != nil  
    }  
    if !req.isReplayable() {  
       // Don't retry non-idempotent requests.  
       return false  
    }  
    if _, ok := err.(transportReadFromServerError); ok {  
       // We got some non-EOF net.Conn.Read failure reading  
       // the 1st response byte from the server.       
       return true  
    }  
    if err == errServerClosedIdle {  
       // The server replied with io.EOF while we were trying to  
       // read the response. Probably an unfortunately keep-alive       // timeout, just as the client was writing a request.       
       return true  
    }  
    return false // conservatively  
}

我们来挨个看看每个判断逻辑,

http2isNoCachedConnError
是关于http2的判断逻辑,这部分逻辑我们先不管。

err == errMissingHost
这是由于请求路径中缺少请求的域名或ip信息,这种情况不需要重试。

pc.isReused()
这个是在判断此次请求的连接是不是属于连接复用情况,因为如果是新创建的连接,服务器正常情况下是没有理由拒绝我们的请求,此时如果请求失败了,则新建连接就好,不需要重试。

if _, ok := err.(nothingWrittenError); ok
这是在判断此次的请求失败的时候是不是还没有向对端服务器写入任何字节,如果没有写入任何字节,并且请求的body是空的,或者有body但是能通过
req.GetBody
恢复body就能进行重试。


通过上篇文章后我们的Seata服务就部署成功了,如果还不清楚怎么部署或者还没有部署Seata服务的朋友可以看我写的上篇文章进行服务部署。Seata部署步骤:
https://www.cnblogs.com/sowler/p/18108102
接下来我们来介绍在项目中如何使用Seata,温馨提示:请确保自己目前已经整合了一套Spring Cloud微服务架构,以更好的实操Seata。本篇只写Seata相关的知识。

1、引入Maven依赖

首先我们需要在需要使用seata的模块引入maven依赖seata 对应版本1.3.0

       <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
           <!--自带的版本可能会和Spring Cloud Alibaba对应的版本不一致 排除到重新引入相对应的版本 如果一致就不需要排除-->
            <exclusions>
                <exclusion>
                    <groupId>io.seata</groupId>
                    <artifactId>seata-spring-boot-starter</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
            <version>1.3.0</version>
        </dependency>

2、下载数据库表文件

Seata实现事务,需要依赖于数据表进行事务处理,所以我们需要下载seata对应版本的数据表导入各各模块的数据库里面。下载路径:
incubator-seata/script/client/at/db/mysql.sql at v1.3.0 · apache/incubator-seata · GitHub
下载成功后导入到库里面。

3、在用户模块配置加入配置

seata:
  enabled: true
  application-id: ${spring.application.name}
  # 客户端和服务端在同一个事务组  default_tx_group
  tx-service-group: order_tx_group
  # 自动数据源代理
  enable-auto-data-source-proxy: true
  # 数据源代理模式(分布式事务方案)
  data-source-proxy-mode: AT
  # 事务群组,配置项值为TC集群名,需要与服务端保持一致
  service:
    # 跟服务端配置保持一致
    vgroup-mapping:
      order_tx_group: default
      order-public: default
      default_tx_group: default
    enable-degrade: false
    disable-global-transaction: false
    grouplist:
      default: 127.0.0.1:8091

4、在订单模块配置中加入配置

seata:
  enabled: true
  application-id: ${spring.application.name}
  # 客户端和服务端在同一个事务组  default_tx_group
  tx-service-group: order_tx_group
  # 自动数据源代理
  enable-auto-data-source-proxy: true
  # 数据源代理模式(分布式事务方案)
  data-source-proxy-mode: AT
  # 事务群组,配置项值为TC集群名,需要与服务端保持一致
  service:
    # 跟服务端配置保持一致
    vgroup-mapping:
      order_tx_group: default
      order-public: default
      default_tx_group: default
    enable-degrade: false
    disable-global-transaction: false
    grouplist:
      default: 127.0.0.1:8091

在需要事务的实现类业务方法上面加入@GlobalTransactional注解实现事务

    @Override
    @GlobalTransactional(rollbackFor = Exception.class,timeoutMills = 30000,name = "order_tx_group") 
	//rollbackFor 报错异常回滚	 timeoutMills 超时时间 		name当前使用的那个事务组
    public List<UserExternal> selectUserAll() {
        //添加blog
        Blog blog = new Blog();
        blog.setUid(UUID.randomUUID().toString());
        blog.setTitle("dubbo事务测试Test");
        blog.setContent("dubbo事务测试Test啊的服务器打");
        blog.setSummary("12");
        blog.setTagUid("3c16b9093e9b1bfddbdfcb599b23d835");
        blogService.insert(blog);
        //处理相关逻辑
        Response<List<UserExternal>> response = userExternalService.selectUserAll();
//        boolean flag = true;
//        if (flag == true){
//            throw new ParamException(500,"用户模块出现错误,需要回滚");
//        }
        UserExternal user = new UserExternal();
        user.setUserName("dubbo事务");
        user.setAccount("system");
        user.setEmail("dubbo@gemail.com");
        Response insert = userExternalService.insert(user);
        System.out.println(insert);
        return response.getModel();
    }

5、测试事务是否触发

启动项目进行数据测试。调用接口添加数据发现在每个模块中会有seata日志输出

订单模块 添加blog表:

2024-04-03 15:09:16.383 INFO  -[DefaultGlobalTransaction.java:108]- Begin new global transaction [172.25.96.1:8091:518805861666131968]
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1ff3054] was not registered for synchronization because synchronization is not active
JDBC Connection [io.seata.rm.datasource.ConnectionProxy@34a12e67] will not be managed by Spring
==>  Preparing: INSERT INTO tb_blog (uid, title, summary, content, tag_uid) VALUES (?, ?, ?, ?, ?) 
==> Parameters: 1c7cd2f7-a690-4942-b01f-9e1b76eb8b0e(String), dubbo事务测试Test(String), 12(String), dubbo事务测试Test啊的服务器打(String), 3c16b9093e9b1bfddbdfcb599b23d835(String)
<==    Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1ff3054]
Response(code=200, msg=success, model=null)
2024-04-03 15:09:16.609 INFO  -[DefaultGlobalTransaction.java:143]- [172.25.96.1:8091:518805861666131968] commit status: Committed
2024-04-03 15:09:16.693 INFO  -[RmBranchCommitProcessor.java:56]- rm client handle branch commit process:xid=172.25.96.1:8091:518805861666131968,branchId=518805861913595905,branchType=AT,resourceId=jdbc:mysql://127.0.0.1:3306/order-mode,applicationData=null
2024-04-03 15:09:16.693 INFO  -[AbstractRMHandler.java:96]- Branch committing: 172.25.96.1:8091:518805861666131968 518805861913595905 jdbc:mysql://127.0.0.1:3306/order-mode null
2024-04-03 15:09:16.693 INFO  -[AbstractRMHandler.java:104]- Branch commit result: PhaseTwo_Committed

看第一行:
Begin new global transaction
开始一个新的全局事务 说明seata配置已经生效。

关键信息:

2024-04-03 15:09:16.383 INFO  -[DefaultGlobalTransaction.java:108]- Begin new global transaction 

2024-04-03 15:09:16.609 INFO  -[DefaultGlobalTransaction.java:143]- [172.25.96.1:8091:518805861666131968] commit status: Committed
2024-04-03 15:09:16.693 INFO  -[RmBranchCommitProcessor.java:56]- rm client handle branch commit process:xid=172.25.96.1:8091:518805861666131968,branchId=518805861913595905,branchType=AT,resourceId=jdbc:mysql://127.0.0.1:3306/order-mode,applicationData=null
2024-04-03 15:09:16.693 INFO  -[AbstractRMHandler.java:96]- Branch committing: 172.25.96.1:8091:518805861666131968 518805861913595905 jdbc:mysql://127.0.0.1:3306/order-mode null
2024-04-03 15:09:16.693 INFO  -[AbstractRMHandler.java:104]- Branch commit result: PhaseTwo_Committed

用户模块 添加用户信息:

Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6d327c7] was not registered for synchronization because synchronization is not active
JDBC Connection [io.seata.rm.datasource.ConnectionProxy@782accbc] will not be managed by Spring
==>  Preparing: INSERT INTO tb_user (user_name, account, email, create_by, create_time) VALUES (?, ?, ?, ?, ?) 
==> Parameters: dubbo事务(String), system(String), dubbo@gemail.com(String), System(String), 1712128156490(Long)
<==    Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6d327c7]
2024-04-03 15:09:16.707 INFO  -[RmBranchCommitProcessor.java:56]- rm client handle branch commit process:xid=172.25.96.1:8091:518805861666131968,branchId=518805862223974401,branchType=AT,resourceId=jdbc:mysql://127.0.0.1:3306/user-mode,applicationData=null
2024-04-03 15:09:16.707 INFO  -[AbstractRMHandler.java:96]- Branch committing: 172.25.96.1:8091:518805861666131968 518805862223974401 jdbc:mysql://127.0.0.1:3306/user-mode null
2024-04-03 15:09:16.707 INFO  -[AbstractRMHandler.java:104]- Branch commit result: PhaseTwo_Committed

关键信息:

2024-04-03 15:09:16.707 INFO  -[RmBranchCommitProcessor.java:56]- rm client handle branch commit process:xid=172.25.96.1:8091:518805861666131968,branchId=518805862223974401,branchType=AT,resourceId=jdbc:mysql://127.0.0.1:3306/user-mode,applicationData=null
2024-04-03 15:09:16.707 INFO  -[AbstractRMHandler.java:96]- Branch committing: 172.25.96.1:8091:518805861666131968 518805862223974401 jdbc:mysql://127.0.0.1:3306/user-mode null
2024-04-03 15:09:16.707 INFO  -[AbstractRMHandler.java:104]- Branch commit result: PhaseTwo_Committed

查看数据表undo_log日志信息,发现该信息在事务执行时,事务信息会添加到每个模块的undo_log日志表中。当执行成功后,会删除undo_log表的日志信息。

当事务开始执行时,查看seata数据库会发现在global_table表中会生成一条事务数据:

当方法执行在订单模块处理数据的时候,会在订单数据库的undo_log表中添加一条数据:

当方法执行到用户模块中时,在用户数据库的undo_log表中也会添加一条数据

通过上面可以发现seata是通过xid字段的全局ID进行事务控制的。我们还可以测试一下,当调用的用户模块出现异常了,事务是如何实现回滚的。好了到此seata的在项目中的使用就介绍完毕了,我们还可以学习一些seata的运行原理来更好的使用seata。

观前须知

本题解全部内容遵循
CC BY-NC-SA 4.0 Deed原则
同步发布于Luogu题解区
更好的观看体验
点这里
笔者的博客主页

正文

Luogu P3294 【SCOI2016】背单词

笔者在刷题的时候看到了这道好题
花了四十分钟切掉以后,看了一下题解
发现自己的想法不太一样
所以想做一篇适合我这样的蒟蒻看的题解awa
那么,我们开始吧~

首先

题意理解

(笔者认为本文最难的一个部分)

给你
\(n\)
个字符串
要求你找一种这
\(n\)
个字符串的排列
规则一:若一个字符串
\(a\)
有一个字符串
\(b\)

\(a\)
的后缀,
(这里的后缀在
\(n\)
个字符串中出现过,且由题意不为该串本身,下文同理)

\(b\)
排在
\(a\)
前,则花费增加
\(n^2\)
规则三:若一个字符串
\(a\)
的所有后缀都排在
\(a\)
前,
则花费增加
\(a\)
到最近一个
\(a\)
的后缀
\(b\)
的距离(即
\(x-y\)

规则二:特别地,若
\(a\)
没有后缀,则花费增加
\(a\)
的排名(即
\(x\)

求最小花费

(吐槽一下,原题真的很不好理解,笔者这里看了十分钟都以为是题目给定了排列顺序)

那么来
简化题意
首先,发现原题中的规则二就是规则三的
特例
,所以不需要考虑
然后,可以发现规则一增加的
\(n^2\)
实在太多了(因为每个规则二最多也只能增加
\(n\)
的花费)
所以不能违反规则一

所有字符串的后缀一定排在这个字符串的前面
(这种情况是一定能完成的,按照字符串长度排序就是一种方案)

那么题意已经变为了
在不违反规则一的情况下
使规则三的花费和最小

建模

发现不能违反规则一后
规则三中的
最近一个后缀
变为了
长度最大的一个后缀
发现每个字符串要么有唯一的一个
长度最大的一个后缀
,要么没有后缀
这和

的结构类似
那么我们可以建立一棵树,满足任意一个节点都是它的所有儿子的长度最大的后缀
(也就是SAM中的后缀树)
对于没有后缀的点,我们建立一个虚根(代码中为0号点),作为它们的父亲
(这里的虚根可以理解为是一个空串,因为空串是每一个字符串的后缀)

下面给出了一棵后缀树方便大家理解:

a ab ba aab aba ababa bbaab bbbbba
后缀树

建好这棵树后,我们就可以开始
贪心

贪心

先直接说贪心策略:
在后缀树上按照dfs序选点
且每个节点先走子树小的

(接下来的证明可以感性理解,建议边想边画图)

首先证明dfs序选点是正确的
对于根节点,它的若干个子节点有若干棵子树
这里我们考虑其中任意两棵
所以我们只需证明,我们要先选完一棵子树,再选择另一棵子树
不妨设先选的子树的树根为
\(x\)
,后选的子树的树根为
\(y\)
首先考虑把
\(y\)
提前到
\(x\)
的子树选完前

\(y\)
提前了
\(a\)
个位置
对于
\(y\)
子树内的第一个子节点,花费增加了
\(a\)
对于插入
\(y\)
后的第一个节点,花费增加了
\(1\)
其余节点花费不变
继续把
\(y\)
的子树内的节点提前,花费不变
所以对于根来说,选完一棵子树后再选另一棵是最优的
递归下去可以证明dfs序选点是最优的

接下来证明要先走子树小的
对于一个节点,考虑它的每一个孩子
发现可以递归处理每一个孩子的子树内的节点,那么我们只需要考虑每个孩子到这个节点的距离
根据上面的结论可得,每个孩子到这个节点的距离就是在这个节点子树中已经走过的节点数
那么为了距离和最小,显然要走过的节点数尽量小
所以子树小的优先选

好的,那么我们的答案就可以由贪心策略算出来了
欸?你问我是不是少了些什么?
好吧
最后一部分

建树

为了建树,我们只需要求出每个节点的父节点
即每个字符串的最长后缀
我们先根据字符串长度排序
那么每个字符串的后缀都在这个字符串前面了
但是后缀不好做
所以我们把每个字符串都倒过来变成前缀
我们把每个字符串依次插入到
Trie树

并在每个字符串的终止结点记录编号
我们可以惊喜地发现:
对于一个字符串的最长后缀(这里已经变为前缀了)
就是在这个字符串在Trie树上的对应路径中
深度最大的终止节点
那么我们就能很
容易地
求出每个节点的父亲
那么就可以建树了
(这块讲的比较抽象,建议配着代码食用,或自己think一下)

一些小细节:
用vector存树方便sort
按照字符串长度排好的顺序其实是后缀树的拓扑逆序,可以直接倒序枚举更新sz
因为字符串长度不确定所以不能用scanf和char数组了(悲)

这份代码最短用时223ms,拿了个次优解直接开润~

#include<bits/stdc++.h>

using namespace std;

static constexpr int AwA = 1e5 + 10;
static constexpr int PwP = 6e5 + 10;

int n;
//因为这道题每个字符串长度不确定,所以我只能抛弃我的char数组了(悲)
string s[AwA];
//记录每个节点算出来的父亲
int fa[AwA];
//字典树,id[u]!=0时记录该节点对应的字符串编号
int ch[PwP][26], id[PwP], tot = 1;

vector<int> tr[AwA];
int sz[AwA];
long long ans;

//贪心选点
void Dfs(int u) {
    int cur = 1;
    for (int v: tr[u]) {
        Dfs(v);
        ans += cur;
        cur += sz[v];
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr), cout.tie(nullptr);

    cin >> n;
    for (int i = 1; i <= n; i++) cin >> s[i];
    //根据字符串长度排序
    sort(s + 1, s + n + 1, [](auto &s1, auto &s2) { return s1.size() < s2.size(); });

    int u, p;
    for (int i = 1; i <= n; i++) {
        //如果路径上没有终止节点,即没有后缀,则父亲为虚根0
        fa[i]=0;
        u = 1;
        //倒叙枚举,变后缀为前缀
        for (auto k = s[i].rbegin(); k != s[i].rend(); k++) {
            p = *k - 'a';
            if (!ch[u][p]) ch[u][p] = ++tot;
            u = ch[u][p];
            //遇到终止节点更新父亲
            if (id[u]) fa[i] = id[u];
        }
        //记录终止节点
        id[u] = i;
    }

    //因为父亲串的长度一定小于儿子,所以根据字符串长度排序后为拓扑逆序
    for (int i = n; i; i--) sz[i]++, sz[fa[i]] += sz[i];
    //建树
    for (int i = 1; i <= n; i++) tr[fa[i]].push_back(i);
    //按子树大小排序,方便贪心选择
    //注意0节点也要排序
    auto cmp = [&](int i, int j) { return sz[i] < sz[j]; };
    for (int i = 0; i <= n; i++) sort(tr[i].begin(), tr[i].end(), cmp);
    Dfs(0);
    printf("%lld\n", ans);
    return 0;
}

希望这篇题解能帮助你更好地理解这道很好的贪心题
完结撒花!~

前言

  • 前篇我们已经介绍了 radash 的相关信息和部分 Array 相关方法,详情可前往主页查看;
  • 本篇我们继续介绍 radash 中 Array 的其他相关方法;

Radash 的 Array 相关方法详解

first:获取数组第一项,不存在返回默认值

  1. 使用说明
    • 参数:目标数组,或者传递两个参数空数组和默认值;
    • 返回值:传目标数组则返回目标数组的第一项,传空数组和默认值则返回默认值。
  2. 使用代码示例
import { first } from 'radash'

const gods = ['lufee', 'loki', 'zeus']

first(gods) // => 'lufee'
first([], 'zuoluo') // => 'zuoluo'
  1. 源码解析
// 定义一个泛型函数 `first`,它接收一个具有只读属性的泛型数组 `array`,
// 和一个可选的默认值 `defaultValue`,其类型可以是泛型 `T` 或 `null` 或 `undefined`,默认值为 `undefined`。
export const first = <T>(
  array: readonly T[],
  defaultValue: T | null | undefined = undefined
) => {
  // 如果数组存在且长度大于0,返回数组的第一个元素。
  // 否则,返回提供的默认值 `defaultValue`。
  return array?.length > 0 ? array[0] : defaultValue
}
  • 方法流程说明:


    1. 检查传入的数组
      array
      是否存在并且长度是否大于0。
    2. 如果数组存在且不为空(长度大于0),则返回数组的第一个元素
      array[0]
    3. 如果数组不存在或为空,返回
      defaultValue


    这个函数对于需要安全地访问数组第一个元素而不抛出错误的情况很有用,特别是在不确定数组是否为空的情况下。通过提供一个默认值,你可以避免在数组为空时访问未定义的索引。如果没有提供默认值,函数将默认返回
    undefined

flat:数组扁平化 —— 把包含多个数组的数组转为一个数组(注意不会递归)

  1. 使用说明
    • 参数:包含多个数组的数组;
    • 返回值:降低一维后的数组;
    • 注意:不会递归降维,只能降一维
  2. 使用代码示例
import { flat } from 'radash'

const gods = [['shy', 'ning'], ['jkl']]

flat(gods) // => [shy, ning, jkl]
  1. 源码解析
// 定义一个泛型函数 `flat`,它接收一个具有只读属性的二维泛型数组 `lists`,
// 并返回一个扁平化的一维数组。
export const flat = <T>(lists: readonly T[][]): T[] => {
  // 使用数组的 `reduce` 方法来累积(合并)所有子数组的元素。
  return lists.reduce((acc, list) => {
    // 使用 `push` 方法的展开语法(...)将当前处理的子数组 `list` 的所有元素添加到累加器 `acc` 中。
    acc.push(...list)
    // 返回更新后的累加器 `acc`,以便它可以在下一次迭代中使用。
    return acc
  }, []) // 初始化累加器 `acc` 为一个空数组。
}
  • 方法流程说明:
    1. 使用
      reduce
      方法遍历二维数组
      lists

      reduce
      方法的累加器
      acc
      是一个一维数组,用于收集所有子数组的元素。
    2. 对于
      lists
      中的每个子数组
      list
      ,使用展开语法
      ...
      将其元素添加到累加器数组
      acc
      中。
    3. 每次迭代结束后,返回更新的累加器
      acc

    4. reduce
      方法完成遍历后,返回最终的累加器
      acc
      ,这时它包含了
      lists
      中所有子数组的元素,形成了一个扁平化的一维数组。
    5. 提示:这个
      flat
      函数在功能上类似于 Array.prototype.flat 方法,但是它是手动实现的,适用于不支持内建
      flat
      方法的环境。

fork:按条件将数组拆分成两个数组,满足条件的一个,不满足条件的一个

  1. 使用说明
    • 参数:目标数组,条件函数;
    • 返回值:返回两个数组,一个保存满足条件的项,另一个保存不满足条件的项。
  2. 使用代码示例
import { fork } from 'radash'

const gods = [
  {
    name: 'Uzi',
    power: 100
  },
  {
    name: 'Xiaohu',
    power: 98
  },
  {
    name: 'Ming',
    power: 72
  },
  {
    name: 'Mlxg',
    power: 100
  }
]

const [finalGods, lesserGods] = fork(gods, f => f.power > 90) // [[Uzi, Xiaohu, Mlxg], [Ming]]
  1. 源码解析
// 定义一个泛型函数 `fork`,它接受一个具有只读属性的泛型数组 `list`,
// 和一个条件函数 `condition`,根据此函数将 `list` 中的元素分成两组。
export const fork = <T>(
  list: readonly T[],
  condition: (item: T) => boolean
): [T[], T[]] => {
  // 如果传入的 `list` 为空,则返回两个空数组。
  if (!list) return [[], []]
  
  // 使用数组的 `reduce` 方法来累积分离出的两个子数组。
  return list.reduce(
    (acc, item) => {
      // 从累加器中解构出两个子数组 a 和 b。
      const [a, b] = acc
      // 如果当前元素 `item` 满足条件函数 `condition`,将其添加到数组 a,否则添加到数组 b。
      if (condition(item)) {
        return [[...a, item], b]
      } else {
        return [a, [...b, item]]
      }
    },
    [[], []] as [T[], T[]] // 初始化累加器为两个空数组。
  )
}
  • 方法流程说明:
    1. 首先检查传入的数组
      list
      是否为空。如果为空,返回一对空数组。
    2. 使用
      reduce
      方法遍历
      list
      数组。
      reduce
      方法的累加器
      acc
      是一个包含两个子数组的元组
      [T[], T[]]
    3. 对于
      list
      中的每个元素
      item
      ,检查它是否满足条件函数
      condition
    4. 如果条件函数返回
      true
      ,则将该元素添加到累加器的第一个子数组
      a
      。如果条件函数返回
      false
      ,则将该元素添加到第二个子数组
      b
    5. 在每次迭代结束后,返回更新后的累加器
      [a, b]

    6. reduce
      方法完成遍历后,返回最终的累加器
      [a, b]
      ,它包含了根据条件函数分离的两个子数组。

group:根据条件函数指定的key构建一个统计对象,key 为指定的 key 有哪些 value ,value 为对应对象

  1. 使用说明
    • 参数:对象数组、条件函数;
    • 返回值:统计对象
  2. 使用代码示例
import { group } from 'radash'

const fish = [
  {
    name: 'Marlin',
    source: 'ocean'
  },
  {
    name: 'Bass',
    source: 'lake'
  },
  {
    name: 'Trout',
    source: 'lake'
  }
]

const fishBySource = group(fish, f => f.source) // => { ocean: [marlin], lake: [bass, trout] }
  1. 源码解析
// 定义一个泛型函数 `group`,它接受一个具有只读属性的泛型数组 `array`,
// 和一个函数 `getGroupId`,该函数用于从数组元素中提取一个标识符作为组的键。
export const group = <T, Key extends string | number | symbol>(
  array: readonly T[],
  getGroupId: (item: T) => Key
// 返回一个对象,其键是通过 `getGroupId` 函数提取的标识符,键对应的值是具有相同标识符的元素数组。
): Partial<Record<Key, T[]>> => {
  // 使用数组的 `reduce` 方法来累积分组结果。
  return array.reduce((acc, item) => {
    // 使用 `getGroupId` 函数从当前元素 `item` 中获取组标识符 `groupId`。
    const groupId = getGroupId(item)
    // 如果累加器 `acc` 中还没有这个组标识符的键,初始化为一个空数组。
    if (!acc[groupId]) acc[groupId] = []
    // 将当前元素 `item` 添加到对应组标识符的数组中。
    acc[groupId].push(item)
    // 返回更新后的累加器 `acc`,以便它可以在下一次迭代中使用。
    return acc
  }, {} as Record<Key, T[]>) // 初始化累加器为一个空对象。
}
  • 方法流程说明:
    1. 遍历传入的数组
      array
      ,对每个元素使用
      getGroupId
      函数来确定它应该属于哪个组。
    2. 对于每个元素,检查累加器
      acc
      (一个对象)中是否已经有一个数组存在于以
      groupId
      为键的位置。如果没有,就在那个位置创建一个空数组。
    3. 将当前元素
      item
      添加到
      acc[groupId]
      数组中。
    4. 继续处理数组的下一个元素,直到所有元素都被处理完毕。
    5. 返回累加器
      acc
      ,它现在包含了按
      groupId
      分组的元素数组。
    6. tips:在TypeScript中,
      Partial<Record<Key, T[]>>
      是一种类型,它表示一个对象,这个对象的键可以是
      Key
      类型,而每个键对应的值是
      T[]
      类型的数组。
      Partial

      Record
      都是TypeScript中的高级类型。

intersects:判断两个数组是否有公共项,返回一个布尔值

  1. 使用说明
    • 参数:数组1,数组2,可选条件函数(用于提取随机标识符,对对象数组进行操作时);
    • 返回值:有返回true,否则返回false。
  2. 使用代码示例
import { intersects } from 'radash'

const oceanFish = ['tuna', 'tarpon']
const lakeFish = ['bass', 'trout']

intersects(oceanFish, lakeFish) // => false

const brackishFish = ['tarpon', 'snook']

intersects(oceanFish, brackishFish) // => true
  1. 源码解析
// 定义一个泛型函数 `intersects`,它接受两个具有只读属性的泛型数组 `listA` 和 `listB`,
// 以及一个可选的函数 `identity`,用于从数组元素中提取一个唯一标识符。
export const intersects = <T, K extends string | number | symbol>(
  listA: readonly T[],
  listB: readonly T[],
  identity?: (t: T) => K
): boolean => {
  // 如果 `listA` 或 `listB` 不存在,返回 false。
  if (!listA || !listB) return false
  // 如果 `identity` 函数未提供,则使用默认函数,它将元素作为其自己的标识符。
  const ident = identity ?? ((x: T) => x as unknown as K)
  // 使用 `listB` 的元素创建一个记录对象 `dictB`,键是通过 `ident` 函数提取的唯一标识符,值为 `true`。
  const dictB = listB.reduce((acc, item) => {
    acc[ident(item)] = true
    return acc
  }, {} as Record<string | number | symbol, boolean>)
  // 检查 `listA` 中是否有元素的唯一标识符存在于 `dictB` 中。
  return listA.some(value => dictB[ident(value)])
}
  • 方法流程说明:
    1. 检查传入的数组
      listA

      listB
      是否存在。如果任何一个不存在,返回
      false
    2. 如果未提供
      identity
      函数,则使用一个默认函数,它将每个元素
      x
      强制转换为
      K
      类型,作为其唯一标识符。
    3. 遍历数组
      listB
      ,使用
      reduce
      方法和
      ident
      函数将其元素映射到一个记录对象
      dictB
      中,其中键是元素的唯一标识符,值为
      true
    4. 使用
      some
      方法检查数组
      listA
      中是否有任何元素的唯一标识符存在于
      dictB
      中。如果存在,
      some
      方法会返回
      true
      ,表示两个数组有交集。
    5. 如果
      listA
      中没有任何元素的唯一标识符在
      dictB
      中找到,
      some
      方法返回
      false
      ,表示两个数组没有交集。
    6. 提示:
      ??
      表示 TypeScript 中的空值合并运算符 。这个运算符用于提供一个默认值,当左侧的操作数
      identity

      null

      undefined
      时,就会使用右侧的操作数作为默认值。
      x as unknown as K
      是 Typescript 的类型断言。

下期我们将介绍以下方法

  • iterate:把一个函数迭代执行指定次数;
  • last:输出数组的最后一项,如果数组为空则输出传入的默认值;
  • list:创建包含特定项的数组;
  • max:获取对象数组中指定标识符最大的项;
  • merge:合并数组,并且会覆盖第一个数组;
  • min:获取对象数组中指定标识符最小的项;
  • objectify:根据函数映射的键与值把数组转换为字典对象;
  • range:根据步长生成一个数值范围内的迭代值;
  • replaceOrAppend:替换对象数组中的项或是追加项(条件函数不满足时追加);
  • replace:替换数组中的第一个匹配项。

写在后面

  • 后续作者会整理一份方法目录上传,方便没法访问外网的朋友查看使用。
  • 该系列会在每周五更新,遇节假日提前。
  • 大家有任何问题或者见解,欢迎评论区留言交流!!!
  • 点击访问:
    radash 官网

扯淡时间


上一篇文章
中我提了一嘴,打算使用esp32cam实现一个延迟摄影,奈何存在各种硬件问题,商家发了好几个地板都不好使(就是那个拼多多商家的问题,还说我供电不稳,我特意买了独立供电的hub),后来逛淘宝的时候又给我推送了esp32的板子,我不信邪的买了一个~他妈的上来就好使,所以才有了这篇文章,嗨嗨嗨
最后我只想对拼多多商家说:

既然能用了,那就开始搞

思路

  1. esp32-cam定时拍摄照片(拍摄速度还是需要跟网络传输速度来定,esp32的网卡太辣鸡),发送到树莓派上存储(可以在树莓派上去合成视频)
  2. 树莓派接受到指令以后,将图片拼接成视频,通过邮件发送给我
  3. 写个页面,能看到esp32拍到的照片,也支持下发指令,生成视频(理想很丰满,现实就是能用,凑活着用,都是bug)

graph TB
subgraph 服务器
id1[esp32拍摄照片]-.http发送到树莓派.->id2[接受]
id2-.存储数据.->id4[图片文件夹]
end

subgraph 页面端
id3[显示最新的条图片]-->id2
end

先看个效果-我买了好几个摄像头,这里发一下视频,可以让大家参考一下再购买

200w摄像头

300w带夜视广角摄像头

500w摄像头

好了我们开始无聊的code时间吧(晚上补充)

  1. esp32拍摄照片+将图片使用http请求发送到服务端
  2. 树莓派4b开启服务器,接收图片并存储到本地,执行合并视频的指令
  3. 给个前端页面展示一下图片(为了调整视角)

还存在的问题

  1. 因为白天太亮了,天空毛线都看不到,本来就是打算来拍云彩的,貌似得整个nd滤镜啥的,需要手动去调整镜头的参数了
  2. 合并视频的时候必须是1,2,3依次的文件名称,我这里虽然做了处理,但是开始不一定是1,我是读取了最后一个然后加一
  3. 没有清除图片的逻辑

为啥不解决呢?因为我又不经常用,就为了玩玩,能达到百分之80的功能就行,有些小bug,没有心劲去解决了