使用 Redis 作为消息队列 - Redis Stream

十来天前写过一篇 Redis 之前如何曲线的方式用作消息队列 使用 Redis 作为消息队列 - Pub/Sub, List, SortedSet. 只能说简单的使用方式勉强还行,离真正意义上的消息队列有些距离。而自 Redis 5.0 加入了 Stream 就更进一步,可望朝着作为正规消息队列的 At most once, At least once, 和 Exactly once 方向迈进。

如果以 Serverless 方式使用 AWS 的 Redis, 那么既然用到高级消息队列的功能,还能省去使用 AmazonMQ(ActiveMQ 或 RabbitMQ) 或 MSK(Kafka) 的高成本。

Redis stream 数据结构像是一个 append-only 日志,但又添加了 O(1) 的随机访问和复杂的消费策略,如消息分组。

Redis Stream 的每条消息会有一个唯一 ID, 支持消费组, Redis 用以支持 Stream 的一系列命令是 X 为前缀的, 完整的 Stream 命令列表阅读全文 >>

调用 AWS Lambda 时如何传送字节数组

本文主要验证用 Python 写的 AWS Lambda 与 Java 客户端之间如何双向传递二进制数据,这里不涉及到 Lambda 流输入输出的问题。比如一个 Python AWS Lambda 的处理方法声明是

def lambda_handler(event, context):
    pass  # or do something

通过我们用 Lambda 调用时会传给 event 一个 JSON 格式的字符串,反应到 AWS Lambda 时 event 就是一个字典。但当要传递二进制数据如何做呢?直觉的做法就是用 base64 编码二进制字节为普通的字符串,比如要节约网络传输的数据量,需要对文本进行压缩,格式可以是这样

{"input": base64Encode(gzipCompress("text content......"))}

然后在 Lambda 端取出 input 的值作相应的 base64 解码再解压缩。

对于大文本,即使是压缩后再编码为 base64 也比直接传送原始文本数据要节约网络带宽。

这种方案实际也是可行的,然而我们在实际使用 Java AWS Lambda SDK 时有些动作会自动帮我们实现的,那就是二进制数据自动 base64 编码。 阅读全文 >>

Java 使用堆外内存(off-heap memory) 作为缓存

我们通常用的 Java 缓存基本可认为是扩展了 HashMap 或 ConcurrentHashMap 的实现,它们各自实现自己的缓存策略,如时间与空间的控制,生命周期管理,是否支持分布式,溢出时能否转储到磁盘。关于 Java 本地缓存的存储分为内存与磁盘,内存多数情况下指的是堆内内存(on-heap), 而介于堆内内存与文件存储之间的就是堆外内存(off-heap)

  1. 堆内存储(on-heap): 操作最快,无需序列化,但大量数据时会影响到 GC 的效率
  2. 堆外存储(off-heap): 存储在 Java 进程内存但非 JVM 堆内(不在 -Xmx 指定的内存范围内),使用或保存时需进行序列化/反序列化过程(在堆内与堆外转换),但不受 GC 影响,有助于提它来 GC 的效率
  3. 文件存储:不仅存在序列化与反序列化过程,还带 IO 操作,所以最慢,唯一优点就是大

我们查看一下当前 Spring 支持的缓存实现, Supported Cache Providers, 列有 Generic, JCache(JSR-107), EhCache 2.x, Hazelcast, Infinispan, Couchbase, Redis, Caffeine, Simple, 这其中无一支持堆外缓存,其中的 EhCache 要付费使用 EhCache 3(Big Memory) 才能支持 off-heap。 阅读全文 >>

用 C++ 写一个 AWS Lambda Hello World

AWS 自 2014 年推出 Lambda 时仅支持 Node.js,而后添加了对 Python, Ruby, Java, C#, F#, PowerShell 的支持,再来到 2018 年可以自定义运行时了,比如用性能较好的 C, C++, Rust, Go 等语言。见 AWS Lambda Now Supports Custom Runtimes and Enables Sharing Common Code Between Functions.

如果使用 Python, Java 写 Lambda 时觉得还不得快,不想要明显的预热过程,也许 1000 毫秒的任务只想要 600 毫秒就能完成,内存还希望再压缩一些,那着实能在每月千百万次 Lambda 调用的情况下节省一笔可观的支出,那么可以试一试 C, C++, Rust, Go 等编译成了机器指令的语言,况且前三者没有 GC, 执行效率会更高。

本日志记录一下如何用 C++ 创建一个 AWS Lambda, 以及可如何应付 Lambda 的复用。本文主要参考自下面两处

  1. Introducing the C++ Lambda Runtime
  2. GitHub 项目 aws-lambda-cpp

自定义运行时可选择 X86_64 或 arm64 的 Amazon Linux 2023 或 Amazon Linux 2。部署时可选择的 runtime 相应有 provided.al2023, provided.al2, 推荐使用 provided.al2023。runtime provided 不被支持了。

C++ 代码可选择用 GCC 或 Clang 来编译,既然 AWS Lambda 实际的运行时会用到 Amazon Linux 2023,那我们就直接选择 Docker 镜像 amazonlinux:2023 作为我们的编译环境。 阅读全文 >>

创建可直接用 root 用户 ssh 登陆的 Docker 镜像

有时候我们在 Mac OS X  或 Windows 平台下需要开发以 Linux 为运行时的应用,IDE 或可直接使用 Docker 容器,或 SSH 远程连接。本地命令行下操作虽然可以用 docker exec 连接正在运行的容器,但 IDE 远程连接的话 SSH 总是一种较为通用的连接方式,所以我们希望做一个能进行 SSH 连接的 Docker 容器。因为是本地运行的 Docker,我们想直接用 root 连接,以获得在容器中最大的运行权限。下面以 ubuntu:2004 基础镜像为例,看如何安装启用 ssh 服务以及允许 root 连接。

创建允许 root + 密码登陆的镜像

我们创建一个基本的 Dockerfile 文件,内容为 阅读全文 >>

C 语言静态库与动态库的生成和使用

在 YouTube 上找到一个视频 动态链接库静态链接库的生成和使用,它把用 GCC 生成静态库和动态库,以及如何使用他们说的很明白,有条件的可以直接看那个视频。本文就是一个观后的实操和笔记,加添了更多如何查看动态库,静态库,目标文件,执行文件的过程。

为什么要了解静态库和动态库呢?这有助于我们理解多模块的 C/C++ 代码是如何联合工作的。我们多数时候使用的 IDE, 一个 Build 帮我们做了太多的事情,反而使我们眼前一抹黑,这背后有怎么把一个个源文件编译成目标文件(*.o) 文件,或生成静态库/动态库,又如何连接静态库/动态库生成可执行文件,等等。

试验中使用的平台是 Linux, 如果没有 Linux 可通过 Docker 容器得到一个,如

$ docker run -it -v $(pwd):/work -w /work rust:1.78-buster bash

为什么使用 rust:1.78 镜像,其实也没什么特别的,因为当前在学习 Rust, 而正好该镜像中有 GCC 编译器。启动该容器后,为编辑需要,最好安装一个 vim,在容器中运行 阅读全文 >>

Rust 调用 C/Rust 生成的动态库

在始终是 C/C++ 有着更优越性能的情况下,因而之前介绍过多种 其他不同的语言如何加载使用 C/C++ 写的动态库,有 Go, Python, Java 和 C#。在学习 Rust 之时也有类似的需求。本文的做法是要用到第三方库 libloading,这里将参考官方的例子。

先来创建一个动态库,使用和 Go 调用 C 写的动态库完整例子(Linux版) 一文中相同的例子,add.c 代码内容如下

在 Linux 中使用如下命令编译出 libadd.so 动态库文件 阅读全文 >>

Java 线程池有限大小工作队列 - 不丢弃任务的实现

我们在创建 Java 线程池,无论是用 Executors, ThreadPoolExecutor, 还是 Spring 的 ThreadPoolTaskExecutor, 如果不指定工作队列的大小的话,默认为 Integer.MAX_VALUE(2147483647), 基本不会把它爆满,但是在许多的任务要执行时大量 Runnable 对象的创建却足以把内存撑爆掉。所以才有必要使用一个有限大小的工作队列,如 5000, 再配上 RejectedExecutionHandler(DiscardOldestPolicy, DiscardPolicy, 或 CallerRunsPolicy)。前两种策略会主动放弃最旧最新的任务,一般不是我们想要的,CallerRunsPolicy 还能主动发挥任务提交者的计算能力,是一种不错的选择(只可能会发生工作队列太小且提交者执行的任务太忙时产生线程池一时的空闲。

所以总结起来我们可以有以下几种实现

直接使用 CallerRunsPolicy

在工作队列满时有效利用提交任务的线程,不让它闲着,这种实现最简单, 像下面那样声明线程池

阅读全文 >>

JDBC 设置 PostgreSQL 查询中 any(?) 的参数

这段时间都纠缠于 Java 如何操作 PostgreSQL 数据库上,千方百计的为求得更好的性能。为此我们用上了 Batch, 或用 id = any(?) 这种更 PostgreSQL 化的数组参数操作。其实它还有更多数组方面的花样可以玩,毕竟 PostgreSQL 数据库有一种广纳百川的胸怀,总有好的新特性能在 PostgreSQL 中首先体验到。

回到之前的一篇 postgres in (?,?) 和 =any(?) 用法/性能对比,其中关于如何向查询语句中 id = any(?) 占位符传入数组参数的代码是

在 PreparedStatement(PgPreparedStatement) 中设置数组参数的函数是用 阅读全文 >>

JDBC 批量调用数据库 SQL, 函数与存储过程

继续上一篇数据库相关操作的话题,在有大量的数据操作时(如增删改,甚至调用函数或存储过程),我们应该尽可能的采用批量化操作(先摆下结论,后面我们会看到原由)。想像一下我们要向数据库插入 10 万条记录,如果逐条插入的话,客户端与数据库之间将会有 10 万网络请求响应来回; 而假如以 1000 条记录为一个 batch, 客户端与数据库之间的网络请求响应次数将缩小到 100。 业务数据的内容总量未变,但 Batch 操作除了可重用预编译的 Statement 外还, 可避免每次请求中重复的元数据,所以从 100,000 到 100 的缩减在时效上的表现是非常可观的,有时就是 60 分钟与 1 分钟的区别(在最后面测试结果显示这一差异更为恐怖)。

当然, JDBC 的批处理功能具体还要相应驱动的支持,通过数据库连接的 conn.getMetaData().supportsBatchUpdates() 可探知是否支持批量操作。

API 方面, 在  Statement 接口中定义了如下 batch 相关的操作方法

  1. void addBatch(String sql): 将显式的 SQL 语句编入到当前 Batch 中
  2. void clearBatch(): 清除当前 Batch 列表,以便于建立新的 Batch
  3. int[] executeBatch(): 执行当前 Batch 列表中的语句,返回每条语句受影响行数组成的数组。0 可能表示执行语句无法确知受影响的行
  4. long[] executeLargeBatch(): 当 Batch 中语句受影响行数可能会超过整数最大值时用这个

阅读全文 >>