## 如何保证万无一失
![](http://cdn.aipin100.cn/18-6-17/87926503.jpg)
假如我们是一家银行,收到第三方发来的一笔转账请求(比如支付宝,微信支付里面的提现操作),收到这笔交易请求后,会进行下面三个动作(有简化,但是能体现核心部分):
1. 创建一条交易记录
2. 将转账操作扔到队列
3. 消费者消费队列,最终完成转账操作
>题外话:这就是为什么我们提现有延时的原因(2小时内到账),因为最先放到队列了,而不是直接处理,为什么不直接处理呢,因为交易量很大,计算能力有限,一时处理不过来。
这关键的三步,怎么保证安全性呢?
什么是安全性呢,在这里,正确性就是安全性。
再来看会出现哪些问题呢?下面我们提出一些问题:
1. 转账失败了。
2. 重复转账了。
3. 转账成功了,但状态还未显示。
……
不管有哪些情况,我们只知道,正确的结果只有一种,那就是转账成功,没有多转也没有少转,并且转账记录显示为成功。
**如果三步都能成功,和预期的一样执行,那么就不会有任何问题了,但关键是没有人能够担保一定不会出问题。**
所以我们就要用一些方法来解决这个不确定性问题了。
先来一步一步的看,假设执行到哪一步失败的话会怎样,以及如何解决。
1. 第一步失败的话,交易记录不能创建,也就没有任何交易被创建。直接响应请求失败。这不会造成什么问题,毕竟这是第一步,第一步失败就不会有后面的事情了,失败了又能有什么问题呢。
2. 第二步失败的话,扔到队列失败了,可是第一步交易已经成功创建了,这可怎么办。
好,先来解决这个问题。
**Q:扔队列操作失败了怎么办?**
**A:** 没人能保证任何操作都一定能成功,队列系统刚好不可用了,只能怪老天了,那此时怎么保证业务不出问题呢,总不能丢失这笔转账吧。这里有几种方式:1. 重试,放队列失败,就不断重试,如果有多条消息,后面消费时要检查交易状态(上锁,不能出现并发问题),要保证幂等性,不能重复消费消息。2. 补偿:在放队列之前(交易创建之后),做一个补偿标记,10小时候或者每隔10小时检查该笔交易的状态(检查交易状态,检查确认,检查队列),如果有问题就重试,或者做好失败记录,记好日志。(这个时间是根据系统处理情况来定的,直到交易状态为完成才删除这个补偿标记)
**解决了这个问题,那么其实下面的n个问题就都解决了,比如队列消息丢失啊,都是同样的思路。不管你哪一步失败,会出现什么问题,我们只知道有且只有一种正确结果,只要保持这种正确结果的最终正确性,系统就是可靠的,万无一失的。**
(如果系统确实出问题了,比如由于前期设计考虑不周全、疏忽而导致的问题,那我们也要最大限度的减少由此带来的影响,最小化损失,并且设法挽回。)
>[danger] 关键词:**重试、补偿、事后校正、确认、ACK确认、容错、柔性事务、两阶段提交、最终一致性、锁、防止并发问题、幂等性、防止重复消费、日志记录**。
* * * * *
### 没有绝对的健壮,但是有相对的
保持悲观的同时也不要忘记乐观。
由于每段代码执行逻辑不同,所处环境也不同,所以出错的几率也不同,**一般主进程存在较小的崩溃概率,因为它逻辑直观,不会掺杂任何的业务逻辑代码,所以几乎不会出错中断(甚至设计中可以认为此部分不会出错,负载均衡部分也同理)**,但是worker进程就不同了,它是业务逻辑的具体执行部分,这里出错是不可预料的,所以对于这部分代码,可以理解为一定会出错,主进程应做好维护工作。
这世界上并没有万无一失,就像两座城堡的通信,信使总是可能不可靠的,你无法确定他一定不会叛变或者旅途中遇到突发情况,不论是什么情况发生,只要城堡是坚固的,我们就是安全的。任何时候我们都不能将安全的赌注压在信使身上。如果你理解这个道理,你构建的系统就是坚固可靠的。
*****
### 一致性保证
例:转账操作
1. **转账表** remittance: (id, from, to, amount, status, create_time, complete_time) : 增加一条转账记录
2. **转账队列消息记录表** remittance_queue: (id, remittance_id status[待消费, 正在执行, 处理完成, 已关闭]) : 增加一条消息记录
> 消费失败,则转入失败记录表进行相应的业务逻辑(如转账日志),而不是标记为处理失败
(有人可能认为 `remittance_queue` 表是不必要的,的确只有 `remittance` 表也做得到行锁并发控制,但是有这张表可以记录某条转账的队列处理记录,并且将锁开销转移到用户不回访问的表上了,所以我认为这张表有存在的必要。同时对于系统内多种这样的操作,可以抽象出一张专门记录队列操作的表,如 队列操作记录表: [操作标识, 对应资源ID, 状态] 这样整个系统只需要这一张表保证就可以。)
3. **插入一条队列消息** `Resque::enqueue()`: task, 载荷(包含 remittance_queue_id)
这样应该被处理的业务逻辑就被 **“装”** 到了队列中,即Broker中。而队列消息是不可靠的(存/取):丢失消息、消息重复(同样的消息存在多条)、重复消费……,即便如此,我们也要在这样的情况下保证整个转账的业务逻辑正确性。
具体要面对和解决的问题就是:幂等性
具体做法:
1. **防止并发问题:** task 消息消费时,必须使用 事务行锁 检查 `remittance_queue` 队列消息的状态是否为 `待消费`,只在 待消费 的状态下执行任务处理。是否已经消费过了,(防止重复消费,或者队列消息不稳定)
2. **补偿机制:** 当一段时间过了还没有到账,说明队列消息可能丢失了,或者其他原因,这时需要重发消息,相当于 **“再次执行转账操作”** (不过只是进行重发队列消息),也就是 **启动补偿机制**。
此时的 **补偿操作** 为:
1. 先 事务行锁 检查这条 转账记录 对应的 队列消息表,根据 `remittance_id` 查到 `remittance_queue` 中所在行,`status` 是否还为 `待消费`,是则将 `status` 标记 `关闭`,相当于抛弃对应的队列消息了,不管你那条队列消息此时到底是跑哪里了。
2. 然后,再次增加一条 队列消息记录
3. 再次插入一条队列
> 上面说补偿时的重发消息相当于 再次执行转账,但这个 “再次执行转账操作” 是加引号的,这个操作与转账操作不同的是,没有其他的业务逻辑(如转账前的相关逻辑),只有安全的队列操作。(操作对象是remittance_queue和队列)
#### 补偿机制如何实现
如:发起一笔转账时,就建立一个补偿,采用延时队列实现,延时时间根据预估,比如转账发起后的12小时。这样当转账开始后的12小时后,补偿机制就会执行。补偿发现转账不成功,会有相应的机制,如重发消息,日志记录,报警等等。如成功了,那么补偿时就什么都不做。
除了延时队列实现补偿,还可以手动触发,比如后台的“重试”按钮,就相当于是手动的补偿机制了。
并且每次操作/补偿,都伴随着补偿(补偿重发时也要再次加个延时补偿),直至最终操作成功。
补偿就像一个护花使者,它不直接与你同行,而是用另一种方式伴随着你。
**另外没有绝对的安全,系统应该定时执行财务对账,这样才能及时发现和规避风险。**
~~~
### 最终一致性 补偿
砍价服务 ⇆ 砍价应用
砍价服务成功调用 砍价应用成功了,但自身的后续处理失败了,那就没办法了,所以调用方需要自己确保自己也能落盘成功
分布式事务很难,只能做到最终一致性,如果失败只能补偿处理了
补偿方案:
1. 被调用方 执行成功后 写入一条 巡视确认记录 后再返回结果
2. 调用方 执行成功后 处理 其巡视记录(删除或标记)
3. 监控经常 定期扫描 超时待确认的巡视记录 就能发现 数据不一致的记录
4. 程序或人工手工修复数据不一致的记录,分析问题原因,这样就能实现 最终一致性
~~~
#### 如果补偿也失败了怎么办?
还有人打破砂锅问到底,说如果补偿、确认等机制也失败了怎么办,好吧,抱着务实的态度,也来说一下:
一般来说,越简单的系统越不容易出错,出错的系统一般是复杂性较大的系统,但凡事总有万一,万一最简单的补偿机制也失效了怎么办?还有报警啊,还有日志啊,那如果报警和日志也都失效了怎么办?好吧。你是不打算放过我了是吧。
其实你不用这么轴,这种情况即使出现我们也不怕,认真的告诉你,我们真的有考虑过这种情况的。别忘了我们还有最后一道防线,人工。人工审查/纠错(成熟的系统都有财务、对账、审查的,即使系统没有这样做,公司也会要求审查对账的)。试想转账超过10天还没有到账的,就算没有审查出来,用户也会打电话来投诉的。
虽然你很烦,不过我还是喜欢这么轴这么认真的你,好样的。
#### 没有绝对的完美
还有人说,我代码很完美了,怎么会有那么多的失败呢,有可能吗?
首先世界上没有最完美的代码,没有永远没有BUG的系统,最完美的代码就是不断进化、升级、更新的代码,最可靠的系统就是得到长期支持和维护的系统。
就算你的代码不出问题,你能控制内因,你也无法控制外因啊。你能预料到地震,海啸,台风等自然灾害吗?硬盘爆炸呢,内存烧焦呢,CPU冒烟呢,停电呢,临时工挖断网线呢,这些你能控制吗,**所以啊,任何时候,任何指令都可能执行失败或者没被执行**,如果你能在脑内模拟出硬件,那么你就能很简单的看出程序是怎么运行的。就能在大脑里面想象程序运行的原理,你就知道这是怎么一回事了。
* * * * *
### 代码的失败率
<p style="background: linear-gradient(to right,#36ff670,#ffffff 5%);">db.connect() // failure rate: 1/千
</p>
<p style="background: linear-gradient(to right,#36ff670,#ffffff 5%);">db.beginTransaction() // failure rate: 1/万
</p>
<p style="background: linear-gradient(to right,#36ff670,#ffffff 5%);">db.ping() // failure rate: 1/十万
</p>
<p style="background: linear-gradient(to right,#36ff670,#ffffff 5%);">db.query() // failure rate: 1/八万
</p>
<p style="background: linear-gradient(to right,#36ff670,#ffffff 5%);">db.query() // failure rate: 1/七万
</p>
sleep(2)
<p style="background: linear-gradient(to right,#36ff670,#ffffff 5%);">db.query() // failure rate: 1/五万
</p>
<p style="background: linear-gradient(to right,#36ff670,#ffffff 5%);">db.commit() // failure rate: 1/三万
</p>
1. 每次调用都是与 db 服务端进行交互
2. 假设 sql 都是正确的,每行代码也都会有执行失败的可能,只是失败概率不同而已
3. 我们无法改变总是会有可能失败的现实,只能尽量让我们的业务代码处在一个较低的失败率之中,并做好容错处理,考虑异常的情况
4. 即代码并不是完全可靠的,但我们要有容错,确保异常是可控的,这样业务才是安全的,正确的
----
### 扩展
我们发现关系型数据库还是很重要的,我们的数据都交给了它,可靠性也交给了它。
[传统事务与柔性事务](https://www.jianshu.com/p/ab1a1c6b08a1)
> 日志,幂等性,业务弹性,最终一致,重试,补偿
[支付宝运营架构中柔性事务指的是什么? - 知乎](https://www.zhihu.com/question/31813039)
> 业务层2PC(两阶段提交),事后校正
[创始人快去跟公司技术人员落实这件事](https://mp.weixin.qq.com/s/YQbxfI389FLVuwIpYhr9sw)
> 有可能出 bug 的代码最终都会出 bug。
>
> 一定。
[RabbitMQ从入门到精通---ACK机制 | 菜鸟IT路](https://www.dev-heaven.com/posts/36563.html)
[RabbitMQ ACK 机制的意义是什么? - 知乎](https://www.zhihu.com/question/41976893)
[TCP报文到达确认(ACK)机制 - CSDN博客](https://blog.csdn.net/wjtxt/article/details/6606022)
[php手册经常见到,什么是“二进制安全”? - zhuocr的博客 - CSDN博客](https://blog.csdn.net/zhuocr/article/details/70591310)(充分了解你使用的系统)
[事务已提交,数据却丢了,赶紧检查下这个配置!!! | 数据库系列](https://mp.weixin.qq.com/s/-Hx2KKYMEQCcTC-ADEuwVA)
* * * * *
last update:2018-10-26 16:49:22
- 开始
- 公益
- 更好的使用看云
- 推荐书单
- 优秀资源整理
- 技术文章写作规范
- SublimeText - 编码利器
- PSR-0/PSR-4命名标准
- php的多进程实验分析
- 高级PHP
- 进程
- 信号
- 事件
- IO模型
- 同步、异步
- socket
- Swoole
- PHP扩展
- Composer
- easyswoole
- php多线程
- 守护程序
- 文件锁
- s-socket
- aphp
- 队列&并发
- 队列
- 讲个故事
- 如何最大效率的问题
- 访问式的web服务(一)
- 访问式的web服务(二)
- 请求
- 浏览器访问阻塞问题
- Swoole
- 你必须理解的计算机核心概念 - 码农翻身
- CPU阿甘 - 码农翻身
- 异步通知,那我要怎么通知你啊?
- 实时操作系统
- 深入实时 Linux
- Redis 实现队列
- redis与队列
- 定时-时钟-阻塞
- 计算机的生命
- 多进程/多线程
- 进程通信
- 拜占庭将军问题深入探讨
- JAVA CAS原理深度分析
- 队列的思考
- 走进并发的世界
- 锁
- 事务笔记
- 并发问题带来的后果
- 为什么说乐观锁是安全的
- 内存锁与内存事务 - 刘小兵2014
- 加锁还是不加锁,这是一个问题 - 码农翻身
- 编程世界的那把锁 - 码农翻身
- 如何保证万无一失
- 传统事务与柔性事务
- 大白话搞懂什么是同步/异步/阻塞/非阻塞
- redis实现锁
- 浅谈mysql事务
- PHP异常
- php错误
- 文件加载
- 路由与伪静态
- URL模式之分析
- 字符串处理
- 正则表达式
- 数组合并与+
- 文件上传
- 常用验证与过滤
- 记录
- 趣图
- foreach需要注意的问题
- Discuz!笔记
- 程序设计思维
- 抽象与具体
- 配置
- 关于如何学习的思考
- 编程思维
- 谈编程
- 如何安全的修改对象
- 临时
- 临时笔记
- 透过问题看本质
- 程序后门
- 边界检查
- session
- 安全
- 王垠
- 第三方数据接口
- 验证码问题
- 还是少不了虚拟机
- 程序员如何谈恋爱
- 程序员为什么要一直改BUG,为什么不能一次性把代码写好?
- 碎碎念
- 算法
- 实用代码
- 相对私密与绝对私密
- 学习目标
- 随记
- 编程小知识
- foo
- 落盘
- URL编码的思考
- 字符编码
- Elasticsearch
- TCP-IP协议
- 碎碎念2
- Grafana
- EFK、ELK
- RPC
- 依赖注入
- 开发笔记
- 经纬度格式转换
- php时区问题
- 解决本地开发时调用远程AIP跨域问题
- 后期静态绑定
- 谈tp的跳转提示页面
- 无限分类问题
- 生成微缩图
- MVC名词
- MVC架构
- 也许模块不是唯一的答案
- 哈希算法
- 开发后台
- 软件设计架构
- mysql表字段设计
- 上传表如何设计
- 二开心得
- awesomes-tables
- 安全的代码部署
- 微信开发笔记
- 账户授权相关
- 小程序获取是否关注其公众号
- 支付相关
- 提交订单
- 微信支付笔记
- 支付接口笔记
- 支付中心开发
- 下单与支付
- 支付流程设计
- 订单与支付设计
- 敏感操作验证
- 排序设计
- 代码的运行环境
- 搜索关键字的显示处理
- 接口异步更新ip信息
- 图片处理
- 项目搭建
- 阅读文档的新方式
- mysql_insert_id并发问题思考
- 行锁注意事项
- 细节注意
- 如何处理用户的输入
- 不可见的字符
- 抽奖
- 时间处理
- 应用开发实战
- python 学习记录
- Scrapy 教程
- Playwright 教程
- stealth.min.js
- Selenium 教程
- requests 教程
- pyautogui 教程
- Flask 教程
- PyInstaller 教程
- 蜘蛛
- python 文档相似度验证
- thinkphp5.0数据库与模型的研究
- workerman进程管理
- workerman网络分析
- java学习记录
- docker
- 笔记
- kubernetes
- Kubernetes
- PaddlePaddle
- composer
- oneinstack
- 人工智能 AI
- 京东
- pc_detailpage_wareBusiness
- doc
- 电商网站设计
- iwebshop
- 商品规格分析
- 商品属性分析
- tpshop
- 商品规格分析
- 商品属性分析
- 电商表设计
- 设计记录
- 优惠券
- 生成唯一订单号
- 购物车技术
- 分类与类型
- 微信登录与绑定
- 京东到家库存系统架构设计
- crmeb
- 命名规范
- Nginx https配置
- 关于人工智能
- 从人的思考方式到二叉树
- 架构
- 今日有感
- 文章保存
- 安全背后: 浏览器是如何校验证书的
- 避不开的分布式事务
- devops自动化运维、部署、测试的最后一公里 —— ApiFox 云时代的接口管理工具
- 找到自己今生要做的事
- 自动化生活
- 开源与浆果
- Apifox: API 接口自动化测试指南