2024年3月

前言

提到这个
%20
,想必大家都见过,熟悉一点编码的人,还会知道这玩意就是空格转换而来! 那么我们一起破解, 如何编码而来?

我们今天继续学习前端编码知识, 其他编码文章:

之后再补上

  • UTF-16 编码
  • UTF-8 编码

前端所需要的基本编码知识体系就基本形成。

更多前端基础进阶知识,可以

  1. 关注专栏
    前端基础进阶
    ,
  2. 关注公众号
    成长的程序世界
  3. 进交流群
    dirge-cloud

Unicode基础知识

Unicode 只是一个字符集, 其为每个字符提供了一个编号,我们称之为
码点

Unicode 可以使用的编码有三种,分别是:

UFT-8
:一种
变长的编码方案
,使用 1~6 个字节来存储。
UTF-16
:对于码点小于
0xFFFF
(65535)的字符,两个字节存储,反之采用 4个字节来存储。
UFT-32
:一种
固定长度的编码方案
,不管字符编号大小,始终使用 4 个字节来存储。

所以UTF-8个UTF-16都属于变长编码方案,而UTF-32属于固定长度编码方案。

固定长度编码方案优点当然是简单啊,缺点嘛,费空间, 这就是为嘛还要有UTF-16和UTF-8。

我们网络传输常用 UTF-8, 而javascript运行时的字符编码是 UTF-16.

%20
怎么来的

我们看看,我们怎么样可以得到这个
%20
:

escape(" ")              "%20"
encodeURI(" ")           "%20"
encodeURIComponent(" ")  "%20"

其是字符的
16进制格式值
, 是百分号编码,之后会细说。

怎么获得这个编码,写一个简单的方法你就懂了

function to16Format(ch){
    return '%' + ch.codePointAt(0).toString(16)
}

to16Format(" ")  //  "%20"

虽然3个方法都能获得同样的值,
很少有人告诉你 esacpe是基于UTF-16,而另外两个是基于 UTF-8, 看个例子:

0-0xFF
码点范围编码结果是一致的,
0xFF
以上,结果就不一样了, 原理我们后面说。

escape("")         //%20
encodeURI("") //%20

escape("人") // "%u4EBA"
encodeURI("人") // "%E4%BA%BA"

escape("

前言

在现代的Web应用开发中,与Excel文件的导入和导出成为了一项常见而重要的任务。无论是数据交换、报告生成还是数据分析,与Excel文件的交互都扮演着至关重要的角色。本文小编将为大家介绍如何在熟悉的电子表格 UI 中轻松导入 Excel 文件,并以编程方式修改表格或允许用户进行编辑,最后使用葡萄城公司的纯前端表格控件
SpreadJS
组件它们导出回 Excel 文件。

我们将按照以下步骤介绍如何在 JavaScript 中导入/导出到 Excel:

  1. 搭建 JavaScript 电子表格项目
  2. 编写 Excel 导入代码并导入 Excel
  3. 将数据添加到导入的 Excel 文件
  4. 为表格添加迷你图
  5. 编写 Excel 导出代码并导出 Excel

操作步骤

1)搭建 JavaScript 电子表格项目

首先,我们可以使用 NPM 来下载 SpreadJS 文件。可以使用以下的命令行来安装 SpreadJS:

npm i @grapecity-software/spread-sheets @grapecity-software/spread-sheets-io file-saver jquery

安装完之后,我们可以在一个简单的 HTML 文件中添加对这些脚本和 CSS 文件的引用,如下所示:

<!DOCTYPE html>
<html>
  <head>
    <title>SpreadJS Import and Export Xlsx</title>
    <script
      src="./node_modules/jquery/dist/jquery.min.js"
      type="text/javascript"
    ></script>
    <script
      src="./node_modules/file-saver/src/FileSaver.js"
      type="text/javascript"
    ></script>
    <link
      href="./node_modules/@mescius/spread-sheets/styles/gc.spread.sheets.excel2013white.css"
      rel="stylesheet"
      type="text/css"
    />
    <script
      type="text/javascript"
      src="./node_modules/@mescius/spread-sheets/dist/gc.spread.sheets.all.min.js"
    ></script>
    <script
      type="text/javascript"
      src="./node_modules/@mescius/spread-sheets-io/dist/gc.spread.sheets.io.min.js"
    ></script>
  </head>
  <body>
    <div id="ss" style="height:600px; width :100%; "></div>
  </body>
</html>

然后我们可以在页面中添加一个脚本来初始化 SpreadJS Workbook 组件和一个 div 元素来作为容器:

    <script type="text/javascript">
        $(document).ready(function () {
            var workbook = new GC.Spread.Sheets.Workbook(document.getElementById("ss"));
        });
    </script>
</head>
<body>
    <div id="ss" style="height:600px ; width :100%; "></div>
</body>

2)编写 Excel 导入代码并导入 Excel

我们需要添加一个 input 元素和一个 button 来选择 Excel 文件。

<body>
  <div id="ss" style="height:700px; width:100%;"></div>
  <input type="file" id="selectedFile" name="files[]" accept=".xlsx" />
  <button class="settingButton" id="open">Open</button>
</body>

然后我们需要使用 spread.import() 方法来导入 Excel 文件。我们将在按钮的点击事件中导入用户选择的本地文件。

document.getElementById("open").onclick = function () {
  var file = document.querySelector("#selectedFile").files[0];
  if (!file) {
    return;
  }
  workbook.import(file);
};

现在就可以在 JavaScript 电子表格组件中导入和查看 Excel (.xlsx) 文件了,如下所示:

3)将数据添加到导入的 Excel 文件

在这里,我们将使用 利润损失表.xlsx 作为模板,如下图所示:

现在我们需要添加一个按钮来将数据添加到导入的 Excel 文件中。

<button id="addRevenue">Add Revenue</button>

可以为该按钮的点击事件编写一个函数来为表格添加一行并复制前一行的样式,为接下来添加数据做准备。要复制样式,我们需要使用 copyTo() 函数并传入:

  • 起始和目标行索引和列索引
  • 复制的行数和列数
  • 复制模式 CopyToOptions 值
document.getElementById("addRevenue").onclick = function () {var sheet = workbook.getActiveSheet();
  sheet.addRows(newRowIndex, 1);
  sheet.copyTo(10,1,
    newRowIndex,1,1,29,GC.Spread.Sheets.CopyToOptions.style
  );
};

下面是用于添加数据和迷你图的代码。对于大多数数据,我们可以使用 setValue() 函数。这允许我们通过传入行索引、列索引和值来设置 Spread 中工作表中的值:

var cellText = "Revenue" + revenueCount++;
sheet.setValue(newRowIndex, 1, cellText);

for (var c = 3; c < 15; c++) {
  sheet.setValue(newRowIndex, c, Math.floor(Math.random() * 200) + 10);
}

在 P 列中设置 SUM 公式以匹配其他行,并为 Q 列设置百分比:

sheet.setFormula(newRowIndex, 15, "=SUM([@[Jan]:[Dec]])");
sheet.setValue(newRowIndex, 16, 0.15);

最后,我们可以再次使用 copyTo() 函数将 R 列到 AD 列的公式从前一行复制到新行,这次使用 CopyToOptions.formula(只复制公式):

sheet.copyTo(
  10,
  17,
  newRowIndex,
  17,
  1,
  13,
  GC.Spread.Sheets.CopyToOptions.formula
);

4)为表格添加迷你图

现在我们可以添加迷你图来匹配其他数据行。为此,我们需要提供一系列单元格来获取数据以及迷你图的一些设置。在这种情况下,我们可以指定:

  • 我们刚刚添加数据的单元格范围
  • 调整迷你图的设置使其更加美观
var data = new GC.Spread.Sheets.Range(newRowIndex, 3, 1, 12);
var setting = new GC.Spread.Sheets.Sparklines.SparklineSetting();
setting.options.seriesColor = "Text 2";
setting.options.lineWeight = 1;
setting.options.showLow = true;
setting.options.showHigh = true;
setting.options.lowMarkerColor = "Text 2";
setting.options.highMarkerColor = "Text 1";

之后,我们调用 setSparkline() 方法并指定:

  • 迷你图的位置
  • 数据的位置
  • 迷你图的方向
  • 迷你图的类型
  • 之前创建的设置
sheet.setSparkline(
  newRowIndex,
  2,
  data,
  GC.Spread.Sheets.Sparklines.DataOrientation.horizontal,
  GC.Spread.Sheets.Sparklines.SparklineType.line,
  setting
);

如果现在尝试运行代码,它可能看起来有点慢,因为每次更改数据和添加样式时工作簿都会重新绘制。为了大幅加快速度并提高性能,Spread.Sheets 提供了暂停绘制和计算的功能。让我们添加代码以在添加行及其数据之前暂停,然后在添加行及其数据之后恢复:

workbook.suspendPaint();
workbook.suspendCalcService();
//...
workbook.resumeCalcService();
workbook.resumePaint();

添加完该代码后,我们可以在浏览器中打开该页面,并看到 Excel 文件加载到 Spread.Sheets 中,并添加了收入行。

5)编写 Excel 导出代码并导出 Excel

最后,我们可以添加一个按钮来导出包含了刚刚添加的收入行的文件。为了实现这个需求,我们可以在单击事件处理程序的导出按钮中调用 Spread.Sheets 中内置的导出方法:

document.getElementById("export").onclick = function () {
  var fileName = $("#exportFileName").val();
  if (fileName.substr(-5, 5) !== ".xlsx") {
    fileName += ".xlsx";
  }
  var json = JSON.stringify(workbook.toJSON());
  workbook.export(
    function (blob) {
      // save blob to a file
      saveAs(blob, fileName);
    },
    function (e) {
      console.log(e);
    },
    {
      fileType: GC.Spread.Sheets.FileType.excel,
    }
  );
};

该代码从 exportFileName 输入元素获取导出文件名。我们可以自定义它的文件名:

<input
  type="text"
  id="exportFileName"
  placeholder="Export file name"
  value="export.xlsx"
/>

然后添加一个调用此函数的按钮:

<button id="export"Export File</button

添加收入行后,使用导出文件按钮导出文件。

文件成功导出后,在 Excel 中打开它,可以看到该文件看起来与导入时一样,只是现在我们添加了一条额外的收入线。

总结

以上就是使用JavaScript 导入和导出 Excel的全过程,如果您想了解更多的信息,欢迎点击这篇
参考资料
查看。

扩展链接:

【干货放送】财务报表勾稽分析要点,一文读尽!

为什么你的财务报表不出色?推荐你了解这四个设计要点和!

纯前端类 Excel 表格控件在报表勾稽分析领域的应用场景解析

1、 前言

r-nacos
是一个用rust实现的nacos服务。相较于java nacos来说,是一个提供相同功能,启动更快、占用系统资源更小(初始内存小于10M)、性能更高、运行更稳定的服务。

r-nacos设计上完全兼容最新版本nacos面向client sdk 的协议(包含1.x的http OpenApi,和2.x的grpc协议), 支持使用nacos服务的应用平迁到 r-nacos。

r-nacos在本地测试使用很简单,通过
./rnacos
直接启动应用即可。
但是生产环境中还是需要更规范的方式部署运行。

目前的linux服务基本默认支持systemd统一管理服务。
本文主要记录使用systemd部分r-nacos的过程说明。

2、规划r-nacos运行位置

  1. 服务应用放到
    /opt/rnacos/
  2. r-nacos配置使用
    /etc/rnacos/env.conf
  3. 数据放到
    /var/rnacos/io/
  4. systemd 服务配置放到
    /lib/systemd/system/rnacos.service

3、部署

  1. 下载服务应用
mkdir -p /opt/rnacos/
cd /opt/rnacos/
#download from github
curl -LO https://github.com/r-nacos/r-nacos/releases/download/v0.5.0/rnacos-x86_64-unknown-linux-musl.tar.gz

#download from gitee
#curl -LO https://gitee.com/hqp/rnacos/releases/download/v0.5.0/rnacos-x86_64-unknown-linux-musl.tar.gz

tar -xvf rnacos-x86_64-unknown-linux-musl.tar.gz
  1. 增加r-nacos服务配置
mkdir -p /etc/rnacos/
cat >/etc/rnacos/env.conf <<EOF
# rnacos 指定配置文件有两种方式:
# 1. 默认文件(放置于运行目录下,文件名为“.env”,自动读取)
# 2. 指定文件(放置于任意目录下, 通过 命令行参数“-e 文件路径”形式指定, 如“./rnacos -e /etc/rnacos/conf/default.cnf”)
# 更多说明请参照  https://r-nacos.github.io/r-nacos/deplay_env.html

# r-nacos监听http端口,默认值:8848
RNACOS_HTTP_PORT=8848

#r-nacos监听grpc端口,默认值:HTTP端口+1000(即9848) 
#RNACOS_GRPC_PORT=9848

#r-nacos独立控制台端口,默认值:HTTP端口+2000(即10848);设置为0可不开启独立控制台
#RNACOS_HTTP_CONSOLE_PORT=10848

#r-nacos控制台登录1小时失败次数限制默认是5,一个用户连续登陆失败5次,会被锁定1个小时 ,默认值:1
RNACOS_CONSOLE_LOGIN_ONE_HOUR_LIMIT=5

#http工作线程数,默认值:cpu核数 
#RNACOS_HTTP_WORKERS=8


#配置中心的本地数据库sled文件夹, 会在系统运行时自动创建 ,默认值:nacos_db
RNACOS_CONFIG_DB_DIR=nacos_db

#节点id,默认值:1
RNACOS_RAFT_NODE_ID=1

#节点地址Ip:GrpcPort,单节点运行时每次启动都会生效;多节点集群部署时,只取加入集群时配置的值,默认值:127.0.0.1:GrpcPort 
RNACOS_RAFT_NODE_ADDR=127.0.0.1:9848

#是否当做主节点初始化,(只在每一次启动时生效)节点1时默认为true,节点非1时为false 
#RNACOS_RAFT_AUTO_INIT=true


#是否当做节点加入对应的主节点,LeaderIp:GrpcPort;只在第一次启动时生效;默认值:空 
#RNACOS_RAFT_JOIN_ADDR=127.0.0.1:9848

#日志等级:debug,info,warn,error;所有http,grpc请求都会打info日志,如果不关注,可以设置为error 减少日志量,默认值:info
RUST_LOG=info
EOF
  1. 初始化r-nacos数据目录
mkdir -p /var/rnacos/io/

# 如果使用rnacos用户运行,则要开放目录写权限给用户
# adduser rnacos
# chown -R rnacos:rnacos /var/rnacos
  1. 初始化r-nacos 服务配置

cat >/lib/systemd/system/rnacos.service <<EOF
[Unit]
Description=r-nacos server
After=network.target

[Service]
#使用指定用户运行
#User=rnacos
#Group=rnacos
ExecStart=/opt/rnacos/rnacos -e /etc/rnacos/env.conf
# 进程异常关闭时会自动重启
Restart=always
WorkingDirectory=/var/rnacos/io

[Install]
WantedBy=multi-user.target
EOF
  1. 重新加载并启动服务
# 重新加载配置
systemctl daemon-reload
# 启用服务并马上启动
systemctl enable --now rnacos

# 查看服务状态
systemctl status rnacos

把上以的脚本连起来执行,r-nacos服务即可部署完成。

4、管理服务

  1. 使用
    systemctl
    管理服务

常用的命令

# 查看服务状态
systemctl status rnacos

# 启动服务
systemctl start rnacos

# 关闭服务
systemctl stop rnacos

# 启动服务,开机自动启动
systemctl enable rnacos

# 禁用服务,开机不启动
systemctl disable rnacos

  1. 同时可以结合使用
    journalctl
    管理查看服务日志
# 查看日志
journalctl -u rnacos
# 查看最新日志
journalctl -u rnacos  -f

其它journalctl日志管理方式,可以单独支了解,这里不展开。

5、验证服务

5.1 shell 本地 http验证

  1. 配置中心http api例子
# 设置配置
curl -X POST 'http://127.0.0.1:8848/nacos/v1/cs/configs' -d 'dataId=t001&group=foo&content=contentTest'

# 查询
curl 'http://127.0.0.1:8848/nacos/v1/cs/configs?dataId=t001&group=foo'

  1. 注册中心http api例子
# 注册服务实例
curl -X POST 'http://127.0.0.1:8848/nacos/v1/ns/instance' -d 'port=8000&healthy=true&ip=192.168.1.11&weight=1.0&serviceName=nacos.test.001&groupName=foo&metadata={"app":"foo","id":"001"}'

curl -X POST 'http://127.0.0.1:8848/nacos/v1/ns/instance' -d 'port=8000&healthy=true&ip=192.168.1.12&weight=1.0&serviceName=nacos.test.001&groupName=foo&metadata={"app":"foo","id":"002"}'

 curl -X POST 'http://127.0.0.1:8848/nacos/v1/ns/instance' -d 'port=8000&healthy=true&ip=192.168.1.13&weight=1.0&serviceName=nacos.test.001&groupName=foo&metadata={"app":"foo","id":"003"}'

# 查询服务实例

curl "http://127.0.0.1:8848/nacos/v1/ns/instance/list?&namespaceId=public&serviceName=foo%40%40nacos.test.001&groupName=foo&clusters=&healthyOnly=true"

5.2 nacos客户端应用验证

如果客户端应用与nacos服务不在同一台机器,需要开放
8848
,
9848
端口给内网应用使用。(注意
8848
,
9848
端口不能暴露到外网上)

在客户端应用中配置好nacos服务后,即可运行验证。

5.3 使用r-nacos控制台

开放
10848
端口后,可以通过对应ip+端口在浏览器访问控制台。

新控制台有完备的用户管理、登陆校验、权限控制,支持对外网暴露。

系统会默认创建一个名为admin的用户,密码为admin。

6、总结

r-nacos是一个用rust实现的nacos服务,我们用它平替java nacos以降低服务占用内存,提升服务的稳定性。

systemd提供便捷的服务托管功能,可以方便的将一个命令运行的应用,转化成一个可方便控制的后台服务。

使用systemd部署r-nacos,是一个比较和合适的、可用于生产环境的部署方案。

1分布式系统介绍

1.1 分布式系统的发展

我们早期的集中式系统都是单体架构的,整个系统作为一个单体粒度的应用存在,所有的模块聚合在一起。
明显的弊端就是不易扩展、发布冗重、服务稳定性治理不好做。
随着微服务架构的不断大规模应用,驱使我们把
整个系统拆分成若干个具备独立运行能力的计算服务的集合

通过交互协作,完成庞大、复杂的业务流程,用户感知单一,但实际上,它是一个分布式服务的集合。

分布式系统主要从以下几个方面进行裂变:

1、应用可以从业务领域拆分成多个module,单个module再按项目结构分成接口层、业务层、数据访问层;也可以按照用户领域区分,如对移动、桌面、Web端访问的入口流量拆分不同类型接口服务。参考我的这篇《
微服务架构拆分策略
》,
2、数据存储层可以按业务类型拆分成多个数据库实例,还可以对单库或单表进行更细粒度的分库分表;参考我的这篇《
MySQL分库分表

3、通过一些业务中间件的支撑来保证分布式系统的可用性,如
分布式缓存、搜索服务、NoSQL数据库、文件服务、消息队列等中间件

1.2 存在的优势和不足

分布式系统可以解决集中式不便扩展的弊端,
提供了便捷的扩展性、独立的服务治理,并提高了安全可靠性
。随着微服务技术(Spring Cloud、Dubbo) 以及容器技术(Kubernetes、Docker)的大热,分布式技术发展非常迅速。
不足的地方:分布式系统虽好,
也给系统带来了复杂性,如分布式事务、分布式锁、分布式session、数据一致性等都是现在分布式系统中需要解决的难题
,虽然已经有很多成熟的方案,但都不完美。
分布式系统的便利,其实是
牺牲了一些开发、测试、发布、运维、资源 成本的,让工作量增加了
,所以分布式系统管理不好反而会变成一种负担。

2 分布式事务及应用场景

2.1 使用分布式事务解决问题

我们上面说了,分布式系统给业务带来了一些复杂性,所以,衍生出分布式事务来应对和解决这些问题。
分布式事务是指允许多个独立的事务资源参与到一个全局的事务中,其参与者、支持事务的服务器、资源服务器以及事务管理器分别位于分布式系统的不同节点之上。
这些节点属于同一个Action行为,如果有一个节点的结果不同步,就会造成整体的数据不一致。分布式事务需要保证这些action要么全部成功,要么全部失败,从而保证单个完整操作的原子性,也保证了各节点数据的一致性。

2.2 CAP定理

CAP 定理(也称为 Brewer 定理),指的是在分布式计算环境下,有3个核心的需求:
1、一致性(Consistency)
:再分布,所有实例节点同一时间看到是相同的数据
2、可用性(Availability)
:不管是否成功,确保每一个请求都能接收到响应
3、分区容错性(Partition Tolerance)
:系统任意分区后,在网络故障时,仍能操作
CAP理论告诉我们,分布式系统不可能同时满足以下三种。最多只能同时满足其中的两项,
大多数分布式业务中P是必须的, 因此往往选择就在CP或者AP

  • CA: 放弃分区容错性。
    非分布式架构,比如关系数据库,因为没有分区,但是在分布式系统下,CA组合就不建议了。
  • AP: 放弃强一致性。
    追求最终一致性,类似的场景比如转账,可以接受两小时后到账,Eureka的注册也是类似的做法。
  • CP: 放弃可用性。
    zookeeper在leader宕机后,选举期间是不提供服务的。类似的场景比如支付完成之后出订单,必须一进一出都完成才行。
    说明:在分布式系统中AP运用的最多,因为他放弃的是强一致性,追求的是最终一致性,性价比最高
    image

2.3 分布式事务应用场景

2.3.1 典型支付场景

这是最经典的场景。支付过程,要先对买家账户进行扣款,同时对卖家账户进行付款,
像这类的操作,
必须在一个事务中执行,保证原子性,要么都成功,要么都不成功。
但是往往买家的支付平台和卖家的支付平台不一致,即使都在一个平台下,所属的业务服务和数据服务
(归属不同表甚至不同库,比如卖家中心库、卖家中心库)也不是同一个。针对于不同的业务平台、不同的数据库做操作必然要引入分布式事务。

2.3.2 在线下单场景

同理,买家在电商平台下单,往往会涉及到两个动作,
一个是扣库存,第二个是更新订单状态
,库存和订单一般属于不同的数据库,需要使用分布式事务保证数据一致性。
image

2.3.3 跨行转账场景

跨行转账问题也是一个典型的分布式事务,用户A同学向B同学的账户转账500,要先进行A同学的账户-500,然后B同学的账户+500,既然是
不同的银行,涉及不同的业务平台,为了保证这两个操作步骤的一致,分布式事务必然要被引入。
image

3 分布式事务解决方案

常见的分布式一致性保障有如下方案

3.1 XA 两阶段提交协议

两阶段提交协议(Two-phase commit protocol,简称2PC)是一种分布式事务处理协议,旨在确保参与分布式事务的所有节点都能达成一致的结果。此协议被广泛应用于许多分布式关系型数据管理系统,以完成分布式事务。
它是一种强一致性设计,引入一个事务协调者的角色来协调管理各参与者的提交和回滚,二阶段分别指的是准备(投票)和提交两个阶段。
1、准备阶段(Prepare phase)
在此阶段,
协调者询问所有参与者是否可以提交事务。如果参与者的事务操作实际执行成功,则返回一个“同意”消息;如果执行失败,则返回一个“终止”消息。
下面是两个参与者都执行成功的结果:
image

准备阶段只要有一个参与者返回失败,那么协调者就会向所有参与者发送回滚事务的请求,即分布式事务执行失败。如下图:
image

2、提交阶段(Commit phase)
协调者根据所有参与者的应答结果判定是否事务可以全局提交(Commit 请求),并通知所有参与者执行该决定。
如果所有参与者都同意提交,则协调者让所有参与者都提交事务,向事务协调者返回“完成”消息。整个分布式事务完成。
如果其中某个参与者终止提交,则协调者让所有参与者都回滚事务。

image

如果其中一个Commit 不成功,那其他的应该也是提交不成功的。
image

3.2 XA三阶段提交

三阶段提交:
CanCommit 阶段、PreCommit 阶段、DoCommit 阶段,简称3PC
三阶段提交协议(Three-phase commit protocol,3PC),是二阶段提交(2PC)的改进版本。与两阶段提交不同的是,三阶段提交有两个改动点:在协调者和参与者中都引入超时机制,同时引入了预提交阶段。

在第一阶段和第二阶段中插入的预提交阶段,保证了在最后提交阶段之前各参与节点的状态是一致的。
即 3PC 把 2PC 的准备阶段再次一分为二,这样三阶段提交就有 CanCommit、PreCommit、DoCommit 三个阶段。
当 CanCommit、PreCommit、DoCommit的任意一个步骤失败或者等待超时,执行RollBack。

image

通过引入PreCommit阶段,3PC在一定程度上解决了2PC中协调者单点故障的问题,因为即使协调者在PreCommit阶段后发生故障,参与者也可以根据自身的状态来决定是否提交事务。然而,3PC并不是完美的解决方案,它仍然有一些缺点,比如增加了协议的复杂性和可能的性能开销。因此,在选择是否使用3PC时,需要根据具体的业务场景和需求进行权衡。

3.3 MQ事务

利用消息中间件来异步完成事务的后半部分更新,实现系统的最终一致性。
这个方式避免了像XA协议那样的性能问题。
下面的图中,使用MQ完成事务在分布式的另外一个子系统上的操作,保证了动作一致性。所以整个消息的生产和消息的消费动作需要全部完成,才算一个事务结束

image

3.4 TCC事务

TCC事务是Try、Confirm、Cancel三种指令的缩写
,其逻辑模式类似于XA两阶段提交,但是实现方式是在代码层面人为实现。
2PC 和 3PC 都是数据库层面的,而 TCC 是业务层面的分布式事务。
这种事务模式特别适用于需要强一致性保证的分布式事务场景,除了上面提到的数据库层面的操作外,例如电商平台的订单系统、跨行转账、分布式资源预订系统以及金融交易处理等。
下图就是一个典型的分布式系统的原子性操作,涉及A、B、C三个服务的执行。
如果有一个服务 try 出问题,整个事务管理器就执行calcel,如果三个try都成功,才执行confirm做正式提交。

image

如图,TCC事务分为三个阶段执行:

  1. Try阶段:主要是对业务系统做检测及资源预留。(执行2、3步骤)
  2. Confirm阶段:确认执行业务操作。如果Try阶段成功,则执行Confirm操作,提交事务。(执行4、5步骤)
  3. Cancel阶段:取消执行业务操作。如果Try阶段失败或超时,则执行Cancel操作,回滚事务。(执行4、5步骤)

3.5 最终补偿机制,同于MQ事务

最后使用补偿机制做最后的一致性保障,MQ方案尽量使用补偿机制进行保障。
如下图,对于发送成功,消费失败的消息,进入
Dead-Letter Queu
,使用单独的作业服务进行独立处理,比如重新发送死信消息进行消费,避免生产和消费的不一致,保证了最终的原子性、一致性。
image

4 总结

本文介绍了分布式系统的基础知识,以及分布式业务场景下保障分布式事务数据一致性、Action原子性的解决方案。
后续章节我们对分布式算法和常用框架进行介绍。

Timer是什么

Timer 是一种用于创建定期粒度行为的机制。

与标准的 .NET System.Threading.Timer 类相似,Orleans 的 Timer 允许在一段时间后执行特定的操作,或者在特定的时间间隔内重复执行操作。

它在分布式系统中具有重要作用,特别是在处理需要周期性执行的任务时非常有用。

Timer的注意事项

  1. 计时器回调不会改变空闲激活的状态,不能用于推迟其他空闲激活的停用。

  2. Grain.RegisterTimer 中传递的时间段取决于上次回调完成到下一次回调开始的时间,因此回调的频率会受到执行时间的影响。

  3. 每次 asyncCallback 调用都会作为单独轮次的激活,并且不会与同一激活的其他轮次同时运行。

代码示例

public classPlayerGrain : Grain, IPlayerGrain
{
public Task<string>GetPlayerInfo()
{
var timer = RegisterTimer(DoSomething, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2));return Task.FromResult($"Player ID: {this.GetPrimaryKeyString()}");
}
private async Task DoSomething(objectstate)
{
//在这里定义要执行的操作 await Task.Delay(5000);
Console.WriteLine($
"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} Timer Triggered: {this.GetPrimaryKeyString()}");
}
}

Reminder与Timer的区别

提醒(Reminder)是一种在 Orleans 中用于处理周期性任务的机制,与计时器类似,但具有一些重要区别:

  1. 永久性触发:提醒是永久性的,除非明确取消,否则会在几乎所有情况下(包括部分或完整群集重启)继续触发。

  2. 定义的持久性:提醒的定义会写入存储,但具体的事件及其时间不会。这意味着如果群集在提醒应该触发时关闭,它将错过该提醒,只会在下次提醒的触发时被重新激活。

  3. 关联于Grain:提醒是与Grain关联的,而不是与任何特定激活Grain。如果提醒的触发时,粒度没有与之关联的激活,则会创建该Grain,并在下次触发时重新激活。

  4. 消息传递:提醒的传递通过消息发生,受到与所有其他粒度方法相同的交错语义的约束。

  5. 适用场景:提醒通常不适用于高频计时器,其周期应该以分钟、小时或天为单位。相比之下,提醒更适用于周期性任务的处理,例如定期执行清理任务或发送通知等。

如果想使用reminder,需要安装nuget包

<PackageReference Include="Microsoft.Orleans.Reminders" Version="8.0.0" />

并开启reminder

silBuilder.UseInMemoryReminderService();

Grain需要实现接口 IRemindable ,并使用this.RegisterOrUpdateReminder 注册reminder

public interfaceIPlayerGrain : IGrainWithStringKey, IRemindable
{
Task
<string>GetPlayerInfo();
}
public classPlayerGrain : Grain, IPlayerGrain
{
public Task<string>GetPlayerInfo()
{
this.RegisterOrUpdateReminder("myReminder", TimeSpan.FromSeconds(5), TimeSpan.FromMinutes(1));return Task.FromResult($"Player ID: {this.GetPrimaryKeyString()}");
}
public Task ReceiveReminder(stringreminderName, TickStatus status)
{
Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} Reminder Triggered: {this.GetPrimaryKeyString()}");
returnTask.CompletedTask;
}
}

Timer 和 Reminder 场景

使用定时器(Timer)的场景:

  1. 对激活状态的要求不高:如果激活被停用或发生故障时,计时器停止运行不会产生重大影响,或者这种行为可接受。
  2. 较小的分辨率:如果需要较小的时间间隔来执行任务,例如以秒或分钟为单位。
  3. 计时器回调与 Grain 生命周期相关:如果需要在 Grain 的生命周期事件(如OnActivateAsync())或者调用粒度方法时启动计时器回调。

使用提醒(Reminder)的场景:

  1. 持久性要求:当需要确保周期性行为在激活和任何故障中都存在时,提醒是一个更好的选择。因为提醒是永久性的,除非明确取消,否则会在几乎所有情况下继续触发。
  2. 较大的时间间隔:当执行不常见的任务,例如以分钟、小时或天为单位的周期性任务时,提醒更为适合。

依赖注入创建Timer与Reminder

将 ITimerRegistry 或 IReminderRegistry 注入粒度的构造函数中,也可以创建Timer与Reminder

publicPlayerGrain(ITimerRegistry timerRegistry,
IReminderRegistry reminderRegistry,
IGrainContext grainContext)
{
timerRegistry.RegisterTimer(grainContext,DoSomething,
null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2));
reminderRegistry.RegisterOrUpdateReminder(grainContext.GrainId,
"testreminder",TimeSpan.FromSeconds(5), TimeSpan.FromMinutes(1));
}