Redis2——协议与异步方式

文章目录

  • Redis2——协议与异步方式
    • 1. Redis Pipeline
    • 2. Redis事务
      • 2.1 无锁事务控制(乐观事务控制)
      • 2.2 事务语句与lua脚本
      • 2.3 事务特性ACID
    • 3. 通信方式
      • 3.1 hiredis库
      • 3.2 同步连接
      • 3.3 异步连接
        • 3.3.1 hiredis管理监听事件接口
        • 3.3.2 hiredis + libevent
        • 3.3.3 hiredis + 自定义reactor
    • 学习参考

Redis2——协议与异步方式

本文讲述了Redis pipeline技术,它被用于一次发送和执行多个命令;事务的ACID特性,Redis只能部分满足;最后介绍了实现了Redis客户端的同步连接和异步连接方式。

1. Redis Pipeline

Redis Pipeline 是一种在 Redis 中批量执行命令的技术,用于减少客户端与 Redis 服务器之间通信的开销,从而提高性能。Redis Pipeline是一种客户端提供的技术。

python代码示例如下:

import redis

# 创建 Redis 连接
r = redis.Redis(host='localhost', port=6379, db=0)

# 使用 pipeline
pipe = r.pipeline()

# 批量命令
pipe.set('foo', 'bar')
pipe.get('foo')
pipe.incr('counter')
pipe.mset({'name': 'Alice', 'age': 30})

# 执行管道中的所有命令
responses = pipe.execute()

# 打印响应
print(responses)

2. Redis事务

定义:事务是由用户定义的一系列操作,被视为一个整体,要么全部执行,要么都不执行,不可分割,其中间状态不能被访问(原子性)。

2.1 无锁事务控制(乐观事务控制)

Redis 的乐观事务主要基于其 WATCHMULTI/EXEC 机制,旨在实现无锁事务控制。通过监控键值变化避免事务冲突,适合高并发、轻量事务。

Redis对于数据的管理使用的是单线程模式,所有的命令会在同一个线程中执行,这使得redis中的大部分命令天然是原子操作,无序借助CAS实现。

  1. 监控键值 (WATCH)
    Redis 使用 WATCH 命令来监视一个或多个键。如果在事务执行之前,任何一个被监视的键发生了变化,事务将被中止。
  2. 事务定义 (MULTI)
    事务通过 MULTI 命令开始,所有后续命令被加入事务队列。
  3. 执行事务 (EXEC)
    当执行 EXEC 时,Redis 检查被监视的键是否被修改过。如果没有,事务中的命令会依次执行;如果有,事务会被放弃。
  4. 回滚机制(DISCARD)
    Redis 的乐观事务没有真正的回滚机制。如果某条命令出错,其他命令依然会执行。

乐观锁的概念

乐观锁是一种基于 “假设冲突较少” 的并发控制策略。它的核心思想是:在操作数据时假设不会发生并发冲突,仅在更新时检查是否有冲突,如果检测到冲突,则采取相应的处理(如重试或报错)。

这种锁机制并不会阻塞其他事务的访问,而是允许多个事务同时操作数据,但在最终提交时检查数据的一致性。

2.2 事务语句与lua脚本

开启事务

MULTI

提交事务

EXEC

回滚事务

DISCARD

监控键值变化

WATCH key

如果事务执行之前,被监控的key值发生了变化,就会导致事务执行失败。

例子如下:

WATCH account1 account2      # 监控两个键
val1 = GET account1          # 获取 account1 的余额
if val1 >= 100:              # 判断余额是否足够
    MULTI                    # 开始事务
    DECRBY account1 100      # 从 account1 扣减 100
    INCRBY account2 100      # 给 account2 增加 100
    EXEC                     # 提交事务
else:
    UNWATCH                  # 取消监控
    # 返回余额不足的错误

Lua脚本实现事务的原子性

执行lua脚本

Redis中内置了一个lua虚拟机。在工程实践中,大多数情况下是使用lua脚本来实现事务的原子性。

EVAL script numkeys key [key ...] [arg ...]

举个例子,将key值加倍并返回:

先定义一个lua脚本

local key = KEYS[1]; 
local val = redis.call("get", key); 
if not val then
    val = 1000
end
redis.call("set", key, val * 2); 
return 2 * val;

执行lua脚本

eval 'local key ... val;' 1 val

缓存脚本

但是这样每次都发送完整的lua脚本会造成流量浪费,我们可以先使用script load将脚本发送给redis服务器,得到一个标识符,然后之后就可以发送标识符代替脚本。

发送脚本,获得标识符

script load <script string>

使用标识符执行脚本

evalsha <script sha code> numkeys key [key ...] [arg ...]

管理脚本标识符

在服务器启动时,先清空原有的脚本缓存

script flush

然后将所有脚本发送给redis并使用一个unordered_map进行缓存,从而可以随时取用。

2.3 事务特性ACID

  1. 原子性(Atomicity) 事务是一个不可分割的单位,要么全部执行,要么全都不执行,其它事务不能访问其中间状态。

    Redis事务具备原子性,但由于Redis不支持回滚,因此即使事务中某些操作执行失败,整个事务也会继续执行下去,直到执行完毕。

  2. 一致性(Consistency)包括数据库本身的完整性约束的一致性和用户定义的逻辑上的一致性。前者举例如类型约束、非空约束R、唯一约束、外键约束等,后者举例如银行转账事务前后,总金额应该保持不变。

    Redis不支持逻辑上的一致性,因为它允许事务中的部分操作执行失败。

  3. 隔离性(Isolation)并发事务之间的影响程度。由于Redis是单线程执行命令,因此天然具有隔离性。

  4. 持久性(Durability)事务的操作的结果是否会持久化到磁盘中,即使数据库重启或崩溃,事务的数据也可以被恢复。

    Redis是基于内存的数据库,其事务是否具备持久性取决于持久化策略和配置:

    • RDB(Redis Database)持久化:通过快照的方式定期将内存中的数据保存到磁盘。

    • AOF(Append-Only File)持久化:通过将每个写命令追加到日志文件中,来记录所有写操作。

    设置持久化策略为每个写命令都持久化到磁盘:

    # redis.conf
    appendonly yes
    appendfsync always 
    

综上,Redis事务具备原子性和隔离性,不具备一致性,是否具备持久性取决于持久化策略

3. 通信方式

3.1 hiredis库

如果是C++,可以使用redis自带的hireds库进行连接。下面介绍一下hiredis的主要接口:

// 建立redis同步连接
redisContext *redisConnect(const char *ip, int port);
// 建立redis异步连接
redisContext *redisConnectNonBlock(const char *ip, int port);
// 发送命令给redis,如果是同步连接,成功发送返回值为reply,否则返回NULL
void *redisCommand(redisContext *c, const char *format, ...);
// 发送异步命令给redis,还可以设置返回后的回调函数
int redisAsyncCommand(redisAsyncContext *ac, redisCallbackFn *fn, void *privdata, const char *format, ...);
// 获取reply
int redisGetReply(redisContext *c, void **reply);
// 销毁reply
void freeReplyObject(void *reply);
// 销毁redis连接上下文
void redisFree(redisContext *c);

3.2 同步连接

同步连接是指与redis进行同步通信的连接,发送数据和接收数据时可能需要等待从而造成线程阻塞,大量时间被浪费在等待IO传输上。因此在业务中一般不采用这种方式。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <hiredis/hiredis.h>

int main() {
    redisContext *c;
    redisReply *reply;
    struct timeval timeout = {1, 500000}; // 1.5 seconds
    
    c = redisConnectWithTimeout("127.0.0.1", 6379, timeout);

    if (c == NULL || c->err)
    {
        printf("Connecting fails\n");
        if (c != NULL)
        {
            printf("error: %s\n", c->errstr);
            redisFree(c);
        }
        exit(1);
    }

    reply = redisCommand(c, "set name eeuu");
    
    if (c->err)
    {
        printf("c->err: %d\n", c->err);
        perror("redis:");
    }
    else
    {
        printf("replay type: %d\n", reply->type);
        printf("%s\n", reply->str);
    }

    freeReplyObject(reply);
    redisFree(c);

    return 0;
}

3.3 异步连接

Redis异步连接依靠非阻塞IO实现,发送命令时不会等待结果返回,而是设置回调函数,当该命令返回结果时自动调用该回调函数来处理结果。

3.3.1 hiredis管理监听事件接口

hiredis主要通过IO事件监测回调函数读写数据的接口与底层的网络IO层交互。

下面看一下IO检测接口:

typedef struct redisAsyncContext {
    ...
    /* Event library data and hooks */
    struct {
        void *data;
        /* Hooks that are called when the library expects to start reading/writing. These functions should be idempotent. */
        void (*addRead)(void *privdata);	// 注册读事件
        void (*delRead)(void *privdata);	// 注销读事件
        void (*addWrite)(void *privdata);	// 注册写事件
        void (*delWrite)(void *privdata);	// 注销写事件
        void (*cleanup)(void *privdata);	// 清空事件 
        void (*scheduleTimer)(void *privdata, struct timeval tv);	// 注册定时事件
    } ev;
} redisAsyncContext;

我们可以在创建redisAsyncContxt时进行设置。

当监听的事件发生时,需要网络层调用hiredis提供的处理函数。

void redisAsyncHandleRead(redisAsyncContext *ac);
void redisAsyncHandleWrite(redisAsyncContext *ac);
void redisAsyncHandleTimeout(redisAsyncContext *ac);

也就是说,网络层需要提供管理IO事件的函数以及当事件发生时调用读写函数。

用户层 hiredis层 网络层 redisAsyncCommand() addWrite() redisAsyncHandleWrite() addread() redisAsyncHandleRead() redisCallbackFn() 用户层 hiredis层 网络层
3.3.2 hiredis + libevent

libevent 是一个高效、跨平台的事件驱动编程库,主要用于构建高性能的网络应用程序和服务。它抽象化了底层的 I/O 多路复用机制,提供了一套简单的 API,用于处理事件通知、定时器和异步 I/O。

在linux平台上,libevent采用epoll实现IO复用。

适配libevent的IO事件管理函数:

static void redisLibeventAddRead(void *privdata) {
    redisLibeventUpdate(privdata, EV_READ, 0);
}

static void redisLibeventDelRead(void *privdata) {
    redisLibeventUpdate(privdata, EV_READ, 1);
}

static void redisLibeventAddWrite(void *privdata) {
    redisLibeventUpdate(privdata, EV_WRITE, 0);
}

static void redisLibeventDelWrite(void *privdata) {
    redisLibeventUpdate(privdata, EV_WRITE, 1);
}

static void redisLibeventCleanup(void *privdata) {
    redisLibeventEvents *e = (redisLibeventEvents*)privdata;
    if (!e) {
        return;
    }
    event_del(e->ev);
    event_free(e->ev);
    e->ev = NULL;

    if (e->state & REDIS_LIBEVENT_ENTERED) {
        e->state |= REDIS_LIBEVENT_DELETED;
    } else {
        redisLibeventDestroy(e);
    }
}

看一下hiredis自带的适配libevent的事件派发函数:

static void redisLibeventHandler(int fd, short event, void *arg) {
    ((void)fd);
    redisLibeventEvents *e = (redisLibeventEvents*)arg;
    e->state |= REDIS_LIBEVENT_ENTERED;

    #define CHECK_DELETED() if (e->state & REDIS_LIBEVENT_DELETED) {\
        redisLibeventDestroy(e);\
        return; \
    }

    if ((event & EV_TIMEOUT) && (e->state & REDIS_LIBEVENT_DELETED) == 0) {
        redisAsyncHandleTimeout(e->context);
        CHECK_DELETED();
    }

    if ((event & EV_READ) && e->context && (e->state & REDIS_LIBEVENT_DELETED) == 0) {
        redisAsyncHandleRead(e->context);
        CHECK_DELETED();
    }

    if ((event & EV_WRITE) && e->context && (e->state & REDIS_LIBEVENT_DELETED) == 0) {
        redisAsyncHandleWrite(e->context);
        CHECK_DELETED();
    }

    e->state &= ~REDIS_LIBEVENT_ENTERED;
    #undef CHECK_DELETED
}
3.3.3 hiredis + 自定义reactor

自定义一个reactor模式的网络层的思路类似libevent,主要是要适配hiredis的接口。

以下是采用异步连接时的主函数,其中有3个hiredis提供的接口:

  1. redisAsyncConnect是hiredis的接口,可以异步建立连接,当连接成功建立后会自动调用用户自定义的ConnectCallback。
  2. redisAsyncSetConnectCallback设置连接建立后回调函数
  3. redisAsyncSetDisconnectCallback设置连接断开后的回调函数
int main(int argc, char **argv) {
    R = create_reactor();
    redisAsyncContext *c = redisAsyncConnect("127.0.0.1", 6379);
    if (c->err) {
        /* Let *c leak for now... */
        printf("Error: %s\n", c->errstr);
        return 1;
    }
    redisAttach(R, c);
    
    redisAsyncSetConnectCallback(c, connectCallback);
    redisAsyncSetDisconnectCallback(c, disconnectCallback);

    eventloop(R);

    release_reactor(R);
    return 0;
}

剩下还有3个用户定义的函数:

  1. create_reactor创建reactor的上下文环境,包括epfd、events缓冲区等。

  2. redisAttach,主要负责将reactor上下文环境与连接上下文绑定,代码如下:

    static int redisAttach(reactor_t *r, redisAsyncContext *ac) {
        redisContext *c = &(ac->c);
        redis_event_t *re;
        
        /* Nothing should be attached when something is already attached */
        if (ac->ev.data != NULL)
            return REDIS_ERR;
    
        /* Create container for ctx and r/w events */
        re = (redis_event_t*)hi_malloc(sizeof(*re));
        if (re == NULL)
            return REDIS_ERR;
    
        re->ctx = ac;
        re->e.fd = c->fd;
        re->e.r = r;
        // dont use event buffer, using hiredis's buffer
        re->e.in = NULL;
        re->e.out = NULL;
        re->mask = 0;
    
        ac->ev.addRead = redisAddRead;
        ac->ev.delRead = redisDelRead;
        ac->ev.addWrite = redisAddWrite;
        ac->ev.delWrite = redisDelWrite;
        ac->ev.cleanup = redisCleanup;
        ac->ev.data = re;
        return REDIS_OK;
    }
    

    可见其主要是创建并填充了一个redis_event_t,然后为redis异步连接上下文设置回调函数,最后将redis_event_t交给连接上下文保存。

    redis_event_t是对reactor的event_t的扩展,结构体如下:

    typedef struct {
        event_t e;	// 保存fd、epoll上下文、缓冲区、回调函数
        int mask;	// 管理已注册的事件
        redisAsyncContext *ctx;
    } redis_event_t;
    
    typedef struct event_s event_t;
    
    struct event_s {
        int fd;
        reactor_t *r;
        buffer_t *in;
        buffer_t *out;
        event_callback_fn read_fn;
        event_callback_fn write_fn;
        error_callback_fn error_fn;
    };
    

    为异步连接上下文设置回调函数是关键步骤,分别对应注册读事件、写事件、注销读事件、写事件、清空事件。hiredis会在需要时调用这些回调函数。例如,当我们调用redisAsyncCommand时,hiredis会注册调用addWrite回调函数,然后在用户代码中,我们再epoll中注册写事件,当写事件触发时,调用我们设置的write_fn。

    read_fn和write_fn被保存于event_t中,也是需要用户定义和设置:

    static void redisReadHandler(int fd, int events, void *privdata) {
        ((void)fd);
        ((void)events);
        printf("redisReadHandler %d\n", fd);
        event_t *e = (event_t*)privdata;
        redis_event_t *re = (redis_event_t *)(char *)e;
        redisAsyncHandleRead(re->ctx);
    }
    
    static void redisWriteHandler(int fd, int events, void *privdata) {
        ((void)fd);
        ((void)events);
        event_t *e = (event_t*)privdata;
        redis_event_t *re = (redis_event_t *)(char *)e;
        redisAsyncHandleWrite(re->ctx);
    }
    

    在上面的回调函数中,调用了redisAsyncHandleRead和redisAsyncHandleWrite这两个hiredis提供的API来将数据从内核读写到用户态的buffer中,这个buffer也是hiredis提供的。redisAsyncHandleWrite函数读取到一个完整的响应后会调用用户在调用redisAsyncCommand时设置的回调函数。

    我们可以选择在注册读写事件的时候设置读写事件发生时的回调函数,当然应该也可以选择在创建redis_event_t的时候就设置。

    static void redisAddRead(void *privdata) {
        redis_event_t *re = (redis_event_t *)privdata;
        re->e.read_fn = redisReadHandler;
        redisEventUpdate(privdata, EPOLLIN, 0);
    }
    static void redisAddWrite(void *privdata) {
        redis_event_t *re = (redis_event_t *)privdata;
        re->e.write_fn = redisWriteHandler;
        redisEventUpdate(privdata, EPOLLOUT, 0);
    }
    

完整代码:

#include <hiredis/hiredis.h>
#include <hiredis/async.h>
#include <hiredis/sds.h>

#include "reactor.h"
#include "adapter_async.h"

static reactor_t *R;

char *rtype[] = {
    "^o^",
    "STRING",
    "ARRAY",
    "INTEGER",
    "NIL",
    "STATUS",
    "ERROR",
    "DOUBLE",
    "BOOL",
    "MAP",
    "SET",
    "ATTR",
    "PUSH",
    "BIGNUM",
    "VERB",
};

void dumpReply(struct redisAsyncContext *c, void *r, void *privdata) {
    redisReply *reply = (redisReply*)r;
    switch (reply->type) {
    case REDIS_REPLY_STATUS:
    case REDIS_REPLY_STRING:
        printf("[req = %s]reply:(%s)%s\n", (char*)privdata, rtype[reply->type], reply->str);
        break;
    case REDIS_REPLY_NIL:
        printf("[req = %s]reply:(%s)nil\n", (char*)privdata, rtype[reply->type]);
        break;
    case REDIS_REPLY_INTEGER:
        printf("[req = %s]reply:(%s)%lld\n", (char*)privdata, rtype[reply->type], reply->integer);
        break;
    case REDIS_REPLY_ARRAY:
        printf("[req = %s]reply(%s):number of elements=%lu\n", (char*)privdata, rtype[reply->type], reply->elements);
        for (size_t i = 0; i < reply->elements; i++) {
            printf("\t %lu : %s\n", i, reply->element[i]->str);
        }
        break;
    case REDIS_REPLY_ERROR:
        printf("[req = %s]reply(%s):err=%s\n", (char*)privdata, rtype[reply->type], reply->str);
        break;
    default:
        printf("[req = %s]reply(%s)\n", (char*)privdata, rtype[reply->type]);
        break;
    }
}

void
connectCallback(const redisAsyncContext *c, int status) {
    if (status != REDIS_OK) {
        printf("Error: %s\n", c->errstr);
        stop_eventloop(R);
        return;
    }
    printf("Connected...\n");
    redisAsyncCommand((redisAsyncContext *)c, dumpReply, 
        "hmset role:10001", 
        "hmset role:10001 name mark age 31 sex male");
    int a = 10;
    redisAsyncCommand((redisAsyncContext *)c, dumpReply, "hgetall role:10001", "hgetall role:10001");
    // ....
}

void
disconnectCallback(const redisAsyncContext *c, int status) {
    if (status != REDIS_OK) {
        printf("Error: %s\n", c->errstr);
        stop_eventloop(R);
        return;
    }

    printf("Disconnected...\n");
    stop_eventloop(R);
}

int main(int argc, char **argv) {
    R = create_reactor();
    redisAsyncContext *c = redisAsyncConnect("127.0.0.1", 6379);
    if (c->err) {
        /* Let *c leak for now... */
        printf("Error: %s\n", c->errstr);
        return 1;
    }
    redisAttach(R, c);
    
    redisAsyncSetConnectCallback(c, connectCallback);
    redisAsyncSetDisconnectCallback(c, disconnectCallback);

    eventloop(R);

    release_reactor(R);
    return 0;
}

学习参考

学习更多相关知识请参考零声 github。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/925668.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【模块一】kubernetes容器编排进阶业务容器化案例

Kubernetes 实战案例 Kubernetes实战案例-规划(基于nerdctl buildkitdcontainerd构建容器镜像) 业务容器化优势&#xff1a; ① 提高资源利用率、节约部署IT成本。 ② 提高部署效率&#xff0c;基于kubernetes实现微服务的快速部署与交付、容器的批量调度与秒级启动。 ③…

政安晨【零基础玩转各类开源AI项目】探索Cursor-AI Coder的应用实例

目录 Cusor的主要特点 Cusor实操 政安晨的个人主页&#xff1a;政安晨 欢迎 &#x1f44d;点赞✍评论⭐收藏 希望政安晨的博客能够对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff01; Cursor 是 Visual Studio Code 的一个分支。这使我们能够…

Android 12.0 DocumentsUI文件管理器首次进入默认显示内部存储文件功能实现

1.前言 在12.0的系统rom定制化开发中,在关于文件管理器的某些功能中,在首次进入文件管理器的时候默认进入下载 文件夹,点击菜单选择内部存储的时候,会显示内部存储的内容,客户开发需要要求默认显示内部存储的文件 接下来分析下功能的实现 如图: 2.DocumentsUI文件管理器首…

9.机器学习--SVM支持向量机

支持向量机&#xff08;Support Vector Machine&#xff0c;SVM&#xff09;是一种二分类监督学习模型。支持向量机最早在 1964 年被提出&#xff0c;1995年前后理论成熟并开始被大量应用与人像识别、文本分类等问题中。它的基本模型是定义在特征空间上的间隔最大的线性分类器&…

数据结构---链表

1. 简介 链表&#xff08;Linked List&#xff09;是一种常见的线性数据结构&#xff0c;它由一系列节点组成&#xff0c;每个节点包含数据部分和指向下一个节点的指针&#xff08;或引用&#xff09;。链表的一个主要优点是能够高效地插入和删除元素&#xff0c;尤其是在数组…

“移门缓冲支架:为家庭安全加码”

在智能家居日益普及的今天&#xff0c;科技不仅改变了我们的生活方式&#xff0c;也提升了家居的安全。移门缓冲支架作为一项结合了现代技术的小型装置&#xff0c;正逐渐成为提升家庭安全的重要配件。它通过吸收门关闭时的冲击力、减缓关门速度以及减少噪音等多重功能&#xf…

vscode、android studio、vim 国产AI编程插件Fitten Code

文章目录 Fitten Code简介vim安装Fitten Code插件Android Studio安装Fitten Code插件Fitten Code功能相关文章 Fitten Code简介 Fitten Code是由非十大模型驱动的AI编程助手&#xff0c;它可以自动生成代码&#xff0c;提升开发效率&#xff0c;帮您调试Bug&#xff0c;节省您…

一个月速成python+OpenCV图像处理

OpenCV是一个广受欢迎且极为流行的计算机视觉库&#xff0c;它因其强大的功能、灵活性和开源特性而在开发者和研究者中备受青睐。 学习OpenCV主要就是学习里面的计算机视觉算法。要学习这些算法的原理&#xff0c;知道它们适用于哪些场景&#xff0c;然后通过Python编写代码来…

深度学习2:从零开始掌握PyTorch:数据操作不再是难题

文章目录 一、导读二、张量的定义与基本操作三、广播机制四、索引与切片五、内存管理六、与其他Python对象的转换本文是经过严格查阅相关权威文献和资料,形成的专业的可靠的内容。全文数据都有据可依,可回溯。特别申明:数据和资料已获得授权。本文内容,不涉及任何偏颇观点,…

win10系统安装docker-desktop

1、开启Hyper-v ———————————————— Hyper-V 是微软提供的一种虚拟化技术&#xff0c;它允许你在同一台物理计算机上运行多个独立的操作系统实例。这种技术主要用于开发、测试、以及服务器虚拟化等领域。 —————————————————————— &#…

如何使用谷歌浏览器访问被屏蔽的网站

在互联网浏览过程中&#xff0c;我们有时会遇到一些网站被屏蔽的情况&#xff0c;这可能是因为地域限制、网络审查或其他原因。对于使用谷歌浏览器的用户来说&#xff0c;有几种方法可以尝试访问这些被屏蔽的网站。本文将详细介绍如何使用谷歌浏览器访问被屏蔽的网站。&#xf…

Next.js -服务端组件如何渲染

#题引&#xff1a;我认为跟着官方文档学习不会走歪路 服务器组件渲染到客户端发生了什么&#xff1f; 请求到达服务器 用户在浏览器中请求一个页面。 Next.js 服务器接收到这个请求&#xff0c;并根据路由找到相应的页面组件。服务器组件的渲染 Next.js 识别出请求的页面包含…

数据结构与算法——N叉树(自学笔记)

本文参考 N 叉树 - LeetBook - 力扣&#xff08;LeetCode&#xff09;全球极客挚爱的技术成长平台 遍历 前序遍历&#xff1a;A->B->C->E->F->D->G后序遍历&#xff1a;B->E->F->C->G->D->A层序遍历&#xff1a;A->B->C->D->…

SpringSecurity6

1.快速入门 2.SpringSecurity底层原理 使用的是委托过滤器,委托过滤器实际上就是 sevlet 过滤器 将自己放入Sevlet环境下 然后里面是一个 过滤器链代理 代理类下又是一个代理过滤器链的集合, 对于不同请求可以有不同的过滤器链, springsecurity有个默认的过滤器链 Defau…

芯片测试-RF中的S参数,return loss, VSWR,反射系数,插入损耗,隔离度等

RF中的S参数&#xff0c;return loss, VSWR&#xff0c;反射系数&#xff0c;插入损耗&#xff0c;隔离度 &#x1f4a2;S参数&#x1f4a2;&#x1f4a2;S11与return loss&#xff0c;VSWR&#xff0c;反射系数&#x1f4a2;&#x1f4a2;S21&#xff0c;插入损耗和增益&#…

前端页面或弹窗在线预览文件的N种方式

需求&#xff1a;后端返回给前端一个地址后&#xff0c;在前端页面上或则在弹框中显示在线的文档、表格、图片、pdf、video等等&#xff0c;嵌入到前端页面 方式一&#xff1a; 使用vue-office 地址&#xff1a;vue-office简介 | vue-office 个人感觉这个插件是最好用的&#x…

剪映自动批量替换视频、图片素材教程,视频批量复刻、混剪裂变等功能介绍

一、三种批量替换模式的区别 二、混剪裂变替换素材 三、分区混剪裂变替换素材 四、按组精确替换素材 五、绿色按钮教程 &#xff08;一&#xff09;如何附加音频和srt字幕 &#xff08;二&#xff09;如何替换固定文本的内容和样式 &#xff08;三&#xff09;如何附加…

【天地图】HTML页面实现车辆轨迹、起始点标记和轨迹打点的完整功能

目录 一、功能演示 二、完整代码 三、参考文档 一、功能演示 运行以后完整的效果如下&#xff1a; 点击开始&#xff0c;小车会沿着轨迹进行移动&#xff0c;点击轨迹点会显示经纬度和时间&#xff1a; 二、完整代码 废话不多说&#xff0c;直接给完整代码&#xff0c;替换…

Node报错:npm error code ETIMEDOUT

1、报错详细信息 npm error code ETIMEDOUT npm error syscall connect npm error errno ETIMEDOUT npm error network request to https://registry.npmjs.org/express failed, reason: connect ETIMEDOUT 104.16.1.35:443 npm error network This is a problem related to ne…

FPGA工具链及功能介绍

一、处理流程 把verilog等源码&#xff0c;变为FPGA中可执行的比特流文件&#xff0c;主要包含这些步骤&#xff1a; 步骤功能转译将verilog代码转化为更详细的语法&#xff0c;增加更多细节内容技术映射将每个vrilog用到的模块&#xff0c;对应到FPGA的物理器件上优化优化冗余…