从零实现kv存储(1):array初版

news2024/11/23 2:56:01

本节开始,逐步实现基于内存的kv存储引擎。

一、项目主要功能和知识点

参照redis,主要实现的功能:
1、数据的插入、查询、删除等操作
1)SET:插入key - value
2)GET:获取key对应的value
3)COUNT:统计已插入多少个key
4)DELETE:删除key以及对应的value
5)EXIST:判断key是否存在

2、实现不同的数据结构存储引擎
不同的数据结构,操作的效率是不一样的。因此我们实现基于数组array、红黑树rbtree、哈希hash、跳表skiptable 这四种数据结构,实现kv存储,并测试相应的性能。

3、测试用例
1)功能测试
2)10w的qps测试

主要涉及的知识点有:
1)基于协程,一个连接对应一个协程
2)tcp网络交互
3)数据结构:数组array、红黑树rbtree、哈希hash、跳表skiptable

二、架构设计

在这里插入图片描述
在这里插入图片描述

三、具体实现

我们先实现简单的,基于数组array的kv存储引擎。

3.1 流程

SET NAME ZXM 为例,大致介绍一下流程:

1)客户端发送 SET NAME ZXM
2)服务器接收 SET NAME ZXM,进入解析流程
3)根据协议进程解析:
∘ \quad \circ 按空格拆分命令,存储到toekns[] tokens[0] = SET,tokens[1] = NAME, tokens[2]: ZXM
∘ \quad \circ 根据tokens[0],解析出对应的数据结构存储引擎,即数组。以及对应的操作命令,即插入。
∘ \quad \circ 根据key、value执行相应的操作命令,即插入新的key,value。
∘ \quad \circ 返回操作结果
4)解析完成,将操作结果返回给客户端。
5)客户端根据接收到的结果判断是否操作成功。

3.3 引擎层

typedef struct kvs_array_item_s {
	char *key;
	char *value;
} kvs_array_item_t;

// kvs_array_table 存储插入的 kv
kvs_array_item_t kvs_array_table [KVS_ARRAY_ITEM_SIEZ] = {0};

3.4 接口层

// 查找 key 在 kvs_array_table 的位置
kvs_array_item_t *kvs_array_search_item (char *key);

// KVS_CMD_EXIST: 判断 key 是否存在,存在返回 1 
int kvs_array_exist (char *key);

// KVS_CMD_SET:插入 kv
int kvs_array_set (char *key, char *value);

// KVS_CMD_GET:获取 key 对应的value
char *kvs_array_get(char *key);

// KVS_CMD_COUNT:统计以及插入多少个 key
int kvs_array_count (void) ;

// KVS_CMD_DELETE:删除 key
int kvs_array_delete(char *key);

3.5 协议层

// 根据msg,解析其具体的命令协议
int kvs_parser_protocol (char *msg, char **tokens, int count) ;

/*分割msg,比如 msg为 SET NAME ZXM ,分割为SET,NAME,ZXM,分别存储在tokens[]
 * tokens[0]: SET	--------- 对应的是命令 cmd
 * tokens[1]: NAME	--------- 对应的是命令 key
 * tokens[2]: ZXM	--------- 对应的是命令 value
 */
int kvs_spilt_tokens (char **tokens, char *msg) ;

// 解析协议
int kvs_protocol (char *msg, int length); 

3.6 网络层

纯c版本的协程实现NtyCo

// 为每一个连接端口创建一个协程
nty_coroutine_create(&co, server, port); 

四、具体代码

4.1 kvstore.c

// gcc -o kvstore1 kvstore1.c -I ./NtyCo/core/ -L ./NtyCo/ -lntyco -lpthread -ldl 

#include "nty_coroutine.h"

#include <arpa/inet.h>

#define TIME_SUB_MS(tv1, tv2)  ((tv1.tv_sec - tv2.tv_sec) * 1000 + (tv1.tv_usec - tv2.tv_usec) / 1000)

// array: set, get, count, delete, exist
// rbtree: rset, rget, rcount, rdelete, rexist
// hash: hset, hget, hcount, hdelete, hexist
// skiptable: zset, zget, zcount, zdelete, zexist



//---------------------------------------------------------------------------------------------
//-------------------------------------- 接口层 -----------------------------------------

typedef enum kvs_cmd_e {
	KVS_CMD_START = 0,

	// array
	KVS_CMD_SET = KVS_CMD_START,
	KVS_CMD_GET,
	KVS_CMD_COUNT,
	KVS_CMD_DELETE,
	KVS_CMD_EXIST,

	// rbtree
	KVS_CMD_RSET,
	KVS_CMD_RGET,
	KVS_CMD_RCOUNT,
	KVS_CMD_RDELETE,
	KVS_CMD_REXIST,

	// hash
	KVS_CMD_HSET,
	KVS_CMD_HGET,
	KVS_CMD_HCOUNT,
	KVS_CMD_HDELETE,
	KVS_CMD_HEXIST,

	// skiptable
	KVS_CMD_ZSET,
	KVS_CMD_ZGET,
	KVS_CMD_ZCOUNT,
	KVS_CMD_ZDELETE,
	KVS_CMD_ZEXIST,

	KVS_CMD_END	= KVS_CMD_ZEXIST,

	// 格式出错
	KVS_CMD_ERROR,
	// 断开
	KVS_CMD_QUIT,

} kvs_cmd_t;

// 命令集
const char *commands[] = {

	"SET", "GET", "COUNT", "DELETE", "EXIST",
	"RSET", "RGET", "RCOUNT", "RDELETE", "REXIST",
	"HSET", "HGET", "HCOUNT", "HDELETE", "HEXIST",
	"ZSET", "ZGET", "ZCOUNT", "ZDELETE", "ZEXIST"

};

// 为了以后优化的可能,把内存开辟释放封装
void *kvs_malloc(size_t size) {
	return malloc(size);
}

void kvs_free(void *ptr) {
	return free(ptr);
}


//-------------------------------------- array -----------------------------------------
#define KVS_ARRAY_ITEM_SIEZ 		1024

typedef struct kvs_array_item_s {
	char *key;
	char *value;
} kvs_array_item_t;

// kvs_array_table 存储插入的 kv
kvs_array_item_t kvs_array_table [KVS_ARRAY_ITEM_SIEZ] = {0};

// 查找 key 在 kvs_array_table 的位置
kvs_array_item_t *kvs_array_search_item (char *key){

	if (!key) return NULL;

	int i = 0;
	// 注意由于前面的 key 被删除而出现的 key == NULL , strcmp 不能与 NULL 比较
	for (i = 0;i < KVS_ARRAY_ITEM_SIEZ; i++) {
		if (kvs_array_table[i].key != NULL && strcmp(kvs_array_table[i].key , key) == 0) {
			return &kvs_array_table[i];
		}
	}

	return NULL;

}

// KVS_CMD_EXIST: 判断 key 是否存在,存在返回 1 
int kvs_array_exist (char *key) {
	if (kvs_array_search_item(key) != NULL) return 1;
}

// KVS_CMD_SET:插入 kv, 成功返回 0
int array_count = 0; 	// 已插入元素的个数
int kvs_array_set (char *key, char *value) {
	
	if (key == NULL || value == NULL || array_count == KVS_ARRAY_ITEM_SIEZ - 1) {
		return -1;
	}

	// key 已存在,不能插入
	if (kvs_array_exist(key)) {
		return -1;
	}

	char *kvs_key = kvs_malloc(strlen(key) + 1);
	if (kvs_key == NULL) return -1;
	strncpy(kvs_key, key, strlen(key) + 1);

	char *kvs_value = kvs_malloc(strlen(value) + 1);
	if (kvs_value == NULL) {
		free(kvs_key);
		return -1;
	}
	strncpy(kvs_value, value, strlen(value) + 1);

	int i = 0;
	for (i = 0;i < KVS_ARRAY_ITEM_SIEZ; i++) {
		if (kvs_array_table[i].key == NULL && kvs_array_table[i].value == 0) {
			break;
		}
	}

	kvs_array_table[i].key = kvs_key;
	kvs_array_table[i].value = kvs_value;
	array_count++;

	return 0;
}

// KVS_CMD_GET:获取 key 对应的value
char *kvs_array_get(char *key) {
	kvs_array_item_t * item = kvs_array_search_item(key);
	if (item) {
		return item->value;
	}
	return NULL;
}

// KVS_CMD_COUNT:统计以及插入多少个 key
int kvs_array_count (void) {

	return array_count;

}

// KVS_CMD_DELETE:删除 key
int kvs_array_delete(char *key) {
	
	if (key == NULL) return -1;

	kvs_array_item_t * item = kvs_array_search_item(key);
	if (item == NULL) {
		return -1;
	}

	if (item->key) {
		kvs_free(item->key);
		item->key = NULL;
	}

	if (item->value) {
		kvs_free(item->value);
		item->value = NULL;
	}
	
	array_count--;

	return 0;

}



//---------------------------------------------------------------------------------------------
//-------------------------------------- 协议层 -----------------------------------------

#define MAX_TOKENS		32
#define CLINET_MSG_LENGTH		1024		// client发送msg的最大长度


// 根据msg,解析其具体的命令协议
int kvs_parser_protocol (char *msg, char **tokens, int count) {

	if (tokens == NULL || tokens[0] == NULL || count == 0) {
		return KVS_CMD_ERROR;
	}

	// 判断命令,即tokens[0],是否在命令集中
	int cmd = KVS_CMD_START;
	for (cmd = KVS_CMD_START; cmd < KVS_CMD_END; cmd++) {
		if (strcmp(tokens[0], commands[cmd]) == 0) {
			break;
		}
	}

	// 根据cmd,选择对应的命令
	switch (cmd) {
	case KVS_CMD_SET: {
		assert(count == 3);		// SET NAME ZXM,应该有三个

		int ret = 0;
		int res = kvs_array_set(tokens[1], tokens[2]);
		if (!res) {
			memset(msg, 0, CLINET_MSG_LENGTH);
			ret = snprintf(msg, CLINET_MSG_LENGTH, "SUCCESS\r\n");
		} else {
			memset(msg, 0, CLINET_MSG_LENGTH);
			ret = snprintf(msg, CLINET_MSG_LENGTH, "FAILED\r\n");			
		}
		return ret;
	}

	case KVS_CMD_GET: {
		assert(count == 2);

		int ret = 0;
		char *value = kvs_array_get(tokens[1]);
		if (value) {
			memset(msg, 0, CLINET_MSG_LENGTH);
			ret = snprintf(msg, CLINET_MSG_LENGTH, "%s\r\n", value);
		} else {
			memset(msg, 0, CLINET_MSG_LENGTH);
			ret = snprintf(msg, CLINET_MSG_LENGTH, "FAILED: NO EXIST\r\n");
		}

		return ret;
	}

	case KVS_CMD_COUNT: {
		assert(count == 1);

		int res = kvs_array_count();
		
		memset(msg, 0, CLINET_MSG_LENGTH);
		int ret = snprintf(msg, CLINET_MSG_LENGTH, "%d\r\n", res);

		return ret;
	}

	case KVS_CMD_DELETE: {
		assert(count == 2);

		int ret = 0;
		int res = kvs_array_delete(tokens[1]);
		if (!res) {
			memset(msg, 0, CLINET_MSG_LENGTH);
			ret = snprintf(msg, CLINET_MSG_LENGTH, "SUCCESS\r\n");
		} else {
			memset(msg, 0, CLINET_MSG_LENGTH);
			ret = snprintf(msg, CLINET_MSG_LENGTH, "FAILED\r\n");
		}
		return ret;
	}
	case KVS_CMD_EXIST: {
		assert(count == 2);

		int res = kvs_array_exist(tokens[1]);
		
		memset(msg, 0, CLINET_MSG_LENGTH);
		int ret = snprintf(msg, CLINET_MSG_LENGTH, "%d\r\n", res);

		return ret;
	}

	}

	return 0;
}


/*分割msg,比如 msg为 SET NAME ZXM ,分割为SET,NAME,ZXM,分别存储在tokens[]
 * tokens[0]: SET	--------- 对应的是命令 cmd
 * tokens[1]: NAME	--------- 对应的是命令 key
 * tokens[2]: ZXM	--------- 对应的是命令 value
 */
int kvs_spilt_tokens (char **tokens, char *msg) {
	
	char *token = strtok(msg, " ");   // 按空格分割

	int count  = 0;
	while (token != NULL) {
		tokens[count++] = token;
		token  = strtok(NULL, " "); 
	}

	return count;
}


// 解析协议
int kvs_protocol (char *msg, int length) {

	char *tokens[MAX_TOKENS] = {0};
	// 分割msg
	int count = kvs_spilt_tokens(tokens, msg);

	// 根据分割后的 msg ,解析其具体命令协议
	// msg:命令    tokens:分割后的 msg   
	return kvs_parser_protocol(msg, tokens, count);

}




//---------------------------------------------------------------------------------------------
//--------------------------------------NtyCo底层的协程-----------------------------------------

void server_reader(void *arg) {
	int fd = *(int *)arg;
	int ret = 0;

	while (1) {
		
		char buf[CLINET_MSG_LENGTH] = {0};
		// 接收msg,存放到buf中
		ret = nty_recv(fd, buf, CLINET_MSG_LENGTH, 0);
		if (ret > 0) {
			printf("read from server: %.*s\n", ret, buf);

			// 根据协议解析msg: rec: SET NAME ZXM  
			// 解析后:SET\r\n NAME\r\n ZXM\r\n
			ret = kvs_protocol(buf, ret);

			// 发送解析后的msg
			ret = nty_send(fd, buf, ret, 0);
			if (ret == -1) {
				nty_close(fd);
				break;
			}
		} else if (ret == 0) {	
			nty_close(fd);
			break;
		}

	}
}


void server(void *arg) {

	unsigned short port = *(unsigned short *)arg;
	free(arg);

	int fd = nty_socket(AF_INET, SOCK_STREAM, 0);
	if (fd < 0) return ;

	struct sockaddr_in local, remote;
	local.sin_family = AF_INET;				// 设置地址族为IPv4
	local.sin_port = htons(port);			// 设置端口号
	local.sin_addr.s_addr = INADDR_ANY;		// 设置IP地址, INADDR_ANY 是一个常量,表示可以接受来自任意 IP 地址的连接。
	bind(fd, (struct sockaddr*)&local, sizeof(struct sockaddr_in));

	listen(fd, 20);
	printf("listen port : %d\n", port);

	//获取当前的时间和日期信息
	struct timeval tv_begin;
	gettimeofday(&tv_begin, NULL);

	while (1) {
		socklen_t len = sizeof(struct sockaddr_in);
		int cli_fd = nty_accept(fd, (struct sockaddr*)&remote, &len);
		if (cli_fd % 1000 == 999) {

			struct timeval tv_cur;
			memcpy(&tv_cur, &tv_begin, sizeof(struct timeval));
			
			gettimeofday(&tv_begin, NULL);
			int time_used = TIME_SUB_MS(tv_begin, tv_cur);
			
			printf("client fd : %d, time_used: %d\n", cli_fd, time_used);
		}
		printf("new client comming\n");

		nty_coroutine *read_co;
		nty_coroutine_create(&read_co, server_reader, &cli_fd);

	}
	
}



int main(int argc, char *argv[]) {
	nty_coroutine *co = NULL;

	unsigned short base_port = 9999;

	unsigned short *port = calloc(1, sizeof(unsigned short));
	*port = base_port ;

	// 为每一个连接端口创建一个协程
	nty_coroutine_create(&co, server, port); 


	nty_schedule_run(); //run

	return 0;
}


4.2 测试案例

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

#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>

#define MAX_MSG_LENGTH 		1024

int connect_kvstore(const char *ip ,int port){

	int connfd = socket(AF_INET, SOCK_STREAM, 0);

	struct sockaddr_in kvs_addr;
	memset(&kvs_addr, 0, sizeof(struct sockaddr_in)); 

	kvs_addr.sin_family = AF_INET;
	kvs_addr.sin_addr.s_addr = inet_addr(ip); // inet_addr把ip转为点分十进制
	kvs_addr.sin_port = htons(port);

	int ret = connect(connfd, (struct sockaddr*)&kvs_addr, sizeof(struct sockaddr_in));
	// ret = 0,成功
	if (ret) { 
		perror("connect\n");
		return -1;
	}

	return connfd;
}

// 发送msg
int send_msg(int connfd, char *msg) {
	int res = send(connfd, msg, strlen(msg), 0);
	if (res < 0) {
		exit(1);
	}

	return res;
}

// 接收数据并存入msg
int recv_msg(int connfd, char *msg) {
	int res = recv(connfd, msg, MAX_MSG_LENGTH, 0);
	if (res < 0){
		exit(1);
	}

	return res;
}

// 对比接收到的结果 result 与 应返回的结果 pattern 是否一致
void equals (char *pattern, char *result, char *casename) {

	if (0 == strcmp(pattern, result)) {
		printf(">> PASS --> %s\n", casename);
	} else {
		printf(">> FAILED --> '%s' != '%s'\n", pattern, result);
	}
}

// cmd: 命令			pattern: 应返回的结果		casename:测试的名称
// 比如测试 SET NAME ZXM,cmd="SET NAME ZXM", pattern="SUCCESS\r\n", 测试的名称casename
void test_case (int connfd, char *cmd, char *pattern, char *casename) {

	char result[MAX_MSG_LENGTH] = {0};
	// 发送命令cmd
	send_msg(connfd, cmd);
	// 接收命令处理后的结果,存入result
	recv_msg(connfd, result);
	// 对比接收到的结果 result 与 应返回的结果 pattern 是否一致
	equals(pattern, result, casename);
}


void array_testcase(int connfd ){

	test_case(connfd, "SET Name zxm", "SUCCESS\r\n", "SetNameCase");
	test_case(connfd, "COUNT", "1\r\n", "COUNTCase");

	test_case(connfd, "SET Sex man", "SUCCESS\r\n", "SetNameCase");
	test_case(connfd, "COUNT", "2\r\n", "COUNT");

	test_case(connfd, "SET Score 100", "SUCCESS\r\n", "SetNameCase");
	test_case(connfd, "COUNT", "3\r\n", "COUNT");

	test_case(connfd, "SET Nationality China", "SUCCESS\r\n", "SetNameCase");
	test_case(connfd, "COUNT", "4\r\n", "COUNT");
	
	test_case(connfd, "EXIST Name", "1\r\n", "EXISTCase");
	test_case(connfd, "GET Name", "zxm\r\n", "GetNameCase");
	test_case(connfd, "DELETE Name", "SUCCESS\r\n", "DELETECase");
	test_case(connfd, "COUNT", "3\r\n", "COUNT");
	test_case(connfd, "EXIST Name", "0\r\n", "EXISTCase");

	test_case(connfd, "EXIST Sex", "1\r\n", "EXISTCase");
	test_case(connfd, "GET Sex", "man\r\n", "GetNameCase");
	test_case(connfd, "DELETE Sex", "SUCCESS\r\n", "DELETECase");
	test_case(connfd, "COUNT", "2\r\n", "COUNT");

	test_case(connfd, "EXIST Score", "1\r\n", "EXISTCase");
	test_case(connfd, "GET Score", "100\r\n", "GetNameCase");
	test_case(connfd, "DELETE Score", "SUCCESS\r\n", "DELETECase");
	test_case(connfd, "COUNT", "1\r\n", "COUNT");

	test_case(connfd, "EXIST Nationality", "1\r\n", "EXISTCase");
	test_case(connfd, "GET Nationality", "China\r\n", "GetNameCase");
	test_case(connfd, "DELETE Nationality", "SUCCESS\r\n", "DELETECase");
	test_case(connfd, "COUNT", "0\r\n", "COUNT");

}

int main(int argc, char *argv[]) {
	if (argc < 3){
		printf("argc < 3\n");
		return -1;
	}

	const char *ip = argv[1];
	int port = atoi(argv[2]);

	int connfd = connect_kvstore(ip, port);

	// array
	printf(" -----> array testcase <-------\n");
	array_testcase(connfd);



	close(connfd);
} 

4.3 结果展示

在这里插入图片描述


注:本专栏知识点是通过<零声教育>的系统学习,进行梳理总结写下文章,对c/c++linux课程感兴趣的读者,可以点击链接,详细查看详细的服务器课程链接 。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【JAVA】数组练习

⭐ 作者&#xff1a;小胡_不糊涂 &#x1f331; 作者主页&#xff1a;小胡_不糊涂的个人主页 &#x1f4c0; 收录专栏&#xff1a;浅谈Java &#x1f496; 持续更文&#xff0c;关注博主少走弯路&#xff0c;谢谢大家支持 &#x1f496; 数组练习 1. 数组转字符串2. 数组拷贝3.…

Maven基础之仓库、命令、插件机制

文章目录 Maven 仓库中央仓库和本地仓库中央仓库本地仓库 Maven 命令generate 命令compile 命令clean 命令test 命令package 命令install 命令 Maven 插件机制官方插件&#xff1a;Compile 插件Tomcat 7 插件 Maven 仓库 中央仓库和本地仓库 [✎] 简单一点说 中央仓库是一个网…

cs231n assignment2 q5 PyTorch on CIFAR-10

文章目录 嫌啰嗦直接看源码Q5 :PyTorch on CIFAR-10three_layer_convnet题面解析代码输出 Training a ConvNet题面解析代码输出 ThreeLayerConvNet题面解析代码输出 Train a Three-Layer ConvNet题面解析代码输出 Sequential API: Three-Layer ConvNet题面解析代码输出 CIFAR-1…

DevOps系列文章之 GitlabCICD自动化部署SpringBoot项目

一、概述 本文主要记录如何通过Gitlab CI/CD自动部署SpringBoot项目jar包。 二、前期准备 准备三台 CentOS7服务器&#xff0c;分别部署以下服务&#xff1a; 序号系统IP服务1CentOS7192.168.56.10Gitlab2CentOS7192.168.56.11Runner &#xff08;安装Docker&#xff09;3Cen…

docker的入门使用—太详细了

docker的使用 一、Docker概念跟普通虚拟机的对比概念Docker部署的优势使用docker的简要介绍dockerfile和docker-compose区别 二、docker命令介绍&#xff1a;1、构建镜像和运行2、 常用命令 三、镜像加速源四、安装五、Docker快速安装软件演示 Docker 安装 Redis安装 Wordpress…

kubernetes中PV和PVC

目录 一、PV、PVC简介 二、PV、PVC关系 三、创建静态PV 1.配置nfs存储 2.定义PV 3.定义PVC 4.测试访问 四、 搭建 StorageClass nfs-client-provisioner &#xff0c;实现 NFS 的动态 PV 创建 1. 配置nfs服务 2.创建 Service Account 3.使用 Deployment 来创建 NFS P…

Unity Bolt使用协程等待

使用Unity bolt插件可以进行一些简单逻辑开发。本质上相当于把C#接口以图形化的方式进行调用。但是怎么使用协程进行等待呢。经过一些研究&#xff0c;可以使用继承WaitUnit的组件方式进行扩展。下面是具体的操作步骤。 1&#xff1a;等待组件扩展。 经过查找&#xff0c;Bol…

成人自考-英语二-名词详细介绍

感谢内容提供者&#xff1a;金牛区吴迪软件开发工作室 文章目录 一、名词简介1.常见名词后缀 二、什么时候空格里填入名词三、名词分类1.可数名词(1)规则变化(2)规则变化中的特例(3)不规则变化 2.不可数名词(1)物质名词(2)抽象名词 四、描述数量1.修饰可数名词2.修饰不可数名词…

WS2812B————动/静态显示

一&#xff0c;系统架构 二&#xff0c;芯片介绍 1.管脚说明 2.数据传输时间 3.时序波形 4.数据传输方法 5.常用电路连接 三&#xff0c;代码展示及说明 驱动模块 在驱动模块首先选择使用状态机&#xff0c;其中包括&#xff0c;空闲状态&#xff0c;复位清空状态&#xff0c…

性能场景和性能需求指标

目录 一 性能场景 1、基准性能场景 2、容量性能场景 3、稳定性性能场景 4、异常性能场景 二 性能需求指标 1、业务指标 2、技术指标 2.1 时间指标 RT 2.2 容量指标 TPS 2.3 资源利用率 3、指标之间的关系 “TPS”与“响应时间” “用户数”与“TPS”与“压力工具中…

【量化课程】08_2.深度学习量化策略基础实战

文章目录 1. 深度学习简介2. 常用深度学习模型架构2.1 LSTM 介绍2.2 LSTM在股票预测中的应用 3. 模块分类3.1 卷积层3.2 池化层3.3 全连接层3.4 Dropout层 4. 深度学习模型构建5. 策略实现 1. 深度学习简介 深度学习是模拟人脑进行分析学习的神经网络。 2. 常用深度学习模型架…

DevOps系列文章 之 SpringBoot整合GitLab-CI实现持续集成

在企业开发过程中&#xff0c;我们开发的功能或者是修复的BUG都需要部署到服务器上去&#xff0c;而这部分部署操作又是重复且繁琐的工作&#xff0c;GitLab-CI 持续集成为我们解决了这一痛点&#xff0c;将重复部署的工作自动化&#xff0c;大大的节省了程序员们的宝贵时间。本…

群晖 NAS 十分精准的安装 Mysql 远程访问连接

文章目录 1. 安装Mysql2. 安装phpMyAdmin3. 修改User 表4. 本地测试连接5. 安装cpolar6. 配置公网访问地址7. 固定连接公网地址 转载自cpolar极点云文章&#xff1a;群晖NAS 安装 MySQL远程访问连接 群晖安装MySQL具有高效、安全、可靠、灵活等优势&#xff0c;可以为用户提供一…

C++初阶之一篇文章教会你queue和priority_queue(理解使用和模拟实现)

queue和priority_queue&#xff08;理解使用和模拟实现&#xff09; 什么是queuequeue的使用1.queue构造函数2.empty()3.size()4.front()5.back();6.push7.emplace8.pop()9.swap queue模拟实现什么是priority_queuepriority_queue的使用1.priority_queue构造函数1.1 模板参数 C…

如何切换goland之中的版本号(升级go 到1.20)

go 安装/版本切换_go 切换版本_云满笔记的博客-CSDN博客 用brew就行&#xff1a; echo export PATH"/opt/homebrew/opt/go1.20/bin:$PATH" >> ~/.zshrc

无涯教程-Perl - scalar函数

描述 此函数强制EXPR的判断在标量context中进行,即使它通常在列表context中也可以使用。 语法 以下是此函数的简单语法- scalar EXPR返回值 此函数返回标量。 例 以下是显示其基本用法的示例代码- #!/usr/bin/perl -wa (1,2,3,4); b (10,20,30,40);c ( a, b ); prin…

如何与 Anheuser-Busch 建立 EDI 连接?

Anheuser-Busch 于 1852 年创立&#xff0c;总部位于美国密苏里州圣路易斯市&#xff0c;出产的百威啤酒(Budweiser)名扬世界&#xff0c;深受各国消费者喜爱。推进数字化转型&#xff0c;科技赋能降费增效。在诸多举措之中&#xff0c;可以看到 EDI 的身影&#xff0c;借助 ED…

Java智慧工地APP源码带AI识别

智慧工地为建筑全生命周期赋能&#xff0c;用创新的可视化与智能化方法&#xff0c;降低成本&#xff0c;创造价值。 一、智慧工地APP概述 智慧工地”立足于互联网&#xff0c;采用云计算&#xff0c;大数据和物联网等技术手段&#xff0c;针对当前建筑行业的特点&#xff0c;…

Apipost接口自动化控制器使用详解

测试人员在编写测试用例以及实际测试过程中&#xff0c;经常会遇到两个棘手的问题&#xff1a; •稍微复杂一些的自动化测试逻辑&#xff0c;往往需要手动写代码才能实现&#xff0c;难以实现和维护 •测试用例编写完成后&#xff0c;需要手动执行&#xff0c;难以接入自动化体…

对比学习论文综述总结

第一阶段:百花齐放(18-19中) 有InstDisc(Instance Discrimination)、CPC、CMC代表工作。在这个阶段方法模型都还没有统一,目标函数也没有统一,代理任务也没有统一,所以说是一个百花齐放的时代 1 判别式代理任务---个体判别任务 1.1 Inst Dict---一个编码器+一个memory…