04.PostgreSQL是如何实现隔离级别的?

PostgreSQL是如何实现隔离级别的?

事务有哪些特性?

事务看起来感觉简单,但是要实现事务必须要遵守 4 个特性,分别如下:

  • 原子性(Atomicity):一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节,而且事务在执行过程中发生错误,会被回滚到事务开始前的状态,就像这个事务从来没有执行过一样,就好比买一件商品,购买成功时,则给商家付了钱,商品到手;购买失败时,则商品在商家手中,消费者的钱也没花出去。
  • 一致性(Consistency):是指事务操作前和操作后,数据满足完整性约束,数据库保持一致性状态。比如,用户 A 和用户 B 在银行分别有 800 元和 600 元,总共 1400 元,用户 A 给用户 B 转账 200 元,分为两个步骤,从 A 的账户扣除 200 元和对 B 的账户增加 200 元。一致性就是要求上述步骤操作后,最后的结果是用户 A 还有 600 元,用户 B 有 800 元,总共 1400 元,而不会出现用户 A 扣除了 200 元,但用户 B 未增加的情况(该情况,用户 A 和 B 均为 600 元,总共 1200 元)。
  • 隔离性(Isolation):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致,因为多个事务同时使用相同的数据时,不会相互干扰,每个事务都有一个完整的数据空间,对其他并发事务是隔离的。也就是说,消费者购买商品这个事务,是不影响其他消费者购买的。
  • 持久性(Durability):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

事务隔离

PostgreSQL 是如何保证isolation的? 接下来我们通过这篇文章来了解 PostgreSQL 如何实现事务隔离

标准SQL事务隔离级别

隔离级别脏读不可重复读幻读
读未提交可能可能可能
读已提交不可能可能可能
可重复读不可能不可能可能
可串行化不可能不可能不可能

SQL标准定义了四个级别的事务隔离。

各个级别不应该发生的现象是:

  • 脏读

    一个事务读取了另一个未提交事务写入的数据。

  • 不可重复读

    一个事务重新读取前面读取过的数据,发现该数据已经被另一个已经提交的事务修改。

  • 幻读

    一个事务重新执行一个查询,返回符合查询条件的行的集合,发现满足查询条件的行的集合因为其它最近提交的事务而发生了改变。

**在PostgreSQL里,你可以请求四种可能的事务隔离级别中的任意一种。但是在内部, 实际上只有三种独立的隔离级别,分别对应读已提交,可重复读和可串行化。**如果你选择了读未提交的级别, 实际上你获得的是读已提交,并且在PostgreSQL的可重复读实现中,幻读是不可能的, 所以实际的隔离级别可能比你选择的更严格。这是 SQL 标准允许的:四种隔离级别只定义了哪种现像不能发生, 但是没有定义那种现像一定发生。PostgreSQL只提供三种隔离级别的原因是, 这是把标准的隔离级别与多版本并发控制架构映射起来的唯一合理方法。

PostgreSQL实现的事务隔离等级如下表所示:

隔离等级脏读不可重复读幻读串行化异常
读已提交不可能可能可能可能
可重复读[1]不可能不可能PG中不可能可能
可串行化不可能不可能不可能不可能

PostgreSQL 在 MVCC 里如何工作的?

PostgreSQL事务标识

每当事务开始时,事务管理器就会为其分配一个称为**事务标识(transaction id, txid)**的唯一标识符。 PostgreSQL的txid是一个32位无符号整数,总取值约42亿。在事务启动后执行内置的txid_current()函数,即可获取当前事务的txid,如下所示。

testdb=# BEGIN;
BEGIN
testdb=# SELECT txid_current();txid_current 
--------------100
(1 row)

PostgreSQL保留以下三个特殊txid

  • 0表示**无效(Invalid)**的txid
  • 1表示**初始启动(Bootstrap)**的txid,仅用于数据库集群的初始化过程。
  • 2表示**冻结(Frozen)**的txid.

txid可以相互比较大小。例如对于txid=100的事务,大于100的txid属于“未来”,且对于txid=100的事务而言都是**不可见(invisible)**的;小于100的txid属于“过去”,且对该事务可见,如下图(a)所示。

img

因为txid在逻辑上是无限的,而实际系统中的txid空间不足(4字节取值空间约42亿),因此PostgreSQL将txid空间视为一个环。对于某个特定的txid,其前约21亿个txid属于过去,而其后约21亿个txid属于未来。如(b)所示。

元组结构

可以将表页中的堆元组分为两类:普通数据元组与TOAST元组。本节只会介绍普通元组。

堆元组由三个部分组成,即HeapTupleHeaderData结构,空值位图,以及用户数据,如下图所示。

元组结构

img

HeapTupleHeaderData结构在src/include/access/htup_details.h中定义。

typedef struct HeapTupleFields
{TransactionId t_xmin;           /* 插入事务的ID */TransactionId t_xmax;          /*删除或锁定事务的ID*/union{CommandId       t_cid;     /* 插入或删除的命令ID */TransactionId     t_xvac;    /* 老式VACUUM FULL的事务ID */} t_field3;
} HeapTupleFields;typedef struct DatumTupleFields
{int32          datum_len_;          /* 变长头部长度*/int32          datum_typmod;           /* -1或者是记录类型的标识 */Oid            datum_typeid;           /* 复杂类型的OID或记录ID */
} DatumTupleFields;typedef struct HeapTupleHeaderData
{union{HeapTupleFields t_heap;DatumTupleFields t_datum;} t_choice;ItemPointerData t_ctid;         /* 当前元组,或更新元组的TID *//* 下面的字段必需与结构MinimalTupleData相匹配! */uint16          t_infomask2;    /* 属性与标记位 */uint16          t_infomask;     /* 很多标记位 */uint8           t_hoff;         /* 首部+位图+填充的长度 *//* ^ - 23 bytes - ^ */bits8           t_bits[1];      /* NULL值的位图 —— 变长的 *//* 本结构后面还有更多数据 */
} HeapTupleHeaderData;typedef HeapTupleHeaderData *HeapTupleHeader;

虽然HeapTupleHeaderData结构包含七个字段,但后续部分中只需要了解四个字段即可。

  • t_xmin保存插入此元组的事务的txid
  • t_xmax保存删除或更新此元组的事务的txid。如果尚未删除或更新此元组,则t_xmax设置为0,即无效。
  • t_cid保存命令标识(command id, cid)cid意思是在当前事务中,执行当前命令之前执行了多少SQL命令,从零开始计数。例如,假设我们在单个事务中执行了三条INSERT命令BEGIN;INSERT;INSERT;INSERT;COMMIT;。如果第一条命令插入此元组,则该元组的t_cid会被设置为0。如果第二条命令插入此元组,则其t_cid会被设置为1,依此类推。
  • t_ctid保存着指向自身或新元组的元组标识符(tid)。tid用于标识表中的元组。在更新该元组时,其t_ctid会指向新版本的元组;否则t_ctid`会指向自己。

元组的增删改查

元组的具体表示如下图所示:

img

插入

在插入操作中,新元组将直接插入到目标表的页面中,如下图所示:

img

假设元组是由txid=99的事务插入页面中的,在这种情况下,被插入元组的首部字段会依以下步骤设置。

Tuple_1

  • t_xmin设置为99,因为此元组由txid=99的事务所插入。
  • t_xmax设置为0,因为此元组尚未被删除或更新。
  • t_cid设置为0,因为此元组是由txid=99的事务所执行的第一条命令所插入的。
  • t_ctid设置为(0,1),指向自身,因为这是该元组的最新版本。
pageinspect

PostgreSQL自带了一个第三方贡献的扩展模块pageinspect,可用于检查数据库页面的具体内容。

testdb=# CREATE EXTENSION pageinspect;
CREATE EXTENSION
testdb=# CREATE TABLE tbl (data text);
CREATE TABLE
testdb=# INSERT INTO tbl VALUES(A);
INSERT 0 1
testdb=# SELECT lp as tuple, t_xmin, t_xmax, t_field3 as t_cid, t_ctid FROM heap_page_items(get_raw_page(tbl, 0));
tuple | t_xmin | t_xmax | t_cid | t_ctid 
-------+--------+--------+-------+--------1 |     99 |      0 |     0 | (0,1)
(1 row)
删除

在删除操作中,目标元组只是在逻辑上被标记为删除。目标元组的t_xmax字段将被设置为执行DELETE命令事务的txid。如下图所示:

img 假设Tuple_1txid=111的事务删除。在这种情况下,Tuple_1的首部字段会依以下步骤设置。

Tuple_1

  • t_xmax被设为111。

如果txid=111的事务已经提交,那么Tuple_1就不是必需的了。通常不需要的元组在PostgreSQL中被称为死元组(dead tuple)

死元组最终将从页面中被移除。清除死元组的过程被称为清理(VACUUM)过程

更新

在更新操作中,PostgreSQL在逻辑上实际执行的是删除最新的元组,并插入一条新的元组。

img

假设由txid=99的事务插入的行,被txid=100的事务更新两次。

当执行第一条UPDATE命令时,Tuple_1t_xmax被设为txid 100,在逻辑上被删除;然后Tuple_2被插入;接下来重写Tuple_1t_ctid以指向Tuple_2Tuple_1Tuple_2的头部字段设置如下。

Tuple_1

  • t_xmax被设置为100。
  • t_ctid(0,1)被改写为(0,2)

Tuple_2

  • t_xmin被设置为100。
  • t_xmax被设置为0。
  • t_cid被设置为0。
  • t_ctid被设置为(0,2)

当执行第二条UPDATE命令时,和第一条UPDATE命令类似,Tuple_2被逻辑删除,Tuple_3被插入。Tuple_2Tuple_3的首部字段设置如下。

Tuple_2

  • t_xmax被设置为100。
  • t_ctid(0,2)被改写为(0,3)

Tuple_3

  • t_xmin被设置为100。
  • t_xmax被设置为0。
  • t_cid被设置为1。
  • t_ctid被设置为(0,3)

与删除操作类似,如果txid=100的事务已经提交,那么Tuple_1Tuple_2就成为了死元组,而如果txid=100的事务中止,Tuple_2Tuple_3就成了死元组。

提交日志(clog)

PostgreSQL在提交日志(Commit Log, clog)中保存事务的状态。提交日志(通常称为clog)分配于共享内存中,并用于事务处理过程的全过程。

事务状态

PostgreSQL定义了四种事务状态,即:IN_PROGRESSCOMMITTEDABORTEDSUB_COMMITTED

前三种状态涵义显而易见。例如当事务正在进行时,其状态为IN_PROGRESS,依此类推。

SUB_COMMITTED状态用于子事务。

提交日志如何工作

提交日志(下称clog)在逻辑上是一个数组,由共享内存中一系列8KB页面组成。数组的序号索引对应着相应事务的标识,而其内容则是相应事务的状态。clog的工作方式如下图所示:

img

T1txid 200提交;txid 200的状态从IN_PROGRESS变为COMMITTEDT2txid 201中止;txid 201的状态从IN_PROGRESS变为ABORTED

txid不断前进,当clog空间耗尽无法存储新的事务状态时,就会追加分配一个新的页面。

当需要获取事务的状态时,PostgreSQL将调用相应内部函数读取clog,并返回所请求事务的状态。

事务快照

**事务快照(transaction snapshot)**是一个数据集,存储着某个特定事务在某个特定时间点所看到的事务状态信息:哪些事务处于活跃状态。这里活跃状态意味着事务正在进行中,或还没有开始。

事务快照在PostgreSQL内部的文本表示格式定义为100:100:。举个例子,这里100:100:意味着txid < 100的事务处于非活跃状态,而txid ≥ 100的事务处于活跃状态。

内置函数txid_current_snapshot及其文本表示

函数txid_current_snapshot显示当前事务的快照。

testdb=# SELECT txid_current_snapshot();
txid_current_snapshot 
-----------------------
100:104:100,102
(1 row)

txid_current_snapshot的文本表示是xmin:xmax:xip_list,各部分描述如下。

  • xmin

    最早仍然活跃的事务的txid。所有比它更早的事务txid < xmin要么已经提交并可见,要么已经回滚并生成死元组。

  • xmax

    第一个尚未分配的txid。所有txid ≥ xmax的事务在获取快照时尚未启动,因而其结果对当前事务不可见。

  • xip_list

    获取快照时活跃事务txid列表。该列表仅包括xminxmax之间的txid

    例如,在快照100:104:100,102中,xmin100xmax104,而xip_list100,102

以下显示了两个具体的示例:

事务快照的表示样例

img

第一个例子是100:100:,如图图(a)所示,此快照表示:

  • 因为xmin为100,因此txid < 100的事务是非活跃的
  • 因为xmax为100,因此txid ≥ 100的事务是活跃的

第二个例子是100:104:100,102,如图(b)所示,此快照表示:

  • txid < 100的事务不活跃。
  • txid ≥ 104的事务是活跃的。
  • txid等于100和102的事务是活跃的,因为它们在xip_list中,而txid等于101和103的事务不活跃。

**事务快照是由事务管理器提供的。在READ COMMITTED隔离级别,事务在执行每条SQL时都会获取快照;其他情况下(REPEATABLE READSERIALIZABLE隔离级别),事务只会在执行第一条SQL命令时获取一次快照。**获取的事务快照用于元组的可见性检查。

使用获取的快照进行可见性检查时,所有活跃的事务都必须被当成IN PROGRESS的事务等同对待,无论它们实际上是否已经提交或中止。这条规则非常重要,因为它正是READ COMMITTEDREPEATABLE READ/SERIALIZABLE隔离级别中表现差异的根本来源,我们将在接下来几节中频繁回到这条规则上来。

如下图所示:

事务管理器与事务img

事务管理器始终保存着当前运行的事务的有关信息。假设三个事务一个接一个地开始,并且Transaction_ATransaction_B的隔离级别是READ COMMITTEDTransaction_C的隔离级别是REPEATABLE READ

  • T1: Transaction_A启动并执行第一条SELECT命令。执行第一个命令时,Transaction_A请求此刻的txid和快照。在这种情况下,事务管理器分配txid=200,并返回事务快照200:200:

  • T2: Transaction_B启动并执行第一条SELECT命令。事务管理器分配txid=201,并返回事务快照200:200:,因为Transaction_A(txid=200)正在进行中。因此无法从Transaction_B中看到Transaction_A

  • T3: Transaction_C启动并执行第一条SELECT命令。事务管理器分配txid=202,并返回事务快照200:200:,因此不能从Transaction_C中看到Transaction_ATransaction_B

  • T4: Transaction_A已提交。事务管理器删除有关此事务的信息。

  • T5: Transaction_BTransaction_C执行它们各自的SELECT命令。

    Transaction_B需要一个新的事务快照,因为它使用了READ COMMITTED隔离等级。在这种情况下,Transaction_B获取新快照201:201:,因为Transaction_A(txid=200)已提交。因此Transaction_A的变更对Transaction_B可见了。

    Transaction_C不需要新的事务快照,因为它处于REPEATABLE READ隔离等级,并继续使用已获取的快照,即200:200:。因此,Transaction_A的变更仍然对Transaction_C不可见。

总结

PostgreSQL 是支持事务的,事务的四大特性是原子性、一致性、隔离性、持久性,我们这次主要讲了隔离性。

当多个事务并发执行的时候,会引发脏读、不可重复读、幻读这些问题,那为了避免这些问题,SQL 提出了四种隔离级别,分别是读未提交、读已提交、可重复读、串行化,从左往右隔离级别顺序递增,隔离级别越高,意味着性能越差,PostgreSQL 引擎的默认隔离级别是读已提交。

**在PostgreSQL里,你可以请求四种可能的事务隔离级别中的任意一种。但是在内部, 实际上只有三种独立的隔离级别,分别对应读已提交,可重复读和可串行化。**如果你选择了读未提交的级别, 实际上你获得的是读已提交,并且在PostgreSQL的可重复读实现中,幻读是不可能的, 所以实际的隔离级别可能比你选择的更严格。

而对于幻读现象,不建议将隔离级别升级为串行化,因为这会导致数据库并发时性能很差。PostgreSQL 引擎的默认隔离级别虽然是「读已提交」

PostgreSQL 事务快照是由事务管理器提供的。在READ COMMITTED隔离级别,事务在执行每条SQL时都会获取快照;其他情况下(REPEATABLE READSERIALIZABLE隔离级别),事务只会在执行第一条SQL命令时获取一次快照。获取的事务快照用于元组的可见性检查;

参考资料

  • [1] Abraham Silberschatz, Henry F. Korth, and S. Sudarshan, “Database System Concepts”, McGraw-Hill Education, ISBN-13: 978-0073523323
  • [2] Dan R. K. Ports, and Kevin Grittner, “Serializable Snapshot Isolation in PostgreSQL”, VDBL 2012
  • [3] Thomas M. Connolly, and Carolyn E. Begg, “Database Systems”, Pearson, ISBN-13: 978-0321523068
  • [4] PostgreSQL指南:内幕探索
  • [5] PostgreSQL 9.4.4 中文手册
  • [6] 事务隔离级别是怎么实现的?

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

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

相关文章

Istio新架构揭秘:环境化Mesh

自问世以来&#xff0c;Istio因其使用Sidecar&#xff08;可编程代理与应用容器一同部署&#xff09;而备受认可。这种架构选择使Istio用户能够享受其好处&#xff0c;而无需对其应用进行 drast 改变。这些可编程代理&#xff0c;与应用容器紧密部署在一起&#xff0c;因其能够…

java学习part27线程死锁

基本就是操作系统的内容 138-多线程-线程安全的懒汉式_死锁_ReentrantLock的使用_哔哩哔哩_bilibili

【大模型】更强的 ChatGLM3-6B 来了,开源可商用

【大模型】更强的 ChatGLM3-6B 来了&#xff0c;开源可商用 简介ChatGLM3-6B 环境配置环境搭建安装依赖 代码及模型权重拉取拉取 ChatGLM3-6B拉取 ChatGLM3-6B 模型权重及代码 终端测试网页测试安装 gradio加载模型并启动服务 参考 简介 ChatGLM3-6B ChatGLM3-6B 是 ChatGLM …

基于STM32的四轴飞行器的控制系统(论文+源码)

1.系统设计 本次基于stm32单片机的四轴飞行器控制系统主要包括硬件和软件这两大部分&#xff0c;其中硬件部分是基于单片机的四轴飞行器控制系统实现的基石&#xff0c;其中主要STM32单片机负责整个系统功能的实现&#xff1b;NRF24L01无线模块负责对四轴飞行器的远程控制&…

rtmp 协议详解

1. handshake 1.1 概述 rtmp 连接从握手开始。它包含三个固定大小的块。客户端发送的三个块命名为 C0,C1,C2&#xff1b;服务端发送的三个块命名为 S0,S1,S2。 握手序列&#xff1a; 客户端通过发送 C0 和 C1 消息来启动握手过程。客户端必须接收到 S1 消息&#xff0c;然后…

MS721仪表总线收发器可Pin to Pin兼容TSS721A

MS721 是为 M-Bus 标准&#xff08;EN1434-3&#xff09;的应用而开发的单片收发电路。MS721 接口电路可以适应从站与主站之间的电压差&#xff0c;总线的连接没有极性要求&#xff0c;电路由主站通过总线供电&#xff0c;这样对于从站电池就不会增加额外的负载&#xff0c;同时…

轻量级万物分割SAM模型——MobileSAM安装实测摘要

目录 0、前言1、准备工作安装python环境说明安装说明 运行测试app安装依赖修改代码 2、实际测试效果自带图片测试其它图片测试1其它图片测试2 总结 0、前言 本文将介绍一种轻量级万物分割SAM模型——MobileSAM的安装和实测情况。SAM是meta公司的一种图像分割大模型&#xff0c…

css中的 Grid 布局

flex布局和grid布局区别 flex布局是 一维布局grid布局是二维布局 flex布局示例 grid布局示例 grid 布局初体验 体验地址 <div class"wrapper"><div class"one item">One</div><div class"two item">Two</div&…

Asp.net core WebApi 配置自定义swaggerUI和中文注释,Jwt Bearer配置

1.创建asp.net core webApi项目 默认会引入swagger的Nuget包 <PackageReference Include"Swashbuckle.AspNetCore" Version"6.2.3" />2.配置基本信息和中文注释&#xff08;默认是没有中文注释的&#xff09; 2.1创建一个新的controller using Micr…

开源堡垒机Jumpserver

文章目录 开源堡垒机JumpserverJumpserver介绍安装环境部署安装jumpserver访问jumpserver的web界面 开源堡垒机Jumpserver Jumpserver介绍 Jumpserver 是全球首款完全开源的堡垒机&#xff0c;使用 GNU GPL v2.0 开源协议&#xff0c;是符合 4A 的运维安全审计系统。 Jumpse…

J-LINK J-FLASH 下载STM32单片机程序使用教程

J-LINK J-FLASH 下载STM32单片机程序使用教程 Chapter1 J-LINK J-FLASH 下载STM32单片机程序使用教程1.安装提供的 JLINK驱动程序2. 点击打开 J-Flash V7.223.点击 create a new project.&#xff08;使用后可以在软件菜单File保存这个烧写工程&#xff0c;后续直接打开使用即可…

SHAP(四):NHANES I 生存模型

SHAP&#xff08;四&#xff09;&#xff1a;NHANES I 生存模型 这是一个 Cox 比例风险模型&#xff0c;基于来自 NHANES I 的数据以及来自 NHANES I 流行病学随访研究。 它旨在说明 SHAP 值如何能够以传统上仅由线性模型提供的清晰度解释 XGBoost 模型。 我们在数据中看到有趣…

JOSEF约瑟时间继电器ARTD-DC110V-2H2D 0.25-2.5s导轨安装

ARTD系列断电延时继电器&#xff1a; ARTD-220VDC-1H1D断电延时继电器&#xff1b;ARTD-220VDC-2H断电延时继电器&#xff1b; ARTD-220VDC-2H2D断电延时继电器&#xff1b;ARTD-220VDC-4H断电延时继电器&#xff1b; ARTD-110VDC-1H1D断电延时继电器&#xff1b;ARTD-110VD…

SSM框架(四):SSM整合 案例 + 异常处理器 +拦截器

文章目录 一、整合流程图1.1 Spring整合Mybatis1.2 Spring整合SpringMVC 二、表现层数据封装2.1 问题引出2.2 统一返回结果数据格式 代码设计 三、异常处理器3.1 概述3.2 异常处理方案 四、前端五、拦截器5.1 概念5.2 入门案例5.3 拦截器参数5.4 拦截器链 一、整合流程图 1.1 S…

本科毕业生个人简历23篇

刚毕业的本科生如何制作一份令招聘方印象深刻的简历&#xff1f;可以参考以下这23篇精选的本科毕业生应聘简历案例&#xff01;无论您的专业是什么&#xff0c;都能从中汲取灵感&#xff0c;提升简历质量&#xff0c;轻松斩获心仪职位&#xff01;小伙伴们快来看看吧&#xff0…

C++作业4

代码整理&#xff0c; 将学过的三种运算符重载&#xff0c;每个至少实现一个运算符的重载 代码&#xff1a; #include <iostream>using namespace std;class Stu {friend const Stu operator*(const Stu &L,const Stu &R);friend bool operator<(const Stu …

抓取检测(Grasp Dection)

抓取检测 抓取检测被定义为能够识别任何给定图像中物体的抓取点或抓取姿势。抓取策略应确保对新物体的稳定性、任务兼容性和适应性&#xff0c;抓取质量可通过物体上接触点的位置和手的配置来测量。为了掌握一个新的对象&#xff0c;完成以下任务&#xff0c;有分析方法和经验…

智慧工地一体化解决方案(里程碑管理)源码

智慧工地为管理人员提供及时、高效、优质的远程管理服务&#xff0c;提升安全管理水平&#xff0c;确保施工安全提高施工质量。实现对人、机、料、法、环的全方位实时监控&#xff0c;变被动“监督”为主动“监控”。 一、建设背景 施工现场有数量多、分布广&#xff0c;总部统…

Woocommerce Private Store私人商店秘密商城插件,适合批发商店,会员制俱乐部

点击访问原文Woocommerce Private Store私人商店秘密商城插件&#xff0c;适合批发商店&#xff0c;会员制俱乐部 - 易服客工作室 WooCommerce Private Store插件是使 WooCommerce 私有的简单方法。密码保护您的整个 WooCommerce 商店并使其隐藏。 非常适合批发商店、会员制俱…

面试就是这么简单,offer拿到手软(二)—— 常见65道非技术面试问题

面试系列&#xff1a; 面试就是这么简单&#xff0c;offer拿到手软&#xff08;一&#xff09;—— 常见非技术问题回答思路 面试就是这么简单&#xff0c;offer拿到手软&#xff08;二&#xff09;—— 常见65道非技术面试问题 文章目录 一、前言二、常见65道非技术面试问题…