26、Azure SQL性能监控、异常处理与应用洞察集成全解析

Azure SQL性能监控、异常处理与应用洞察集成全解析

1. Query Store的强大功能

Query Store是Azure SQL中一个非常实用的功能,它能帮助我们监控和排查数据库性能问题。以下是一些具体的应用场景和操作方法:
- 查询最近执行时间较长的查询

SELECT TOP 10 rs.avg_duration, qt.query_sql_text,
    q.query_id, qt.query_text_id, p.plan_id, rs.runtime_stats_id,
    rsi.start_time, rsi.end_time, rs.avg_rowcount, rs.count_executions
FROM sys.query_store_query_text AS qt
JOIN sys.query_store_query AS q
    ON qt.query_text_id = q.query_text_id
JOIN sys.query_store_plan AS p
    ON q.query_id = p.query_id
JOIN sys.query_store_runtime_stats AS rs
    ON p.plan_id = rs.plan_id
JOIN sys.query_store_runtime_stats_interval AS rsi
    ON rsi.runtime_stats_interval_id = rs.runtime_stats_interval_id
WHERE rs.last_execution_time > DATEADD(hour, -1, GETUTCDATE())
ORDER BY rs.avg_duration DESC;

这个查询会返回最近1小时内平均执行时间较长的前10个查询。
- 查询最近24小时内I/O读取较多的查询

SELECT TOP 10 rs.avg_physical_io_reads, qt.query_sql_text,
    q.query_id, qt.query_text_id, p.plan_id, rs.runtime_stats_id,
    rsi.start_time, rsi.end_time, rs.avg_rowcount, rs.count_executions
FROM sys.query_store_query_text AS qt
JOIN sys.query_store_query AS q
    ON qt.query_text_id = q.query_text_id
JOIN sys.query_store_plan AS p
    ON q.query_id = p.query_id
JOIN sys.query_store_runtime_stats AS rs
    ON p.plan_id = rs.plan_id
JOIN sys.query_store_runtime_stats_interval AS rsi
    ON rsi.runtime_stats_interval_id = rs.runtime_stats_interval_id
WHERE rsi.start_time >= DATEADD(hour, -24, GETUTCDATE())
ORDER BY rs.avg_physical_io_reads DESC;

此查询会返回最近24小时内平均物理I/O读取较多的前10个查询。

2. 对比不同时间段的查询执行情况

当应用程序性能在过去一周下降时,我们可以使用Query Store对比不同时间段的查询执行情况,找出性能下降的原因。具体步骤如下:
1. 定义时间范围

--- "Recent" workload - last 4 hours
DECLARE @recent_start_time datetimeoffset;
DECLARE @recent_end_time datetimeoffset;
SET @recent_start_time = DATEADD(hour, -4, SYSUTCDATETIME());
SET @recent_end_time = SYSUTCDATETIME();

--- "History" workload – last week
DECLARE @history_start_time datetimeoffset;
DECLARE @history_end_time datetimeoffset;
SET @history_start_time = DATEADD(day, -14, SYSUTCDATETIME());
SET @history_end_time = DATEADD(day, -7, SYSUTCDATETIME());
  1. 计算不同时间段的查询统计信息
WITH
hist AS
(
    SELECT
        p.query_id query_id,
        ROUND(ROUND(CONVERT(FLOAT, SUM(rs.avg_duration * rs.count_executions)) * 0.001, 2), 2) AS total_duration,
        SUM(rs.count_executions) AS count_executions,
        COUNT(distinct p.plan_id) AS num_plans
     FROM sys.query_store_runtime_stats AS rs
        JOIN sys.query_store_plan AS p ON p.plan_id = rs.plan_id
    WHERE (rs.first_execution_time >= @history_start_time
               AND rs.last_execution_time < @history_end_time)
        OR (rs.first_execution_time <= @history_start_time
               AND rs.last_execution_time > @history_start_time)
        OR (rs.first_execution_time <= @history_end_time
               AND rs.last_execution_time > @history_end_time)
    GROUP BY p.query_id
),
recent AS
(
    SELECT
        p.query_id query_id,
        ROUND(ROUND(CONVERT(FLOAT, SUM(rs.avg_duration * rs.count_executions)) * 0.001, 2), 2) AS total_duration,
        SUM(rs.count_executions) AS count_executions,
        COUNT(distinct p.plan_id) AS num_plans
    FROM sys.query_store_runtime_stats AS rs
        JOIN sys.query_store_plan AS p ON p.plan_id = rs.plan_id
    WHERE  (rs.first_execution_time >= @recent_start_time
               AND rs.last_execution_time < @recent_end_time)
        OR (rs.first_execution_time <= @recent_start_time
               AND rs.last_execution_time > @recent_start_time)
        OR (rs.first_execution_time <= @recent_end_time
               AND rs.last_execution_time > @recent_end_time)
    GROUP BY p.query_id
)
  1. 查询性能下降的查询
SELECT
    results.query_id AS query_id,
    results.query_text AS query_text,
    results.additional_duration_workload AS additional_duration_workload,
    results.total_duration_recent AS total_duration_recent,
    results.total_duration_hist AS total_duration_hist,
    ISNULL(results.count_executions_recent, 0) AS count_executions_recent,
    ISNULL(results.count_executions_hist, 0) AS count_executions_hist
FROM
(
    SELECT
        hist.query_id AS query_id,
        qt.query_sql_text AS query_text,
        ROUND(CONVERT(float, recent.total_duration/
                   recent.count_executions-hist.total_duration/hist.count_executions)
               *(recent.count_executions), 2) AS additional_duration_workload,
        ROUND(recent.total_duration, 2) AS total_duration_recent,
        ROUND(hist.total_duration, 2) AS total_duration_hist,
        recent.count_executions AS count_executions_recent,
        hist.count_executions AS count_executions_hist
    FROM hist
        JOIN recent
            ON hist.query_id = recent.query_id
        JOIN sys.query_store_query AS q
            ON q.query_id = hist.query_id
        JOIN sys.query_store_query_text AS qt
            ON q.query_text_id = qt.query_text_id
) AS results
WHERE additional_duration_workload > 0
ORDER BY additional_duration_workload DESC
OPTION (MERGE JOIN);

这个查询会计算出与之前执行周期相比,哪些查询引入了额外的执行时间,并返回最近和历史执行次数、总执行时间等信息。

3. 强制查询计划

对于性能下降的查询,我们可以尝试强制使用特定的执行计划来解决问题。具体操作如下:
- 强制使用特定计划

EXEC sp_query_store_force_plan @query_id = 48, @plan_id = 49;
  • 取消强制计划
EXEC sp_query_store_unforce_plan @query_id = 48, @plan_id = 49;
4. 使用可视化工具进行查询分析

除了使用T-SQL查询,我们还可以使用SQL Server Management Studio的可视化界面来分析Query Store信息。具体操作步骤如下:
1. 展开数据库中的Query Store节点,查看支持的场景。
2. 点击“Regressed Queries”,默认会显示最近1小时内性能下降的前25个查询。
3. 可以在多个维度(如CPU时间、IO和内存)上对数据进行切片和分析,找到需要的信息。
4. 从同一界面中,还可以查看给定查询的可用查询计划,并通过点击“Force Plan”按钮强制使用最优化的计划。

5. 查询等待统计信息

查询等待统计信息可以帮助我们了解数据库的整体性能。具体操作如下:
1. 在数据库树中点击“Query Wait Statistics”选项,会弹出一个新窗口。
2. 窗口中会以图表形式显示最重要的等待状态类别,并按总等待时间排序。
3. 可以深入挖掘影响最大的类别,查看导致这些等待状态的顶级查询及其相关查询计划,然后强制使用更适合特定工作负载的计划。

6. Query Performance Insight

Azure SQL提供的Query Performance Insight是基于Query Store数据的智能查询分析工具,可用于单数据库和池数据库。它能帮助我们识别工作负载中消耗资源最多和执行时间最长的查询,从而优化查询以提高整体工作负载性能,并有效利用付费资源。具体操作如下:
1. 登录Azure门户,访问Query Performance Insight。
2. 可以深入到单个查询级别,获取资源消耗、查询执行次数等详细信息。
3. 还可以与Database Advisor提供的性能建议进行交互,通过点击“Automate”按钮自动执行这些建议,让Azure SQL保持数据库处于最佳状态。

7. SQL中的异常处理

在Transact-SQL中,错误处理类似于传统编程语言中的异常处理。我们可以使用TRY…CATCH结构来捕获和处理错误。以下是一个示例:

BEGIN TRANSACTION;
BEGIN TRY
    -- Generate a constraint violation error.
    DELETE FROM Production.Product
    WHERE ProductID = 980;
END TRY
BEGIN CATCH
    SELECT
        ERROR_NUMBER() AS ErrorNumber
       ,ERROR_SEVERITY() AS ErrorSeverity
       ,ERROR_STATE() AS ErrorState
       ,ERROR_PROCEDURE() AS ErrorProcedure
       ,ERROR_LINE() AS ErrorLine
       ,ERROR_MESSAGE() AS ErrorMessage;
    IF @@TRANCOUNT > 0
        ROLLBACK TRANSACTION;
END CATCH;
IF @@TRANCOUNT > 0
    COMMIT TRANSACTION;
GO

在CATCH块中,我们可以使用系统函数(如ERROR_NUMBER()、ERROR_SEVERITY()等)获取错误详细信息。如果在TRY块中发生错误,控制将传递到CATCH块;如果没有错误,控制将传递到END CATCH语句后的语句。

8. 嵌套TRY…CATCH结构

我们可以通过嵌套多个TRY…CATCH结构来创建复杂的错误管理逻辑。当CATCH块中发生错误时,如果该块包含嵌套的TRY…CATCH结构,嵌套TRY块中的任何错误将传递到嵌套的CATCH块;如果没有嵌套结构,错误将传递回调用者。

9. 异常处理的注意事项

在处理T-SQL代码中的错误时,需要注意以下几点:
- 严重性等于或低于10的错误不会被TRY…CATCH块捕获,因为它们被视为警告。
- 严重性等于或高于20的某些错误会导致Azure SQL停止处理该会话上的其他任务,TRY…CATCH块无法捕获这些错误。
- 编译错误(如语法错误)不会被TRY…CATCH块捕获,因为这些错误发生在编译时,而不是运行时。
- 语句级重新编译或对象名称解析错误(如尝试使用已删除的视图)也不会被捕获。

10. 使用XACT_STATE进行异常处理

当TRY块中发生的错误使当前事务状态无效时,该事务将被分类为不可提交事务。我们可以使用XACT_STATE函数来验证当前事务是否为不可提交事务。以下是一个更复杂的示例:

-- Check to see whether this stored procedure exists.
IF OBJECT_ID (N'usp_GetErrorInfo', N'P') IS NOT NULL
    DROP PROCEDURE usp_GetErrorInfo;
GO

-- Create procedure to retrieve error information.
CREATE PROCEDURE usp_GetErrorInfo
AS
    SELECT
         ERROR_NUMBER() AS ErrorNumber
        ,ERROR_SEVERITY() AS ErrorSeverity
        ,ERROR_STATE() AS ErrorState
        ,ERROR_LINE () AS ErrorLine
        ,ERROR_PROCEDURE() AS ErrorProcedure
        ,ERROR_MESSAGE() AS ErrorMessage;
GO

-- SET XACT_ABORT ON will cause the transaction to be uncommittable
-- when the constraint violation occurs, as it automatically rollback the transaction
SET XACT_ABORT ON;
BEGIN TRY
    BEGIN TRANSACTION;
        -- A FOREIGN KEY constraint exists on this table. This
        -- statement will generate a constraint violation error.
        DELETE FROM Production.Product
            WHERE ProductID = 980;
    -- If the DELETE statement succeeds, commit the transaction.
    COMMIT TRANSACTION;
END TRY
BEGIN CATCH
    -- Execute error retrieval routine.
    EXECUTE usp_GetErrorInfo;
    -- Test XACT_STATE:
        -- If 1, the transaction is committable.
        -- If -1, the transaction is uncommittable and should
        --     be rolled back.
        -- XACT_STATE = 0 means that there is no transaction and
        --     a commit or rollback operation would generate an error.
    -- Test whether the transaction is uncommittable.
    IF (XACT_STATE()) = -1
    BEGIN
        PRINT
            N'The transaction is in an uncommittable state.' +
            'Rolling back transaction.'
        ROLLBACK TRANSACTION;
    END;
    -- Test whether the transaction is committable.
    IF (XACT_STATE()) = 1
    BEGIN
        PRINT
            N'The transaction is committable.' +
            'Committing transaction.'
        COMMIT TRANSACTION;
    END;
END CATCH;
GO
11. 保持代码简洁

除了T-SQL,.NET或Python等编程语言也有很好的异常支持。使用XACT_ABORT ON可以让我们的代码更加简洁。以下是一个示例:

SET XACT_ABORT ON
BEGIN TRAN
INSERT INTO Orders VALUES (2,1,getdate());
UPDATE Inventory SET QuantityInStock=QuantityInStock-1
  WHERE ProductID=1
COMMIT TRAN

在上述代码中,由于XACT_ABORT设置为ON,INSERT和UPDATE操作要么都无错误地执行,然后提交事务;要么在执行过程中出现任何错误,整个事务将自动回滚,代码执行也会中断,并将错误返回给调用者。

12. 与Application Insights集成

Application Insights是Azure Monitor的一个功能,是一个可扩展的应用性能管理(APM)服务,可帮助我们监控部署在Azure平台上的实时应用程序。具体操作步骤如下:
- 安装SDK或启用代理
- 可以在应用程序中安装一个小的检测包(作为SDK),或者在支持的情况下(如Azure虚拟机)使用Application Insights Agent启用Application Insights。
- 检测程序会监控应用程序,并使用唯一的GUID(称为检测密钥)将遥测数据发送到Azure Application Insights资源。
- 配置Java Spring Boot应用程序
1. 添加Maven依赖:

<dependency>
  <groupId>com.microsoft.azure</groupId>
  <artifactId>applicationinsights-spring-boot-starter</artifactId>
  <version>2.5.1</version>
</dependency>
2. 配置应用程序属性,传递检测密钥:
# Specify the instrumentation key of your Application Insights resource.
azure.application-insights.instrumentation-key=974d297f-aaaa-aaaa-bbbb-4abcdabcd050
# Specify the name of your spring boot application. This can be any logical 
name you would like to give to your app.
spring.application.name=SpringBootInAzureDemo
3. 使用TelemetryClient跟踪指标:
@RestController
@RequestMapping("/")
public class Controller {
  @Autowired
  UserRepo userRepo;
  @Autowired
  TelemetryClient telemetryClient;

  @GetMapping("/greetings")
  public String greetings() {
    // send event
    telemetryClient.trackEvent("URI /greeting is triggered");
    return "Hello World!";
  }

  @GetMapping("/users")
  public List<User> users() {
    try {
      List<User> users;
      // measure DB query benchmark
      long startTime = System.nanoTime();
      users = userRepo.findAll();
      long endTime = System.nanoTime();
      MetricTelemetry benchmark = new MetricTelemetry();
      benchmark.setName("DB query");
      benchmark.setValue(endTime - startTime);
      telemetryClient.trackMetric(benchmark);
      return users;
    } catch (Exception e) {
      // send exception information
      telemetryClient.trackEvent("Error");
      telemetryClient.trackTrace("Exception: " + e.getMessage());
      throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage());
    }
  }
}
  • 配置.NET/.NET Core应用程序
    1. 初始化DependencyTrackingTelemetryModule:
DependencyTrackingTelemetryModule depModule = new DependencyTrackingTelemetryModule();
depModule.Initialize(TelemetryConfiguration.Active);
2. 在服务级别启用遥测:
services.ConfigureTelemetryModule<DependencyTrackingTelemetryModule>((module, o) => { module.EnableSqlCommandTextInstrumentation = true; });
3. 在applicationInsights.config配置文件中明确选择收集SQL命令:
<Add Type="Microsoft.ApplicationInsights.DependencyCollector.DependencyTrackingTelemetryModule, Microsoft.AI.DependencyCollector">
<EnableSqlCommandTextInstrumentation>true</EnableSqlCommandTextInstrumentation>
</Add>

通过以上配置,我们可以调查应用程序代码调用的外部服务的性能,查看数据库调用和响应时间,并深入了解单个样本的端到端事务流程,包括T-SQL查询文本。

通过以上介绍,我们可以看到Azure SQL在性能监控、异常处理和与应用洞察集成方面提供了丰富的功能和工具,帮助我们更好地管理和优化数据库应用程序。

Azure SQL性能监控、异常处理与应用洞察集成全解析

13. 应用集成效果展示

集成Application Insights之后,我们可以通过各种监控页面和自定义仪表盘查看应用程序的性能指标。例如,在Application Map中可以看到应用程序与其他服务(如SQL)的交互情况,包括调用次数和响应时间。对于.NET/.NET Core应用程序,配置完成后可以看到数据库调用和响应时间,并且可以深入查看单个样本的端到端事务流程以及T - SQL查询文本。

14. 综合应用案例分析

假设我们有一个基于Java Spring Boot的电商应用程序,在运行过程中出现了性能下降的情况。我们可以按照以下步骤进行排查和优化:
1. 使用Query Store分析查询性能
- 首先,使用前面提到的对比不同时间段查询执行情况的方法,找出哪些查询在近期引入了额外的执行时间。
- 例如,执行以下代码定义时间范围:

--- "Recent" workload - last 4 hours
DECLARE @recent_start_time datetimeoffset;
DECLARE @recent_end_time datetimeoffset;
SET @recent_start_time = DATEADD(hour, -4, SYSUTCDATETIME());
SET @recent_end_time = SYSUTCDATETIME();

--- "History" workload – last week
DECLARE @history_start_time datetimeoffset;
DECLARE @history_end_time datetimeoffset;
SET @history_start_time = DATEADD(day, -14, SYSUTCDATETIME());
SET @history_end_time = DATEADD(day, -7, SYSUTCDATETIME());
- 然后计算不同时间段的查询统计信息并查询性能下降的查询:
WITH
hist AS
(
    SELECT
        p.query_id query_id,
        ROUND(ROUND(CONVERT(FLOAT, SUM(rs.avg_duration * rs.count_executions)) * 0.001, 2), 2) AS total_duration,
        SUM(rs.count_executions) AS count_executions,
        COUNT(distinct p.plan_id) AS num_plans
     FROM sys.query_store_runtime_stats AS rs
        JOIN sys.query_store_plan AS p ON p.plan_id = rs.plan_id
    WHERE (rs.first_execution_time >= @history_start_time
               AND rs.last_execution_time < @history_end_time)
        OR (rs.first_execution_time <= @history_start_time
               AND rs.last_execution_time > @history_start_time)
        OR (rs.first_execution_time <= @history_end_time
               AND rs.last_execution_time > @history_end_time)
    GROUP BY p.query_id
),
recent AS
(
    SELECT
        p.query_id query_id,
        ROUND(ROUND(CONVERT(FLOAT, SUM(rs.avg_duration * rs.count_executions)) * 0.001, 2), 2) AS total_duration,
        SUM(rs.count_executions) AS count_executions,
        COUNT(distinct p.plan_id) AS num_plans
    FROM sys.query_store_runtime_stats AS rs
        JOIN sys.query_store_plan AS p ON p.plan_id = rs.plan_id
    WHERE  (rs.first_execution_time >= @recent_start_time
               AND rs.last_execution_time < @recent_end_time)
        OR (rs.first_execution_time <= @recent_start_time
               AND rs.last_execution_time > @recent_start_time)
        OR (rs.first_execution_time <= @recent_end_time
               AND rs.last_execution_time > @recent_end_time)
    GROUP BY p.query_id
)
SELECT
    results.query_id AS query_id,
    results.query_text AS query_text,
    results.additional_duration_workload AS additional_duration_workload,
    results.total_duration_recent AS total_duration_recent,
    results.total_duration_hist AS total_duration_hist,
    ISNULL(results.count_executions_recent, 0) AS count_executions_recent,
    ISNULL(results.count_executions_hist, 0) AS count_executions_hist
FROM
(
    SELECT
        hist.query_id AS query_id,
        qt.query_sql_text AS query_text,
        ROUND(CONVERT(float, recent.total_duration/
                   recent.count_executions-hist.total_duration/hist.count_executions)
               *(recent.count_executions), 2) AS additional_duration_workload,
        ROUND(recent.total_duration, 2) AS total_duration_recent,
        ROUND(hist.total_duration, 2) AS total_duration_hist,
        recent.count_executions AS count_executions_recent,
        hist.count_executions AS count_executions_hist
    FROM hist
        JOIN recent
            ON hist.query_id = recent.query_id
        JOIN sys.query_store_query AS q
            ON q.query_id = hist.query_id
        JOIN sys.query_store_query_text AS qt
            ON q.query_text_id = qt.query_text_id
) AS results
WHERE additional_duration_workload > 0
ORDER BY additional_duration_workload DESC
OPTION (MERGE JOIN);
  1. 强制查询计划
    • 对于性能下降的查询,检查其近期的查询计划是否与之前不同。如果发现问题,可以尝试强制使用之前性能较好的查询计划。
    • 例如,执行以下代码强制使用特定计划:
EXEC sp_query_store_force_plan @query_id = 48, @plan_id = 49;
  1. 使用Application Insights监控应用性能
    • 在Java Spring Boot应用程序中集成Application Insights,按照前面提到的步骤进行配置:
      • 添加Maven依赖:
<dependency>
  <groupId>com.microsoft.azure</groupId>
  <artifactId>applicationinsights-spring-boot-starter</artifactId>
  <version>2.5.1</version>
</dependency>
    - 配置应用程序属性,传递检测密钥:
# Specify the instrumentation key of your Application Insights resource.
azure.application-insights.instrumentation-key=974d297f-aaaa-aaaa-bbbb-4abcdabcd050
# Specify the name of your spring boot application. This can be any logical 
name you would like to give to your app.
spring.application.name=SpringBootInAzureDemo
    - 使用TelemetryClient跟踪指标:
@RestController
@RequestMapping("/")
public class Controller {
  @Autowired
  UserRepo userRepo;
  @Autowired
  TelemetryClient telemetryClient;

  @GetMapping("/greetings")
  public String greetings() {
    // send event
    telemetryClient.trackEvent("URI /greeting is triggered");
    return "Hello World!";
  }

  @GetMapping("/users")
  public List<User> users() {
    try {
      List<User> users;
      // measure DB query benchmark
      long startTime = System.nanoTime();
      users = userRepo.findAll();
      long endTime = System.nanoTime();
      MetricTelemetry benchmark = new MetricTelemetry();
      benchmark.setName("DB query");
      benchmark.setValue(endTime - startTime);
      telemetryClient.trackMetric(benchmark);
      return users;
    } catch (Exception e) {
      // send exception information
      telemetryClient.trackEvent("Error");
      telemetryClient.trackTrace("Exception: " + e.getMessage());
      throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage());
    }
  }
}
- 通过Application Insights的监控页面和仪表盘,查看应用程序的性能指标,如请求率、响应时间和失败率等,找出性能瓶颈。
15. 总结与最佳实践

在使用Azure SQL进行性能监控、异常处理和应用洞察集成时,我们可以遵循以下最佳实践:
- 性能监控
- 定期使用Query Store分析查询性能,对比不同时间段的执行情况,及时发现性能下降的查询。
- 利用可视化工具(如SQL Server Management Studio)和Query Performance Insight,更直观地查看和分析数据。
- 异常处理
- 使用TRY…CATCH结构捕获和处理错误,同时注意异常处理的注意事项,如严重性级别和编译错误等。
- 结合XACT_ABORT ON让代码更加简洁,同时确保事务的一致性。
- 应用洞察集成
- 及时集成Application Insights,对应用程序进行全面的性能监控,包括与Azure SQL的交互情况。
- 根据监控数据进行针对性的优化,如强制查询计划、调整数据库配置等。

16. 流程图总结

下面是一个mermaid格式的流程图,总结了排查和优化Azure SQL应用程序性能问题的主要步骤:

graph LR
    A[应用程序性能下降] --> B[使用Query Store分析查询性能]
    B --> C{是否有性能下降的查询}
    C -- 是 --> D[检查查询计划是否不同]
    D -- 是 --> E[强制使用特定查询计划]
    C -- 否 --> F[继续监控]
    E --> G[使用Application Insights监控应用性能]
    G --> H{是否找到性能瓶颈}
    H -- 是 --> I[针对性优化]
    H -- 否 --> F
    I --> F

通过以上全面的介绍和分析,我们可以充分利用Azure SQL的各种功能,对数据库应用程序进行有效的性能监控、异常处理和应用洞察集成,从而提升应用程序的稳定性和性能。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值