告别 Shuffle!深入探索 Spark 的 SPJ 技术

随着 Spark >= 3.3(在 3.4 中更加成熟)中引入的存储分区连接(Storage Partition Join,SPJ)优化技术,您可以在不触发 Shuffle 的情况下对分区的数据源 V2 表执行连接操作(当然,需要满足一些条件)。

6df043eab7ad2d8326982ef4916e9410.png

Shuffle 是昂贵的,尤其是在 Spark 中的连接操作中,主要原因包括:

•Shuffle 需要跨网络传输数据,这是 CPU 密集型的。•在 Shuffle 过程中,Shuffle 文件被写入本地磁盘,这是磁盘 I/O 昂贵的。

数据源 V2 表是开放格式表,例如 Apache Hudi、Apache Iceberg 和 Delta Lake 表。

在撰写本文时,SPJ 支持目前仅在 Apache Iceberg 1.2.0 及以上版本中提供。

本文将涵盖以下内容:

•SPJ 工作的要求是什么?•需要设置哪些配置才能让 SPJ 工作?•如何检查 SPJ 是否为您的 Spark 作业工作?•通过了解设置的配置更深入地了解 SPJ。

让我们开始介绍吧。

6674125955c03a67219053aeeef1637e.gif

SPJ 的要求

•目标表和源表都必须是 Iceberg 表。•源表和目标表应该有相同的分区(至少有一个分区列应该相同)。•连接条件必须包括分区列。•必须设置好相关配置•Apache Iceberg 版本 >= 1.2.0 和 Spark 版本 >= 3.3.0。

配置

•spark.sql.sources.v2.bucketing.enabled = true•spark.sql.sources.v2.bucketing.pushPartValues.enabled = true•spark.sql.iceberg.planning.preserve-data-grouping = true•spark.sql.requireAllClusterKeysForCoPartition = false•spark.sql.sources.v2.bucketing.partiallyClusteredDistribution.enabled = true

Partitioning Keys 和 Clustering Keys 指的是同一个概念,可以互换使用。请不要混淆它们。

我将使用 Spark 3.5.0 和 Iceberg 1.5.0 来进行这个操作。

在我们深入探讨 SPJ 之前,让我们先创建一些模拟数据,并查看当 SPJ 不工作时 Spark join plan 的实际样子:

初始化 SparkSession

我们将初始化一个 Spark Session,其中包含所有与 Iceberg 相关的配置,但首先不包含 SPJ 配置:

from pyspark.sql import SparkSession, Row


# update here the required versions
SPARK_VERSION = "3.5"
ICEBERG_VERSION = "1.5.0"
CATALOG_NAME = "local"


# update this to your local path where you want tables to be created
DW_PATH = "/path/to/local/warehouse"


spark = SparkSession.builder \
    .master("local[4]") \
    .appName("spj-iceberg") \
    .config("spark.sql.adaptive.enabled", "true")\
    .config('spark.jars.packages', f'org.apache.iceberg:iceberg-spark-runtime-{SPARK_VERSION}_2.12:{ICEBERG_VERSION},org.apache.spark:spark-avro_2.12:3.5.0')\
    .config('spark.sql.extensions','org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions')\
    .config(f'spark.sql.catalog.{CATALOG_NAME}','org.apache.iceberg.spark.SparkCatalog') \
    .config(f'spark.sql.catalog.{CATALOG_NAME}.type','hadoop') \
    .config(f'spark.sql.catalog.{CATALOG_NAME}.warehouse',DW_PATH) \
    .config('spark.sql.autoBroadcastJoinThreshold', '-1')\
    .enableHiveSupport()\
    .getOrCreate()

准备数据

我们将创建并写入两张 Iceberg 表:

•Customers 和 Orders 这两个表都按区域进行了分区/聚类。•它们都可以通过 customer_id 进行连接,并包含区域详情以及其他一些常见的详细信息,如姓名、电子邮件等。

数据是使用 Faker Python 库模拟生成的。如果你没有这个库:

pip install faker
# Creating Mockup data for Customers and Orders table.


from pyspark.sql import Row
from faker import Faker
import random




# Initialize Faker
fake = Faker()
Faker.seed(42)


# Generate customer data
def generate_customer_data(num_customers=1000):
    regions = ['North', 'South', 'East', 'West']


    customers = []
    for _ in range(num_customers):
        signup_date = fake.date_time_between(start_date='-3y', end_date='now')
        customers.append(Row(
            customer_id=fake.unique.random_number(digits=6),
            customer_name=fake.name(),
            region=random.choice(regions),
            signup_date=signup_date,
            signup_year=signup_date.year  # Additional column for partition evolution
        ))


    return spark.createDataFrame(customers)


# Generate order data
def generate_order_data(customer_df, num_orders=5000):
    customers = [row.customer_id for row in customer_df.select('customer_id').collect()]


    orders = []
    for _ in range(num_orders):
        order_date = fake.date_time_between(start_date='-3y', end_date='now')
        orders.append(Row(
            order_id=fake.unique.random_number(digits=8),
            customer_id=random.choice(customers),
            order_date=order_date,
            amount=round(random.uniform(10, 1000), 2),
            region=random.choice(['North', 'South', 'East', 'West']),
            order_year=order_date.year  # Additional column for partition evolution
        ))


    return spark.createDataFrame(orders)


# Generate the data
print("Generating sample data...")
customer_df = generate_customer_data(1000)
order_df = generate_order_data(customer_df, 5000)


customer_df.show(5, truncate=False)
order_df.show(5, truncate=False)

将数据写入到 Iceberg 表:

customer_df.writeTo("local.db.customers") \
    .tableProperty("format-version", "2") \
    .partitionedBy("region") \
    .create()


order_df.writeTo("local.db.orders") \
    .tableProperty("format-version", "2") \
    .partitionedBy("region") \
    .create()

关闭 SPJ 来 JOIN customers 和 orders 表:

CUSTOMERS_TABLE = 'local.db.customers'
ORDERS_TABLE = 'local.db.orders'


cust_df = spark.table(CUSTOMERS_TABLE)
order_df = spark.table(ORDERS_TABLE)


# Joining on region
joined_df = cust_df.join(order_df, on='region', how='left')


# Generated plan from
joined_df.explain("FORMATTED")


# triggering an action
joined_df.show(1)

下面是这个查询的执行计划图:

== Physical Plan ==
AdaptiveSparkPlan (9)
+- Project (8)
   +- SortMergeJoin LeftOuter (7)
      :- Sort (3)
      :  +- Exchange (2)
      :     +- BatchScan local.db.customers (1)
      +- Sort (6)
         +- Exchange (5)
            +- BatchScan local.db.orders (4)

上述计划中的 Exchange 节点代表了 shuffle 操作。

如果你更习惯使用 Spark UI,那么也可以在那里看到这个信息。

89aed43b36de367db3986901b70aaedc.png

开启 SPJ 来 JOIN customers 和 orders 表:

设置以下参数将在查询中开启 SPJ

# Setting SPJ related configs
spark.conf.set('spark.sql.sources.v2.bucketing.enabled','true') 
spark.conf.set('spark.sql.sources.v2.bucketing.pushPartValues.enabled','true')
spark.conf.set('spark.sql.iceberg.planning.preserve-data-grouping','true')
spark.conf.set('spark.sql.requireAllClusterKeysForCoPartition','false')
spark.conf.set('spark.sql.sources.v2.bucketing.partiallyClusteredDistribution.enabled','true')

我们来执行上面一样的查询,然后查看执行计划有什么变化:

joined_df = cust_df.join(order_df, on='region', how='left')
joined_df.explain("FORMATTED")
joined_df.show()

我们在下面的执行计划中看不到 Exchange 节点了,这代表没有 SHUFFLE 操作!

== Physical Plan ==
AdaptiveSparkPlan (7)
+- Project (6)
   +- SortMergeJoin LeftOuter (5)
      :- Sort (2)
      :  +- BatchScan local.db.customers (1)
      +- Sort (4)
         +- BatchScan local.db.orders (3)

我们可以到 Spark UI 确定这个:

9707b7d1e7c5d97ea6b727a1f74e37f3.png

这确实令人惊叹,但嘿,等等,那是理想情况,我们的表以相同方式分区,并且连接仅使用分区列。在现实世界中,这种情况很少见。

有道理!让我们深入了解它的工作原理,并查看一些类似于现实情况的连接条件,以检查 SPJ 是否会起作用。

了解 SPJ 中使用的配置

Storage Partitioned Join 利用现有的存储布局来避免 shuffle 阶段。

SPJ 工作的必要和最低要求是设置可以提供此信息的配置,即:

spark.sql.iceberg.planning.preserve-data-grouping 当为真时,查询计划期间保留分区信息。这防止了不必要的重新分区,通过减少执行期间的 shuffle 成本来优化性能。

spark.sql.sources.v2.bucketing.enabled 当为真时,尝试通过使用兼容的 V2 数据源报告的分区来消除 shuffle。

让我们看看各种连接场景:

场景 1:连接键与分区键相同

416e276ce0eb4597dc29a28b80f05906.png

# Setting up the minimum configuration for SPJ
spark.conf.set("spark.sql.sources.v2.bucketing.enabled", "true")
spark.conf.set("spark.sql.iceberg.planning.preserve-data-grouping", "true")


joined_df = cust_df.join(order_df, on="region", how="left")
joined_df.explain("FORMATTED")
== Physical Plan ==
AdaptiveSparkPlan (7)
+- Project (6)
   +- SortMergeJoin LeftOuter (5)
      :- Sort (2)
      :  +- BatchScan local.db.customers (1)
      +- Sort (4)
         +- BatchScan local.db.orders (3)

计划中没有 Exchange 节点。所以在这种情况下,最小配置是有效的。

场景 2:双方的分区不匹配

6d1a6e96b25203a10045729aff6b1d98.png

让我们通过从 Orders 表中删除一个分区来创建这种场景

# Deleting all the records for a region
spark.sql("DELETE FROM {ORDERS_TABLE} where region='West'")


# Validating if the partition is dropped
orders_df.groupBy("region").count().show()
+------+-----+
|region|count|
+------+-----+
|  East| 1243|
| North| 1267|
| South| 1196|
+------+-----+

现在让我们检查相同连接条件下的计划:

joined_df = cust_df.join(order_df, on="region", how="left")
joined_df.explain("FORMATTED")
== Physical Plan ==
AdaptiveSparkPlan (9)
+- Project (8)
   +- SortMergeJoin LeftOuter (7)
      :- Sort (3)
      :  +- Exchange (2)
      :     +- BatchScan local.db.customers (1)
      +- Sort (6)
         +- Exchange (5)
            +- BatchScan local.db.orders (4)

Exchange(Shuffle)又回来了..‼️ 🤨

为了处理这种情况,Spark 在启用上述配置后会为缺失的分区值创建空分区:

spark.sql.sources.v2.bucketing.pushPartValues.enabled 当启用时,如果连接的一侧缺少另一侧的分区值,尝试消除 shuffle。

89d8d9d83ae084912de9c65586b2e6f7.png

我在代码里面开启 spark.sql.sources.v2.bucketing.pushPartValues.enabled

# Enabling config when there are missing partition values
spark.conf.set('spark.sql.sources.v2.bucketing.pushPartValues.enabled','true')
joined_df = cust_df.join(order_df, on='region', how='left')
joined_df.explain("FORMATTED")

这时候的查询计划如下:

== Physical Plan ==
AdaptiveSparkPlan (7)
+- Project (6)
   +- SortMergeJoin LeftOuter (5)
      :- Sort (2)
      :  +- BatchScan local.db.customers (1)
      +- Sort (4)
         +- BatchScan local.db.orders (3)

不再有 shuffle..!! 🥳

场景 3:连接键与分区键不匹配

这种情况可能有以下两种情况:

•连接键是分区键的超集•连接键是分区键的子集

3.1 连接键是分区键的超集

这些是在连接中除了分区键之外还有额外字段的查询,例如:

Select * from Customers as t1 
join 
Orders as t2
on t1.region = t2.region
and 
t1.customer_id = t2.customer_id -- additional column `customer_id`

默认情况下,Spark 要求所有分区键必须相同并且有序,以消除 shuffle。可以通过以下设置关闭此行为:

spark.sql.requireAllClusterKeysForCoPartition 当设置为真时,要求连接或合并键与分区键相同且顺序一致,以消除 shuffle。这就是将其设置为 false 的原因。

# Setting up another config to support SPJ for these cases
spark.conf.set('spark.sql.requireAllClusterKeysForCoPartition','false')
joined_df = cust_df.join(order_df, on=['region','customer_id'], how='left')
joined_df.explain("FORMATTED")

关闭 spark.sql.requireAllClusterKeysForCoPartition 后的查询执行计划:

== Physical Plan ==
AdaptiveSparkPlan (8)
+- Project (7)
   +- SortMergeJoin LeftOuter (6)
      :- Sort (2)
      :  +- BatchScan local.db.customers (1)
      +- Sort (5)
         +- Filter (4)
            +- BatchScan local.db.orders (3)

可以看到,已经没有 shuffle..!!! 🥳

3.2 连接键是分区键的子集

在 Spark < 4.0 中,SPJ 不适用于这种情况。下面的代码示例是在本地构建的最新 Spark 4.0 代码中测试的。

这些情况可能是表格没有以相同方式分区的情况,例如:

•Customers 表按 region 和 bucket(customer_id,2) 分区•Orders 表按 region 和 bucket(customer_id, 4) 分区

ecf0247eb2ecb20afb82b4fcafcb8c1e.png

或者是在多个列上对表进行分区,而连接仅使用其中的少数列进行连接的情况。

在这种情况下,Spark 4.0 会在列 regions 上对输入分区进行分组,类似于下面的方式:

8de25557d1230b5b9644fb29beeb2287.png

Spark 4.0 提供了一个配置来启用这一功能—— spark.sql.sources.v2.bucketing.allowJoinKeysSubsetOfPartitionKeys.enabled 当启用时,如果连接条件不包含所有分区列,则尝试避免 shuffle。

// Spark 4.0 SPJ Subset Join Keys test


import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.functions.col


object SPJTest {
  def main(args: Array[String]): Unit = {
    // SparkSession creation
    val spark = SparkSession.builder......


    //Setting all SPJ configs available in Spark 3.4.0
    spark.conf.set("spark.sql.sources.v2.bucketing.enabled","true")
    spark.conf.set("spark.sql.iceberg.planning.preserve-data-grouping","true")
    spark.conf.set("spark.sql.sources.v2.bucketing.pushPartValues.enabled",
    "true")
    spark.conf.set("spark.sql.requireAllClusterKeysForCoPartition","false")
    // Configuration from Spark 4.0
    spark.conf.set("spark.sql.sources.v2.bucketing.allowJoinKeysSubsetOfPartitionKeys.enabled", "true")


    // CUSTOMER  table partitioned on region, year(signup_date), bucket(2, customer_id)
    // ORDER table partitioned on region, year(order_date), bucket(4, customer_id)
    val CUSTOMER_TABLE = "local.db.customers_buck"
    val ORDERS_TABLE = "local.db.orders_buck"
    val cust_df = spark.table(CUSTOMER_TABLE)
    val orders_df = spark.table(ORDERS_TABLE)


    // join cust_df and orders_df on region alone
    val joined_df = cust_df.alias("cust")
      .join(orders_df.alias("ord"),
        col("cust.region") === col("ord.region"),
        "left")
    println(joined_df.explain("FORMATTED"))

上面查询执行计划如下:

== Physical Plan ==
AdaptiveSparkPlan (6)
+- SortMergeJoin LeftOuter (5)
   :- Sort (2)
   :  +- BatchScan local.db.customers_buck (1)
   +- Sort (4)
      +- BatchScan local.db.orders_buck (3)

场景 4:分区中的数据偏斜

如果您正在处理繁重的工作负载,数据偏斜是相当常见的问题。假设您的数据分布如下所示:

ad876d49afa32a7b5ce14eced302058e.png

不幸的是,即使经过多次尝试,我也无法复制这种情况,因此这只能是理论上的,也许我会在将来能够复制这种情况后立即更新。

因此,从理论上讲,Spark 提供了一种配置:

spark.sql.sources.v2.bucketing.partiallyClusteredDistribution.enabled 当设置为真,并且连接不是全外连接时,启用偏斜优化,以在避免 shuffle 时处理包含大量数据的分区。 

启用此配置后,Spark 会将偏斜的分区拆分为多个拆分,并将同一分区的另一侧分组并复制以匹配相同的分区。

c41608aab16e297c2904724018f99952.png

region=East 倾斜分区从 Customers 表中拆分为 2 个小分区,在 Orders 表侧,创建了 2 个 region=East 的副本。

参考文献

[1] Storage Partitioned Join Design Doc:https://docs.google.com/document/d/1foTkDSM91VxKgkEcBMsuAvEjNybjja-uHk-r3vtXWFE/edit?ref=guptaakashdeep.com

[2] Spark PR for SPJ:https://github.com/apache/spark/pull/32875?ref=guptaakashdeep.com

[3] Spark PR for Partially Clustered Distribution:https://github.com/apache/spark/pull/32875?ref=guptaakashdeep.com

[4] Spark 4.0.0 preview2 documentation:https://spark.apache.org/docs/4.0.0-preview2/sql-performance-tuning.html?ref=guptaakashdeep.com#converting-sort-merge-join-to-shuffled-hash-join

本文翻译自:https://www.guptaakashdeep.com/storage-partition-join-in-apache-spark-why-how-and-where/

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

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

相关文章

解决Springboot整合Shiro自定义SessionDAO+Redis管理会话,登录后不跳转首页

解决Springboot整合Shiro自定义SessionDAORedis管理会话&#xff0c;登录后不跳转首页 问题发现问题解决 问题发现 在Shiro框架中&#xff0c;SessionDAO的默认实现是MemorySessionDAO。它内部维护了一个ConcurrentMap来保存session数据&#xff0c;即将session数据缓存在内存…

【蓝桥杯——物联网设计与开发】基础模块8 - RTC

目录 一、RTC &#xff08;1&#xff09;资源介绍 &#x1f505;简介 &#x1f505;时钟与分频&#xff08;十分重要‼️&#xff09; &#xff08;2&#xff09;STM32CubeMX 软件配置 &#xff08;3&#xff09;代码编写 &#xff08;4&#xff09;实验现象 二、RTC接口…

API安全学习笔记

必要性 前后端分离已经成为web的一大趋势&#xff0c;通过TomcatNgnix(也可以中间有个Node.js)&#xff0c;有效地进行解耦。并且前后端分离会为以后的大型分布式架构、弹性计算架构、微服务架构、多端化服务&#xff08;多种客户端&#xff0c;例如&#xff1a;浏览器&#x…

2011-2020年各省城镇职工基本医疗保险年末参保人数数据

2011-2020年各省城镇职工基本医疗保险年末参保人数数据 1、时间&#xff1a;2011-2020年 2、来源&#xff1a;国家统计局 3、指标&#xff1a;省份、时间、城镇职工基本医疗保险年末参保人数 4、范围&#xff1a;31省 5、指标解释&#xff1a;参保人数指报告期末按国家有关…

【蓝桥杯——物联网设计与开发】拓展模块4 - 脉冲模块

目录 一、脉冲模块 &#xff08;1&#xff09;资源介绍 &#x1f505;原理图 &#x1f505;采集原理 &#xff08;2&#xff09;STM32CubeMX 软件配置 &#xff08;3&#xff09;代码编写 &#xff08;4&#xff09;实验现象 二、脉冲模块接口函数封装 三、踩坑日记 &a…

Kubernetes Gateway API-2-跨命名空间路由

1 跨命名空间路由 Gateway API 具有跨命名空间路由的核心支持。当多个用户或团队共享底层网络基础设施时,这很有用,但必须对控制和配置进行分段,以尽量减少访问和容错域。 Gateway 和 Route(HTTPRoute,TCPRoute,GRPCRoute) 可以部署到不同的命名空间中,路由可以跨命名空间…

Windows Powershell实战指南(未完成)

目前只作简单了解&#xff0c;开始吧。 一、初识Powershell 目标 初步认识 Powershell和其集成环境 Ise&#xff0c;学会基本设置 实验 我们从简单的例子开始&#xff1a;希望你能从控制台和ISE的配置中实现相同的结果。然后按照下面五步进行。 &#xff08;1&#xff09;选…

Android着色器SweepGradient渐变圆环,Kotlin

Android着色器SweepGradient渐变圆环&#xff0c;Kotlin import android.content.Context import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint import android.graphics.Path import android.graphics.SweepGradient import android…

项目上传到gitcode

首先需要在个人设置里面找到令牌 记住自己的账号和访问令牌&#xff08;一长串&#xff09;&#xff0c;后面git要输入这个&#xff0c; 账号是下面这个 来到自己的仓库 #查看远程仓库&#xff0c;是不是自己的云仓库 git remote -v # 创建新分支 git checkout -b llf # 三步…

SAQ问卷的定义,SAQ问卷是什么?

SAQ问卷&#xff0c;全称为可持续发展评估问卷&#xff08;Sustainability Assessment Questionnaire&#xff09;&#xff0c;是一种在线自评工具&#xff0c;其深远意义与广泛应用在当今商业环境中愈发凸显。它不仅是一种衡量企业在环境、社会和治理&#xff08;ESG&#xff…

SpringBoot获取bean的几种方式

目录 一、BeanFactory与ApplicationContext的区别 二、通过BeanFactory获取 三、通过BeanFactoryAware获取 四、启动获取ApplicationContext 五、通过继承ApplicationObjectSupport 六、通过继承WebApplicationObjectSupport 七、通过WebApplicationContextUtils 八、通…

web3基于zkEVM的L2扩容方案-Scroll

项目简介 Scroll 是2021年由华人创始团队推出的 基于zkEVM 的 以太坊ZKR扩容方案&#xff0c;不同于zkSync的语言级别兼容&#xff0c;Scroll实现了完全EVM等效&#xff0c;即字节码层级兼容&#xff0c;除了数据结构和状态树等部分&#xff0c;zkEVM看起来与以太坊完全一样&a…

深入浅出 Linux 操作系统

深入浅出 Linux 操作系统 引言 在当今数字化的时代&#xff0c;Linux 操作系统无处不在。从支撑互联网巨头庞大的数据中心&#xff0c;到嵌入智能家居设备的微型芯片&#xff0c;Linux 都发挥着关键作用。然而&#xff0c;对于许多人来说&#xff0c;Linux 仍笼罩着一层神秘的…

Python毕业设计选题:基于python的白酒数据推荐系统_django+hive

开发语言&#xff1a;Python框架&#xff1a;djangoPython版本&#xff1a;python3.7.7数据库&#xff1a;mysql 5.7数据库工具&#xff1a;Navicat11开发软件&#xff1a;PyCharm 系统展示 管理员登录 管理员功能界面 用户管理 白酒管理 系统管理 看板展示 系统首页 白酒详情…

【赵渝强老师】MongoDB逻辑存储结构

MongoDB的逻辑存储结构是一种层次结构&#xff0c;主要包括了三个部分&#xff0c;即&#xff1a;数据库&#xff08;Database&#xff09;、集合&#xff08;Collection&#xff0c;也可以叫做表&#xff09;和文档&#xff08;Document&#xff0c;也可以叫做记录&#xff09…

Python数据可视化小项目

英雄联盟S14世界赛选手数据可视化 由于本学期有一门数据可视化课程&#xff0c;课程结课作业要求完成一个数据可视化的小Demo&#xff0c;于是便有了这个小项目&#xff0c;课程老师要求比较简单&#xff0c;只要求熟练运用可视化工具展示数据&#xff0c;并不要求数据来源&am…

继承超详细介绍

一 、继承 1 继承的概念 继承是面向对象程序设计使得代码可以复用的最重要手段&#xff0c;它使得我们可以在原有类的特性的基础上进行扩展&#xff0c;增加方法和属性&#xff08;成员函数与成员变量&#xff09;&#xff0c;这样产生新的类&#xff0c;叫作派生类。继承呈现了…

Numpy指南:解锁Python多维数组与矩阵运算(上)

文章一览 前言一、nmupy 简介和功能二、numpy 安装三、numpy基本使用3.1、ndarray 对象3.2、基础数据结构 ndarray 数组3.3、ndarray 数组定义3.4、ndarray 数组属性计算3.5、ndarray 数组创建3.5.1 通过 array 方式创建 ndarray 数组3.5.2 通过 arange 创建数组3.5.3 通过 lin…

C++:单例模式

创建自己的对象&#xff0c;同时确保对象的唯一性。 单例类只能有一个实例☞静态成员static☞静态成员 必须类外初始化 单例类必须自己创建自己的唯一实例 单例类必须给所有其他对象提供这一实例 静态成员类内部可以访问 构造函数私有化☞构造函数私有外部不能创建&#x…

【火猫DOTA2】VP一号位透露队伍不会保留原阵容

1、最近VP战队的一号位选手Kiritych在直播中透露,VP战队的阵容将会有新的变动,原有的阵容将不再保留。 【目前VP战队阵容名单如下】 一号位:Kiritych 二号位:squad1x 三号位:Noticed 四号位:Antares 五号位:待定 2、Spirit的战队经理Korb3n在直播时谈到了越来越多的职业选…