2024年4月

需求

设置飞机的一些坐标位置(经纬度高度),插值得到更多的坐标位置,然后飞机按照这些坐标集合形成的航线飞行,飞机的朝向、俯仰角以及飞机转弯时的翻转角根据坐标集合计算得出,而不需要手动设置heading、pitch、roll。

坐标插值

不知道为什么,可能是飞行速度变化太大,我用Cesium自带的插值,计算出的航线很奇怪

// 如下代码插值计算出的航线有问题
property.setInterpolationOptions({ 
    interpolationDegree : 5, 
    interpolationAlgorithm : Cesium.LagrangePolynomialApproximation 
}); 

自己写的插值计算,效果等同于Cesium自带的线性插值。
思路很简单,每次插值,就是取时间的中点,两个坐标的中点。
代码如下:

/**
 * 重新采样this.DronePositions
 */
DetectsDrones.prototype.sameple = function () {
    for (let i = 0; i < 3; i++) {
        this.samepleOnce();
    }
}

/**
 * 重新采样this.DronePositions
 */
DetectsDrones.prototype.samepleOnce = function () {
    for (let i = 0; i < this.DronePositions.length - 1; i += 2) {
        let pos1 = this.DronePositions[i];
        let pos2 = this.DronePositions[i + 1];
        let time1 = dayjs(pos1.time, 'YYYY-MM-DD HH:mm:ss');
        let time2 = dayjs(pos2.time, 'YYYY-MM-DD HH:mm:ss');
        let time = time1.add(time2.diff(time1) / 2.0, 'millisecond');
        let lng = (pos1.targetPosition.lng + pos2.targetPosition.lng) / 2.0;
        let lat = (pos1.targetPosition.lat + pos2.targetPosition.lat) / 2.0;
        let height = (pos1.targetPosition.height + pos2.targetPosition.height) / 2.0;
        let heading = (pos1.targetPosition.heading + pos2.targetPosition.heading) / 2.0;
        let pitch = (pos1.targetPosition.pitch + pos2.targetPosition.pitch) / 2.0;
        let roll = (pos1.targetPosition.roll + pos2.targetPosition.roll) / 2.0;
        let pos = {
            time: time.format('YYYY-MM-DD HH:mm:ss.SSS'),
            targetPosition: {
                lng: lng,
                lat: lat,
                height: height,
                heading: heading,
                pitch: pitch,
                roll: roll,
            }
        }
        this.DronePositions.splice(i + 1, 0, pos);
    }
}

根据航线坐标集合计算heading、pitch、roll

从网上抄的计算heading和pitch的方法(参考博客:
https://blog.csdn.net/u010447508/article/details/105562542?_refluxos=a10
):

/**
 * 根据两个坐标点,获取Heading(朝向)
 * @param { Cesium.Cartesian3 } pointA 
 * @param { Cesium.Cartesian3 } pointB 
 * @returns 
 */
function getHeading(pointA, pointB) {
    //建立以点A为原点,X轴为east,Y轴为north,Z轴朝上的坐标系
    const transform = Cesium.Transforms.eastNorthUpToFixedFrame(pointA);
    //向量AB
    const positionvector = Cesium.Cartesian3.subtract(pointB, pointA, new Cesium.Cartesian3());
    //因transform是将A为原点的eastNorthUp坐标系中的点转换到世界坐标系的矩阵
    //AB为世界坐标中的向量
    //因此将AB向量转换为A原点坐标系中的向量,需乘以transform的逆矩阵。
    const vector = Cesium.Matrix4.multiplyByPointAsVector(
        Cesium.Matrix4.inverse(transform, new Cesium.Matrix4()),
        positionvector,
        new Cesium.Cartesian3()
    );
    //归一化
    const direction = Cesium.Cartesian3.normalize(vector, new Cesium.Cartesian3());
    //heading
    let heading = Math.atan2(direction.y, direction.x) - Cesium.Math.PI_OVER_TWO;
    heading = Cesium.Math.TWO_PI - Cesium.Math.zeroToTwoPi(heading);
    return Cesium.Math.toDegrees(heading);
}

/**
 * 根据两个坐标点,获取Pitch(仰角)
 * @param { Cesium.Cartesian3 } pointA 
 * @param { Cesium.Cartesian3 } pointB 
 * @returns 
 */
function getPitch(pointA, pointB) {
    let transfrom = Cesium.Transforms.eastNorthUpToFixedFrame(pointA);
    const vector = Cesium.Cartesian3.subtract(pointB, pointA, new Cesium.Cartesian3());
    let direction = Cesium.Matrix4.multiplyByPointAsVector(Cesium.Matrix4.inverse(transfrom, transfrom), vector, vector);
    Cesium.Cartesian3.normalize(direction, direction);
    //因为direction已归一化,斜边长度等于1,所以余弦函数等于direction.z
    let pitch = Cesium.Math.PI_OVER_TWO - Cesium.Math.acosClamped(direction.z);
    return Cesium.Math.toDegrees(pitch);
}

根据航线坐标集合计算heading、pitch、roll:
代码中this.DronePositions是无人机群的坐标集合,坐标放在targetPosition属性中

/**
 * 计算无人机群的heading
 */
DetectsDrones.prototype.calcHeading = function () {
    // 清空原有heading
    this.DronePositions.map(pos => {
        pos.targetPosition.heading = undefined;
    });

    for (let i = 0; i < this.DronePositions.length - 1; i++) {
        let pos1 = this.DronePositions[i];
        let pos2 = this.DronePositions[i + 1];
        let heading = -90 + getHeading(Cesium.Cartesian3.fromDegrees(pos1.targetPosition.lng, pos1.targetPosition.lat), Cesium.Cartesian3.fromDegrees(pos2.targetPosition.lng, pos2.targetPosition.lat));
        if (!pos1.targetPosition.heading) {
            pos1.targetPosition.heading = heading;
        }
        pos2.targetPosition.heading = heading;
    }
}

/**
 * 计算无人机群的pitch
 */
DetectsDrones.prototype.calcPitch = function () {
    // 清空原有pitch
    this.DronePositions.map(pos => {
        pos.targetPosition.pitch = undefined;
    });

    for (let i = 0; i < this.DronePositions.length - 1; i++) {
        let pos1 = this.DronePositions[i];
        let pos2 = this.DronePositions[i + 1];
        let pitch = getPitch(Cesium.Cartesian3.fromDegrees(pos1.targetPosition.lng, pos1.targetPosition.lat, pos1.targetPosition.height), Cesium.Cartesian3.fromDegrees(pos2.targetPosition.lng, pos2.targetPosition.lat, pos2.targetPosition.height));
        if (!pos1.targetPosition.pitch) {
            pos1.targetPosition.pitch = pitch;
        }
        pos2.targetPosition.pitch = pitch;
    }
}

/**
 * 计算无人机群的roll(不支持转弯大于90度)
 */
DetectsDrones.prototype.calcRoll = function () {
    // 清空原有roll
    this.DronePositions.map(pos => {
        pos.targetPosition.roll = undefined;
    });

    for (let i = 1; i < this.DronePositions.length - 1; i++) {
        let pos1 = this.DronePositions[i];
        let pos2 = this.DronePositions[i + 1];
        let deltaHeading = pos2.targetPosition.heading - pos1.targetPosition.heading;
        pos2.targetPosition.roll = deltaHeading / 1.5;
    }
}

效果

主要是飞机的朝向和转弯时的翻滚,俯仰角这里没体现。

遇到的问题

  1. 插值计算的问题,就是设置的坐标集合,是拆线,最好把它插值成平滑曲线,但是Cesium自带的插值,有时间参数,而我想仅仅通过经纬度集合来插值。
  2. 我写的计算roll的方法有问题,不支持转弯大于90度的情况,花了一些时间,没搞定。转弯小于90度,凑合用,测试了几组数据没问题,但仍不确定有没有BUG。严格来讲,根据这些参数,这个roll是算不出来的,但是,该算法要求根据飞机的转弯半径及方向,给出一个相对合理的roll值。
    抛砖引玉,有没有高手给个提示,插值问题怎么解决?roll的正确的通用的计算方法?

gin

  • star:74.6k

  • 地址:
    https://github.com/gin-gonic/gin

  • gin是最受开发者欢迎的 Web 框架,它有诸多的优点,性能高、轻量级和简洁的 API 设计,社区活跃度高,灵活、可扩展性强。当然了,最最主要的就是性能非常高,能够处理大量的并发请求。是web框架的不二之选。

  • 使用体验:我们新的项目都是使用gin框架,优点很明显,高性能、轻量、灵活;缺点就是太灵活了,就缺少了很多模块,比如ORM模块、MySQL模块、Redis模块等,对初学者来说还是有一定门槛的,想直接拿着gin撸一个项目出来还是有一定难度。

gorm

  • star:35k

  • 地址:
    https://github.com/go-gorm/gorm

  • gorm是Go语言中最受欢迎的ORM(Object-Relational Mapping)框架,它提供了强大的功能和简洁的 API,让数据库操作变得更加简单和易维护,避免了手写SQL语句的麻烦。

  • 使用体验:GO的orm使用的种类不多,只使用过gorm和beego的orm,使用感受上没有太大的区别。

beego

  • star:30.7k

  • 地址:
    https://github.com/beego/beego

  • Beego是一个开源的web框架,被广泛应用与Go语言的 web应用程序 的开发。它支持路由控制、配置管理、Session管理、日志、ORM等各种功能。它的核心设计是简单、易于学习和开发。

  • 使用体验:beego是我们使用最多的框架,当时大多数同学都是从PHP转过来的,几乎没任何门槛,就是按照PHP的方式编写代码,只不过把语言换了一下,集成的模块比较多,配置文件解析、LOG、ORM、session等等,看它的文档很容易撸出一个新项目,初学者、跨语言的建议使用beego,感官上没有用 Go 语言的思维去设计框架,各种模块比较臃肿,性能上跟轻量型的框架还是有差距的。

cli

  • star:21.4k

  • 地址:
    https://github.com/urfave/cli

  • cli提供了简单快速的构建命令行的功能。可以很容易的通过命令设定参数和配置执行业务逻辑。

  • 使用体验:我们的crontab的定时脚本都是使用cli的命令行执行的,挺好用的。当然了也没使用过其他的命令行的类库。

zap

  • star:20.6k

  • 地址:
    https://github.com/uber-go/zap

  • zap是 uber 开源的 Go 高性能日志库,支持不同的日志级别,支持日志记录结构化,分配资源最小。

  • 使用体验:高性能和灵活性兼具的日志服务,碰过的几乎所有的项目都是用 zap 记录日志,当然了,还是有一些门槛的。可供选择的好用的日志类库也不多。

mysql

  • star:14.1k

  • https://github.com/go-sql-driver/mysql

  • 第三方的 MySQL 驱动,专为 Go 的 sql 标准库设计。它提供了对 MySQL 特性的支持,包括连接池、事务处理等,它的特点是高性能、安全性。

  • 使用体验:这个感觉没啥可说的,底层的协议驱动,各种ORM的框架、拼SQL的写法,后面都是使用mysql驱动的。

redigo

  • star:9.7k

  • 地址:
    https://github.com/gomodule/redigo

  • Redigo 是一个Go 语言 Redis 客户端库,它提供了一个简单的接口来执行 Redis 命令,它支持 Redis 的多种数据类型和操作,包括字符串、哈希、列表、集合和有序集合等。它也支持发布/订阅模式、事务、管道和连接池等功能。

  • 使用体验:我们的项目使用Redis客户端都是redigo,可使用的Redis客户端就上面这两个,找一个顺眼的用就行了。

errors

  • star:8.1k

  • 地址:
    https://github.com/pkg/errors

  • pkg/errors 是一个 Go 语言的错误处理包,它提供了一个用于错误处理的机制,旨在简化错误信息的创建和传播。这个包提供了一种构建错误的原因和上下文的方法,使得在调试和错误追踪时更加直观和方便。

  • 使用体验:我们的每个项目错误处理都是使用errors,它的的优势是错误多次封装包裹和传寄,可以很方便的拿到错误的调用链和堆栈信息。

goconvey

  • star:8.1k

  • 地址:
    https://github.com/smartystreets/goconvey

  • GoConvey 是一个用于 Go 程序 测试框架。它通过提供一种易于阅读和编写的测试风格,帮助开发者定义和执行测试用例。特别适合于编写复杂的测试场景,提高代码的可读性和可维护性。

  • 使用体验:好用,本来需要输出打印测试结果,GoConvey 让测试成为项目的一部分,提高测试代码的可读性,所有人读代码一目了然。

gin

  • star:4.2k

  • 地址:
    https://github.com/codegangsta/gin

  • gin是用于实时加载Go Web应用程序的程序。只需 gin 运行在应用程序的目录中,gin就是实时监测,检测到代码更改后,将自动重新编译代码,应用在下次收到HTTP请求时就是用的修改后台的代码。

  • 使用体验:觉得热加载服务很有必要,这个也特别好用,不知道star为啥这么少,只要文件有修改,codegangsta/gin 就会自动编译然后执行,在代码编写和调试的阶段非常有用,极大的提高了效率。

Mojo 是一种面向 AI 开发者的新型编程语言。它致力于将 Python 的简洁语法和 C 语言的高性能相结合,以填补研究和生产应用之间的差距。Mojo 自去年 5 月发布后,终于又有动作了。最近,Mojo 的标准库核心模块已在 GitHub 上开源,采用 Apache 2 开源协议,开源后迅速受到广泛关注,登上了 GitHub Trending 热榜。

接下来是上周的热门开源项目,AI 生成音乐的 Suno 平台刚火,GitHub 上就有非官方的 API 服务了。说到 AI,一站式体验 LLMs 的桌面应用 jan 已经持续上榜两周了,我试了一下。虽然它开箱即用、界面清爽,但下载模型会失败我下载了多次才成功、偶尔还会出现程序崩溃的情况,我感觉瑕不掩瑜、值得一试。

最后,推荐一个清爽的古诗词网站和一本《一人企业方法论》的开源书籍,清明节假期将至提前祝大家踏春愉快、享受阳光。

  • 本文目录
    • 1. 开源新闻
      • 1.1 Mojo 开源标准库
      • 1.2 开源软件 xz 后门事件
    • 2. 开源热搜项目
      • 2.1 非官方的 Suno API 服务:Suno-API
      • 2.2 构建跨平台应用的 Rust 框架:Dioxus
      • 2.3 《一人企业方法论》第二版:one-person-businesses-methodology-v2.0
      • 2.4 终端里的 Git 客户端:lazygit
      • 2.5 利用企业数据定制人工智能的平台:mindsdb
    • 3. HelloGitHub 热评
      • 3.1 现代化的古诗词学习网站:aspoem
      • 3.2 一站式体验 LLMs 的桌面应用:jan
    • 4. 往期回顾

1. 开源新闻

1.1 Mojo 开源标准库

Mojo 编程语言的作者是 LLVM 和 Swift 编程语言的联合创始人 Chris Lattner,它之所以这么火,一方面是因为它出色性能和兼容 Python 生态。它到底有多快?在发布 Mojo 支持 Mac(苹果芯片)的文章中,Mojo 官方做了一个测试:

在 Apple MacBook Pro M2 Max 上,用 Mojo 运行一个矩阵乘法
示例
,大概比纯 Python 快 90,000 倍,

GitHub 地址:
https://github.com/modularml/mojo

1.2 开源软件 xz 后门事件

Linux 上广泛使用的无损压缩软件包 xz-utils(xz),被该开源项目的一位维护者秘密植入了后门。存在后门的版本是 v5.6.0 和 v5.6.1,后门版本尚未进入 Linux 发行版的生产版本,因此影响范围有限,主要影响的是测试版本的 Debian 和 Red Hat 发行版,以及 Arch 和 openSUSE 等。

攻击者潜伏长达 3 年时间,他从 2021 年开始为 xz 贡献代码,22 年成为项目的维护者,23 年取得足够的信任和更高的权限,24 年开始悄悄加入恶意代码,2024 年 3 月 29 日 Andres Freund 在对 PostgreSQL 数据库进行基准测试时,发现该后门并公开更多
技术细节

目前,该项目已被 GitHub 封禁,无法查看。

GitHub 地址:
https://github.com/tukaani-project/xz

2. 开源热搜项目

2.1 非官方的 Suno API 服务:Suno-API

主语言:Python

Star:618

周增长:600

Suno AI 是一款免费的 AI 音乐生成工具,用户可以通过文本提示词生成包含歌声和乐器的完整音乐作品。该项目是基于 Python 和 FastAPI 开发的套壳 API 服务,支持生成歌曲、歌词等功能。需要用户手动填入官网获取的 token,但无需担心 token 过期的问题。

GitHub 地址→
https://github.com/SunoAI-API/Suno-API

2.2 构建跨平台应用的 Rust 框架:Dioxus

主语言:Rust

Star:16k

周增长:300

这是一个受 React 启发的 Rust 库,可使用 Rust 语言构建跨平台的用户界面。它专注于开发人员的使用体验,可以用于快速开发网页前端、桌面应用、静态网站、移动端应用、TUI 程序等多种类型的平台应用。

fn app() -> Element {
    let mut count = use_signal(|| 0);

    rsx! {
        h1 { "High-Five counter: {count}" }
        button { onclick: move |_| count += 1, "Up high!" }
        button { onclick: move |_| count -= 1, "Down low!" }
    }
}

GitHub 地址→
https://github.com/DioxusLabs/dioxus

2.3 《一人企业方法论》第二版:one-person-businesses-methodology-v2.0

主语言:Other

Star:1.5k

周增长:1k

该书作者之前在 GitHub 上分享过一篇长文,叫做《一人公司的方法论》。它主要是针对独立开发者分享运营一人企业的一些经验。经过不断地迭代,作者发布了 2.0 版的《一人企业方法论》。新版最大的不同,是引入了系统化的思维和面向所有副业创业人群。

GitHub 地址→
https://github.com/easychen/one-person-businesses-methodology-v2.0

2.4 终端里的 Git 客户端:lazygit

主语言:Go

Star:44k

这是一个懒人版 Git 命令行工具,它采用 Go 语言编写,提供了支持键盘和鼠标的 Git 命令行交互界面,支持轻松添加文件、解决合并冲突、快速进行 push/pull 操作、滚动查看 branches/commits/stash 的日志和差异信息等功能。

GitHub 地址→
https://github.com/jesseduffield/lazygit

2.5 利用企业数据定制人工智能的平台:mindsdb

主语言:Python

Star:21k

周增长:1k

该项目把机器学习引入 SQL 数据库,将模型作为虚拟表(AI-table),从而省去了数据准备、预处理等步骤,可以直接用 SQL 查询时间序列、回归、分类预测的结果,实现简化机器学习开发流程的效果。

GitHub 地址→
https://github.com/mindsdb/mindsdb

3. HelloGitHub 热评

在这个章节,将会分享下本周 HelloGitHub 网站上的热门开源项目,欢迎与我们分享你上手这些开源项目后的使用体验。

3.1 现代化的古诗词学习网站:aspoem

主语言:TypeScript

这是一个更加注重阅读体验和 UI 的诗词网站,采用 TypeScript、Next.js、Tailwind CSS 构建。它拥有简洁清爽的界面和好看的字体,提供了古诗词的拼音、注释、译文以及移动端适配、搜索和一键分享等功能。

项目详情→
https://hellogithub.com/repository/ca765afd047741b884513e3811fbb5d4

3.2 一站式体验 LLMs 的桌面应用:jan

主语言:TypeScript

这是一个支持在本地运行开源 LLMs 和连接 ChatGPT 服务的 AI 对话桌面应用,它开箱即用、界面清爽、不挑硬件,支持设置代理、接入 ChatGPT、一键下载/接入适配当前电脑配置的大模型、离线运行等功能,适用于 Windows、Linux、macOS 操作系统。

项目详情→
https://hellogithub.com/repository/6b25f5dc4a694ccca078d975280b6811

4. 往期回顾

随着 AI 技术的不断发展,越来越多的开源项目开始服务于 AI 应用的需求。无论是为 AI 开发优化的编程语言 Mojo,还是利用企业数据定制 AI 模型的 mindsdb 平台,都体现了开源社区对 AI 领域的热情和创新。与此同时,也不乏一些安全隐患,像 xz 后门事件为我们敲响警钟,在享受开源带来便利的同时,也要保持警惕、时刻关注开源项目的安全动态。

往期回顾:

以上为本周的「GitHub 热点速递」如果你发现其他好玩、有趣的 GitHub 项目,就来
HelloGitHub
和大家一起分享下吧。

1 背景

我们之前介绍过分布式事务的解决方案,参考作者这篇《
五种分布式事务解决方案(图文总结)
》。
在那篇文章中我们介绍了分布式场景下困扰我们的3个核心需求(CAP):一致性、可用性、分区容错性,以及在实际场景中的业务折衷。
1、
一致性(Consistency):
再分布,所有实例节点同一时间看到是相同的数据
2、
可用性(Availability):
不管是否成功,确保每一个请求都能接收到响应
3、
分区容错性(Partition Tolerance):
系统任意分区后,在网络故障时,仍能操作
image

而本文我们聚焦高并发下如何保障 Data Consistency(数据一致性)。

2 分布式常见一致性问题

2.1 典型支付场景

这是最经典的场景。支付过程,要先查询买家的账户余额,然后计算商品价格,最后对买家进行进行扣款,像这类的分布式操作,
如果是并发量低的情况下完全没有问题的,但如果是并发扣款,那可能就有一致性问题。
。在高并发的分布式业务场景中,类似这种 “查询+修改” 的操作很可能导致数据的不一致性。
image

2.2 在线下单场景

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

2.3 跨行转账场景

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

3 一致性解决方案

3.1 分布式锁

分布式锁的实现,比较常见的方案有3种:
1、基于数据库实现分布式锁
2、基于缓存(Redis或其他类型缓存)实现分布式锁
3、基于Zookeeper实现分布式锁

这3种方案,从实现的复杂度上来看,从1到3难度依次递增。而且并不是每种解决方案都是完美的,它们都有各自的特性,还是需要根据实际的场景进行抉择的。

能力组件 实现复杂度 性能 可靠性
数据库
缓存
zookeeper

详细可以参考我的这篇文章《
分布式锁方案分析

因为缓存方案是采用频率最高的,所以我们这边对Redis分布式锁进行详细介绍:

3.1.1 基于缓存实现分布式锁

相比较于基于数据库实现分布式锁的方案来说,基于缓存来实现在性能方面会表现的更好一点。类似Redis可以多集群部署的,解决单点问题。
基于Redis实现的锁机制,主要是依赖redis自身的原子操作,例如:


# 判断是否存在,不存在设值,并提供自动过期时间
SET key value NX PX millisecond

# 删除某个key
DEL key [key …]

NX
:只在在键不存在时,才对键进行设置操作,SET key value NX 效果等同于 SETNX key value
PX millisecond
:设置键的过期时间为millisecond毫秒,当超过这个时间后,设置的键会自动失效

如果需要把上面的支付业务实现,则需要改写如下:


# 设置账户Id为17124的账号的值为1,如果不存在的情况下,并设置过期时间为500ms
SET pay_id_17124 1 NX PX 500

# 进行删除
DEL pay_id_17124

上述代码示例是指,当redis中不存在pay_key这个键的时候,才会去设置一个pay_key键,键的值为 1,且这个键的存活时间为500ms。
当某个进程设置成功之后,就可以去执行业务逻辑了,等业务逻辑执行完毕之后,再去进行解锁。而解锁之前或者自动过期之前,其他进程是进不来的。

实现锁机制的原理是:
这个命令是只有在某个key不存在的时候,才会执行成功。那么当多个进程同时并发的去设置同一个key的时候,就永远只会有一个进程成功。解锁很简单,只需要删除这个key就可以了。

另外,针对redis集群模式的分布式锁,可以采用redis的Redlock机制。

3.1.2 缓存实现分布式锁的优缺点

优点:Redis相比于MySQL和Zookeeper性能好,实现起来较为方便。
缺点:通过超时时间来控制锁的失效时间并不是十分的靠谱;这种阻塞的方式实际是一种悲观锁方案,引入额外的 依赖(Redis/Zookeeper/MySQL 等),降低了系统吞吐能力。

3.2 乐观模式

对于概率性的不一致的处理,需要乐观锁方案,让你的系统更具健壮性。
分布式CAS(Compare-and-Swap)模式就是一种无锁化思想的应用,它通过无锁算法实现线程间对共享资源的无冲突访问。
CAS模式包含三个基本操作数:内存地址V、旧的预期值A和要修改的新值B。在更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。

我们以
2.1节

典型支付场景
作为例子分析(参考下图):

  • 初始余额为 800
  • 业务1和业务2同时查询余额为800
  • 业务1执行购买操作,扣减去100,结果是700,这是新的余额。理论上只有在原余额为800时,扣减的Action才能执行成功。
  • 业务2执行生活缴费操作(比如自动交电费),原余额800,扣减去200,结果是600,这是新的余额。理论上只有在原余额为800时,扣减的Action才能执行成功。可实际上,这个时候数据库中的金额已经变为600了,所以业务2的并发扣减不应该成功。

根据上面的CAS原理,在Swap更新余额的时候,加上Compare条件,跟初始读取的余额比较,只有初始余额不变时,才允许Swap成功,这是一种常见的降低读写锁冲突,保证数据一致性的方法。
image

go 代码示例(使用Baidu Comate AI 生成,已调试):

package main  
  
import (  
	"fmt"  
	"sync/atomic"  
)  
  
// Compare 函数比较当前值与预期值是否相等  
func Compare(addr *uint32, expect uint32) bool {  
	return atomic.LoadUint32(addr) == expect  
}  
  
func main() {  
	var value uint32 = 0 // 共享变量  
  
	// 假设我们期望的初始值是0  
	oldValue := uint32(0)  
  
	// 使用Compare函数比较当前值与期望值  
	if Compare(&value, oldValue) {  
		fmt.Println("Value matches the expected old value.")  
		// 在这里,你可以执行实际的交换操作,但请注意,  
		// 在并发环境中,你应该使用atomic.CompareAndSwapUint32来确保原子性。  
		// 例如:  
		// newValue := uint32(1)  
		// if atomic.CompareAndSwapUint32(&value, oldValue, newValue) {  
		//     fmt.Println("CAS succeeded, value is now", newValue)  
		// } else {  
		//     fmt.Println("CAS failed, value was changed by another goroutine")  
		// }  
	} else {  
		fmt.Println("Value does not match the expected old value.")  
	}  
  
	// 修改value的值以演示Compare函数的行为变化  
	atomic.AddUint32(&value, 1)  
  
	// 再次比较,此时应该不匹配  
	if Compare(&value, oldValue) {  
		fmt.Println("Value still matches the expected old value, but this shouldn't happen.")  
	} else {  
		fmt.Println("Value no longer matches the expected old value.")  
	}  
}

3.3 解决CAS模式下的ABA问题

3.3.1 什么是ABA问题?

在CAS(Compare-and-Swap)操作中,ABA问题是一个常见的挑战。ABA问题是指一个值原来是A,被另一个线程改为B,然后又被改回A,当前线程使用CAS Compare检查时发现值仍然是A,从而误认为它没有被其他线程修改过。
image

3.3.2 如何解决?

为了避免ABA问题,可以采取以下策略:

1. 使用版本号或时间戳

  • 每当共享变量的值发生变化时,都递增一个与之关联的版本号或时间戳。
  • CAS操作在比较变量值时,同时也要比较版本号或时间戳。
  • 只有当变量值和版本号或时间戳都匹配时,CAS操作才会成功。

2. 不同语言的自带方案

  • Java中的
    java.util.concurrent.atomic
    包提供了解决ABA问题的工具类。
  • 在Go语言中,通常使用sync/atomic包提供的原子操作来处理并发问题,并引入版本号或时间戳的概念。

那么上面的代码就可以修改成:

type ValueWithVersion struct {  
	Value     int32  
	Version   int32  
}  
  
var sharedValue atomic.Value // 使用atomic.Value来存储ValueWithVersion的指针  
  
func updateValue(newValue, newVersion int32) bool {  
	current := sharedValue.Load().(*ValueWithVersion)  
	if current.Value == newValue && current.Version == newVersion {  
		// CAS操作:只有当前值和版本号都匹配时,才更新值  
		newValueWithVersion := &ValueWithVersion{Value: newValue, Version: newVersion + 1}  
		sharedValue.Store(newValueWithVersion)  
		return true  
	}  
	return false  
}  

3. 引入额外的状态信息

  • 除了共享变量的值本身,还可以引入额外的状态信息,如是否已被修改过。
  • 线程在进行CAS操作前,会检查这个状态信息,以判断变量是否已被其他线程修改过。

需要注意的是,
避免ABA问题通常会增加并发控制的复杂性,并可能带来性能开销。
因此,在设计并发系统时,需要仔细权衡ABA问题的潜在影响与避免它所需的成本。在大多数情况下,如果ABA问题不会导致严重的数据不一致或逻辑错误,那么可能不需要专门解决它。

4 总结

在高并发环境下保证数据一致性是一个复杂而关键的问题,涉及到多个层面和策略。
除了上面提到的方案外,还有一些常见的方法和原则,用于确保在高并发环境中保持数据一致性:

  1. 事务(Transactions)


    • 使用数据库事务来确保数据操作的原子性、一致性、隔离性和持久性(ACID属性)。
    • 通过锁机制(如行锁、表锁)来避免并发操作导致的冲突。
  2. 分布式锁


    • 当多个服务或节点需要同时访问共享资源时,使用分布式锁来协调这些访问。
    • 例如,使用Redis的setnx命令或ZooKeeper的分布式锁机制。
  3. 乐观锁与悲观锁


    • 乐观锁假设冲突不太可能发生,通常在数据更新时检查版本号或时间戳。
    • 悲观锁则假设冲突很可能发生,因此在数据访问时立即加锁。
  4. 数据一致性协议


    • 使用如Raft、Paxos等分布式一致性算法,确保多个副本之间的数据同步。
  5. 消息队列


    • 通过消息队列实现数据的异步处理,确保数据按照正确的顺序被处理。
    • 使用消息队列的持久化、重试和顺序保证特性。
  6. CAP定理与BASE理论


    • 理解CAP定理(一致性、可用性、分区容忍性)的权衡,并根据业务需求选择合适的策略。
    • BASE理论(Basically Available, Soft state, Eventually consistent)提供了一种弱化一致性要求的解决方案。
  7. 缓存一致性


    • 使用缓存失效策略(如LRU、LFU)和缓存同步机制(如缓存穿透、缓存击穿、缓存雪崩的应对策略),确保缓存与数据库之间的一致性。
  8. 读写分离读写


    • 使用主从复制、读写分离读写等技术,将读操作和写操作分散到不同的数据库实例上,提高并发处理能力。
  9. 数据校验与重试


    • 在数据传输和处理过程中加入校验机制,确保数据的完整性和准确性。
    • 对于可能失败的操作,实施重试机制,确保数据最终的一致性。
  10. 监控与告警


    • 实时监控数据一致性相关的关键指标,如延迟、错误率等。
    • 设置告警阈值,及时发现并处理可能导致数据不一致的问题。

在实际应用中,通常需要结合具体的业务场景和技术栈来选择合适的策略。

大家好,我是狂师!

今天给大家推荐一款开源的HTTP测试工具:
Hurl
,相比
curl

wget
功能更强大,且更容易上手、很适用新手使用。

1、项目介绍

Hurl
是一个使用
Rust
语言开发的命令行工具,它允许用户运行以简单纯文本格式定义的HTTP请求。这个工具不仅适用于获取数据,还非常适合用于测试HTTP会话和API。

项目地址:

https://github.com/Orange-OpenSource/hurl

Hurl的主要特性和用途包括但不限:

  • 请求发送与捕获
    :Hurl可以发送HTTP请求,并捕获响应中的值。这使得用户可以方便地执行各种HTTP操作,并收集所需的响应数据。
  • 查询与评估
    :Hurl支持对标头和正文响应进行查询和评估。用户可以使用XPath和JSONPath等多种查询方式,以满足不同的测试需求。
  • 链式调用
    :Hurl支持多个请求的链式调用,这使得用户可以方便地构建复杂的测试用例,从而更全面地测试HTTP会话或API。
  • 集成与报告
    :Hurl易于集成到CI/CD(持续集成/持续部署)流程中,支持生成多种格式的报告,如文本报告、JUnit报告和HTML报告,这有助于用户分析和理解测试结果。
  • 适用于多种场景
    :Hurl不仅适用于REST/JSON API的测试,还适用于HTML内容、GraphQL以及SOAP API等多种场景。

总的来说
,Hurl是一个非常灵活且功能丰富的命令行工具,它不仅可以发送HTTP请求,还可以将这些请求链接在一起,形成请求链。这个特性使得Hurl在模拟复杂的用户交互场景时特别有用。此外,Hurl还能够捕获请求中的特定值,并对响应头部和响应正文中的信息进行查询和评估。
无论是对于初学者还是经验丰富的测试人员,Hurl都是一个值得考虑的选择。

2、不同平台安装下载

Hurl作为一个功能强大的命令行HTTP请求工具,其安装步骤在不同的操作系统上会有所不同。以下是针对各个平台的安装操作步骤:

  • Mac用户
    :可以通过Homebrew来安装Hurl。在终端中输入命令,按照提示完成安装过程。
brew install hurl
  • Windows用户
    :可以访问Hurl的GitHub发布页面,下载最新版本的安装包,然后按照提示进行安装。
https://github.com/Orange-OpenSource/hurl/releases

  • Linux用户
    :可以使用包管理器来安装Hurl。例如,在基于Debian的系统上,可以使用
    apt-get install hurl
    命令来安装。

3、Hurl使用

1、GET请求

# Get home:
GET https://example.org
HTTP 200
[Captures]
csrf_token: xpath "string(//meta[@name='_csrf_token']/@content)"


# Do login!
POST https://example.org/login?user=toto&password=1234
X-CSRF-TOKEN: {{csrf_token}}
HTTP 302

2、POST请求

POST https://example.org/api/tests
{
    "id": "4568",
    "evaluate": true
}
HTTP 200
[Asserts]
header "X-Frame-Options" == "SAMEORIGIN"
jsonpath "$.status" == "RUNNING"    # Check the status code
jsonpath "$.tests" count == 25      # Check the number of items
jsonpath "$.id" matches /\d{4}/     # Check the format of the id

Hurl虽是一个命令行工具,但Hurl的主要使用方式是通过编写Hurl文件,这些文件包含了要发送的HTTP请求的定义。然后,用户可以通过Hurl命令行工具来运行这些文件,发送请求并获取响应。

示例一:发送GET请求并输出响应

1、创建Hurl文件内容 (example1.hurl)

GET https://api.example.com/data

2、执行命令行

hurl example1.hurl

example1.hurl 是包含HTTP请求的Hurl文件。
执行此命令后,Hurl会发送一个GET请求到
https://api.example.com/data
,并在终端输出服务器的响应。

示例二:发送POST请求并携带JSON数据

1、创建Hurl文件内容 (example2.hurl)

POST https://api.example.com/create  
Content-Type: application/json  
  
{  
  "name": "John Doe",  
  "age": 30  
}

2、执行命令行

hurl example2.hurl

执行命令后,Hurl会发送POST请求到
https://api.example.com/create
,并在请求体中携带JSON数据。

示例三:使用变量和链式请求

1、创建Hurl文件内容 (example3.hurl)

GET https://api.example.com/user/123  
  
# 捕获响应中的token  
{{token}} = response.headers.get("X-Auth-Token")  
  
GET https://api.example.com/data  
Authorization: Bearer {{token}}

2、执行命令行

hurl example3.hurl

3、执行解释:

  • 第一个GET请求用于获取用户的认证token。
    {{token}} = response.headers.get("X-Auth-Token") 这行代码捕获响应头中的X-Auth-Token值,并将其存储在token变量中。
  • 第二个GET请求使用了前面捕获的token变量作为Authorization头的值,用于后续的认证。
  • 执行命令后,Hurl会按照顺序执行两个请求,并在第二个请求中使用第一个请求的响应数据。

示例四:包含断言和隐式验证

1、创建Hurl文件内容 (example4.hurl)

GET https://api.example.com/status  
  
# 隐式验证:检查状态码是否为200  
HTTP/1.1 200  
  
# 显式断言:检查响应体是否包含特定文本  
assert contains(response.body, "OK")

2、执行命令行:
hurl example4.hurl

3、执行解释

  • 发送GET请求到https://api.example.com/status。
  • 隐式验证是通过在Hurl文件中直接指定期望的HTTP状态码(这里是200)来完成的。如果服务器的响应状态码与指定的不同,Hurl会报错。
  • 显式断言使用assert关键字来检查响应体是否包含"OK"文本。如果不包含,测试将失败。

这些示例展示了Hurl的基本用法和一些高级特性,如变量捕获、链式请求和断言验证。

你可以根据自己的需求,结合Hurl的文档和这些示例,构建更复杂的HTTP测试场景。