开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

什么是布隆过滤器?如何实现布隆过滤器?

发表于 2024-01-05

布隆过滤器(Bloom Filter)是一种空间效率极高的概率型数据结构,用于判断一个元素是否在一个集合中。它基于位数组和多个哈希函数的原理,可以高效地进行元素的查询,而且占用的空间相对较小,如下图所示:

根据 key 值计算出它的存储位置,然后将此位置标识全部标识为 1(未存放数据的位置全部为 0),查询时也是查询对应的位置是否全部为 1,如果全部为 1,则说明数据是可能存在的,否则一定不存在。

也就是说,如果布隆过滤器说一个元素不在集合中,那么它一定不在这个集合中;但如果它说一个元素在集合中,则有可能是不存在的(存在误差)。

1.布隆执行过程

布隆过滤器的具体执行步骤如下:

  1. 在 Redis 中创建一个位数组,用于存储布隆过滤器的位向量。
  2. 初始化多个哈希函数,并将每个哈希函数的计算结果对应的位数组位置设置为 1。
  3. 添加元素到布隆过滤器时,对元素进行多次哈希计算,并将对应的位数组位置设置为 1。
  4. 查询元素是否存在时,对元素进行多次哈希计算,并检查对应的位数组位置是否都为 1。

2.布隆使用场景

布隆过滤器的主要使用场景有以下几个:

  1. 大数据量去重:可以用布隆过滤器来进行数据去重,判断一个数据是否已经存在,避免重复插入。
  2. 缓存穿透:可以用布隆过滤器来过滤掉恶意请求或请求不存在的数据,避免对后端存储的频繁访问。
  3. 网络爬虫的 URL 去重:可以用布隆过滤器来判断 URL 是否已经被爬取,避免重复爬取。

3.如何实现布隆过滤器?

在 Redis 中不能直接使用布隆过滤器,但我们可以通过 Redis 4.0 版本之后提供的 modules (扩展模块) 的方式引入,它的实现步骤如下。

① 打包RedisBloom插件

git clone github.com/RedisLabsMo…

cd redisbloom

make # 编译redisbloom

编译正常执行完,会在根目录生成一个 redisbloom.so 文件。

② 启用RedisBloom插件

重新启动 Redis 服务,并指定启动 RedisBloom 插件,具体命令如下:

redis-server redis.conf –loadmodule ./src/modules/RedisBloom-master/redisbloom.so

③ 创建布隆过滤器

创建一个布隆过滤器,并设置期望插入的元素数量和误差率,在 Redis 客户端中输入以下命令:

BF.RESERVE my_bloom_filter 0.01 100000

④ 添加元素到布隆过滤器

在 Redis 客户端中输入以下命令:

BF.ADD my_bloom_filter leige

⑤ 检查元素是否存在

在 Redis 客户端中输入以下命令:

BF.EXISTS my_bloom_filter leige

课后思考

以上我们介绍了什么是布隆过滤器?它的使用场景和执行流程,以及在 Redis 中它的使用,那么问题来了,在日常开发中,也就是在 Java 开发中,我们又将如何操作布隆过滤器呢?欢迎评论区留下您的实现方案。

本文已收录到我的面试小站 www.javacn.site,其中包含的内容有:Redis、JVM、并发、并发、MySQL、Spring、Spring MVC、Spring Boot、Spring Cloud、MyBatis、设计模式、消息队列等模块。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

支付系统日志设计完全指南:构建高效监控和问题排查体系的关键基

发表于 2024-01-04

这是《百图解码支付系统设计与实现》专栏系列文章中的第(7)篇。

在一家头部互联网公司发现一些工作多年的同学打印的日志也是乱七八糟的,所以聊聊这个话题。

本文主要讲结构清晰的日志在支付系统中的重要作用,设计日志规范需要遵守的一些基本原则,以及接口摘要日志、业务摘要日志、详细日志、异常日志等常用日志设计的最佳实践。

专栏地址: 百图解码支付系统设计与实现

通过这篇文章,你可以了解到:

  1. 我为什么要写这个话题
  2. 什么是日志
  3. 日志对于支付系统运行保障的重要性
  4. 日志设计的常见误区
  5. 设计清晰日志规范的基本原则
  6. 几个最佳实践
  1. 我为什么要写这个话题

写过代码的同学,一定打印过日志,但经常发现一些工作多年的同学打印的日志也是乱七八糟的。我曾经在一家头部互联网公司接手过一个上线一年多的业务,相关日志一开始就没有设计好,导致很多监控无法实现,出了线上问题也不知道,最后只能安排同学返工改造相关的日志。所以有必要聊聊这个话题。

  1. 什么是日志

写过代码的同学一定再熟悉不过。日志本质就是一种系统记录文件,用于存储发生在操作系统、应用软件、网络和存储设备上的事件,主要用于问题诊断、审计和性能监控。

  1. 日志对于支付系统运行保障的重要性

日志的重要性相信不必多说,没有日志,系统上线后出问题就等于抓瞎。

在支付系统中,日志不仅用于记录交易详情和系统状态,还起到监控和安全审计的核心作用。它们帮助我们实时监控系统的健康状态,快速排查线上的问题。此外,在支付领域日志对于交易验证和法律合规性文档记录都是不可或缺的。

  1. 日志设计的常见误区

设计日志系统时常见的误区包括:

  1. 过度记录:记录大量无用信息,导致重要信息难以识别。
  2. 格式混乱: 不统一的日志格式给监控告警、日志分析和问题定位带来困难。
  3. 忽视隐私和安全: 未对敏感信息进行脱敏处理,增加数据泄露风险。比如卡号,身份证号,手机号等。
  1. 设计清晰日志规范的基本原则

根据这么多年的实践,设计一个清晰的日志系统最少应遵循以下原则:

结构化日志: 使用结构化数据格式记录,便于机器解析。这个尤其对监控系统有用。

日志分级: 合理设置日志级别(如DEBUG、INFO、WARN、ERROR),便于过滤和搜索。

标准化字段: 标准化常用字段(如时间戳、日志级别、请求ID等),保持一致性。

上下文信息: 确保日志含有足够的上下文信息,方便定位问题。尤其是详细日志,一定要打印上下文信息。

脱敏处理: 对于敏感数据,如手机号、卡号等,进行适当的脱敏处理。

分布式追踪ID: 引入分布式追踪系统,为跨服务的请求分配唯一的追踪ID。

  1. 几个最佳实践

首先我们要明白日志是用来做什么的。只是先弄明白做事的目的,我们才能更好把事情做对。在我看来,日志有两个核心的作用:1)监控,诊断系统或业务是否存在问题;2)排查问题。

对于监控而言,我们需要知道几个核心的数据:业务/接口的请求量、成功量、成功率、耗时,系统返回码、业务返回码,异常信息等。对于排查问题而言,我们需要有出入参、中间处理数据的上下文,报错的上下文等。

接下来,基于上面的分析,我们就清楚我们应该有几种日志:

  1. 接口摘要日志。监控接口的请求量、成功量、耗时、返回码等。使用固定格式,需要打印:时间、接口名称、结果(成功/失败)、返回码、耗时等基本信息就足够。
  2. 业务摘要日志。监控业务的请求量、成功量、核心业务信息、返回码等。使用固定格式,需要打印:时间、业务类型、上一步状态、当前状态、返回码、核心业务信息(不同业务有不同的核心业务信息,比如流入,就有支付金额/退款金额,卡品牌,卡BIN等)。
  3. 详细日志。用于排查问题,不用于监控。格式不固定。主要包括时间、接口、入参、出参,中间处理数据输入,异常的堆栈信息等。
  4. 系统异常日志。同时用于监控。格式固定。需要打印:时间、错误码、错误信息、堆栈信息等。

补充一个典型的支付场景下的业务日志格式如下:

文件名:payment.biz.digest.log

格式规范:

[时间,分布式追踪ID,环境标,压测标,站点标,请求来源系统,上游请求ID,上游支付ID,我方系统业务ID],[交易类型,交易币种,交易金额,上一个状态,当前状态],[渠道名,收单国家,发卡行,卡品牌,风控参数],[我方标准返回码,我方标准返回码描述,渠道返回码,渠道返回码描述]

日志示例:[2024-01-04 20:02:32.239,293242318382329329232,P,0,UK,payment,2024010401203223220001,2024010401203223220001,2024010401203223220003],[pay,USD,2392,INIT,PAYING],[WPG,US,CMB,VISA,2D],[-,-,-,-]

说明:上面的日志使用[]进行了块分隔,第一个[]里面是基础信息,第二个[]里面是交易信息,第三个[]里面是渠道信息,第四个[]里面是返回码信息。

使用[]切割的好处是,如果后面要加字段,可以找到对应的位置增加。不影响现有监控。比如我要加个卡BIN,那就可以在风控参数后面加,不影响返回码监控位置。

有几点特别补充说明:

  1. 正常业务和系统异常需要拆分出来。NPE就是系统异常,余额不足就是一个预期内的业务场景,不要打印到异常文件中。
  2. 业务摘要信息需要根据业务不同,设计不同的业务摘要日志格式。比如支付、流出提现的交易,路由、渠道咨询等,监控诉求是不一样的,所以需要单独设计。拿路由举例,需要监控哪些渠道分流了多少,命中了哪个规则等,必然不能直接使用支付、退款的业务摘要日志格式。所以每出现一种新业务,就需要单独设计一种业务日志。
  3. 接口摘要日志,不要打印出入参。因为出入参有可能包含非常多数据,而接口我们只关注请求量、结果、耗时这些就够了,如果想查出入参,就去详细日志里面查。
  4. 系统异常日志一定要有单独的日志文件。因为正常的系统,绝大部分是业务上报错,比如余额不足,而不应该有很多NPE等系统异常。我们需要监控异常日志的行数或特定错误码的频率,比如每分钟有X行或每分钟有Y个特定错误码就需要告警出来。
  1. 结束语

日志系统是支付系统关键的支撑组件,一个良好设计的日志系统可以为监控、告警和问题排查提供强有力的支持,反之,对于线上问题简直就是恶梦。

今天主要讲了日志格式规范的设计,对于log4j的配置什么的,网上已经有很多公开资料,这里不再赘述。另外,分布式环境下面还有日志转存、查询等,也是一个很庞大的体系,后面有机会再细聊。

  1. 传送门

支付系统设计与实现是一个专业性非常强的领域,里面涉及到的很多设计思路和理论也可以应用到其它行业的软件设计中,比如幂等性,加解密,领域设计思想,状态机设计等。

在《百图解码支付系统设计与实现》的知识宇宙,每一篇深入浅出的文章都是一颗既独立但又彼此强关联的星球,有必要提供一个传送门以便让大家即刻到达想要了解的文章。

专栏地址 : 百图解码支付系统设计与实现

领域相关:

行业黑话与术语:支付行业黑话:支付系统必知术语一网打尽

基本概念与概要设计:跟着图走,学支付:在线支付系统设计的图解教程

收单结算设计:支付交易的三重奏:收单、结算与拒付在支付系统中的协奏曲

技术专题:

与数据库自增ID不同的业务ID:交易流水号的艺术:掌握支付系统的业务ID生成指南

签名验签:揭密支付安全:为什么你的交易无法被篡改

加密解密:金融密语:揭秘支付系统的加解密艺术

日志格式设计规范:支付系统日志设计完全指南:构建高效监控和问题排查体系的关键基石

幂等性设计:避免重复扣款:分布式支付系统的幂等性原理与实践

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

订单系统设计

发表于 2024-01-04

一、订单系统介绍:

订单系统在电商系统中承载着非常重要的角色,在设计订单系统之前,必须先梳理订单系统上下游关系,只有划分清业务系统边界,才能确定订单系统的职责与功能,进而保证各系统之间高效简洁的工作。

image.png

由此可见,订单系统对上接收用户信息,将用户信息转化为产品订单,同时管理并跟踪订单信息和数据,承载了公司整个交易线的重要对客环节。对下则衔接产品系统、促销系统、仓储系统、会员系统、支付系统等,对整个电商平台起着承上启下的作用。

二、生成订单的流程:

订单本身内容并不复杂,只是需要在数据库中保存订单信息和订单商品明细,但是由于订单系统在整个电商系统中起着承上启下的作用,因此生成订单前后需要关联很多模块系统,比如仓储库存、会员系统、优惠券和赠品、配送系统等等模块,下图列举出了与订单系统关联的常见模块(当然不同的电商系统关联的模块肯定有不同):

image.png

从用户选择商品下单开始,生成一个订单的最基本简单的流程如下:

image.png

在生成订单之前会做安全性检查:包括库存检查,订单金额计算检查,优惠券和赠品检查等等,如果把这些检查写死在生成订单的流程中,以后需要扩充调整时就得修改生成订单的代码,那么这个地方可以应用责任链模式,抽象出拦截器链,当以后需要扩充时,只需要再单独增加一个拦截器的实现。

由于每个拦截器检查时,都需要将前端传的订单信息作为参数传入,因此这里需要定义一个OrderContext类,将前端传的订单信息放到OrderContext中。

拦截器的定义如下:

1
2
3
vbnet复制代码public Interface OrderInterceptor {
Boolean check(OrderContext context);
}

三、售后的流程:

一个最简单的售后基本流程如下:

image.png

四、订单系统常见问题:

1、订单超时未支付则自动关闭订单如何实现:

a、定时任务查询并关闭超时未支付的订单;缺点很明显,时间不精准,有一定的延时性;

b、利用RabbitMQ死信队列机制来实现:当RabbitMQ中的一条正常的消息,因为过了存活时间(TTL过期),就会变成Dead Message,即死信。当一个消息变成死信之后,他就能被重新发送到死信队列中,基于这样的机制,就可以实现延迟消息了;

c、利用JDK的延迟队列DelayQueue来实现;

2、重复提交订单如何解决:

订单重复提交的原因无外乎两种:一是由于用户在短时间内多次点击下单按钮,或浏览器刷新按钮导致,二则是由于Nginx或类似于SpringCloud Gateway的网关层,进行超时重试造成的;

常见解决方案有如下几种:

a、前端在点击下单按钮后将按钮置灰;

b、后端提供一个接口用于生成全局唯一订单号,比如UUID或者NanoID,前端进入创建订单页面时先调用这个接口获取订单唯一ID,然后提交订单时再调用创建订单按钮时传入这个ID,如果是两次重复提交,则订单ID是相同的,那么后面那次的提交则可以丢弃;

c、与上面的方法基本相同,只是全局唯一订单ID是由前端生成的;

d、可以用 ”用户ID + 分隔符 + 商品ID“ 作为唯一标识,让持有相同标识的请求在短时间内不能重复下单。可以将这个唯一标识保存到redis中,并且设置在指定的秒数后自动删除,这样第二次重复的请求因为redis中存在这个唯一标识而认为是重复提交并丢弃。

3、库存扣减的时机:

a、第一种方案是在生成订单时检查库存是否充足,支付成功回调的时候扣减库存,退货时回滚库存。但是有可能出现用户在支付完成后的回调时扣减库存时发现库存不够的情况,解决办法是在支付之前再次检查下库存,如果库存不足则提示用户因为库存不足的原因不能支付订单。

b、第二种方案是在生成订单前检查库存是否充足,下单成功后扣减库存,如果用户超时未支付成功或者退货时回滚库存。这种方式就相当于在提交订单时临时锁住库存,直到超时未支付时就解锁库存,库存锁住的这段时间内,锁住的这个库存其他用户就不能再下单,很明显,锁住库存的时间也就是订单超时未支付的最大时间,这个时间不能太长,否则会导致库存被长期占用导致其他用户不能下单。

4、订单支付完成回调如何去重:

当用户支付订单后,微信或者支付宝通常会以异步的方式通知商家服务器,商家服务器需要返回success,如果不是,则微信或者支付宝则会不断重复通知商家服务器。既然这样,支付回调接口就需要进行幂等性处理。

这个问题很好解决,因为支付完成的回调数据里面包含订单号out_trade_no,同一笔订单的多次调用,这个订单号是相同的,因此可以用这个订单号来做幂等性处理。

5、用户已经支付,但是因为各种原因导致未收到支付回调而关闭的订单如何解决:

这种情况会导致订单因为没有收到支付回调,导致订单超时未支付而被关闭。解决办法有以下几种:

a、订单超时未支付时,通过微信或者支付宝的接口查询订单状态,如果是已经支付,则根据查询状态更新订单状态;

b、定时任务查询所有超时未支付的订单,去微信或支付宝查询订单状态,如果是已经支付,则根据查询状态更新订单状态;为优化效率,避免未支付的订单每次都去查询,每次查询完之后标识下这些订单是已经查询过的,这样定时任务下次查询时就不再去查询这些订单了;

c、增加手工处理订单状态的机制:当用户支付完后如果发现订单还是未支付状态,则用户联系客服,由客服确认后手工干预订单状态。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

金融密语:揭秘支付系统的加解密艺术 1 什么是加密解密 2

发表于 2024-01-02

这是《百图解码支付系统设计与实现》专栏系列文章中的第(5)篇。也是支付安全系列的第(2)篇。

本文主要讲清楚加解密技术在支付系统中的重要地位,核心应用场景,哪些是安全的算法,哪些是不安全的算法,以及对应的核心代码实现。

专栏地址: 百图解码支付系统设计与实现

通过这篇文章,你可以了解到:

  1. 什么是加解密
  2. 支付系统中哪些核心场景需要用到加解密技术
  3. 哪些是安全的加解密算法,哪些是不安全的加解密算法
  4. 常见加解密算法核心代码
  5. 日常研发过程中常见的问题
  1. 什么是加密解密

在数字经济的舞台上,在线支付系统扮演着至关重要的角色。究竟是什么技术让金钱能够在公开的互联网上安全无忧地穿梭?加密和解密是这舞台背后隐秘的艺术,同时确保资金的安全流转和个人信息安全。

在数字通信中,加密是将明文通过一定的算法和密钥转换成无法识别的密文的过程。这样即使数据被截获,未经授权的第三方也无法理解其内容。

解密则是加密的逆向过程,通过一定的算法和密钥将密文转换成明文的过程。

  1. 核心应用场景

支付系统做为一个安全系数非常高的系统,加解密技术在里面起到了极其重要的作用。通常以下几个核心应用场景都会用到加解密技术:1)传输加密;2)存储加密。

  1. 传输加密:保护交易数据在互联网上传输过程中的安全,防止数据被窃听或篡改。

具体的实现通常有两种:

1)通道加密:比如使用HTTPS,或者VPN、专线等,实现数据传输端到端的加密。HTTPS和VPN可以参考网络上公开的文档。

2)部分字段单独加密:比如把卡号等关键信息进行加密后再发出去。

3)整体报文单独加密:先生成业务报文,然后对整个报文加密再发出去。

  1. 存储加密:对敏感数据比如信用卡信息、用户身份证信息、密码等需要进行加密后存储到数据库中,以防止数据泄露。

具体的实现通常也会分两种:

1)直接加密:原始信息直接加密。通常用于信用卡、身体证等常规数据的加密。

2)加盐值(SALT)后再加密:原始信息先加上盐值,然后再进行加密。通常用于密码管理。

  1. 密码的特殊处理

密码的存储比较特殊,值得单独说一说。

前面有说过,登录或支付密码需要加上盐值后,再进行加密存储。那为什么密码管理需要使用盐值?为了提高密码安全性。

  1. 防止彩虹表攻击。彩虹表是一种预先计算出来的哈希值数据集,攻击者可以使用它来查找和破译未加盐的密码。通过为每个用户加盐,即使是相同的密码,由于盐值不同,加密后的密文也是不一样的。
  2. 保护相同密码的用户。如果多个用户使用了相同的密码,没有盐值情况下,一个被破解后,就能找到使用相同密码的其它用户。每个用户不同的盐值,确保生成的密文不同。
  3. 增加破解难度。尤其是密码较弱时,显著增加攻击者难度。

在实现时,需要留意加盐策略:

  1. 随机和唯一:每个用户都是随机和唯一的。
  2. 存储盐值:每个用户的密码和盐值都需要配对存储。因为在加密密钥更新时,需要使用盐值一起先解密再重新加密。
  3. 盐值足够长:增加复杂性,推荐至少128位。
  1. 加解密算法选择推荐

推荐的算法如下:

AES:当前最广泛使用的对称加密算法,速度快,适用于高速加密大量数据。密钥长度推荐256或以上。

RSA:广泛使用的非对称加密算法,安全性比AES更高,但是加密速度慢,适用于小量数据或做为数字签名使用。密钥长度推荐2048或以上。

在https里面,数据加密使用AES,AES密钥通过RSA加密后传输,这样既解决了安全性,又解决了加密速度的问题。

当前公认不够安全的算法,不推荐使用,主要有:

DES:密钥长度较短,不够安全。

特别强调一点:千万千万不要自己去发明一种【私有的】,【自己认为很安全】的算法,并应用到生产环境。因为业界推荐的这些算法的安全性是经过大量数字家和计算机科学家论证过的,也经过工业界持续地验证,每天都有无数的攻击或破解在进行,一旦有被破解的风险就会很快知道。

  1. 加密密钥的存储及更新

明文数据被加密存储,安全了,那加密明文数据的密钥怎么办?

加密密钥有多重要呢?有一个公式是这样的:密钥的价值 = 密文的价值。比如你加密存储的密文价值10亿,那对应的密钥价值也有10亿。

密钥的管理涉及4个方面:密钥存储、更新、备份和恢复、废止和销毁。

密钥存储:

安全存储环境:密钥保存在特殊的安全环境中,包括服务器、网络环境、硬件加密机等。

最小权限原则:管理密钥的人越少越好。

密钥分为主密钥和工作密钥,其中工作密钥用来加解密普通的业务数据,而主密钥用来加解密工作密钥。

一般来说主密钥应该存储在专门的硬件安全模块(HSM)中,俗称:硬件加密机,安全性极高。但是相对来说性能有限,且价格昂贵,管理复杂。

工作密钥一般由主密钥加密后保存在DB中,在需要的时候调用主密钥解密后,缓存在内存中,然后再去加解密普通的业务数据。

密钥更新机制:

  1. 需要定期更新,减少被破解的风险。
  2. 自动定时更新,减少人为失误。‘
  3. 版本控制和回滚:要有版本号,要能快速回滚。

密钥备份和恢复,废止和销毁等机制,以及如何设计主密钥和工作密钥等细节,后面在介绍密钥中心设计与实现的章节再详细说明。

  1. 常见加解密算法核心代码

以经常使用的AES加解密为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
java复制代码import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.security.spec.KeySpec;
import java.util.Base64;
import java.security.SecureRandom;
import javax.crypto.SecretKey;

public class AESWithPasswordExample {
private static final String PASSWORD = "123456";
private static final int ITERATION_COUNT = 65536;
private static final int KEY_LENGTH = 256; // AES密钥长度可以是128、192或256比特
private static final String ALGORITHM = "AES/CBC/PKCS5Padding";

// 使用PBKDF2从密码派生AES密钥
private static SecretKey getKeyFromPassword(String password) throws Exception {
SecureRandom random = new SecureRandom();
byte[] salt = new byte[16];
random.nextBytes(salt); // 创建安全随机盐

KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, ITERATION_COUNT, KEY_LENGTH);
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
byte[] secretKey = factory.generateSecret(spec).getEncoded();
return new SecretKeySpec(secretKey, "AES");
}

// 加密
public static String encrypt(String data, SecretKey key, IvParameterSpec iv) throws Exception {
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, key, iv);
byte[] encrypted = cipher.doFinal(data.getBytes("UTF-8"));
return Base64.getEncoder().encodeToString(encrypted);
}

// 解密
public static String decrypt(String encryptedData, SecretKey key, IvParameterSpec iv) throws Exception {
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, key, iv);
byte[] original = cipher.doFinal(Base64.getDecoder().decode(encryptedData));
return new String(original, "UTF-8");
}

public static void main(String[] args) throws Exception {
String originalData = "Confidential data that needs to be encrypted and decrypted";

// 生成密钥和初始化向量 (IV)
SecretKey key = getKeyFromPassword(PASSWORD);
byte[] ivBytes = new byte[16]; // AES使用16字节的IV
SecureRandom random = new SecureRandom();
random.nextBytes(ivBytes);
IvParameterSpec iv = new IvParameterSpec(ivBytes);

// 加密数据
String encryptedData = encrypt(originalData, key, iv);
System.out.println("Encrypted data: " + encryptedData);

// 解密数据
String decryptedData = decrypt(encryptedData, key, iv);
System.out.println("Decrypted data: " + decryptedData);
}
}

请注意,这是一个简化的版本,实际应用中需要采取更多的安全措施,比如加密密钥的存储,盐值需要一起保存等。

  1. 日常研发过程中的常见问题

曾经碰到的常见问题有:

密钥管理不规范:把密钥加密后保存在数据库,但是加密密钥用的密钥是123456。

算法选择不合适:大批量数据选择使用速度极慢的非对称的RSA算法。

兼容性算法不对:尤其是模式、填充方式是直接影响加解密结果的。比如AES下面仍然细分为:ECB,CBC,CFB,OFB,CTR,GCM等模式,以及PKCS7/PKCS5填充,零填充等填充方式。具体的可以找密码学相关资料参考。

异想天开地使用自己创造的私有算法:以为很安全,其实太傻太天真。

管理机制不完善:没有制定严格的规范,或有规范执行不严重,导致密钥能被轻易访问。

  1. 结束语

在数字支付世界里,加解密是支付系统安全的基石之一,和众多安全措施一起保护用户和平台的资产。安全的加解密算法,严谨的管理密钥,是支付系统安全的两大支柱。

加解密涉及的密码学是一个很大的领域,支付系统的安全则是一个更大的领域,因篇幅关系,这里只介绍了一些入门知识,不过对于支付系统日常研发已经足够。

  1. 传送门

支付系统设计与实现是一个专业性非常强的领域,里面涉及到的很多设计思路和理论也可以应用到其它行业的软件设计中,比如幂等性,加解密,领域设计思想,状态机设计等。

在《百图解码支付系统设计与实现》的知识宇宙,每一篇深入浅出的文章都是一颗既独立但又彼此强关联的星球,有必要提供一个传送门以便让大家即刻到达想要了解的文章。

专栏地址 : 百图解码支付系统设计与实现

领域相关:

行业黑话与术语:支付行业黑话:支付系统必知术语一网打尽

基本概念与概要设计:跟着图走,学支付:在线支付系统设计的图解教程

收单结算设计:支付交易的三重奏:收单、结算与拒付在支付系统中的协奏曲

技术专题:

与数据库自增ID不同的业务ID:交易流水号的艺术:掌握支付系统的业务ID生成指南

签名验签:揭密支付安全:为什么你的交易无法被篡改

加密解密:金融密语:揭秘支付系统的加解密艺术

日志格式设计规范:支付系统日志设计完全指南:构建高效监控和问题排查体系的关键基石

幂等性设计:避免重复扣款:分布式支付系统的幂等性原理与实践

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

古茗是如何做前端数据中心的

发表于 2024-01-02

banner

简介

古茗的前端数据中心包含了前端监控、性能、日志、埋点等能力,还支持错误分析、埋点分析报表等功能
不仅支持小程序、web 还支持客户端 flutter、服务端 nodejs 等。由于我们有不少的 nodejs 应用,所以 nodejs 的监控也是必不可少的。

功能设计

在功能设计上分为了 8 个大模块,包含了前端所有需要的数据,故命名为大前端数据中心。

image.png

架构设计

由于平台设计包含了大量数据上报/分析的能力,所以架构设计上需要满足以下一些要求:

  • 实时性:异常发现、异常告警需要较高的实时性
  • 高可用:服务集群需要具有较高的可靠性,具备快速恢复、容灾等能力
  • 稳定性:具备一定弹性、有处理高并发、高峰值、流量波动的能力
  • 高吞吐:服务有接收大量数据日志和埋点数据,需要具备大量吞吐能力
  • 客户端容错:服务故障/SDK 报错,不应该影响业务正常运行

所以我们初版架构设计如下:

image.png

客户端

客户端 SDK 中,我们设计了部分资源采样能力,这是因为我们监控了资源加载、请求、性能等数据,对于正常的数据,例如正常请求、正常加载的资源、正常性能数据,我们可以配置开启采样来减少服务端压力、当然也可以根据业务需求开启全量上报。

另外我们还支持了配置下发能力,例如采样率,上报开关、上报队列、上报通道等等。

数据网关

在数据网关中,我们也支持采样(服务端采样),用户可以选择其中之一开启,与客户端采样的区别是服务端采样更加精确和实时。

数据分流

我们将数据分流为3个通道,大数据通道、持久化通道和实时计算通道,用于满足不同的业务需求:

  • 大数据通道:用于埋点业务分析、商业分析等
  • 实时计算通道:用于实时计算指标,例如产品创建的分析图表、平台内置的指标等等
  • 持久化通道:用于将数据写入 ES 集群,便于后置的查询和分析原始数据

埋点分析

对于错误和性能等等常规监控,大家一般都大同小异,这里介绍一下我们是如何做埋点分析的。

对于上报的埋点数据,不管是无痕埋点还是手动埋点,我们要分析就需要去查看各种图表,所以我们设计了自定义图表创建功能,可以让产品主动创建分析图表,要达到这个目的我们就需要走一个埋点的流程。

在这之前我们的流程是这样的:产品需要先和开发者提出埋点的需求,等埋点上线后,再和大数据提出数据分析的需求,最后才能根据需求定制图表。

image.png

但是在数据平台上我们就变成了这样:
产品自助创建埋点需求(直接创建点位指定给对应开发者),然后产品可以同时根据点位创建图表,上线后就可以直接查看图表了。

image.png

那么,在技术上我们做了哪些能力来支持这个功能呢:

  1. 预解析:我们对每个图都做了实时计算指标,可以做到创建后即可以查看,因为是计算好的指标,出图速度可以达到几十毫秒左右(直接查询 influxdb)
  2. 历史数据分析:对于没有事先计算好指标的历史数据,我们也提供了速度较慢的实时分析能力(查询ES)
  3. 另外,对于 web 端,我们支持了点位管理、事件管理、属性管理等等能力

日志查询

在 SDK 侧我们可以通过调用 logger API 来上报数据,SDK 会自动采集上下文和环境信息并且还可以关联用户埋点数据:

1
2
3
4
ts复制代码logger().debug(`xxxxx`)
logger().warn(`xxxxx`)
logger().error(`xxxxx`)
logger().info(`xxxxx`)

在平台侧我们就可以通过事件、环境信息、用户信息、日志内容等等维度来搜索日志,还可以直接指定设备来实时输出日志(规划中)。

数据串联

我们对所有数据都进行了关联,例如查询日志的时候可以通过 userId/traceid 等标记信息来串联日志、请求、埋点、错误等等。

image.png

所以不管是发现了日志不对、代码错误、请求错误等信息,我们就可以将所有链路串联起来排查问题。

Nodejs

Nodejs 模块是一个相对独立的模块,我们的 SDK 也是直接整合在框架内部,由于 nodejs 是服务端应用,所以我们针对 nodejs 做了很多单独的模块,例如 cpu 分析、GC 监控分析、服务数据监控等等。

CPU分析

主要是用包 GitHub - davidmarkclements/0x: 🔥 single-command flamegraph profiling 🔥 来进行 cpu 火焰图分析,cpu 数据使用 c++ 编写的扩展(调用 v8 api)来实现,直接使用 node node_modules/0x/cmd.js --visualize-cpu-profile 命令来生成图表(需要收集的 cpu 数据符合 0x 的格式)。

收集流程如下:

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cpp复制代码// v8::CpuProfiler 实例
static CpuProfiler *current_cpuprofiler = CpuProfiler::New(Isolate::GetCurrent());

// 启动 v8 cpu profiling
void StartCpuProfiling(const FunctionCallbackInfo<Value> &info) {
HandleScope scope;
Local<String> profilerTitle = New<String>("cpu_profiler").ToLocalChecked();
current_cpuprofiler->StartProfiling(profilerTitle, true);
}

// 停止 v8 cpu profiling
void StopCpuProfiling(const FunctionCallbackInfo<Value> &info) {
HandleScope scope;
const CpuProfile *profile = current_cpuprofiler->StopProfiling(profilerTitle);
/// - 后续对 profile 数据进行解析写入等等操作
}

GC 分析

GC 数据也是使用 c++ 编写的扩展(调用 v8 api)来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
cpp复制代码NAN_GC_CALLBACK(GCTracerPrologueCallback) {
/// - 记录GC
}

NAN_GC_CALLBACK(GCTracerEpilogueCallback) {
/// - 记录GC
}


void StartGC(const FunctionCallbackInfo<Value> &info) {
/// - 做一些初始化操作
/// - 例如判断文件在不在什么的

InitGcStatusHooks(); /// - 初始化
AddGCPrologueCallback(GCPrologueCallback);
AddGCEpilogueCallback(GCEpilogueCallback);
}


void StopGC(const FunctionCallbackInfo<Value> &info) {
RemoveGCPrologueCallback(GCPrologueCallback);
RemoveGCEpilogueCallback(GCEpilogueCallback);
RemoveGcStatusHooks();
/// - 做一些收尾操作
/// - 比如重置数据之类的
}

和 cpu 数据一样,收集完成后上传到 oss 保存,然后解析数据进行图表等等分析。

结尾

古茗大前端数据平台才刚上线不久,已经扮演了不可或缺的角色,但是还需要不断打磨,希望后续可以在业务治理当中发挥重要的作用,由于平台功能繁多,架构复杂,这篇文章也是做一个简简单单的介绍,如果有什么问题欢迎大家提问和讨论。

最后

📚 小茗文章推荐:

  • 你一定要知道的「React组件」两种调用方式
  • 中后台业务开发(一)「表单原理」
  • 手摸手教运营小姐姐搭建一个表单

关注公众号「Goodme前端团队」,获取更多干货实践,欢迎交流分享~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

揭密支付安全:为什么你的交易无法被篡改 1 什么是签名验签

发表于 2024-01-01

这是《百图解码支付系统设计与实现》专栏系列文章中的第(4)篇。也是支付安全系列的第(1)篇。

本文主要讲清楚支付系统中为什么要做签名验签,哪些是安全的算法,哪些是不安全的算法,以及对应的核心代码实现。

专栏地址: 百图解码支付系统设计与实现

通过这篇文章,你可以了解到:

  1. 什么是签名验签
  2. 支付系统为什么一定要做签名验签
  3. 哪些是安全的算法,哪些是不安全的算法
  4. 常见签名验签算法核心代码
  5. 联调中常见的问题
  1. 什么是签名验签

在电子支付的万亿市场中,安全无疑是核心中的核心。有一种称之为“签名验签”的技术在支付安全领域发挥着至关重要的作用。那什么是签名验签呢?

签名验签是数字加密领域的两个基本概念。

签名:发送者将数据通过特定算法和密钥转换成一串唯一的密文串,也称之为数字签名,和报文信息一起发给接收方。

验签:接收者根据接收的数据、数字签名进行验证,确认数据的完整性,以证明数据未被篡改,且确实来自声称的发送方。如果验签成功,就可以确信数据是完好且合法的。

假设被签名的数据(m),签名串(Σ),散列函数(H),私钥(Pr),公钥(Pu),加密算法(S),解密算法(S^),判断相等(eq)。

简化后的数学公式如下:

签名:Σ=S[H(m), Pr]。

验签:f(v)=[H(m) eq S^(Σ, Pu)]。

流程如下:

签名流程:

  1. 散列消息:对消息(m)应用散列函数(H)生成散列值(h)。
  2. 加密散列值:使用发送方的私钥 ( Pr ) 对散列值 ( h ) 进行加密,生成签名 ( Σ )。 Σ = S(h, Pr)

把数字签名(Σ)和原始消息(m)一起发给接收方。

验签流程:

  1. 散列收到的消息:使用同样的散列函数 ( H ) 对消息 ( m ) 生成散列值 ( h’ )。 h’ = H(m)
  2. 解密签名:使用发送方的公钥 ( Pu ) 对签名 (Σ ) 进行解密,得到散列值 ( h )。 h = S^(Σ, Pu)
  3. 比较散列值:比较解密得到的散列值 ( h ) 与直接对消息 ( m ) 散列得到的 ( h’ ) 是否一致。 验证成功条件: h = h’ 。

如果两个散列值相等,那么验签成功,消息(m)被认为是完整的,且确实来自声称的发送方。如果不一致,就是验签失败,消息可能被篡改,或者签名是伪造的。

现实中的算法会复杂非常多,比如RSA,ECDSA等,还涉及到填充方案,随机数生成,数据编码等。

  1. 支付系统为什么一定要做签名验签

银行怎么判断扣款请求是从确定的支付平台发出来的,且数据没有被篡改?商户不承认发送过某笔交易怎么办?这都是签名验签技术的功劳。

签名验签主要解决3个问题:

  1. 身份验证:确认支付信息是由真正的发送方发出,防止冒名顶替。

如果无法做身份验证,支付宝就无法知道针对你的账户扣款99块的请求是真实由你楼下小卖部发出去的,还是我冒充去扣的款。

  1. 完整性校验:确认支付信息在传输过程中未被篡改,每一笔交易都是完整、准确的。

如果无法校验完整性,那么我在公共场景安装一个免费WIFI,然后截获你的微信转账请求,把接收者修改成我的账号,再转发给微信,微信就有可能会把钱转到我的账号里。

  1. 防抵赖性:避免任何一方否则曾经进行过的交易,提供法律证据支持。

比如微信支付调用银行扣款100块,银行返回成功,商户也给用户发货了,几天后银行说这笔扣款成功的消息不是他们返回的,他们没有扣款。而签名验签就能让银行无法抵赖。

流程:

  1. 双方先交换密钥,可以通过线下邮件交换,也可以通过线上自助平台交换。
  2. 请求方发出交易报文前使用自己的私钥进行签名,接收方接收报文后先进行验签,验签通过后再进行业务处理。
  3. 接收方处理完业务,返回前使用自己的私钥进行签名,请求方接收返回报文后先进行验签,验签通过后再进行业务处理。
  1. 安全签名验签算法推荐

安全一直是一个相对的概念,很多曾经是安全的算法,随着计算机技术的发展,已经不安全了,以后到了量子计算的时代,现在大部分的算法都将不再安全。

一般而言,安全同时取决于算法和密钥长度。比如SHA-256就比MD5更安全,RSA-2048就比RSA-1024更安全。

已经被认为不安全的算法有MD5、SHA-1等算法,容易受到碰撞攻击,不应该在支付系统中使用。

仍然被认为是安全的算法有:SHA-256,SHA-3, RSA-1024,RSA-2048,ECDSA等。

当前最常见推荐的算法是RSA-2048。RSA-1024以前使用得多,但因为密钥长度较短,也已经不再推荐使用。

SHA-256只是一种单纯的散列算法,其实是不适合做签名验签算法的,因为需要双方共用一个API密钥,一旦泄露,无法确认是哪方被泄露,也就是只解决了完整性校验,无法解决身份验证和防抵赖性。但因为使用简单,国内外仍然有不少的支付公司公司在大量使用。

  1. 常见签名验签算法核心代码

下面以RSA(SHA256withRSA)为例,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
java复制代码import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;

public class RSASignatureUtil {

// 使用私钥对数据进行签名
public static byte[] sign(byte[] data, byte[] privateKey) throws Exception {
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(privateKey);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey priKey = keyFactory.generatePrivate(pkcs8KeySpec);
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(priKey);
signature.update(data);
return signature.sign();
}

// 使用公钥验证签名
public static boolean verify(byte[] data, byte[] publicKey, byte[] signatureBytes) throws Exception {
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKey);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey pubKey = keyFactory.generatePublic(keySpec);
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initVerify(pubKey);
signature.update(data);
return signature.verify(signatureBytes);
}
}

签名输出是字节码,还需要编码,一般是base64。

如果使用SHA-256(很多公司仍在使用,但不推荐),如下:

1
2
3
4
5
6
7
8
9
10
java复制代码import java.security.MessageDigest;

public class SHA256Util {

// 使用SHA-256对数据进行散列
public static byte[] hash(byte[] data) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
return digest.digest(data);
}
}

这里data已经是加了API密钥(也称为API KEY)。所谓的API密钥,就是交易双方共享的一个密钥,这样双方生成的哈希值才会一致。

  1. 联调中常见的问题

不管是与商户的联调,还是与支付渠道(或银行)之间的联调,签名验签都是非常耗费精力的环节。验签不通过通常有以下几个情况:

  1. 密钥不匹配:双方以为自己都配置了正确的密钥,但实际没有。
  2. 数据编码不一致:比如一方使用GBK,一方使用UTF-8。
  3. 原始数据选择不一致:比如接口文档要求拼接10个字段,但是代码实现却只拼接了9个字段。或者一方没有把空值放入计算,另一方把空值也放入计算。
  4. 原始数据排序方式不一致:比如接口要求按key的升序排列,调用方却忘记排序就进行签名。
  5. 字符转义不一致:特殊字段的转义必须保持一致。

解决上述问题的最好办法,就是让服务提供方提供一段示例代码,以及示例报文+示例签名,然后在本地使用main方法先跑成功,再移植到项目代码中。

  1. 结束语

本章主要讲了签名验签名的概念,对于支付系统的重要性,以及常见签名验签名算法及JAVA代码实现。

但是还有一个同样非常重要的问题没有讲:如何安全储存密钥? 如果密钥放在代码里或数据库里,开发人员是可以直接获得的,如果不小心泄露出去怎么办?

应对的解决方案就是创建一个密钥中心专门负责密钥的管理,无论加密解密还是签名验签,全部调用密钥中心来处理,业务系统不接触密钥明文。

那又来了一个新的问题:这个密钥中心如何设计和实现,才能既保证很高的安全性,又能有非常高的性能表现呢?

后面有机会再开一个密钥中心的设计和实现专题来聊。

  1. 传送门

支付系统设计与实现是一个专业性非常强的领域,里面涉及到的很多设计思路和理论也可以应用到其它行业的软件设计中,比如幂等性,加解密,领域设计思想,状态机设计等。

在《百图解码支付系统设计与实现》的知识宇宙,每一篇深入浅出的文章都是一颗既独立但又彼此强关联的星球,有必要提供一个传送门以便让大家即刻到达想要了解的文章。

专栏地址 : 百图解码支付系统设计与实现

领域相关:

行业黑话与术语:支付行业黑话:支付系统必知术语一网打尽

基本概念与概要设计:跟着图走,学支付:在线支付系统设计的图解教程

收单结算设计:支付交易的三重奏:收单、结算与拒付在支付系统中的协奏曲

技术专题:

与数据库自增ID不同的业务ID:交易流水号的艺术:掌握支付系统的业务ID生成指南

签名验签:揭密支付安全:为什么你的交易无法被篡改

加密解密:金融密语:揭秘支付系统的加解密艺术

日志格式设计规范:支付系统日志设计完全指南:构建高效监控和问题排查体系的关键基石

幂等性设计:避免重复扣款:分布式支付系统的幂等性原理与实践

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

交易流水号的艺术:掌握支付系统的业务ID生成指南 1 什么

发表于 2023-12-31

这是《百图解码支付系统设计与实现》专栏系列文章中的第(3)篇。

本章主要讲清楚支付系统中为什么要有业务ID,各子域的业务ID为什么要统一规范,以及最佳实践。

专栏地址: 百图解码支付系统设计与实现

  1. 什么是业务ID

数据库一般都会设计一个自增ID做为主键,同时还会设计一个能唯一标识一笔业务的ID,这就是所谓的业务ID(也称业务键)。比如收单域有收单单号,支付域有支付号,渠道网关域有渠道支付号等,这些都属于业务ID。

为什么有了自增ID后,还需要有业务ID呢?一般来说有以下几个核心原因:

  1. 分库分表的强诉求。一旦分库分表,各表之间的自增ID就一定会重复。
  2. 全球化部署的强诉求。在跨境支付系统建设时,部分国家地区要求本地化部署,需要通过业务ID知道业务运行在哪个机房。
  3. 标识业务语义,在处理故障时能快速定位是哪个域哪个业务。
  4. 方便系统升级。通过业务ID的版本所在位判断业务应该走新系统,还是走老系统。
  1. 为什么业务ID要统一规范

互联网支付系统基本都是微服务化部署,每个子域都是相对独立的一些同学在研发,架构实现差异非常大,但是业务ID是必须要统一的。主要有以下几个原因:

  1. 减少维护成本。避免在不同服务中重复发明相似机制,也减少了沟通成本。方便做成统一的组件。
  2. 加速异常处理和诊断。在分布式环境下发现和解决问题一般都比较复杂,统一的业务ID规范可以快速判断问题所在的域,以及对应的业务。
  3. 避免新同学因经验不足导致设计缺陷,在后期无法满足业务诉求。
  1. 常见业务ID生成规范及应用场景

业务ID生成规则有很多种,比如知名的Snowflake算法,UUID算法,时间戳+随机数/序列号等。以下是部分规范的简要介绍。

  1. Snowflake算法

组成:时间戳 + 数据中心标识 + 机器节点 + 序列号。

适用场景:无中心化的环境中生成大量的唯一ID,无具体业务语义,且性能要求极高。比如社交媒体的聊天消息记录。

  1. UUID算法

高度唯一且随机。

适用场景:不想让外界感知内部系统的交易量级。比如调用外部渠道的请求号,如果使用序列号,有可能会让外部猜测出交易的规模。

  1. 编码系统

特定组织中心化生成。

适用场景:药品或供应链管理,全球范围内标识或追踪商品。

  1. 业务规则编码

把一些业务语义编码到ID中。

适用场景:金融支付、电商订单等。

  1. 支付系统业务ID生成最佳实践

4.1. 业务ID生成规范

下面以32位的支付系统业务ID生成为例说明。实际应用时可灵活调整。

第1-8位:日期。通过单号一眼能看出是哪天的交易。

第9位:数据版本。用于单据号的升级。

第10位:系统版本。用于内部系统版本升级,尤其是不兼容升级的时候,老业务使用老的系统处理,新业务使用新系统处理。

第11-13位:系统标识码。支付系统内部每个域分配一段,由各域自行再分配给内部系统。比如010是收单核心,012是结算核心。

第14-15位:业务标识位。由各域内部定,比如00-15代表支付类业务,01支付,02预授权,03请款等。

第16-17位:机房位。用于全球化部署。

第18-19位:用户分库位。支持百库。

第20-21位:用户分表位。支持百表。

第22位:预发生产标识位。比如0代表预发环境,1代表生产环境。

第23-24位:预留。各域根据实际情况扩展使用。

第24-32位:序列号空间。一亿规模,循环使用。一个机房一天一亿笔是很大的规模了。如果不够用,可以扩展到第24位,到十亿规模。

4.2. 业务ID生成技术实现

序列号通常采用数据库生成,保证机房内唯一性。

简要流程如下:

  1. DB初始化序列号数据。KEY为业务类型,VALUE初始为0;
  2. 调用业务ID生成组件。核心传参:数据版本号,系统版本号,系统名,业务类型等。
  3. 业务ID生成组件查看对应业务类型是否有缓存数据。如果没有,就以指定步长(比如100)去更新数据库,然后缓存起来。
  4. 在内存中加一,然后根据规则生成业务ID,返回给调用方。

这里使用指定步长去更新数据库,主要是考虑提高性能。但是存在一定的损失,比如发布重启,缓存中的序列号就会被浪费掉。但因为是循环使用,所以基本上对业务没有影响。

  1. 结束语

本章主要讲了业务ID是什么,业界常见生成规则及适用场景,以及支付系统业务ID生成的最佳实践。

  1. 传送门

支付系统设计与实现是一个专业性非常强的领域,里面涉及到的很多设计思路和理论也可以应用到其它行业的软件设计中,比如幂等性,加解密,领域设计思想,状态机设计等。

在《百图解码支付系统设计与实现》的知识宇宙,每一篇深入浅出的文章都是一颗既独立但又彼此强关联的星球,有必要提供一个传送门以便让大家即刻到达想要了解的文章。

专栏地址 : 百图解码支付系统设计与实现

领域相关:

行业黑话与术语:支付行业黑话:支付系统必知术语一网打尽

基本概念与概要设计:跟着图走,学支付:在线支付系统设计的图解教程

收单结算设计:支付交易的三重奏:收单、结算与拒付在支付系统中的协奏曲

技术专题:

与数据库自增ID不同的业务ID:交易流水号的艺术:掌握支付系统的业务ID生成指南

签名验签:揭密支付安全:为什么你的交易无法被篡改

加密解密:金融密语:揭秘支付系统的加解密艺术

日志格式设计规范:支付系统日志设计完全指南:构建高效监控和问题排查体系的关键基石

幂等性设计:避免重复扣款:分布式支付系统的幂等性原理与实践

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

图解收单平台:打造商户收款的高效之道 1 收单结算概述 2

发表于 2023-12-30

这是《百图解码支付系统设计与实现》专栏系列文章中的第(3)篇。

收单结算是支付系统最重要的子域之一,行业内经常把有牌照的支付平台称为“收单机构”就可见一斑。

本章主要讲清楚支付系统中收单涉及的基本概念,产品架构、系统架构,以及一些核心的流程和相关领域模型、状态机设计。

  1. 收单结算概述

收单和结算结合很紧密,我们先讲一下收单结算的整体概念,再单独细讲收单平台,结算平台,拒付平台。

1.1. 基本概念

我们通常把收单、结算、拒付放在一起讲,主要是因为这三个都是面向商户的最核心的服务。简要如下:

收单: 帮商户把钱从用户手里收进来。

结算: 把从用户收进来的钱结转给商户。

拒付: 在用户发起拒付后需要从商户待结算款里面扣除拒付的这部分钱(因为这部分钱需要退回给用户)。在国际收单场景比较常见。

这三者紧密联系却又彼此各有侧重点,下面分开讲述。

1.2. 整体产品架构图

从图中可以看到,最上层是收单的产品层,负责对商户提供直接的服务,并且封装个性化的收银台产品。主要包括有:

收银台支付:需要跳转到收银台进行支付;

二维码支付:需要先调用码平台进行解码,解码后就和普通的支付流程是一样的;

代扣/协议支付:商户后台发起扣款,不需要跳转到收银台。

再下面是三个核心,分别为收单核心、结算核心、拒付核心。三者的职能如下:

收单核心: 主要负责处理商户订单的全生命周期管理:订单创建、支付推进、退款、撤销等。

结算核心: 主要负责把商户应收账款算清楚,把结算款按合同约定结转给商户。

拒付核心: 主要负责处理用户的拒付和对应的抗辩以及最后的判责。

  1. 收单演进形态

无收单机构模式

这就是小时候去小卖部买糖的模式,一手交钱一手交货。

好处:足够简单。不足:无法完成线上交易。

行内收单模式

所谓行内收单,就是发行卡和收单是同一家银行。

好处:手续费低,成功率高。不足:业务比较受限,以线下收单为例,商户无法部署所有的银行POS机。

发卡行与收单行分离模式

大部分情况下,用户的发卡行和商户的收单行是不同的银行。

不过,这种情况基本也已经灭绝,因为需要发卡行和收单行两两对接,形成一个巨大的网状结构,维护成本高昂。

清算机构模式

发卡行和收单行之间不再直连,而是通过清算机构。清算机构通常是央行下面的特许经营的金融机构。这样围绕清算机构形成一个星形架构,所有银行只需要和清算机构对接就行。

当前银行间的交易基本上是这种形态。比如中国的银联,国外的VISA,MASTERCARD等,是卡组,也是清算机构。

第三方支付(电子钱包)形态

随着互联网支付的兴起,以第三方支付为中心形成另外一个星形结构。

上图做了很大的简化。在中国因为断直连的关系,支付宝、微信支付背后的财付通等这些第三方支付机构都是对接银联、网联,而不是直连银行。但是在国外仍然是允许直连银行的。

  1. 收单在支付系统中的位置

把收单和结算再细化分拆,收单的地位如下图所示:

如果用一句话说明收单的核心能力和定位,那就是:负责商户收单业务的全生命周期管理。

  1. 收单产品架构

前面说过,收单核心负责商户业务单的全生命周期管理,对外提供下单、退款、撤销、查询等接口。

行业内对于交易模式基本有3种:

    1. 即时到账模式:用户支付完成后,钱就到商户账户。注意:这个“即时”是相对概念。商户真正拿到钱还需要看结算协议及结算进度。去小卖部拿支付宝、微信支付买零食都是这种模式。
      1. 预授权模式:先从用户账上预授权一笔钱,交易完成后再进行请款。最常见的就是酒店住宿场景,入住时先预授权一笔钱做押金,离店时根据消费情况做请款,并撤销(Void)多余的预授权款项。
      2. 担保交易模式:用户支付完成后,钱先扣在支付平台,用户确认收货后,再通知支付平台放款给商户。在淘宝买东西就是这种模式。

为支持对外的能力,收单需要建设很多基础服务,包括下单、支付推进、退款、撤销、通知、冻结解冻等。

  1. 收单系统架构

这个系统架构图中包含了收单最基本的能力。

收单在收到商户的请求后,需要调用会员、商户等域校验合法性,还会调用合约中心等校验商户的权限,全部通过后,就会创建收单单据。

如果只是预下单,收单单据创建完成后就直接返回给商户订单创建成功,并返回收银台地址,供用户跳转到收单台继续支付。

如果是下单并支付(比如代扣),收单单据创建完成后,调用收银支付域进行支付扣款流程。

  1. 收单核心流程

6.1. 极简支付流程

上面的时序图已经清楚说明支付整体的流程,以及各子域之间的交互。部分子域没有画出来,比如支付过程中需要调用风控、卡中心、额度中心等,这些在后面讲收银支付域时重点说明,本次聚集在收单领域。

另外,这里只画了类似代扣场景的支付流程,现实中的预授权、请款,担保交易、预付款(多次支付)等模式会复杂很多,后面有机会再单独开章节讲。

6.2. 极简退款流程

用户发起退款后,在商户侧会进行校验,校验通过后就会给用户展示退款已提交之类的提示。

收单接收到商户的退款请求后,需要先查询历史合约,检查合约是否支持退款,是否过了退款有效期,是否满足最小退款金额,全部通过后,就创建退款单并保存。

接下来会进入退款资金准备阶段,因为从资损防控的角度,除非另有合约约定,否则支付平台一般是不会做垫资退款的。在退款资金准备阶段,需要实时扣减商户待结算户的钱,这是与支付流程很大不同的点。当然,有些支付公司可能和商户约定从独立的退款账户进行扣款,那也需要保证这个退款账户余额充足。

上面最后一步的记账只写到了充退待清算户,之后等到清算文件过来后,会再继续推进充退待清算户到渠道应收的记账。

  1. 收单领域模型设计

这是精简后的模型,对于说清楚收单的核心能力建设已经足够。真实场景下还需要增加很多必要的字段,比如产品码,合约号,冻结标识等。在做详细设计时根据业务诉求去增加就行。

从图中也可以看出,最核心是交易主单,所有其它单据都与交易主单关联。

比较特别的是里面有普通支付单和预授权单。正常都是普通支付单,只有预授权产品才会有预授权单,对应的还有请款单和预授权撤销单。

  1. 收单状态机设计

下面以交易主单、普通支付单、退款单、预授权和请款单等常见的单据状态机做说明。

特别需要说明的是,状态机的推进一定要设计好,不能使用if else来写,要牢记“终态不可变更”的原则,否则容易出问题。具体怎么使用代码实现状态机,以及为什么“终态不可变更”,后面单独开章节来详细论述。

8.1. 交易主单状态机

交易主单创建初始入库就是INIT。

如果是下单并支付场景(比如代扣),就先推进到PAYING,然后调用收银支付进行扣款操作。如果是部分请款,也是支付中,全额请款完成或未请款部分撤销了预授权,就推进到SUCCESS。

如果是预下单,那停留在INIT,然后等支付域支付成功回执回来,推进到SUCCSSS。

如果订单关闭,就推进到CLOSE。

需要特别说明一点的是,一些经验不足的同学在交易主单耦合了很多不应该放在交易主单的状态,比如退款成功,撤销成功等。这导致交易主单的状态机特别复杂,非常容易出错。 比较好的经验就是,交易主单、支付单、退款单、撤销单等全部只管自己的状态机。

8.2. 支付单状态机

支付单创建初始状态就是INIT。

收到支付域的支付成功回执,更新为SUCCESS。

交易超时关闭,推进到CLOSE 。

8.3. 退款单状态机

退款单初始为INIT。

然后推进退款资金准备,这个过程把要退款的钱从商户待结算户或指定账户扣到退款过渡户,如果收单合约中还要求退款退费,还需要从收费账户把手续费扣出来。

退款资金准备成功后,推进到PREPARE_CUSS。

然后向支付域发起退款,支付受理成功后,推进到SUCCESS。

8.4. 预授权状态机

预授权单初始为INIT。

预授权失败回执推进到CLOSE。预授权成功后,用户全额撤销,也推进到CLOSE。

成功回执推进到AUTHED。

开始请款为CAPTURING,部分请款成功仍然为CAPTURING。

全额请款完成推进SUCCESS,或部分请款后,剩下额度被撤销,也推进到SUCCESS。

8.5. 请款状态机

请款单初始为INIT。

请款失败回执推进到CLOSE。

请款成功回执推进到SUCCESS。

  1. 资金流

9.1. 即时到账

即时到账比较简单,支付过程,从支付网关过滤户到商户待结算户,再到商户余额。

退款则相反,在退款资金准备阶段需要从商户待结算户到退款网关过渡户。

9.2. 担保交易

担保交易模式比即时到账多了一个担保户。

9.3. 预授权模式

预授权模式比即时到账模式多了一个请款过渡户。

  1. 结束语

本章主要讲了收单的基本概念,以及对应的产品和系统架构图,一些核心的领域模型和状态机设计。

接下来几章会讲和收单紧密结合的结算平台和拒付平台。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

2024年10个值得继续学习Sprint Boot的理由

发表于 2023-12-30

大家好,我是安图新。今年很多技术人员估计也不是太好过了,毕竟裁员潮的到来,甚至各大厂也无法避免,这间接导致了供过于求,僧多肉少的情况下,应聘各大技术岗位的竞争尤为激烈。

八股文都已经是最低标配了,所以说庆幸经过多轮裁员潮还有饭碗的小伙伴,就要好好珍惜不要任性了,苟着也是一种幸福。

既然现在技术那么卷,那在未来的 2024 年,作为广大 Java 出身或者既然投入到 Java 怀抱的程序员,我们是否值得继续学习 Spring Boot 呢?说到 Spring Boot,熟悉的小伙伴应该都有一个共同的担忧 —— Spring Boot 在开发速度方面很慢。

因此,我决定拿它来与 Nodejs、Django 等主流语言框架进行对比测试,不是写个简单的待办事项列表或 hello-world 应用程序,而是用我以前写过的一些应用程序的复杂部分。坦白说,测试的结果让我得出了不同的结论。我强烈反对 Spring 在开发速度方面更慢的观点。事实上,恰恰相反。

接下来,我将列出学习 Spring Boot 的价值所在,以及它与其他后端技术和替代 Java 语言框架的比较,希望可以给你一个更加清晰的理解和对比。

  1. Spring Boot 热度仍然很高

我有幸认识一些长期在国内大厂做技术骨干,以 Java 为主要开发语言的朋友并聊过,他们多年来一直深入研究 Java 语言和 Spring,他们都承认 Spring Boot 的发布无疑是一种解脱。

配置 Spring 应用程序的各个方面的负担是压倒性的,特别是对于新手来说,Spring Boot 的到来反而标志着一个转折点。它让 Java 的开发难度和效率带来了前所未有的简单,让开发人员可以摆脱大量样板代码的负担,专注于真正重要的事情: 编写健壮的业务逻辑。

但还不止这些,真正使 SpringBoot 与众不同的是它的迭代进化能力。它不仅仅是过去的技术。相反,它不断适应现代软件发展不断变化的需求。

Spring Boot 的自古至今以不断创新、充满活力的社区以及一直保持在技术潮流前沿为特征,这证明了它的可靠性。

Spring Boot 现在提供了企业级的特性,并与微服务和云本地化趋势很好地集成,提供了一个完全适合在以云为中心的世界中构建可伸缩的分布式应用程序的平台。根据最近的JetBrains 开发者生态系统调查,Spring Boot 和 Spring MVC 仍然是最常用的 Java 语言开发框架。

  1. 语言生态和广泛依赖库相结合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
xml复制代码<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-graphql</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.graphql</groupId>
<artifactId>spring-graphql-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

我一直记得“左耳朵耗子”皓哥说过的一句话,如果你决定想好好学习编程成为技术专家的话,Java 语言你一定要学习,因为它是一门工业化语言,这是目前所有其他编程语言暂时还不能完全做到的。

那什么是工业化语言?

作为当今的一个开发者,你会发现自己在很多工具和库之间游刃有余地构建应用程序。这些工具包括编程语言、框架、库、数据库、 Hibernate 和 JPA 这样的数据库连接器,以及 Apache Kafka 和 RabbitMQ 这样的消息系统。一个有完整生态和成熟脚手架工具的语言,才叫做工业化语言,Java 无疑是。

假设你正在构建一个 Web 应用程序。你可以使用 Python 这样的编程语言、后端 web 框架、前端框架和 PostgreSQL 这样的数据库。你可能还需要利用各种第三方 API 来实现支付处理、用户身份验证,有时还需要地理定位服务等功能。作为一名开发者,你的工作就是让所有这些组件无缝地协同工作。

像 Spring Boot 这样的现代框架让你的开发者工作变得更加容易。与其在每次开发新产品的时候都试图整合这些工具,不如通过简化将这些技术整合到项目中的过程,让你的开发者生活变得更加轻松。

这减少了你自己整合它们的时间,使你成为一个更有效率的开发者。

如果你在过去几年中一直关注技术领域的发展,那么你实际上可以证明 Spring Boot 在跟上新出现的库和工具方面做得很好。无论你是使用 GraphQL 开发强大的查询语言,还是使用 Spring WebFlux 的反应式编程,或者使用云计算本地技术,比如 Docker 和 Kubernetes,Spring Boot 都可以实现无缝集成,这使得它成为现代开发的绝佳选择。

  1. Spring 无处不在

当回顾我的开发者旅程时,我不禁感激 Spring Boot 是如何成为我职业生涯中不可或缺的一部分的。

让我印象深刻的是我个人和公司发展经历之间的无缝过渡。这意味着我在使用 SpringBoot 的个人项目中练习的经验可以直接转移到我在公司的工作中,这意味着你可以在任何地方使用 SpringBoot。虽然目前环境下,很多公司都在裁员,就业市场很卷,但是 SpringBoot 作为一个主流的框架,它的使用率还是非常高的,迟早值得你去学习。

因此,无论你是想在工作场所构建一个简单的个人项目还是一个大型企业应用程序,Spring Boot 都是应对这种情况的首选框架。

在企业,SpringBoot 的采用是非常常见的。它不仅仅是一个框架的选择,它已经成为许多企业级应用程序的标准和主干,你看知道现在,很多大厂,尤其是业务复杂度高的,无疑都会优先选择 Java 作为主流语言区开发应用。

它的可靠性、社区支持和成熟的集成生态系统也使它成为构建可伸缩、可维护和企业级应用程序的首选解决方案。

  1. Spring Boot 的可测试性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class ProductServiceTest {
@Autowired
private ProductService productService;
@Test
public void testProductCreation() {

Product product = new Product("Sample Product", 10.0);
// Act
Product savedProduct = productService.createProduct(product);
// Assert
assertNotEquals(0, savedProduct.getId());
assertEquals("Sample Product", savedProduct.getName());
assertEquals(10.0, savedProduct.getPrice(), 0.01);
}
}

测试是每个开发者都必须写和做的一个开发流程,对吧(虽然国内很多公司为了赶进度,都有跳过写单元测试和阶段测试的情况,但我告诉你,这些都是技术债,你迟早要还的)?应该是的,但让我带你回到那个烦人苦恼低效而不是解脱的时期看看。

在我作为开发者的早期,写测试是一项令人畏惧的实践,尤其是当我还在试图掌握编程的基本概念时。设置测试用例就像解决一个谜语,捕捉异常总是一个自我持续的斗争。这个过程是如此的繁琐,以至于我在编写全面的测试用例时总是感到沮丧,并且经常会找到逃避它们的方法(以前根本想懒得写哈哈…)。

几年后,当我开始使用 Spring Boot 框架编写单元测试时,集成测试不再是一个令人头疼的问题,而是一个令人满意的例行程序。我也鼓励你选择一个好的框架来帮助你提高编写测试用例。

SpringBoot 其实是一个开发具有健壮测试特性的应用程序的极佳选择,原因如下:

  • Spring Boot 与流行的测试框架(如 jUnit、 Mockito 和 Spring Test)无缝集成。
  • Spring Boot 提供了广泛的测试注释,包括@SpringBootTest、@DataJpaTest 和@WebMvcTest,它们帮助为特定类型的测试配置应用程序上下文,从而简化流程。
  • Spring Boot 天生具有 Spring 框架庞大生态系统作为基础,比如用于安全测试的 Spring Security 和用于数据库相关测试的 Spring Data。
  • Spring Boot 天生支持 Mock 框架,比如 Mockito,这样你就可以使用工具来生成测试数据,从而构建更加健壮的应用。
  • Spring Boot 与 TestContainer 集成良好,可以快速跟踪集成测试。

说多一句,如果你正在构建一个分布式应用的话,测试就更没有商量余地了。

  1. 监测和可观察性

我是在大学才开始学习编程语言的,那时的生态只是让我们开发一些基本的单体应用程序(那时前后端分离的概念还不火),这种架构还是相对简单的,复杂性不会太高,毕竟所有的事都在一个项目内调用就可以完成。这些单页应用程序通常有一个简单的前端、小量的数据处理和请求的服务器。使用这种类型的应用程序,比较容易去 debug 问题并在运行过程中进行调整修复。

然而,随着时间的推移,我的项目在范围和复杂性上都有所增长,现在我发现自己要处理更复杂的应用程序。这些应用程序大多以多个微服务、数据库、外部集成和复杂的业务逻辑为特色。

在这段时间里,随着我工作的系统复杂性的增加,我还需要了解如何评估度量、日志和分布式跟踪。

Spring Boot,特别是 Spring Boot 3,提供了内置的可观察性功能,使你更容易监视、诊断和了解生产环境中应用程序的内部状态。以下是一些 Spring Boot 的特性,你可以通过这些特性来观察和监控你的应用:

1. Actuator

1
2
3
4
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

Actuator 是 Spring Boot 一套内置的系统内省和监控工具,可以查看应用配置的详细信息,例如自动化配置信息、创建的 Spring beans 以及一些环境属性等。以下是一些具体介绍:

  • /actuator/health,提供有关应用程序健康状况的信息
  • /actuator/metrics,提供了应用程序各个方面的详细度量,比如内存使用量、垃圾回收。
  • /actuator/info,允许我们公开自定义应用程序信息。
  • /actiator/env,提供对应用程序环境属性的访问,这对于解决配置问题非常有用。
  • 你还可以生成自定义 Actuator 端点,以帮助公开特定的数据或触发器操作。

2. 监控指标 - Micrometer 集成

我已经看到了微服务和云本地架构的兴起是如何使得监视这些分布式系统变得绝对必要,这就是 Micrometer 发挥作用的地方,它将自己作为应用程序指标的首选“SLF4J”。

Micrometer 就像一个全方位的应用指标,支持广泛的监控系统集成。对于开发人员来说,它就像一个多用途工具,使我们能够从应用程序中收集有价值的见解,而无需被锁定在特定的监视解决方案中。

这一切都是为了让我们有灵活性为工作选择正确的工具,并确保我们不会局限于一个单一的监控系统作为解决方案。例如,你可以直接集成使用 Prometheus、 Datadog、InfluxDB 等工具。

Micrometer 还有丰富的监控集合,如量规、计数器、计时器和分布式信息摘要等。

3. 日志

1
2
3
4
5
txt复制代码2023-03-05 10:57:51.112  INFO 45469 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/7.0.52
2023-03-05 10:57:51.253 INFO 45469 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2023-03-05 10:57:51.253 INFO 45469 --- [ost-startStop-1] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 1358 ms
2023-03-05 10:57:51.698 INFO 45469 --- [ost-startStop-1] o.s.b.c.e.ServletRegistrationBean : Mapping servlet: 'dispatcherServlet' to [/]
2023-03-05 10:57:51.702 INFO 45469 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]

随着时间的推移,日志的重要性已经从一个简单的调试工具发展成为一种可以构建可靠、可维护和高效的应用监控和审计工具。

开箱即用,Spring Boot 为 Java 语言提供 Util Logging、 Log4j2 和 Logback 作为默认配置。在每个场景中,日志记录器都被预先配置为在控制台,即标准输出上显示日志消息,而且如果需要的话,你还可以直接将日志定向到一个文件。

如果你正在使用”Starters”,那么 SpringBoot 默认情况下会为你的日志配置设置 Logback。不仅如此,它还能处理 Logback 路由,确保即使你有使用不同日志框架的依赖库,比如 Java 语言的 Util Logging、Commons Logging、 Log4J 或 SLf4J,一切都能协调集成进来。

你可以自由选择你喜欢的日志框架,但是 Spring Boot 默认的 Logback 设置简化了你初始化的上手使用,使得你可以很容易的开始而不用担心其他库的兼容性问题。

1
2
txt复制代码'dispatcherServlet' to [/]
2023-03-05 10:57:51.702 INFO 45469 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]

4. 分布式跟踪 - OpenTelemetry

假设你是一个相对新手的开发者,负责维护一个大型电子商务平台。有一天,你收到来自客户支持团队的报告,指出一些客户在结帐过程中遇到了问题。用户抱怨订单没有得到处理,他们无法完成客户的商品购买。

如果没有适当的可观察性工具和分布式跟踪,你就不能对请求流进行端对端可视化。你对微服务之间的交互视而不见,并且很难跟踪每个请求是如何处理的,以及错误和瓶颈在哪里。

如今,OpenTelemetry 技术已被认为是服务跟踪的标准,有着广泛的开放源码软件和支持它的商业工具,如 Jaeger、 Zipkin 和一些企业 APMs。幸运的是,Spring 在其许多项目和库中都对 OTEL 提供了广泛的支持。Spring Boot 同时支持 OTEL Instrumentation —— 这是一种非常快速的开发和追踪方式,也支持更高性能的 Micrometer Tracing 库调用。

如果你需要一个更强大灵活的方式来启动并追踪你的 Spring Boot 应用,甚至想把所有重要的数据集成到你的开发过程中,你可以考虑一下使用这个免费的开发者工具,Digma。Digma 是一个持续反馈(CF)工具,旨在简化从 OTEL 代码来源收集和处理可观察性数据的工作。Digma 作为一个 IDE 插件在本地运行,并在你代码的时候收集关于你编码的数据,从跟踪到日志和度量。这意味着你可以随时检测到问题并且实现快速修复。

  1. 简单易用和发展迅速

在任何项目开始时选择正确的工具都相当于是一个战略决策,会产生深远的影响。它可以影响一切,从开发过程、性能到安全、可扩展性和用户体验等等。

同样,选择正确的框架也会对你的学习曲线和早期经历产生重大影响。如果你刚刚开始你的编程之旅,Spring Boot 是一个很好的选择,因为它的“约定优于配置”的哲学。

Spring Boot 减少了 Spring 的冗长和复杂的设置。这意味着,作为一个初学者,你可以专注于编码应用程序的核心逻辑,而不会迷失在大量的配置文件中。使用 SpringBoot,你可以创建自包含的、可执行的 JAR 文件,这使得部署非常容易。不需要管理应用服务器或复杂的部署。

Spring Boot 还提供了预先配置的模板或依赖项集合,可以简化应用程序的设置和配置; 这些都被称为 Spring Boot starter。这些启动程序旨在查看你的类路径和已配置的 bean,并对可能缺少的内容做出合理的假设并反馈给你。然后,它们会自动添加必要的组件、库和配置,以使应用程序快速启动并运行。

  1. 微服务支持

随着微服务开发慢慢成为主流,很明显,管理许多微服务可能是一个真正令人头疼的问题。编排和监控所有这些组件的挑战可能导致复杂性一步步的加重。

这是,Spring Boot 来救场了。你会发现它的特性使得使用微服务远没有那么难和容易维护。Spring Boot 与 Spring 集成的云服务为服务发现、负载平衡和分布式配置提供了工具。这些特性简化了微服务的创建和管理,确保它们能够无缝地协同工作。

在使用 Spring Boot 时,它也有一些其他特色属性包括对集装箱化技术(如 Docker)和编配平台(如 Kubernetes)的支持,以及具有与广泛技术集成的无缝接入,还有安全性。

  1. 自带 Web 服务器属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
xml复制代码<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>8.5.23</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-el</artifactId>
<version>8.5.23</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-websocket</artifactId>
<version>8.5.23</version>
<scope>compile</scope>
</dependency>

为了让你更好理解,我来模拟下以前通常需要手动设置和配置 Web 服务器来部署 Web 应用程序的时代,当时的部署和集装箱化技术还不像今天这么普遍。

当你有了一个 web 应用,你通常会把它部署在独立的 web 服务器上,比如 Apache HTTP 服务器或者 Nginx。这些 Web 服务器主要负责处理传入的 HTTP 请求,并将它们转发到运行在不同监听端口的 Web 应用程序上。

配置这些外部独立 Web 服务器需要编写大量的 XML 配置文件。你还必须在这些配置文件中指定 servlet 映射、数据源连接和其他特定于部署的详细信息。

与之相反,现在的 SpringBoot 嵌入式 Web 服务器使得部署更加容易。SpringBoot 包括对嵌入式 Tomcat、 Jetty 和 Undertwow 服务器的支持,这些服务器与应用程序捆绑在一起,允许你将应用程序打包为独立的 JAR 文件。这种方法简化了部署,不需要单独的服务器安装和复杂的服务器配置。

  1. 开源

SpringBoot 的开源特性让我和许多其他开发人员产生了深刻的共鸣。我喜欢 SpringBoot 的开源特性是因为它为每个人提供了公平的竞争环境。因此,无论你是学生、技术新人还是经验丰富的开发者,你都有机会获得与其他人相同的代码、相同的文档和相同的贡献机会。

开源促进了代码的透明度。换句话说,你不是工作在一个完全模糊,不了解的黑盒环境,你可以调查引擎在引擎盖下是如何工作的,甚至可以根据自己的需要进行修改。这也意味着没有任何附加成本。你可以在个人项目、副业或企业级应用程序中免费使用框架,而不用担心成本。

当然,你甚至可以随时通过修复 bug、文档改进,甚至开发新的 Spring Boot 特性来做出自己的贡献。

  1. 社区和支持

到我接触代码进入编程世界到现在,满打满算也有十年了,这十年,我一直是一个热衷技术的开发者,我已经记不清自己多少次寻求并得到不同社区的支持。这些社区分布在不同的平台上,比如 Baidu、Google、GitHub、CSDN 和 Stack Overflow 等。

作为一个开发者,人们往往很容易忘乎所以,认为待在自己的小房间里,24 小时与世隔绝的编码是一种进步的方式。然而,软件发展的现实却恰恰相反。编程领域本质上是协作性质的,由持续不断的思想交流和团队一起共同解决复杂问题来驱动。

成为社区的一员是非常有帮助的,特别是当你刚刚开始你的编程之旅的时候。

Spring Boot 已经存在一段时间了,它拥有一个由开发人员、架构师和专家组成的欣欣向荣的生态系统,这些人经常共享想法和资源,并互相帮助解决问题。由于它的开源特性,开发人员还可以分享改进 Spring Boot 框架的想法,你也可以参与其中。

Spring Boot VS Django 或 NodeJS

在我的经历和职业生涯中,我也逐渐体会到不同技术的独特优势和平衡。许多开发人员在将 Spring Boot 与其他 Java 语言生态系统之外的框架进行比较时,似乎有着相似的看法。

Java 语言的生态系统有一个相对较高的初始学习曲线,但是因为它的稳健性和强大完善的依赖库而脱颖而出。Spring Boot 在可靠性和可扩展性等非常重要的场景中表现出色。但是,与其他框架相比,有一个不太好的缺点就是启动时间稍微长一些。

另一方面,Python 和 Django 为 web 开发提供了令人愉快的体验,特别是在开发者生产力和快速迭代至关重要的项目中。

在某些情况下,Python 的性能可能不如 Java 语言,但它有一个可读和清晰的代码语法。Django 已经表明,对于内容管理系统、 Web 应用程序以及优先考虑项目启动和快速运行的情况,它是一个很好的选择。

拥有 JavaScript 生态基础的 Node.js 一直是实时应用程序和微服务的首选。它的性能,特别是对于 I/O 受限和实时的场景,是非常出色的。

我使用 Node.js 的经验是,在速度和可扩展性至关重要的情况下,它是一个完美的选择,尤其是在构建聊天平台等实时应用时。此外,为客户和服务器使用同一种语言的能力会让你的编程生活更加轻松,毕竟一个编程语言就可以开发全端平台了,我这里主要指前端、后端和手机端。

结论: Spring Boot 值得学习

Spring Boot 仍然是框架 Java 语言的领导者,而且似乎不会很快改变。

Spring Boot 仍然非常重要。如果或者真的当它被其他框架超越时,你学习到的上述这些概念,其实很有可能也会在其他框架中使用到。此外,随着新的 Spring Boot 发布,我认为至少在未来几年,Spring Boot 可能会继续保持事实上最主流的 Java 语言框架甚至是编程开发框架。

同样重要的是要注意到像 Micronaut 和 Quarkus 这样的框架日益增长的相关性,它们为特定类型的项目提供了独特的优势。探索这些选择并理解何时应用它们也可以进一步提高你作为开发者的能力。

自问自答环节

Q:什么是 Spring Boot? 为什么我要考虑在 2024 年学习它?

A:Spring Boot 是一个流行的开源 Java 语言框架,它使得创建微服务和 web 应用变得更加容易。在 2024 年学习 Spring Boot 仍然是值得的,因为它仍然为现代软件开发项目提供了许多优势和大量的工作机会。

Q:目前有哪一些大厂使用到 Spring Boot?

A:SpringBoot 用于广泛的应用程序,从 Web 开发和微服务到 API 创建和后端系统。Netflix、阿里巴巴、 LinkedIn、 Uber 和 Groupon 等公司都有一直在使用 Spring Boot。

本文由mdnice多平台发布

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

全网最细的Java动态填充Html模版并转PDF 标题:Th

发表于 2023-12-29

标题:Thymeleaf与wkhtmltopdf:技术对比与结合应用

一、引言

随着Web开发的不断发展,前端技术日新月异,后端技术也在不断进步。在后端技术中,模板引擎和PDF生成工具是两个非常重要的领域。Thymeleaf和wkhtmltopdf是这两个领域的杰出代表。本文将介绍Thymeleaf和wkhtmltopdf的技术特点,并探讨它们在Web开发中的应用。

二、Thymeleaf技术介绍

1. Thymeleaf概述

Thymeleaf是一个Java库,用于在Web应用程序中处理HTML、XML、JavaScript、CSS和文本文件。它是一种声明式模板引擎,可以在服务器端生成动态内容。

2. Thymeleaf特点

(1)易于使用:Thymeleaf语法简单明了,易于学习和使用。

(2)支持国际化:Thymeleaf支持多语言环境,方便实现国际化。

(3)与Spring框架集成:Thymeleaf与Spring框架无缝集成,方便开发人员快速构建Web应用程序。

三、wkhtmltopdf技术介绍

1. wkhtmltopdf概述

wkhtmltopdf是一个开源工具,用于将HTML页面转换为PDF文件。它基于WebKit引擎,可以生成高质量的PDF文件。

2. wkhtmltopdf特点

(1)高质量输出:wkhtmltopdf可以生成高质量的PDF文件,保持原始HTML页面的布局和样式。

(2)多种输出格式:除了PDF格式外,wkhtmltopdf还支持其他输出格式,如PostScript、EPS等。

(3)命令行工具:wkhtmltopdf提供了命令行工具,方便用户在终端中直接使用。

四、Thymeleaf与wkhtmltopdf的结合应用

1. 生成动态PDF文件

使用Thymeleaf模板引擎生成动态HTML页面,然后通过wkhtmltopdf工具将动态HTML页面转换为PDF文件。这种方式适用于需要生成动态PDF文件的应用场景,如在线文档、报告等。

2. 自动化测试报告生成

在Web应用程序的自动化测试中,可以使用Thymeleaf模板引擎生成测试报告的HTML页面,然后通过wkhtmltopdf工具将测试报告的HTML页面转换为PDF文件。这种方式适用于需要生成自动化测试报告的应用场景。

3. 自定义PDF文件生成

使用Thymeleaf模板引擎定义PDF文件的布局和样式,然后通过wkhtmltopdf工具将定义的PDF文件输出为最终的PDF文件。这种方式适用于需要自定义PDF文件生成的应用场景,如定制化的合同、发票等。

五、Spring Boot + maven项目 实际应用

准备工作

从wkhtmltox官网上下载linux的包

官网地址

image.png

安装命令rpm -Uvh wkhtmltox-0.12.6-1.centos7.x86_64.rpm
安装完用wkhtmltopdf -V查看版本验证是否安装成功

image.png

安装时候如果报错就是缺少包 yum install xxx安装即可

image.png

安装字体

fc-list :lang=zh-cn查询有没有中文字体如果没有下载中文字体上传至/usr/share/fonts下面
执行fc-cache -fv

thymeleaf使用

一、引入pom包

1
2
3
4
5
6
7
8
9
10
java复制代码<!-- html模版转化 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-java8time</artifactId>
<version>3.0.4.RELEASE</version>
</dependency>

二、application.yml 配置模版位置

1
2
3
4
5
6
7
8
9
10
java复制代码# thymeleaf
spring:
thymeleaf:
prefix: classpath:/templates/ #prefix:指定模板所在的目录
check-template-location: true #check-tempate-location: 检查模板路径是否存在
cache: false #cache: 是否缓存,开发模式下设置为false,避免改了模板还要重启服务器,线上设置为true,可以提高性能。
suffix: .html
#encoding: UTF-8
#content-type: text/html
mode: HTML

三、设置html模版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
html复制代码<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div class="app-container">
<div class="con">
<div class="title">xxx医院电子病历</div>
<div></div>
<div class="el-row" style="margin-left: -5px; margin-right: -5px;">
<div class="el-col el-col-4 is-guttered" style="padding-right: 5px; padding-left: 5px;"> 病案号:<span th:text="${dto.getRecordNo()}"></span></div>
<div class="el-col el-col-2 is-guttered" style="padding-right: 5px; padding-left: 5px;"> 科室:<span th:text="${dto.getDeptName()}"></span></div>
<div class="el-col el-col-3 is-guttered" style="padding-right: 5px; padding-left: 5px;"> 就诊日期:<span th:text="${dates.format(dto.getCreateTime(), 'yyyy-MM-dd')}"></span></div>
</div><!---->
<div class="box">
<div class="el-row" style="margin-left: -5px; margin-right: -5px;">
<div class="el-col el-col-4 is-guttered" style="padding-right: 5px; padding-left: 5px;"> 姓名:<span th:text="${dto.getCusPatient().getRealName()}"></span></div>
<div class="el-col el-col-2 is-guttered" style="padding-right: 5px; padding-left: 5px;"> 性别:<span th:text="${dto.getCusPatient().getGender() == 1 ? '男' : '女'}"></span></div>
<div class="el-col el-col-3 is-guttered" style="padding-right: 5px; padding-left: 5px;"> 年龄:<span th:text="${dto.getPatientAge()}"></span></div>
</div>
<div class="el-row" style="margin-left: -5px; margin-right: -5px;">
<div class="el-col el-col-12 is-guttered" style="padding-right: 5px; padding-left: 5px;"> 身份证:<span th:text="${dto.getCusPatient().getIdNumber()}"></span></div>
<div class="el-col el-col-12 is-guttered" style="padding-right: 5px; padding-left: 5px;"> 联系电话:<span th:text="${dto.getCusPatient().getPhoneNumber()}"></span></div>
</div>
<div class="box-bottom">
<div>主诉:<span th:text="${dto.getChiefComplaint()}"></span></div>
<div>诊断:<span th:text="${dto.getDiseaseIcdText()}"></span></div>
<div>病史描述:</div>
<div><span th:text="${dto.getDiseaseDesc()}"></span></div>
<div>处理:<span th:text="${dto.getTreatment()}"></span></div>
<div style="overflow: hidden">
<div style="float: right;">医生签名:<span th:text="${dto.getDoctorName()}"></span></div>
</div>
</div>
</div>

</div>
</div>
</body>
<style>
* {
margin: 0;
padding: 0;
font-size: 16px;
color: #333333;
}
.app-container .con {
box-sizing: border-box;
width: 100%;
line-height: 23px;
font-size: 14px;
color: #333;
}
.title {
text-align: center;
height: 31px;
font-size: 22px;
font-weight: 600;
color: #333333;
line-height: 26px;
margin-bottom: 10px;
}

.box {
border: 1px solid #999;
}
.box .el-row {
border-bottom: 1px solid #888;
padding: 10px;
}
.box .el-row .el-col {
height: 22px;
}
.con > .el-row {
margin-bottom: 10px;
}
.el-row {
box-sizing: border-box;
overflow: hidden;
margin-left: 0!important;
margin-right: 0!important;
}
.el-row .el-col {
float: left;
width: 33.33%;
padding-left: 0 !important;
padding-right: 0 !important;
}
.el-row .el-col-12 {
width: 50%;
}
.box-bottom {
padding: 10px;
}
.box-bottom > div {
margin-bottom: 6px;
line-height: 1.5;
}
</style>
</html>

四、定义controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
java复制代码/**
* 导出pdf
*
* @param response 流
* @param medicalRecordId 病历id
*/
@GetMapping("/patient/patMedicalRecord/export/{medicalRecordId}")
public void exportPatMedicalRecord(HttpServletResponse response, @PathVariable Long medicalRecordId) {

PatMedicalRecordDetailDTO dto = patMedicalRecordFacade.getByMedicalRecordId(medicalRecordId);
dto.setCusPatient(SystemCacheUtils.getPatientById(dto.getPatientId()));

// 创建一个容器模板
TemplateSpec templateSpec = new TemplateSpec("patMedicalRecord.html", TemplateMode.HTML);
// 设置模板变量
Context context = new Context();
context.setVariable("dto", dto);
// 渲染并返回新的HTML文本
String newHtml = templateEngine.process(templateSpec, context);
String fileName = StrUtil.format("{}patMedicalRecord_tmp_{}", tcmTemplateConfigProperties.getPath(),
new Date().getTime());
String templatePath = fileName + ".html";
String pdfPath = fileName + ".pdf";
FileUtil.writeString(newHtml,templatePath, "UTF-8");
WKHtmlToPdfUtil.convert(templatePath, pdfPath);
File file = FileUtil.file(pdfPath);

// 设置响应头
response.setContentType(ContentType.OCTET_STREAM.getValue());
response.setCharacterEncoding("utf-8");
response.setHeader("Content-Disposition", "attachment; filename="" + file.getName() + """);

// 将文件内容写入响应流
try (InputStream inputStream = new FileInputStream(file)) {
IoUtil.copy(inputStream, response.getOutputStream());
} catch (IOException e) {
// 异常处理
log.info("导出电子病历写入流失败,{}", e.getMessage());
}
// 导出完删除
FileUtil.del(file);
FileUtil.del(templatePath);
}

WKHtmlToPdfUtil 转pdf工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
java复制代码
package com.hkt.tcm.common.util.pdf;

import com.hkt.tcm.common.def.consts.Tools;
import lombok.extern.slf4j.Slf4j;

import java.io.File;

/**
* @author lixin
* @date 2023-12-22 15:52
**/
@Slf4j
public class WKHtmlToPdfUtil {

/**
* html转pdf
*
* @param srcPath html路径,可以是硬盘上的路径,也可以是网络路径
* @param destPath pdf保存路径
* @return 转换成功返回true
*/
public static boolean convert(String srcPath, String destPath) {
File file = new File(destPath);
File parent = file.getParentFile();
// 如果pdf保存路径不存在,则创建路径
if (!parent.exists()) {
parent.mkdirs();
}
StringBuilder cmd = new StringBuilder();
// wkhtmltopdf的路径
if (System.getProperty("os.name").contains("Mac")) {
cmd.append(Tools.WkHtmlToPdf.PATH_MAC);
} else if(System.getProperty("os.name").contains("Windows")) {
cmd.append(Tools.WkHtmlToPdf.PATH_WINDOWS);
} else {
cmd.append(Tools.WkHtmlToPdf.PATH_LINUX);
}
cmd.append(" -L 5mm -R 5mm");
cmd.append(" --no-stop-slow-scripts --load-error-handling ignore");
cmd.append(" --enable-local-file-access");
// cmd.append(StrUtil.format(" --header-right {} --header-line --header-spacing 3", ""));
// cmd.append(StrUtil.format(" --header-right {} --header-spacing 3", ""));
cmd.append(" ");
cmd.append("--enable-local-file-access");
cmd.append(" ");
cmd.append("--disable-smart-shrinking ");

cmd.append(" "");
cmd.append(srcPath);
cmd.append("" ");
cmd.append(" ");
cmd.append(destPath);

System.out.println(cmd.toString());
boolean result = true;
Process proc;
try {
if (!System.getProperty("os.name").contains("Windows")) {
// 非windows 系统
proc = Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", cmd.toString()});
} else {
proc = Runtime.getRuntime().exec(cmd.toString());
}
HtmlToPdfInterceptor error = new HtmlToPdfInterceptor(proc.getErrorStream());
HtmlToPdfInterceptor output = new HtmlToPdfInterceptor(proc.getInputStream());
error.start();
output.start();
proc.waitFor();
} catch (Exception e) {
log.error("Convert to pdf error", e);
result = false;
}

return result;
}


}

六、总结与展望

本文介绍了Thymeleaf和wkhtmltopdf的技术特点和应用场景,并探讨了它们在Web开发中的结合应用。随着Web开发的不断发展,模板引擎和PDF生成工具将会更加普及和重要。未来,我们可以期待更多的技术进步和创新,为Web开发带来更多的便利和可能性。

最后感谢这两天给我投票的jym

image.png

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…656667…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%