2024年3月

第1章  单网卡环境修改网卡名

■ 修改网卡配置。

• 检查当前网卡名称和MAC
地址,网卡名称
ens
33

M
AC地址00:0c:29:ab:3a:40。

[root@localhost ~]# ip a1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00inet127.0.0.1/8scope host lo

valid_lft forever preferred_lft forever

inet6 ::
1/128scope host

valid_lft forever preferred_lft forever
2: ens33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000link/ether 00:0c:29:ab:3a:40brd ff:ff:ff:ff:ff:ff

inet
192.168.1.161/24 brd 192.168.1.255scope global noprefixroute ens33

valid_lft forever preferred_lft forever

inet6 fe80::588d:c898:
3370:2cca/64scope link noprefixroute

valid_lft forever preferred_lft forever

• 修改网卡的配置文件名称。

[root@localhost ~]# cd /etc/sysconfig/network-scripts/[root@localhost network-scripts]# mv ifcfg-ens33 ifcfg-eth0

• 修改网卡配置内容,将网卡和绑定设备名改成eth0,其他不用修改。

[root@localhost network-scripts]# vi ifcfg-eth0

NAME
=eth0

DEVICE
=eth0

[root@localhost network
-scripts]#

■ 修改BIOS参数。

• 更新内核文件,于"GRUB_CMDLINE_LINUX="行尾额外添加"net.ifnames=0 biosdevname=0"两条参数。

[root@localhost ~]# vi /etc/default/grub

GRUB_CMDLINE_LINUX
="resume=/dev/mapper/klas-swap rd.lvm.lv=klas/root rd.lvm.lv=klas/swap rhgb quiet crashkernel=1024M,high audit=0 net.ifnames=0 biosdevname=0"

• 检查系统引导启动方式。

[root@localhost ~]# ll /sys/firmware/efils: cannot access '/sys/firmware/efi': No such fileor directory

[root@localhost
~]# dmesg | grep "EFI v"[root@localhost~]#

说明:
一般系统有
U
EFI
和传统
B
IOS两种引导启动方式,如上述命令中,没有/sys/firmware/efi
文件,并且
dmesg命令没有E
FI
关键词输出,说明当前系统是传统
B
IOS
方式引导启动;如果有目录和关键词输出,说明是
U
EFI启动。

• 重新生成内核引导文件。

[root@localhost ~]# grub2-mkconfig -o /boot/grub2/grub.cfg



#执行成功输出如下:

Generating grub configuration
file...

Found linux image:
/boot/vmlinuz-4.19.90-52.22.v2207.ky10.x86_64

Found initrd image:
/boot/initramfs-4.19.90-52.22.v2207.ky10.x86_64.img

Found linux image:
/boot/vmlinuz-0-rescue-c30be0c3a35649f1b686342f9354e4aa

Found initrd image:
/boot/initramfs-0-rescue-c30be0c3a35649f1b686342f9354e4aa.imgdone注意:如果系统引导启动方式是UEFI,那么不能在/boot/grub2目录下生成内核引导文件,需要执行命令:“grub2-mkconfig -o /boot/efi/EFI/kylin/grub.cfg”,将内核文件生成到/boot/efi/EFI/kylin/下才能生效。

■ 修改udev,配置网卡名和MAC地址绑定。

• 注释第一行,并手动添加一行,通过网卡MAC
地址绑定
eth
0网卡名。

• [root@localhost ~]# vi /usr/lib/udev/rules.d/60-net.rules

#ACTION
=="add", SUBSYSTEM=="net", DRIVERS=="?*", ATTR{type}=="1", PROGRAM="/lib/udev/rename_device", RESULT=="?*", NAME="$result"ACTION=="add", SUBSYSTEM=="net", DRIVERS=="?*", ATTR{type}=="1", ATTR{address}=="00:0c:29:ab:3a:40", NAME="eth0"

■ 验证修改结果。

• 重启系统后配置生效。

[root@localhost ~]# reboot

• 检查网卡,成功将网卡名修改为eth0。

[root@localhost ~]# ip a1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00inet127.0.0.1/8scope host lo

valid_lft forever preferred_lft forever

inet6 ::
1/128scope host

valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000link/ether 00:0c:29:ab:3a:40brd ff:ff:ff:ff:ff:ff

inet
192.168.1.161/24 brd 192.168.1.255scope global noprefixroute eth0

valid_lft forever preferred_lft forever

inet6 fe80::
3445:a251:8e9:8c84/64scope link noprefixroute

valid_lft forever preferred_lft forever

第2章  多网卡环境修改网卡名

说明:
多网卡修改网卡名的步骤和单网卡步骤一致,不同点是
udev配置文件中需要加多行参数,对所有网卡名和网卡M
AC地址进行绑定。

■ 修改网卡配置。

• 检查当前网卡名称和MAC
地址,当前有
ens
33

ens3
7

ens3
8
,共
3块网卡。

[root@localhost network-scripts]# ip a1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00inet127.0.0.1/8scope host lo

valid_lft forever preferred_lft forever

inet6 ::
1/128scope host

valid_lft forever preferred_lft forever

2: ens33
: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000

link/ether 00:0c:29:ab:3a:40 brd ff:ff:ff:ff:ff:ff

inet 192.168.1.161/24 brd 192.168.1.255 scope global noprefixroute ens33

valid_lft forever preferred_lft forever

inet6 fe80::588d:c898:3370:2cca/64 scope link noprefixroute

valid_lft forever preferred_lft forever

3: ens37
: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000

link/ether 00:0c:29:ab:3a:4a brd ff:ff:ff:ff:ff:ff

inet 192.168.1.162/24 brd 192.168.1.255 scope global noprefixroute ens37

valid_lft forever preferred_lft forever

inet6 fe80::7820:36ad:e6f7:59c9/64 scope link noprefixroute

valid_lft forever preferred_lft forever

4: ens38
: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000

link/ether 00:0c:29:ab:3a:54 brd ff:ff:ff:ff:ff:ff

inet 192.168.1.163/24 brd 192.168.1.255 scope global noprefixroute ens38

valid_lft forever preferred_lft forever

inet6 fe80::f4e9:65e6:f38d:cba3/64 scope link noprefixroute

valid_lft forever preferred_lft forever

• 修改网卡的配置文件名称。

[root@localhost ~]# cd /etc/sysconfig/network-scripts/[root@localhost network-scripts]# mv ifcfg-ens33 ifcfg-eth0

[root@localhost network
-scripts]# mv ifcfg-ens37 ifcfg-eth1

[root@localhost network
-scripts]# mv ifcfg-ens38 ifcfg-eth2

• 修改网卡配置内容,将网卡和绑定设备名改成ethxx。

[root@localhost network-scripts]# vi ifcfg-eth0

NAME
=eth0

DEVICE
=eth0

[root@localhost network
-scripts]# vi ifcfg-eth1

NAME
=eth1

DEVICE
=eth1

[root@localhost network
-scripts]# vi ifcfg-eth2

NAME
=eth2

DEVICE
=eth2

[root@localhost network
-scripts]#

修改
B
IOS参数。

• 更新内核文件,于"GRUB_CMDLINE_LINUX="行尾额外添加"net.ifnames=0 biosdevname=0"两条参数。

[root@localhost ~]# vi /etc/default/grub

GRUB_CMDLINE_LINUX
="resume=/dev/mapper/klas-swap rd.lvm.lv=klas/root rd.lvm.lv=klas/swap rhgb quiet crashkernel=1024M,high audit=0 net.ifnames=0 biosdevname=0"

• 检查系统引导启动方式。

[root@localhost ~]# ll /sys/firmware/efils: cannot access '/sys/firmware/efi': No such fileor directory

[root@localhost
~]# dmesg | grep "EFI v"[root@localhost~]#

说明:
一般系统有UEFI和传统BIOS两种引导启动方式,如上述命令中,没有/sys/firmware/efi文件,并且dmesg命令没有EFI关键词输出,说明当前系统是传统BIOS方式引导启动;如果有目录和关键词输出,说明是UEFI启动。

• 重新生成内核引导文件。

[root@localhost ~]# grub2-mkconfig -o /boot/grub2/grub.cfg 

#执行成功输出如下:
Generating grub configuration
file...
Found linux image:
/boot/vmlinuz-4.19.90-52.22.v2207.ky10.x86_64

Found initrd image: /boot/initramfs-4.19.90-52.22.v2207.ky10.x86_64.img

Found linux image: /boot/vmlinuz-0-rescue-c30be0c3a35649f1b686342f9354e4aa

Found initrd image: /boot/initramfs-0-rescue-c30be0c3a35649f1b686342f9354e4aa.img

done

注意:
如果系统引导启动方式是
U
EFI
,那么不能在
/
boot/grub2
目录下生成内核引导文件,需要执行命令:

grub2-mkconfig -o /boot/efi/EFI/kylin/grub.cfg”,将内核文件生成到/boot/efi/EFI/kylin/下才能生效。

■ 修改udev,配置网卡名和MAC地址绑定。

• 注释第一行,并手动添加3行,通过网卡MAC
地址绑定
ethxx网卡名。

• [root@localhost ~]# vi /usr/lib/udev/rules.d/60-net.rules

#ACTION
=="add", SUBSYSTEM=="net", DRIVERS=="?*", ATTR{type}=="1", PROGRAM="/lib/udev/rename_device", RESULT=="?*", NAME="$result"ACTION=="add", SUBSYSTEM=="net", DRIVERS=="?*", ATTR{type}=="1", ATTR{address}=="00:0c:29:ab:3a:40", NAME="eth0"ACTION=="add", SUBSYSTEM=="net", DRIVERS=="?*", ATTR{type}=="1", ATTR{address}=="00:0c:29:ab:3a:4a", NAME="eth1"ACTION=="add", SUBSYSTEM=="net", DRIVERS=="?*", ATTR{type}=="1", ATTR{address}=="00:0c:29:ab:3a:54", NAME="eth2"

■ 验证修改结果。

• 重启系统后配置生效。

[root@localhost ~]# reboot

• 检查网卡,成功将网卡名修改为ethx。

[root@localhost ~]# ip a1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00inet127.0.0.1/8scope host lo

valid_lft forever preferred_lft forever

inet6 ::
1/128scope host

valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000link/ether 00:0c:29:ab:3a:40brd ff:ff:ff:ff:ff:ff

inet
192.168.1.161/24 brd 192.168.1.255scope global noprefixroute eth0

valid_lft forever preferred_lft forever

inet6 fe80::
3445:a251:8e9:8c84/64scope link noprefixroute

valid_lft forever preferred_lft forever
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000link/ether 00:0c:29:ab:3a:4a brd ff:ff:ff:ff:ff:ff

inet
192.168.1.162/24 brd 192.168.1.255scope global noprefixroute eth1

valid_lft forever preferred_lft forever

inet6 fe80::
6812:c44f:ab83:9d8a/64scope link noprefixroute

valid_lft forever preferred_lft forever
4: eth2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000link/ether 00:0c:29:ab:3a:54brd ff:ff:ff:ff:ff:ff

inet
192.168.1.163/24 brd 192.168.1.255scope global noprefixroute eth2

valid_lft forever preferred_lft forever

inet6 fe80::
5534:cbf5:8f66:963e/64scope link noprefixroute

valid_lft forever preferred_lft forever

第3章   网卡名故障处理

■ 说明:本故障处理方案适用于多网卡环境下,仅为某个或某几个网卡修改网卡名称。

1.1、 现象描述


需求:当前系统部署
k8s集群,需要k8s集群中所有节点网卡名称统一为eth
0
,虽然系统中有多块网卡,但是只有最后一块网卡是
U
P
状态,现在需要将最后一块网卡名更改为
eth
0。


问题现象:如下图所示,在
udev中将最后一块网卡名称更改为eth
0
,重启后网卡名变成了
rename
4,并且IP
地址在
D
OWN的网卡上,导致网络不通。

1.2、 问题分析


因为内核参数中增加了

net.ifnames=0 biosdevname=0

,重启系统生效后会自动分配
ethx的名称给每一块网卡,当前eth
0
名称已经被第一块网卡占用,所以修改
udev后会发生冲突,导致最后一块网卡名变为rename
4。

1.3、 解决思路

先将第一块网卡名通过
udev改名,如改成eth
8
,解决
eth
0
名称占用问题;再将
rename
4
网卡名改成
eth
0,恢复网络故障。

1.4、 处理步骤

• 将第一块网卡名称修改为eth8
,使用第一块网卡的
M
AC
地址修改网卡名,修改
udev需要重启系统生效。

[root@localhost ~]# vi /usr/lib/udev/rules.d/60-net.rules

#ACTION
=="add", SUBSYSTEM=="net", DRIVERS=="?*", ATTR{type}=="1", PROGRAM="/lib/udev/rename_device", RESULT=="?*", NAME="$result"ACTION=="add", SUBSYSTEM=="net", DRIVERS=="?*", ATTR{type}=="1", ATTR{address}=="00:0c:29:ab:3a:40", NAME="eth8"[root@localhost~]# reboot

重启后如下图,第一块网卡名更改为
eth
8:

将rename4
网卡名称修改为
eth
0
,并检查网卡配置文件
N
AME

D
EVICE
是否为
eth
0。

[root@localhost ~]# vi /usr/lib/udev/rules.d/60-net.rules
#ACTION
=="add", SUBSYSTEM=="net", DRIVERS=="?*", ATTR{type}=="1", PROGRAM="/lib/udev/rename_device", RESULT=="?*", NAME="$result"ACTION=="add", SUBSYSTEM=="net", DRIVERS=="?*", ATTR{type}=="1", ATTR{address}=="00:0c:29:ab:3a:40", NAME="eth8"ACTION=="add", SUBSYSTEM=="net", DRIVERS=="?*", ATTR{type}=="1", ATTR{address}=="00:0c:29:ab:3a:54", NAME="eth0"
[root@localhost ~]# cat /etc/sysconfig/network-scripts/ifcfg-eth0
TYPE
=Ethernet
PROXY_METHOD
=none
BROWSER_ONLY
=no
BOOTPROTO
=none
DEFROUTE
=yes
IPV4_FAILURE_FATAL
=no
IPV6INIT
=yes
IPV6_AUTOCONF
=yes
IPV6_DEFROUTE
=yes
IPV6_FAILURE_FATAL
=no
IPV6_ADDR_GEN_MODE
=stable-privacy
NAME
=eth0
DEVICE
=eth0
ONBOOT
=yes
IPADDR
=192.168.1.163NETMASK=255.255.255.0GATEWAY=192.168.1.254[root@localhost~]# reboot


验证故障恢复,如下图所示,状态为
U
P
的网卡更改为
eth
0,并恢复网络故障。

API接口是不同软件系统之间进行通信的重要方式,良好的API接口设计规范可以提高系统的可维护性、可扩展性和易用性。本文介绍了一套详细的API接口开发规范,包括命名规范、请求和响应规范、安全规范等内容,旨在帮助开发团队统一规范API接口的设计和实现。

一、命名规范

URL命名规范

使用小写字母和短横线来命名URL路径,不要使用大写字母或下划线。
使用名词表示资源,使用复数形式表示集合资源,例如:/users表示用户集合,/users/{id}表示单个用户。

接口命名规范

使用动词表示操作,例如:GET用于获取资源,POST用于创建资源,PUT用于更新资源,DELETE用于删除资源。
使用小写字母和下划线来命名接口,例如:get_user_info、create_user。

请求规范

请求方法

使用标准的HTTP请求方法来定义接口操作,包括GET、POST、PUT、DELETE等。

请求参数

使用URL参数传递查询参数,使用请求体传递复杂数据。
对于GET请求,避免使用过多的查询参数,可以考虑使用分页参数来控制数据量。
对于POST和PUT请求,使用JSON格式或表单形式传递数据。

请求头

使用标准的HTTP请求头,如Content-Type、Authorization等。
对于需要身份验证的接口,使用Bearer Token进行身份验证。

响应规范

响应状态码

使用标准的HTTP状态码来表示请求的处理结果,如200表示成功,400表示请求参数错误,401表示未授权,404表示资源不存在,500表示服务器内部错误等。

响应体

使用JSON格式返回响应数据,使用统一的数据结构表示响应体,包括code、message和data字段。
code字段表示请求处理结果的状态码,message字段用于返回请求处理的相关信息,data字段用于返回请求结果的数据。

{
    "code": 200,
    "message": "请求处理成功",
    "data": {
        "id": 1,
        "name": "John Doe",
        "email": "john@example.com"
    }
}

安全规范

身份认证

对于需要身份认证的接口,使用JWT或OAuth2.0等标准认证协议进行身份验证。
在请求头中添加Authorization字段,并使用Bearer Token进行身份认证。

参数验证

对于接口的输入参数进行有效性验证,包括参数类型、长度、格式等。
在接口文档中明确指定每个参数的验证规则和取值范围。

异常处理

错误处理

对于可能发生的异常情况进行适当的处理,并返回相应的错误信息。
使用统一的错误码和错误消息,便于客户端进行错误处理和调试。

异常日志

记录接口调用过程中发生的异常信息,并将异常日志持久化存储。
异常日志应包括异常类型、发生时间、请求信息等关键信息,便于排查问题和分析原因。

文档规范

接口文档

编写详细的接口文档,包括接口的URL、请求方法、请求参数、响应状态码、响应体等信息。
使用Swagger、OpenAPI等工具自动生成接口文档,保持文档与代码的同步更新。

示例代码

为每个接口提供示例代码,包括Python、Java、JavaScript等不同语言的示例代码。
示例代码应包括接口调用的完整流程,便于开发人员快速上手和使用接口。

版本管理

对API接口进行版本管理,使用URL路径或请求头等方式指定接口版本。
在接口文档中明确指定每个接口的版本号和更新内容,便于客户端进行版本适配和升级。

性能优化

对接口进行性能测试和压力测试,发现和解决潜在的性能瓶颈和问题。
使用缓存、异步处理等技术来优化接口性能,提高系统的响应速度和吞吐量。

结语

API接口是软件系统之间进行通信和交互的重要方式,良好的API接口设计规范可以提高系统的可维护性、可扩展性和易用性。本文介绍了一套详细的API接口开发规范,旨在帮助开发团队统一规范API接口的设计和实现,提高团队的协作效率和开发质量。

备注

{
    "status": "success",
    "code": 200,
    "data": {
        "list": [
            {
                "title": "num 1"
            },
            {
                "title": "num 2"
            }
        ],
        "user": {
            "id": 123,
            "username": "john_doe",
            "email": "john.doe@example.com",
            "created_at": "2024-03-05T12:00:00Z",
            "updated_at": "2024-03-05T14:30:00Z"
        }
    }
}
{
    "status": "error",
    "code": 404,
    "message": "未找到"
}

在 PostgreSQL 中,
bytea_output
参数控制在查询结果中
bytea
类型的显示格式。默认情况下,
bytea_output
的值为
hex
,这意味着在查询结果中,
bytea
类型的数据以十六进制格式显示。但是,如果你的应用程序期望以二进制格式获取图像数据,则将
bytea_output
设置为
escape
可能更适合。无论
bytea_output
参数设置为
hex
还是
escape
,你都可以通过 C# 访问 PostgreSQL 数据库,并且正常获取并显示图片。本篇随笔介绍这个问题的处理过程。

1、碰到的数据库图片在界面显示问题

在我们的Winform框架中,由于底层是支持多种数据库的设计,因此可以兼容MS SQLServer、Oracle、Mysql、PostgreSQL、SQLite等数据库的,但是一般我们用的是SQLServer、MySql居多,有客户切换到PostgreSQL数据库的时候,发现图片显示不正常,需要对图片进行十六进制转换才能正常显示。

默认的方式,这里方框在SQLServer等数据库上是正常显示图标的,打开编辑也是可以展示菜单的图表的,不过由于切换到PostgreSQL后,这里图标消失,检查数据库操作,默认的处理都是一致的,因此考虑是否为数据库参数配置问题。

2、解决问题

打开ChatGPT,或者百度、Google一下,细心都可以发现,在 PostgreSQL 中默认情况下,
bytea_output
的值为
hex
,这意味着在查询结果中,
bytea
类型的数据以十六进制格式显示。如果你的应用程序期望以二进制格式获取图像数据,则将
bytea_output
设置为
escape
可能更适合。

我们找到PostgreSQL的安装目录,找到 C:\Program Files\PostgreSQL\13\data\postgresql.conf里面的数据库配置文件,找到
bytea_output
的值查看。

果然发现其默认值为hex,我们按要求修改为 escape,并去掉注释符号#,如下所示。

重启PostgreSQL,并测试系统数据库,显示和保存处理正常。

3、两种方式处理的差异

如果
bytea_output
参数设置为
hex
,你可以通过将读取到的十六进制字符串转换为字节数组,然后使用这些字节数组来创建图像对象。以下是一个示例代码,演示了如何在 C# 中获取并显示图片,即使
bytea_output
参数设置为
hex

classProgram
{
static voidMain()
{
string connString = "Host=myServerAddress;Port=myPort;Username=myUsername;Password=myPassword;Database=myDatabase";using (var conn = newNpgsqlConnection(connString))
{
conn.Open();
//执行 SQL 查询以获取图像数据 string sql = "SELECT image_column FROM your_table WHERE id = @id";int id = 1; //替换为你要查询的图像的 ID using (var cmd = newNpgsqlCommand(sql, conn))
{
cmd.Parameters.AddWithValue(
"@id", id);//读取图像数据 using (var reader =cmd.ExecuteReader(CommandBehavior.SequentialAccess))
{
if(reader.Read())
{
//获取十六进制字符串 string hexString = reader.GetString(0);//将十六进制字符串转换为字节数组 byte[] imageData =StringToByteArray(hexString);//创建图像对象 using (MemoryStream ms = newMemoryStream(imageData))
{
Image image
=Image.FromStream(ms);//显示图像 ShowImage(image);
}
}
}
}
}
}
static voidShowImage(Image image)
{
//创建一个新的窗体 using (var form = newSystem.Windows.Forms.Form())
{
//创建 PictureBox 控件 var pictureBox = newSystem.Windows.Forms.PictureBox();
pictureBox.Dock
=System.Windows.Forms.DockStyle.Fill;
pictureBox.Image
=image;//将 PictureBox 添加到窗体中 form.Controls.Add(pictureBox);//设置窗体大小并显示 form.Size = newSystem.Drawing.Size(image.Width, image.Height);
form.ShowDialog();
}
}
static byte[] StringToByteArray(stringhex)
{
int NumberChars = hex.Length / 2;byte[] bytes = new byte[NumberChars];using (var sr = newStringReader(hex))
{
for (int i = 0; i < NumberChars; i++)
bytes[i]
= Convert.ToByte(new string(new char[2] { (char)sr.Read(), (char)sr.Read() }), 16);
}
returnbytes;
}
}

如果
bytea_output
参数设置为
escape
,则可以直接使用 Npgsql 从数据库中读取图像数据,并将其转换为字节数组,而不需要进行额外的处理。以下是示例代码:

usingNpgsql;usingSystem;usingSystem.Data;usingSystem.Drawing;usingSystem.IO;classProgram
{
static voidMain()
{
string connString = "Host=myServerAddress;Port=myPort;Username=myUsername;Password=myPassword;Database=myDatabase";using (var conn = newNpgsqlConnection(connString))
{
conn.Open();
//执行 SQL 查询以获取图像数据 string sql = "SELECT image_column FROM your_table WHERE id = @id";int id = 1; //替换为你要查询的图像的 ID using (var cmd = newNpgsqlCommand(sql, conn))
{
cmd.Parameters.AddWithValue(
"@id", id);//读取图像数据 using (var reader =cmd.ExecuteReader(CommandBehavior.SequentialAccess))
{
if(reader.Read())
{
//获取图像数据字节数组 byte[] imageData = (byte[])reader["image_column"];//创建图像对象 using (MemoryStream ms = newMemoryStream(imageData))
{
Image image
=Image.FromStream(ms);//显示图像 ShowImage(image);
}
}
}
}
}
}
static voidShowImage(Image image)
{
//创建一个新的窗体 using (var form = newSystem.Windows.Forms.Form())
{
//创建 PictureBox 控件 var pictureBox = newSystem.Windows.Forms.PictureBox();
pictureBox.Dock
=System.Windows.Forms.DockStyle.Fill;
pictureBox.Image
=image;//将 PictureBox 添加到窗体中 form.Controls.Add(pictureBox);//设置窗体大小并显示 form.Size = newSystem.Drawing.Size(image.Width, image.Height);
form.ShowDialog();
}
}
}

无论
bytea_output
参数设置为
hex
还是
escape
,你都可以通过 C# 访问 PostgreSQL 数据库,并且正常获取并显示图片。

概述

在昨天员外分享的
《TorchV的RAG实践分享(1)——RAG的定位、技术选型和RAG技术文章目录》
一文中介绍了TorchV的由来,也分享了我们的几个基线产品和应用架构的方向,我们想的是在创业的过程中,将我们自己的一些产品理念、技术心得都通过公众号发文的方式分享出来,更多的和行业内的专家们共同交流,这对我们自己也是一种提升和锻炼,也期待和客户一起共创成长,逐步把产品打磨好。

在目前大模型应用技术架构中,通过召回上下文来回答用户的问题是解决大模型当下的幻觉问题最靠谱/经济实惠的一种解决方案,RAG检索增强技术在整个LLM技术架构体系中的作用越来越明显。而检索召回和用户的query问句的质量则直接关系到最终大模型的生成结果。在向量数据库基础设施普及的今天,仅仅通过语义搜索召回已经无法满足企业级的需求,大家发现传统的搜索技术(基于关键词、词频等相关性的搜索)的作用也显得尤为重要,混合检索也成为了目前在RAG的技术架构中的主流检索方式,混合检索通过扬长避短的方式,在不同的业务应用场景中形成了很好的互补,对于不同的业务场景需求中,可以更灵活的进行配置满足业务需要,是RAG技术架构体系中非常重要的重要一环。

本文中所提到的混合检索主要是两种搜索技术的结合,主要如下:

  • 相关性搜索: 基于BM25、TF-IDF算法,主要适用于文本精确匹配的相关性匹配搜索,它在匹配特定术语(如产品名或专业术语)方面表现出色,但对拼写错误和同义词较为敏感,可能会忽略一些重要的上下文信息。
  • 语义搜索: 基于向量的Knn算法进行的语义检索,它能够基于用户的query语义含义进行多语言和多模态搜索,对拼写错误具有较好的容错性,但可能会忽视关键词。此外,它的效果依赖于向量嵌入的质量,并对非专业领域的术语较为敏感。

本文针对ElasticSearch中间件来实现整个外部知识库向量的存储和计算,在RAG技术架构中的混合检索进行探索和分析,结合我们自己的实际业务情况,如何通过底层的技术驱动,完善我们的产品设计,改善整个产品流程。

整篇文章主要包括:

  • 简介:简要概述ElasticSearch中间件以及在RAG技术架构的选型及实现
  • 算法理论:参数在混合检索过程中涉及的算法理论知识,面向的业务场景及选择方式
  • 召回Score分值计算:讲解ElasticSearch组件在召回计算过程中的Score分值规则及算法细节
  • TorchV产品驱动:技术推动我们TorchV产品的产品架构设计,如何影响产品流程
  • 结论:整篇的总结概述及参考文章

ElasticSearch简介

在介绍ElasticSearch的混合检索之前,我们需要先简单回顾ElasticSearch这个中间件如何在目前AI技术场景的落地情况

在目前的RAG大模型技术架构体系中,向量
Vector
技术已经作为大模型外挂知识库的非常重要的技术栈,向量的核心对于数据的表征(Embedding)然后执行相似度(Similarity)计算。2023年随着大模型技术的发布带火了非常多的向量数据库,特别是
LangChain

llama_index
等LLM数据应用框架的发布,包括:
Milvus

Qdrant

Pinecone

Chroma
等等专业的向量数据库中间件。向量数据结构的存储与计算可以说是当前做大模型应用的基建产品了,就好像传统软件工程中的数据库一样。

而对于ElasticSearch而言也同样如此,对于之前使用ElasticSearch中间件的开发人员而言,可能对于向量数据的存储和计算是比较陌生的,在传统软件工程用ES来储存搜索主要还是基于关键词搜索技术(BM25、TF-IDF)等实现,本质还是基于文本的精确匹配。而在最近ES组件发布的版本来看,特别是ES 8.0版本发布对于KNN算法搜索的优化支持来看,AI大模型这场技术革命风暴,似乎也不想袖手旁观。

我们选择ElasticSearch作为TorchV的基础RAG架构组件也是出于以下几个方面考虑:

  • 开箱即用的语义搜索功能以及一流的相关性检索(BM25/TF-IDF)算法实现
  • 区别于其它向量数据库所不具备的特有功能,包括:聚合、过滤、集群、分布式等等特性。
  • 多年的技术沉淀和社区发展,不同编程语言的生态完善成熟度等

在ElasticSearch的目前的版本中,要使用向量实现存储和计算对于开发者使用上非常简单,开发者在定义ES的索引结构时,定义向量字段类型
dense_vector
,并且自定义向量维度
dims
(最大维度不超过4096(自8.x版本开始)),如下索引结构:

PUTtest-001{"mappings":{"properties":{"my_vector":{"type":"dense_vector","dims":3},"my_text":{"type":"keyword"}}}}

在执行搜索时则可以通过
k-最近邻(KNN)搜索
找到与查询向量最近的K个向量结果值来获取结果,通过相似度值来衡量获取文档片段。

GETtest-001/_knn_search{"knn":{"field":"my_vector","query_vector":[0.3,0.1,1.2],"k":10,"num_candidates":100},"_source":["name","date"]}

而我们在前面提到,混合检索(语义搜索+相关性搜索)是目前做RAG的非常重要文档召回技术手段,纯KNN搜索并不能完全满足业务的需求,因此在当前的RAG技术架构体系中,ES在保持传统相关性搜索的基础上,增加对语义搜索的技术支持就显得很有冲击力,毕竟在向量搜索火爆之前,ES作为搜索引擎的老大哥,在企业级的产品应用体系中,应用范围还是非常广泛的。

算法&业务场景

在做混合检索时,我们会接触到两类算法,需要对算法有一个基础了解,这有助于我们在应用产品的技术体系中做决策:

  • 语义检索:基于向量空间的KNN算法
  • 相关性检索:传统的文本精确匹配方法,包括BM25、TF-IDF

语义检索(knn)

KNN算法
:k近邻算法,是机器学习算法中一种基本分类和回归方法。在给定的一个数据集中,对于新的数据实例,找到与该实例最邻近的k个实例,这k个实例的多数属于某个分类。

这就像你在一个陌生的城市,你可能会问周围的k个人哪家餐馆最好。如果大多数人都推荐同一家餐馆,那么你可能会选择去那家餐馆。

而我们在选择餐馆的过程中,每一个餐馆会有非常多的维度来描述这个餐馆的信息,包括:地理位置、菜系、价格、环境、口味等等,这一系列参数属性就是特征工程,目前的向量Embedding模型用来对一段文本进行Embedding,其实就是对于该文本内容的的特征信息进行提取描述。

这个时候,你会根据你自己的诉求,对于餐馆的不同特征要求,最终选择你要去哪家餐馆吃饭。

图1-KNN算法图例

在Elasticsearch中,KNN搜索主要通过使用向量相似度(特征空间中的两个实例点间的距离可以反映出两点间的相似程度)进行度量,文档根据向量数据集与查询向量的相似度进行排名。每个文档的
_score
将从相似度中得出,以确保分数为正并且分数越高对应于越高的排名。

ES目前主要提供了三种度量的标准供我们选择(考虑到本文是基于es,因此也只对该三种度量标准做介绍,对于其它的向量计算距离的方式,开发者可以自行搜索了解)

  • L2_norm(欧式距离):这是最常用的距离度量方式,它计算的是两个向量在笛卡尔坐标系中的直线距离。文档的
    score
    计算方式为:
    1 / (1 + l2_norm(query, vector)^2)
  • dot_product(点积):点积是两个向量的对应元素相乘然后求和,文档
    _score
    计算为
    (1 + dot_product(query, vector)) / 2
  • cosine(余弦相似度,default):计算两个向量余弦相似度,余弦相似性度量的是两个向量之间的角度,而不是距离。它的值范围是 -1 到 1,值越接近 1,表示两个向量越相似,文档
    _score
    计算为
    (1 + cosine(query, vector)) / 2

我们在开发RAG的大模型应用产品中,通常会将外部的知识库通过chunk分段存储处理,对于用户的query,通过Embedding模型进行表征为向量后,与chunk片段的向量进行距离计算,此时作为距离度量的方式考虑,那么根据实际的业务场景,就可以考虑上面的三种类型中的一种。

一般默认选择cosine余弦相似度进行计算召回,主要考虑:

  • 长度不敏感:在文本数据中,文档的长度可能会有很大的差异,这会影响到向量的长度。余弦相似性只关注向量的方向,而不关注长度,因此它对尺度不敏感,适合处理这种情况(虽然我们在使用向量Embedding模型进行表征时,向量的维度都是固定的)。
  • 方向敏感:在问答系统中,我们通常关心的是文档的主题或者内容是否相似,而不是文档的长度。余弦相似性度量的是两个向量之间的角度,可以很好地反映出文档的主题或者内容是否相似。
  • 高维数据:向量Embedding模型表征的高维度(768/1024/1536…等等)向量,适合余弦相似性适合处理这种高维稀疏的数据。

而ES自8.0版本发布后,同样也提供了对
KNN搜索
的优化,主要提供了两种策略:

  • 近邻KNN搜索算法(ANN):数据结构基于
    HNSW算法
    索引实现,近似 kNN 提供较低的延迟,但代价是索引速度较慢且准确性不完善(这也为后来RAG架构中的文档检索结果做ReRank重排埋下伏笔,可以关注员外的这篇
    《Rerank——RAG中百尺竿头更进一步的神器,从原理到解决方案》
    )。
  • 精确、强力的 kNN搜索(暴力搜索):基于函数实现,这种方式能够保证结果的准确性,通过计算
    script_score
    函数扫描每个匹配文档计算向量距离获取文档结果集,这会导致搜索速度缓慢(大数据集的应用场景下)。

开发者在选择KNN搜索的算法策略时,可以根据自己的实际业务需要进行抉择。

相关性检索(BM25/TF-IDF)

ES自5.0版本之后,针对文档的相关性评分机制默认采用了BM25相似度算法(之前是基于TF-IDF),BM25全称
Okapi BM25
。Okapi 是使用它的第一个系统的名称,即Okapi信息检索系统,于 20 世纪 80 年代和 1990 年代在伦敦城市大学实施。 BM则是best matching的缩写。

因此对于词的相关性检索方案,我们对于TF-IDF和BM25也需要有一个基础的了解。

TF-IDF(Term Frequency-Inverse Document Frequency):词频-逆文档频率是一个常用于信息检索和文本挖掘的权重计算方法,函数公式如下:

tf-idf

主要由两部分组成:

  • TF(Term Frequency,词频):衡量一个词在文档中出现的频率。假设某一词条在文本中出现的次数为
    n
    ,文本的总词条数为
    m
    ,则词频TF为n/m,也就是词频,比如一个单词:
    旅游
    在我们的一篇文档中出现了4次,而我们的文档总共包含的词条数量是100,那么词频的值就是
    4/100
    。词频越高,说明这个词在文档中越重要。
  • IDF(Inverse Document Frequency,逆文档频率):衡量一个词是否常见的度量。如果某个词在很多文档中都出现,那么它可能就不具有区分能力(比如常用词等)。它的计算公式是:log(文档总数(N) / 包含该词的文档数(df))。逆文档频率越大,说明这个词越不常见,可能就越重要。

TF-IDF就是将这两个值相乘,得到的结果就是一个词的权重,这个权重可以用来表示这个词对于文档的重要性,也可以用来比较不同文档的相似性。

TF-IDF在实践的发展中会存在一些问题:

  • 文档长度问题:在长文档中,某个词可能会因为文档本身的长度就有更高的出现次数,而不是因为这个词对于文档的主题更重要。这可能会导致TF-IDF对长文档中的词给予过高的权重,而忽视了短文档中的重要词
  • 词频不饱和:在实际的业务场景中,词的重要性并不总是随着它的出现次数线性增加的。例如,一个词在文档中出现100次可能并不意味着它比出现10次的词10倍重要,相反,对于IDF而言,如果一个词在文档集中出现的次数越少,那么它的IDF值就越高,被认为越重要,也并非一定符合实际业务场景。

这些问题都在BM25中得到了改进,BM25的词频部分使用了一个饱和函数,使得词频达到一定值后,增加词频对于最终得分的影响会变小。同时,BM25还考虑了文档长度的影响,通过一个归一化因子来调整不同长度文档中的词频。这使得BM25在处理词频未饱和和文档长度问题时,比TF-IDF有更好的性能。

BM25 的计算公式:

BM25

和TF-IDF的计算公式相比,BM25的公式着实有点吓人,不过其实我们关注几个核心的参数即可。

对于BM25算法在ElasticSearch中的应用公式和参数变量说明,可以参考这篇文章
《BM25 算法及其变量》
,这里我们只关心几个核心的参数

  • k1:控制非线性项频率归一化(词频饱和度)。默认值为
    1.2
    。较低的值导致较快的饱和,较高的值导致较慢的饱和。

Term frequency saturation for TF/IDF and BM25

  • b:该参数控制字段长度归一化的影响程度。b的值在0到1之间,当b为0时,表示完全不考虑字段长度的影响;当b为1时,表示完全考虑字段长度的影响。默认为
    0.75
    。这个参数值也是针对上面我们提到TF-IDF在文档长度未考虑的情况下一个加权计算,当然默认值
    0.75
    是官方基于大量的数据实验得到的一个值,在默认场景下都会有较好的效果,我们可以不用调整。如果我们的默认检索效果不佳,应该从其它方面来考虑优化,这个后面我们再说

Score分值计算&注意事项

在理解了算法、es中间件之后,结合实战+Score分值的计算使用过程,包括配合ES的Explain接口,讲清楚Score的计算规则,原理

在前面了解了ES的整个检索Score算法介绍之后,其实对于文本内容的检索召回Score分值计算,就比较清晰了,先说结论:

ElasticSearch在使用KNN+BM25检索的混合检索分值Score计算公式是:knn_score+bm25_score

使用ES混合检索的语法如下:

POSTimage-index/_search{"query":{"match":{"title":{"query":"mountain lake","boost":0.9}}},"knn":{//字段"field":"image-vector",//输入向量"query_vector":[54,10,-2],//k值"k":5,//每个分片要考虑的最近邻居候选数。不能超过10,000"num_candidates":50,//加权参数值"boost":0.1,//档被视为匹配所需的最小相似度,配合filter使用,提高检索效率"similarity":0.7,//过滤条件"filter":{"term":{"file-type":"png"}}},"size":10}

query
部分的检索所代表的是BM25算法的Score计算分值召回,而
knn
部分的检索所代表的则是语义向量空间的距离Score分值,最终的结果值相加后倒排的一个文档列表结果集

score=
match_score*0.9 + knn_score*0.1

BM25的Score

对于BM25算法的检索分值计算,开发者可以使用
Explain API
来查看整个
Score
的计算过程,整个计算过程就和BM25算法公式那样,如下图:

图2-BM25算法score解释接口

BM25算法会将用户输入的match参数,计算每一个分词的score分值,最终加起来,得到一个总的分值score数据,对于每一个分词,都可以通过该接口查看到完整的计算过程,是非常方便的开发者进行理解的。

在这里进行BM25计算时,我前面提到BM25算法可能存在检索不到最终我们说期望的文本,会有一些其它参数影响最终效果,并非需要更改算法中的k1和b这两个参数,主要考虑如下:

  • ES是一个分布式搜索和分析引擎,数据被分为多个分片(shards),每个分片可以在任何节点上存储。这使得数据可以在多个节点之间进行分布,从而提高系统的容量和性能,最终数据在存储构建索引的时候,es会均衡的进行分布存储,而在召回计算的过程中,数据也会从各个shards分片进行召回计算。开发者在创建索引(index)的时候,可以设置shards的分片为1或者3,来查看区别。
  • es默认提供了非常多的tokenizer分词器,而对于中文用户的使用者来说,哪些词该分,哪些词不该分,包括同义词的影响等等,都会影响整个Score分值的计算,在目前es的生态中,
    ik分词器
    可能是当下最成熟的一个plugin插件,ik提供了一个基础的词库,同时支持热更新,对于上层应用产品的设计融合,非常刚需。

KNN的Score

对于KNN的检索分值计算,就非常的简单了,开发者在构建用户索引的时候,选择具体的向量距离类型,es在计算knn的时候,就会根据其算法进行计算

PUTmy-index-2{"mappings":{"properties":{"my_vector":{"type":"dense_vector","dims":1024,//选择类型,cosine、dot_product、l2_norm"similarity":"cosine"}}}}

选择不同的类型, 就是单纯的向量距离计算了,按公式套用就可以了。

不过值得注意的是,对于使用最多的
cosine
的文档
_score
计算为
(1 + cosine(query, vector)) / 2

⚠️注意事项

当我们使用混合检索的时候,有一些注意事项值得我们关注:

  • 开发者在使用Explain API接口进行调试的时候,由于KNN的分值是单独计算,所以在分析的时候,不能有KNN的部分
  • KNN检索的参数,可以配置多个knn的向量查询值,另外
    filter
    过滤参数会提高检索的效率,但是提高检索效率的同时,由于总是会计算召回文档进行相似度计算,所以可以配合
    similarity
    来一起使用。

TorchV产品驱动&总结

对于混合检索,我们在算法层面有了直接的了解后,最终在产品层面会影响一些设计。

1、混合检索的权重设置:在上面的score分值计算公式中,我们其实知道es最终是通过
bm25*boost
+
knn*boost
,那么这个
boost
则可以影响我们最终的内容,因为并不是所有的客户和业务场景都适合knn检索,可能在其他关键的场景中,关键词检索会更适合(比如一些利用大模型做一些异步的任务提取,报告输出等等业务场景),我们在产品设计中则可以根据不同的客户诉求以及业务诉求,就可以设置这个
boost
来影响最终的召回结果天平,从而改善我们的产品效果。

在我们的TorchV的产品设计中,我们设计了一个
alpha
参数值,取值范围在0-1之间,具体来说:

  • alpha = 1
    :完全基于向量的搜索,也就是KNN近邻搜索
  • alpha = 0
    :完全基于关键词的搜索,基于ES的BM25算法检索

2、在BM25算法的场景中,分词是非常重要的一个特性,对于不同的行业客户,词库的收集建立对于产品应用的提升肯定是会有质的提升,也是每个公司做RAG产品的核心竞争力。

3、持续运营能力的重要性,RAG问答检索功能在技术架构迭代优化上是一个方面,但是运营能力同样重要,哪怕是ChatGPT4,在针对特殊的数据文件,如果数据混乱,知识库质量不高,那么同样回答准确率不会很好的,这在我们和客户进行沟通交流的同时,虽然RAG可能会给客户眼前一亮的感觉,但是持续的提升他的能力,发挥更大的作用,产品的持续运营能力是必不可少的。

参考

好了,全文完. 如果你也在关注大模型、RAG检索增强生成技术,欢迎关注我(公众号‬:八一菜刀‬)

数据类型

在JavaScript中,数据类型可以分为两类:基础数据类型、引用/复杂数据类型

1.基础类型:

  • String -> 表示文本类型,如
    "Hello World!"

  • Number -> 表示数字,可以是整数或者浮点数,例如
    3
    或者
    3.141592
    ,在JavaScript中,所有数字都是浮点数类型,即使没有小数部分

  • Boolean -> 表示逻辑实体,只有两个值:
    true

    false

  • Undefined -> 表示表里已被声明但未赋值,或者直接没有被声明

  • Null -> 表示一个为空的值

  • BigInt(ES2020引入) -> 表示大于
    2^53 - 1
    的整数,这是Number类型能安全表示的最大整数。BigInt类型的值通过在整数后面加n来创建,例如
    9007199254740991n

  • Symbol(ES6引入) --> 符号类型,表示唯一、不可变的数据值,常用于对象属性的键

2.对象/复杂类型

Object
,表示一个实例对象,是键值队的集合。在JavaScript中,几乎所有事物都是对象,包括数组、函数等。

  • Function
    ,特殊类型的对象,可调用,有时被视为一种数据结构,因为它们可以像对象一样具有属性和方法

  • Array
    ,特殊类型的对象,用于存储数据集合跟列表

此外,
Date

RegExp
(正则表达式)、
Map

Set

WeakMap

WeakSet
等都是特殊的对象类型,都可以用来存储和管理数据。


下面重点说一下
Symbol
类型,面试爱问o(╥﹏╥)o

1. 作为对象属性的键:

使用Symbol创建的变量始终是唯一的,把它作为属性的键,可以有效避免冲突或者意外覆盖问题

let price = {
    "影流之主": 6800,
    "影流之主": 4800,
}
console.log(price);//{ '影流之主': 4800 }

//可以看到打印出来的对象只有一个属性,下面改写一下:

const KEY_ONE = Symbol();
const KEY_TWO = Symbol();
let price2 = {
    [KEY_ONE]: 6800,
    [KEY_TWO]: 4800,
};
const MY_KEY = Symbol();
price2[MY_KEY] = 3600;
console.log(price2[KEY_ONE]);//6800
console.log(price2);//{ [Symbol()]: 6800, [Symbol()]: 4800, [Symbol()]: 3600 }
2. Symbol值的描述

它可以接受一个字符串作为参数,并且通过description属性可以获取到对应的描述,进而区分不同的Symbol

const MY_NAME = Symbol('MY_NAME');
const YOUR_NAME = Symbol('YOUR_NAME');
let person = {
    [MY_NAME]: '你比从前快乐KX',
    [YOUR_NAME]: '甜酒果子',
}
console.log(person[MY_NAME]);//你比从前快乐KX
console.log(person[YOUR_NAME]);//甜酒果子
console.log(MY_NAME.description);//MY_NAME
console.log(YOUR_NAME.description);//YOUR_NAME

//需要说明的是,带有相同参数的两个Symbol值不相等,这个参数只是表示Symbol值的描述而已

const id1 = Symbol('id');
const id2 = Symbol('id');
console.log(id1 === id2);//false
3. 隐藏属性

使用Symbol创建的变量作为对象的属性,不能被常规方法访问到,所以可以使用这一点将属性隐藏,造成一个假的私有属性现象

const MY_NAME = Symbol('MY_NAME');
const YOUR_NAME = Symbol('YOUR_NAME');
let person = {
    [MY_NAME]: '你比从前快乐KX',
    [YOUR_NAME]: '甜酒果子',
    'name': '张三',
}
console.log(Object.values(person));//[ '张三' ]
console.log(Object.keys(person));//[ 'name' ]
console.log(Object.getOwnPropertyNames(person));//[ 'name' ]
for (const key in person) {
    console.log(key);//name
}

可以遍历到Symbol的方法:

Object.getOwnPropertySymbols() :返回对象中只包含symbol类型key的数组

Reflect.ownKeys() :返回对象中所有类型key的数组(包含symbol)

console.log(Object.getOwnPropertySymbols(person));//[ Symbol(MY_NAME), Symbol(YOUR_NAME)
console.log(Reflect.ownKeys(person));//[ 'name', Symbol(MY_NAME), Symbol(YOUR_NAME) ]
4. Symbol自带的方法

Symbol.for(),在全局 Symbol 注册表中搜索键为 key 的 Symbol。如果找到,则返回它;否则,将创建一个与给定键相关联的新 Symbol。这使得多个独立的代码片段可以通过给定的键共享 Symbol。

Symbol.keyFor(),接受一个通过 Symbol.for 方法创建的 Symbol,并返回该 Symbol 注册表中的键。如果 Symbol 不是全局注册的,则返回 undefined。

由于Symbol创建的值独一无二,但有时候我们可能希望使用同一个Symbol值,这时候就可以通过
Symbol.for()
创建

const KEY1 = Symbol.for('KEY');
const KEY2 = Symbol.for('KEY');
console.log(KEY1 === KEY2);//true
console.log(Symbol.keyFor(KEY1));//KEY
console.log(Symbol.keyFor(KEY3));//undefined


3. 包装类型

在JavaScript中,包装类型(Wrapper Objects)不被视为独立的数据类型,而是存在于语言的运行时行为中,用于提供一种方式将原始数据类型(如 string、number、boolean、symbol、bigint)转换为对象。这样,原始值就可

以像对象一样使用,访问方法和属性。如下:

let heroName = 'LeBlanc';
console.log(heroName.toLowerCase());//leblanc
console.log(heroName.toString());//LeBlanc

在上面的例子中,字符串 heroName 是一个原始类型的值。当调用 .toLowerCase() 方法时,JavaScript临时将 heroName 包装为一个 String 对象,以便可以访问 .toLowerCase() 方法。方法调用完成后,返回的是一个新的原

始类型的字符串,而
临时创建的包装对象被丢弃

尽管包装对象在技术上不是JavaScript的基本数据类型,但它们是理解和使用语言中的原始值与对象之间互动的重要概念。然而,直接使用这些包装对象的构造函数来创建对象(例如,使用 new String("LeBlanc"))是
不推荐

的,因为它可能导致混淆和错误。在大多数情况下,最好让JavaScript自动处理原始值和对象之间的转换。


另外,如果你尝试在原始值上设置一个属性,JavaScript 会创建一个临时对象,就像访问属性或方法时一样,但是设置的属性不会保留,因为这个临时对象会被
立即丢弃

这个行为可能初看起来有些反直觉。以下是一个例子来说明这个过程:

let assassin = 'LeBlanc';
assassin.chineseName = '乐芙兰';
console.log(assassin);//LeBlanc
console.log(assassin.chineseName);//undefined

在这个例子中,assassin 是一个字符串,因此是一个原始值。当我们尝试给它设置 chineseName 属性时,JavaScript 会临时将 assassin 转换成一个 String 包装对象,并在这个对象上设置 chineseName 属性。但是,一旦这

个操作完成,这个临时的包装对象就会被丢弃,所以 chineseName 并没有被真正添加到 assassin 上。当我们接下来尝试访问 assassin.chineseName 时,你会得到 undefined,因为 assassin 是一个原始值,它没有任何附加的

属性。

这也是为什么通常不建议在原始值上设置属性:这些属性不会被永久保存,因此这个操作没有任何实际效果。这个行为是 JavaScript 自动装箱机制的一部分,设计是为了让原始值表现得像对象一样,同时保持原始值的轻量和高效。