- 整个支付系统的核心是所有接口的幂等性保证+整个事务的可查性
- 幂等性保证是为了防止重试情况下的多次操作,一般每次操作都需要分配唯一ID保证幂等性
- 可查性是为了保证事务出现宕机之后恢复依旧能恢复到正常状态,主从事务可以直接使用事件中心消息队列,但是多主事务需要维护本地消息表
复式记账法
- 恒等式: 资产=负债+所有者权益
- 等式的左边资产是可用的资源,是公司的权利,而等式的右边是为了解释左边而存在,用来说明资产的来源,是公司的义务
- 核心就是借贷相等,使用记录两次的办法实现.一笔数至少记两次,至少会在两个相关的账户中记录金额相等,借贷相反的记录,两个账户的类型可以是相同的(比如都是负债类账户),也可以是不同的。
数据库设计
个人余额
- 账户ID,这个ID有的平台会有编码规则,比如某些位用来区分个人或者商户,区分币种以及校验位等。
- 币种
- 余额,一般使用币种最小单位。
- 账户状态,比如是正常态、支付或者冻结等。
- 余额版本号,这个字段非常重要,体现了余额的变化过程,是与流水进行关联的关键。
流水信息
当余额发生变更时,需要记录流水,以此来跟踪余额的变化
- 流水ID
- 凭证ID,一般是业务单号(如订单ID、退款单ID等),也就是下面凭证中的ID。
- 发生金额
- 发生币种
- 起始余额
- 终止余额
- 余额版本号,与余额信息中的版本号字段对应。
- 资金方向,标识是入账还是出账。
- 源账户ID
- 目的账户ID
- 交易时间戳
凭证
凭证用来记录交易过程中的信息,是用户交易的依据。凭证对应到支付平台内部的各种单类,比如充值单,体现单,交易单等等。理论上可以放到业务层,不放在账户核心层,这个相当于本地消息表的功能,记录事务目前的状态,也是非常重要
- 凭证ID
- 交易参与方,可能是两方,也可能是多方
- 交易金额、交易类型
- 交易状态,比如支付中,支付成功,转退款等
- 交易渠道
账户
- 个人账户,一个人一个账户,这个被叫做是C账户,是现金账户,个人可以完全支配。
- 商户账户,商户账户因为需要结算等,所以一个商户有两个账户,一个B账户(中介账户,理解为待结算账户),一个C账户(现金账户)。当用户支付时,实际支付到了商户的B账户,这个账户里面的资金商户不能自由支配,待平台结算后(扣除手续费等),资金被转移到C账户,这个账户商户是可以自由支配的,比如可以提现到商户自己的银行卡。
- 银行账户,第三方支付公司为各个银行设置的账户,这个账户是一个总账账户,记录与银行之间的资金变动,一般不记录余额,而只是记录流水,方便跟各个银行进行对账。
分布式事务模型
-
和微信支付的分布式事务类似
微信支付
- 使用两阶段提交+MQ事务的方法保证最终一致性,分为主事务和从事务,主事务成功后,不停尝试执行从事务
- 如果是面对面转账这种强一致性的多主事务,需要使用mysql的事务机制,加上TCC的机制
- 如果为代扣的这种事务就是先创单(主事务),然后延迟打钱(从事务)
事件中心
- 底层是消息队列
- 解耦,就是对事务分主次:
- 主事务一般是核心逻辑,逻辑重,同步调用;
- 从事务一般是次要逻辑,逻辑轻,异步调用。
- 第一阶段是prepare,将消息暂存到事件中心,但是不发布,等待二次确认;prepare后,业务执行主事务(一般是rpc远程调用),成功就发commit给事件中心,投递消息到从事务;失败就发rollback给事件中心,不做投递。
- 这里需要两阶段提交的原因是:我们常规理解的入队操作,也就是一阶段提交,无论是放在主事务执行前,还执行后,都无法保证最终一致。考虑如下场景
如果是先做主事务,再入队,那可能入队前就宕机了; 如果是先入队再做主事务,那可能主事务没做成功,但从事务做成功了。
- 所以无论哪种做法都有问题,二阶段提交是必须的。
反查
- 如果prepare后没有进行commit或者rollback(消息丢失或者系统重启),事件中心就会主动下发消息询问主事务执行的结果
普通消息
- 普通消息直接通过事务中心执行所有事务
- 两个必须要用事务消息的场景:
第一是事务逻辑复杂,也就是发生逻辑失败的概率大,比如扣款前要检查余额是否足够,如果余额不足,那在异步流程中重试多少次都是失败。
第二是事务不可重入,例如业务系统入队时并未确定一个唯一事务ID,那各事务就无法保证幂等,假设如果其中一个事务是创建订单,不能保证幂等的话,重试多次就会产生多个订单;所以这里需要用事务消息,明确一个分布式事务的开始,生成一个唯一事务ID,让各个事务能以这个事务ID来保证幂等。
事件中心设计
- 事件中心的本质是队列驱动事务,所以要满足常见的队列功能,比如多订阅、出队有序、限速、重试等等
- 订阅者用于执行从事务
- Producer是发布者,Consumer是消费者,Consumer用推模式将消息推给Subscriber订阅者,这都是比较好理解的。然后来看Store,队列存储Store的实现选择了Paxos,是因为Paxos能保证副本一致,可避免乱序/去重问题,非常适合队列模型。Paxos协议的正确运行需要同步刷盘,副本同步数3份,这能提高数据可靠性。朴素Paxos的性能不是很好,所以通过批量提交的方式保证写入性能。
- 接着来看Scheduler,Scheduler是Consumer的协调者,通过与Consumer维持心跳,定义Consumer的生死,实现容灾;同时收集Consumer负载信息,实现负载均衡。Sched还依赖分布式锁Lock来选举master,只有master提供服务,自然实现容灾和服务一致性。
- 最后来看Lock,Lock是一个分布式锁,不仅用来给Scheduler选master,他还服务于Consumer,防止负载均衡流程中多个消费者同时处理一条队列。
- 这里Consumer的负载均衡流程也是一个二阶段提交,第一阶段是Consumer先跟Scheduler确定自己该处理哪些队列,第二阶段是访问Lock对队列抢锁,只有抢到锁后才能开始处理。
- kv查无记录时反查业务。只有确认commit,Consumer才会投递消息给订阅者。
多个主事务
-
类似TCC的机制,预锁资源的方法
这里也可以通过补偿的方式,但是中间态会短暂暴露
-
事务预写的具体做法是,将主事务1中会导致逻辑失败的部分,提前到prepare前执行,从而减少prepare...commit/rollback之间的事务个数。主事务1的执行结果在提交前并不对外可见,所以即使该结果不回滚,也不破坏一致性。与TCC中的Try操作类似。
优缺点
优点
- 减低耦合度和业务入侵性,相当与把本地的事务表存储在MQ中
- 吞吐量大,不会涉及2pc的阻塞问题
缺点
- 从事务默认可以通过重试成功(因为没有提供主事务补偿的机制),导致不能覆盖所有应用场景
- 需要提供反查的接口,增加复杂度
参考
- https://zhuanlan.zhihu.com/p/263555694
- https://www.cnblogs.com/cbvlog/p/15458737.html
先借后贷
- 先在mysql中启动事务减少用户金额并生成用户流水
- 异步增加商户的金额并生成商户流水,也是mysql事务实现,如果失败了就重试
- 一定先减少后增加,因为增加一般能成功,扣款不一定
二维码付款
核心代码没有权限看,以下为个人推测
商户扫用户码
-
用户如果开通离线付款就后台下发一批code_id(按照顺序),否则就没每次在线生成一个code,这个code按照唯一key插入数据库中(生成的过程需要验票),这样也可以分为两种code,key和个人的信息,过期时间等加密生成token(这部分也可以使用session)之后发送给用户(这里需要长连接)
-
商家的pos机扫码之后,将token和自己要的钱带上自己的商户票据发送到后台服务器,服务器检测code的有效性(过期时间),商户账户有效性等
-
生成凭证,mysql的一条记录,其中包括了交易目前的状态等信息,生成交易的唯一ID,code_id作为唯一key,防止重入插入,订单的唯一性id可能是 业务+hash(uid)+string(time)+自增id 最后异或的方式生成,可以保证唯一性,也可以参考 分布式ID生成
-
向事务中心publish, 凭证的ID作为key
-
调用扣款的rpc接口作为主事务
- 开启主事务
- 生成流水,插入mysql中的流水中,这里的凭证ID需要作为唯一key防止重入
- 扣用户钱
- 提交主事务
-
如果扣款rpc返回成功,更改订单状态为已扣款(这里需要使用状态机的性质切换)
即使在这里崩溃了,通过事务中心的重查也能在这里进行重试
-
提交commit到事务中心
-
返回客户端成功,并提交事务到事件中心,主流程就不管了,自己编写订阅者订阅消息
- 订阅者调用扣钱的rpc作为从事务
- 从事务开启
- 插入流水ID,这里需要使用凭证ID作为private key防止重入(这里如果出现两个事务都在操作同一个订单必定出现一个成功一个失败,通过mysql保证)
- 商家加钱
- 提交从事务
- 如果加钱不成功判断是否为重入,重入判定为成功(说明已经扣钱成功),如果错误,返回让事务中心进行重试
- 更新订单状态为已加款
- 订阅者调用扣钱的rpc作为从事务
用户扫商家二维码
- 这部分感觉比较普通,因为这个二维码可以随意传播,因此肯定不会放敏感内容(不会放token),可能设置一个session用于标记二维码是否过期以及基本信息等,转账的校验核心在扫码拉起付款界面之后才进行后端校验
微信红包系统
[!help] 这部分可以作为秒杀系统的一个例子demo,秒杀参考
红包金额分配
redis和mysql数据一致性问题
- 无法保证强一致性,只能保证最终一致性
- 根本原因是因为,redis和mysql操作不是原子操作(因为跨系统)
先操作缓存
- 删除redis缓存
- 更新mysql数据库
缺点
- 更新mysql时候其他线程读取mysql,导致旧值存在于缓存中,因为这种可能发生可能性大,因此用的少
先操作数据库Cache Aside Pattern(旁路缓存模式)
- 更新mysql
- 删除redis缓存
优点
- 就算有旧值后面也会被删除
缺点
- 更新mysql时候其他线程读到旧值,但是因为网络波动,在删除缓存后写入旧值,导致旧值存在缓存中(发生概率还是比较小的,用的比较多)
延时双删
- 删除缓存(避免读到旧值,但是通常可省略)
- 更新数据库
- 睡觉一会(几十ms左右)
- 删除缓存(删除可能其他线程写的脏缓存)
优点
- 更大可能性不会出现第二种的问题
缺点
- 时间难以把控
- 性能减低(睡觉的原因)
其他
MQ
- 通过MQ把DB和redis解耦,把删除任务扔到MQ中,通过MQ保证执行的顺序和串行化
为什么不是update而是delete
实际上字节用的就是这种
- 避免A先改,B后改,但是因为网络问题,B先更新缓存,A把旧缓存更新的问题(这个感觉发生可能性比较小)
- 如果写的比较多,那么频繁更新缓存影响性能
解决缓存命中率低
- 如果要求强一致性,那么可以通过分布式锁避免其他线程操作
- 最终一致性,可以DB更新后,更新一个生存时间非常短的缓存,减低影响
总结
- 只能用于一致性要求不这么高,但是更新频繁,读取频繁的情况,比如好友数量,消息热度这种
- 真正账户余额这种还是要么不用redis,直接mysql读(数据小),要么上etcd这种强一致性的数据库
数据一致性的其他解决方案
binlog 删除
- 订阅mysql binlog, 如果出现update 情况就删除 redis 中的数据
- 读时候miss就顺便缓存
优点
- 写操作和 redis接耦
- 删除缓存极端 case 得到缓解
缺点
- 多出一个组件, 复杂性增加
- 更新之前的缓存是脏的, 短时间的错误数据
binlog 更新
- 优点和缺点类似
- 这个将删除改为更新, 但是有个问题时这个依赖 mq 的顺序消费的特性, 都者数据会是错误的
- 这个也会出现一段时间缓存是错误的
read-Through和write-Back
- 相当于外部封装了一层sdk, 所有的操作直接写入缓存之后马上返回, 返回之后缓存再写入 db中, 读的时候也是读缓存, 读取不到 sdk自行处理(感觉相当于封装了一个存储)
facebook 一致性方案
- get 使用 udp 进行连接数据传输, set/delete 使用tcp进行数据传输, udp失败的情况直接当做 cache miss处理
- 使用 lease 机制保证按顺序写入缓存, 针对一个特定的Key,当第一次查询出现Cache Miss的时候,会为它产生一个Lease返回给Client,Client在查询到真正的值之后Set Memcache的时候必须带上这个Lease,这样才能通过合法性检查。多个并发 get 的情况下只有一个能拿到 lease 写入 cache 其他需要等待 cache写入后读取
- 这里可能出现问题时拿到 lease 及进程意外退出, 导致 lease 无法写入, 这里可以通过设置 lease 有效期进行缓解(但是在有效期期间这个 key还是处于失败的状态)
缓存雪崩
- 缓存大规模失效,导致请求大规模打到mysql情况
- 此时mysql宕机,马上重启也会有大量数据导致宕机
产生原因
- redis崩了
- key失效时间设置得都差不多
解决办法
- 失效时间设置好一点(比如加上随机数)
- 熔断保护机制.当流量到达一定的阈值时,就直接返回“系统拥挤”之类的提示,防止过多的请求打在数据库上。至少能保证一部分用户是可以正常使用,其他用户多刷新几次也能得到结果.
缓存击穿
- 某个热点key没了,导致大量请求打到mysql上(mysql存在,redis不存在)
解决办法
- 设置热点key永不过期
缓存穿透
- 大量恶意的不存在的数据请求,导致大量请求持续打到mysql上(mysql不存在,redis不存在)
解决办法
- 其他系统 > 布隆过滤器,一种bitmap,将存在的key或得到结果,如果查询的key和其或结果和原来不一样,说明key不存在
缓存预热
- 在刚启动的缓存系统中,如果缓存中没有任何数据,如果依靠用户请求的方式 重建缓存数据,那么对数据库的压力非常大,而且系统的性能开销也是巨大 的。
mysql索引失效
- 对索引使用左或者左右模糊匹配
- 使用左或者左右模糊匹配的时候,也就是
like %xx
或者like %xx%
这两种方式都会造成索引失效。因为索引相当于前缀匹配 - 使用左模糊匹配(like "%xx")并不一定会走全表扫描,关键还是看数据表中的字段。如果数据库表中的字段只有主键+二级索引,那么即使使用了左模糊匹配,也不会走全表扫描(type=all),而是走全扫描二级索引树(type=index)
- 对索引使用函数或者进行表达式计算
- 如
select * from t_user where id + 1 = 10;
- 对索引隐式类型转换
- 联合索引非最左匹配
- WHERE 子句中的 OR
redis为什么这么快
- Redis 的大部分操作都在内存中完成,并且采用了高效的数据结构,因此 Redis 瓶颈可能是机器的内存或者网络带宽,而并非 CPU,既然 CPU 不是瓶颈,那么自然就采用单线程的解决方案了;
- Redis 采用单线程模型可以避免了多线程之间的竞争,省去了多线程切换带来的时间和性能上的开销,而且也不会导致死锁问题。
- Redis 采用了 I/O 多路复用机制处理大量的客户端 Socket 请求,IO 多路复用机制是指一个线程处理多个 IO 流,就是我们经常听到的 select/epoll 机制。简单来说,在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听 Socket 和已连接 Socket。内核会一直监听这些 Socket 上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。
redis做消息队列的几种方式以及缺点
1. LIST+BRPOP
优点
- 消息下发延迟小
- 消息积压下表现好
- 多个程序BRPOP此时有数据只会通知一个程序
缺点
- 消息ack麻烦,无法确定是否成功处理,无法真正保证必达性
- 不能做广播模式
- 不支持重复消费以及分组消费
2. 发布订阅模型
优点
- 多信道订阅,消费者可以同时订阅多个信道,从而接收多类消息,典型的广播模式,一个消息可以发布到多个消费者
- 消息即时发送,消息不用等待消费者读取,消费者会自动接收到信道发布的消息
缺点
- 消息一旦发布,不能接收。换句话就是发布时若客户端不在线,则消息丢失,不能寻回
- 不能保证每个消费者接收的时间是一致的
- 若消费者客户端出现消息积压,到一定程度,会被强制断开,导致消息意外丢失。通常发生在消息的生产远大于消费速度时 可见,Pub/Sub 模式不适合做消息存储,消息积压类的业务,而是擅长处理广播,即时通讯,即时反馈的业务/
3. Stream流模型
[!tip] 参考 https://www.jianshu.com/p/d32b16f12f09 https://zhuanlan.zhihu.com/p/496944314 https://cloud.tencent.com/developer/article/2331486 https://juejin.cn/post/7094646063784525832 https://www.cnblogs.com/coloz/p/13812840.html
使用命令
结构体
stream
typedef struct stream {
rax *rax; // 是一个 `rax` 的指针,指向一个 Radix Tree,key 存储消息 ID,value 实际上指向一个 listpack 数据结构,存储了多条消息,每条消息的 ID 都大于等于 这个 key 的消息 ID
uint64_t length; // 该 Stream 的消息条数
streamID last_id; // 当前 Stream 最后一条消息的 ID。
streamID first_id; // 当前 Stream 第一条消息的 ID。
streamID max_deleted_entry_id; // 当前 Stream 被删除的最大的消息 ID。
uint64_t entries_added;// 总共有多少条消息添加到 Stream 中,`entries_added = 已删除消息条数 + 未删除消息条数`
rax *cgroups;// rax 指针,也指向一个 Radix Tree ,**记录当前 Stream 的所有 Consume Group**,每个 Consume Group 的名称都是唯一标识,作为 Radix Tree 的 key,Consumer Group 实例作为 value
} stream;
// 结构体,消息 ID 抽象,一共占 128 位,内部维护了毫秒时间戳(字段 ms);一个毫秒内的自增序号(字段 seq),**用于区分同一毫秒内插入多条消息**。
typedef struct streamID {
uint64_t ms;
uint64_t seq;
} streamID;
- stream中使用树和堆 > radix tree实现消息列表而不是list
[!info] 原因
- redis的消息支持根据id删除,因此需要有索引的出现,因此不能单纯用list
- redis的消息支持顺序消费,而且key大量重复,因此不能直接用hash
- 节省空间,而且因为redis默认使用时间戳+顺序编号作为id,公共前缀比较长,可以节省空间
- value的类型是redis实现 > 压缩列表,直接存储的就是消息的id和内容
- listpack的所有key都是增加的,比叶子节点的node大
- 消息的id是(毫秒时间戳-序号)
consumer group
/* Consumer group. */
typedef struct streamCG {
streamID last_id;// **已经获取了,无论是否ack的id**
long long entries_read;
rax *pel;
rax *consumers;// key是消费者name,val是消费者实体
} streamCG;
/* Pending (yet not acknowledged) message in a consumer group. */
typedef struct streamNACK {
mstime_t delivery_time;
uint64_t delivery_count;
streamConsumer *consumer;
} streamNACK;
typedef struct streamConsumer {
mstime_t seen_time;
sds name;
rax *pel;
} streamConsumer;
- 没有ack的消息可能在consumer中的pel和group的pel都记录一次,但是这两个指向的都是同一个streamNACK结构体,因此是共享的
- pel是整个维护必达性的核心结构体,所有没有被ack的数据都会放到这里,保证至少被消费一次
- 消费者和消费者组参考消息队列
Iterator
typedef struct raxStack {读取之后无论是否ack,last_id都会更新
void **stack; /*用于记录路径,该指针可能指向static_items(路径较短时)或者堆空间内存; */
size_t items, maxitems; /* 代表stack指向的空间的已用空间以及最大空间 */
void *static_items[RAX_STACK_STATIC_ITEMS];
int oom; /* 代表当前栈是否出现过内存溢出. */
} raxStack;
typedef struct raxIterator {
int flags; //当前迭代器标志位,目前有3种,RAX_ITER_JUST_SEEKED代表当前迭代器指向的元素是刚刚搜索过的,当需要从迭代器中获取元素时,直接返回当前元素并清空该标志位即可;RAX_ITER_EOF代表当前迭代器已经遍历到rax树的最后一个节点;AX_ITER_SAFE代表当前迭代器为安全迭代器,可以进行写操作。
rax *rt; /* 当前迭代器对应的rax */
unsigned char *key; /*存储了当前迭代器遍历到的key,该指针指向
key_static_string或者从堆中申请的内存。*/
void *data; /* 当前key关联的value值 */
size_t key_len; /* key指向的空间的已用空间 */
size_t key_max; /*key最大空间 */
unsigned char key_static_string[RAX_ITER_STATIC_LEN]; //默认存储空间,当key比较大时,会使用堆空间内存。
raxNode *node; /* 当前key所在的raxNode */
raxStack stack; /* 记录了从根节点到当前节点的路径,用于raxNode的向上遍历。*/
raxNodeCallback node_cb; /* 为节点的回调函数,通常为空*/
} raxIterator;
- 使用还是通过栈+中序遍历节点的方式寻找下一个
整体流程
写入
- 先创建一个stream,创建raxio tree
- 根据last_id,生成要插入的新的ID,找到最大的tree的节点(最右边的节点)
- 判断listpack是否还能插入,能插入能插入
- 不能就根据key创建一个新的listpack
读取
- 所有的读取行为以group的last_id进行读取
- 读取之后无论是否ack,last_id都会更新
- 没有ack的消息都会被扔到pel中和消费者的pel中,被分配给消费者的消息不会再给其他消费者,也只能特定的消费者来ack
[!info] 重发时机
- 检测到消费者有断线的情况
- 消息过期,这部分xadd指定时间
redis如何实现延迟队列
- 使用 zset这个命令,用设置好的时间戳作为score进行排序,使用
zadd score1 value1 ....
命令就可以一直往内存中生产消息。再利用 zrangebysocre 查询符合条件的所有待处理的任务,通过循环执行队列任务即可。也可以通过zrangebyscore key min max withscores limit 0 1
查询最早的一条任务,来进行消费
如何避免SQL注入进攻
- 预处理:采用预编译语句集
redis和memcached优劣势
- redis支持的数据结构比memcached更加丰富,mem只支持字符串和数字类型
- mem不支持持久化存储,redis支持持久化存储
- mem占用的内存小,redis更多,mem不支持主从复制,mem使用更加简单
微信拿到红包分配
分配算法
- 随机,额度在0.01和剩余平均值*2之间.例如:发100块钱,总共10个红包,那么平均值是10块钱一个,那么发出来的红包的额度在0.01元~20元之间波动。
- 领取红包后继续使用同样的方法计算
幂等性处理
mysql的private key
- 让业务方生成唯一ID,这个ID作为mysql唯一约束,如果插入失败则说明已经处理
mysql乐观锁version
- 先将唯一id插入,下一个请求来的时候查询是否已经插入,如果发现已经插入这说明已经处理(这个不需要使用锁,但是需要在业务层对version进行判断),tira-pay使用的方式就是这个
Redis的setNX
- 将ID拼接一些信息插入redis中,设置3分钟缓存,无法插入则说明已经处理(tira-im使用的方法)
列数据库
特点
- 读多于写
- 大宽表,读大量行但是少量列,结果集较小
- 数据批量写入,且数据不更新或少更新
- 无需事务,数据一致性要求低
- 灵活多变,不适合预先建模
优点
- 同一列中的数据属于同一类型,压缩效果显著。列存往往有着高达十倍甚至更高的压缩比,节省了大量的存储空间,降低了存储成本。
- 更高的压缩比意味着更小的data size,从磁盘中读取相应数据耗时更短
- 更加适合于大量数据的读取分析
缺点
- 一行的全量查询慢(例如查需一个人的所有信息)
- 基本不支持ACID事务
应用场景
hbase 就是列数据库
- 类似数据分析(批量分析某个值的特征),大规模日志存储,大规模打点监控(压缩特性,并且可以接受秒级延迟)用的比较多
- 基本上是数仓和日志在用,通常是 hbase 配合 hadoop(基于mapreduce 的分布式系统基础架构,也是数仓的主要构成)
innodb和myisam的区别
- InnoDB支持事务,MyISAM不支持
- InnoDB支持表、行(默认)级锁,而MyISAM支持表级锁
- MyISAM 中 B+ 树的数据结构存储的内容是实际数据的地址值,它的索引和实际数据是分开的,只不过使用索引指向了实际数据。这种索引的模式被称为非聚集索引。InnoDB 中 B+ 树的数据结构中存储的都是实际的数据,这种索引有被称为聚集索引。
- myisam比较快,但是功能比较少
redis缓存的缺点(微信支付不用)
- 数据的一致性的问题,本质类似CAP,只能保证最终一致性,或者放弃可用性,
- 会有一种错误的安全感,高并发下会出现缓存失效的情况
使用缓存时容易有一种「虚假」的安全感,因为缓存的存在,会认为服务端性能能抗住热点时的请求,所以当缓存失败,峰值又上来之后,很快就把服务打挂了。因此,微信支付内部在做性能测试时,都需要先把缓存关掉。 即使是使用缓存,也只会使用单机的缓存,如同机部署的memcached,因为使用分布式的缓存,有多个写入来源的话,一旦缓存被写坏,排查起来会非常麻烦,因为根本不知道是在哪里被写坏的。
- 掉电丢失记录,可能出现数据的丢失
慢sql
特征
- 数据库CPU负载高。一般是查询语句中有很多计算逻辑,导致数据库cpu负载。
- IO负载高导致服务器卡住。这个一般和全表查询没索引有关系。
- 查询语句正常,索引正常但是还是慢。如果表面上索引正常,但是查询慢,需要看看是否索引没有生效。
查看
- 使用explain可以查看一个语句是否使用了索引
- 打开慢日志查询(会记录每一条执行时间长的sql),一般是由于没有索引,或者索引失效,或者数据量太大造成的
关系型和非关系型数据库区别
- 关系模型可以简单理解为二维表格模型,而一个关系型数据库就是由二维表及其之间的关系组成的一个数据组织。
- 非关系型数据库又被称为 NoSQL(Not Only SQL ),意为不仅仅是 SQL。通常指数据以对象的形式存储在数据库中,而对象之间的关系通过每个对象自身的属性来决定,常用于存储非结构化的数据。
- 参考
拉取数据时候根据时间戳而不是id
- 现在基本上的拉取方式是 offset+limit,实际上mysql会将所有的数据拉取出来,然后排序好在偏移到offset拿出limit,因此数据量会非常大
- 但是如果是通过时间戳拉取的话,只会根据时间戳的范围进行拉取而不会全量拉取.因此业界通常的做法是通过 begin_time, end_time 拉取,而不是用limit
关系型数据库优缺点
- 采用二维表结构非常贴近正常开发逻辑(关系型数据模型相对层次型数据模型和网状型数据模型等其他模型来说更容易理解);
- 支持通用的SQL(结构化查询语言)语句;
- 丰富的完整性大大减少了数据冗余和数据不一致的问题。并且全部由表结构组成,文件格式一致;
- 可以用SQL句子多个表之间做非常繁杂的查询;
- 关系型数据库提供对事务的支持,能保证系统中事务的正确执行,同时提供事务的恢复、回滚、并发控制和死锁问题的解决。
- 海量数据情况下读写效率低:对大数据量的表进行读写操作时,需要等待较长的时间等待响应。
- 可扩展性不足:不像web server和app server那样简单的添加硬件和服务节点来拓展性能和负荷工作能力。
- 数据模型灵活度低:关系型数据库的数据模型定义严格,无法快速容纳新的数据类型(需要提前知道需要存储什么样类型的数据)。(比如巨大的表格增加字段)
非关系型数据库的优缺点
- 非关系型数据库存储数据的格式可以是 key-value 形式、文档形式、图片形式等。使用灵活,应用场景广泛,而关系型数据库则只支持基础类型。
- 速度快,效率高。 NoSQL 可以使用硬盘或者随机存储器作为载体,而关系型数据库只能使用硬盘。
- 非关系型数据库具有扩展简单、高并发、高稳定性、成本低廉的优势。
- 可以实现数据的分布式处理。
- 非关系型数据库暂时不提供 SQL 支持,学习和使用成本较高。
- 非关系数据库没有事务处理,无法保证数据的完整性和安全性。适合处理海量数据,但是不一定安全。
- 功能没有关系型数据库完善。
- 复杂表关联查询不容易实现。
为什么mysql2000万行数据之后性能急剧下降
- mysql中索引B+树的高度大约是3,在2000w行数据内,并且页的大小是和磁盘的格子大小相关的
- 2000万行一下时候,基本上都是3层索引(大概是3层索引时候底层数据的极限容纳值),使得查找的效率相差不大
- 超过2000w行之后,B+需要变高,变高导致最底层的8页变成16页,多出16个页面,页面直接从磁盘中读取,导致io次数变多,性能变差,参考
查询的时候limit和offset有什么缺陷
- 数据库实际上是查找了offset+limit条,然后返回最后的limit条,当出现offset非常大的时候(特别是下滑刷新的时候),导致没吃查询的速度变得很慢
- 因此toC的一般使用时间戳的方式select,类似序号的形式,只是序号使用时间戳
mysql 在数据量非常大情况下如何统计行数
- 使用
select table_name,table_rows from information_schema.tables where TABLE_SCHEMA='effect_user_busi' and table_name='user_favorite' LIMIT 1;
可以查到大概的数量级(有少量误差)
如何提高秒杀中数据库瓶颈
- 热点行事务想收集多行,然后只需要加锁一次一次性运行多个事务语句直接处理
- 参考 什么是热点行性能优化_云原生数据库 PolarDB(PolarDB)-阿里云帮助中心
如何选择数据库
- 如果出现ACID的需求支持事务性,类似支付这种业务,只能选择mysql,如果流量大或者分布式就用消息队列 和分布式事务,具体参考 支付系统
- 如果出现强一致性和分布式强需求,但是对事物性要求不高,类似 服务注册发现中心,就用etcd,参考 微服务框架 > coa
- 如果需要数据高速读写,但是不需要持久化存储(比如一些临时的排行榜),直接上redis redis实现
- 如果需要数据高速读写,需要持久化存储,但是对数据的一致性要求没这么高的,用redis+mysql 具体参考 数据库总结 > redis和mysql数据一致性问题
- 这样的缺点就是 需要解决一致性问题,以及还有穿透雪崩风险,错误概率大,成本高
- 而且redis一旦出问题挂了,mysql也会马上因为流量太大挂掉
- 如果需要数据较高速读写,需要持久化存储,对数据的一致性要求高的,用leveldb,参考LevelDB底层
- 如果需要灵活的数据结构和更舒适的开发体验,对事务没有强需求,小型项目,数据频繁变化,存储维度多样(有子结构和数组这种),类似文档型数据,用mongodb Mongodb基础
- 如果需要数据高速读写,需要强持久化存储(数据完全不丢失),对数据的一致性要求高的. 那基本是不可能的
- 高速读写意味着只能站在缓存进行,持久化只能在存储进行,一致性高只能在同一个系统进行(不同进程会出现cap问题),导致这三者必须牺牲掉一个或者减少一个去平衡其他两个,leveldb就是其中佼佼者
- 如果数据量非常大,重复率高,需要进行大量存储和离线数据分析(类似用户数据大表的备份,程序日志搜索等),对实时性要求不高(能接受秒级延迟),用列数据库 数据库总结 > 列数据库
- 出现用户大量的关系需要存储,类似关注,特别是共同关注这种需求,业务查询的时候通常以用户维度查询而不会用范围查询(数据分析的需求通常倒入列数据库离线分析),要求高性能查询扩展关系查询,通常用图数据库 Neo4j底层原理
- 如果是关键词搜索,通常使用ES这种倒排索引的数据库 zincsearch底层实现
Mysql Redis DRC 同步
- DRC (Data Replicate Center) 用于数据库的同步数据
- 使用类似从库复制的手法解决 , 同机房有 sync-out 模拟从库消费主库的 binlog , 发送到对机房的 sync-in (当然中间会放 mq) , sync-in 同步binlog到当地的机房
- 如果出现冲突遵循最后更新原则(即按照时间判断这个更新是否生效), 但是核心还是需要避免冲突, 根据用户 did划分机房(不用uid 是处理没有登陆的情况下)
- https://juejin.cn/post/6964531365643550751
- https://mp.weixin.qq.com/s/bWofuM5eS2Q8ylF-4AD0kA
- https://zhuanlan.zhihu.com/p/346651831
- https://xiaolincoding.com/mysql/index/index_interview.html#%E4%BB%80%E4%B9%88%E6%98%AF%E7%B4%A2%E5%BC%95
- https://mp.weixin.qq.com/s?__biz=MzUxODAzNDg4NQ==&mid=2247503394&idx=1&sn=6e5b7b2c9bd9002a4b2dfa69273069b3&chksm=f98d8a88cefa039e726f1196ba14210ddbe49b5fcbb6da620778a7497fa25404433ef0b76268&scene=21#wechat_redirect
- https://mp.weixin.qq.com/s?__biz=MzU3ODA4NTc2Ng==&mid=2247484078&idx=1&sn=87a62dbe2d4ca7e56f2e0319465147a3&chksm=fd7bf447ca0c7d51dbbce887ce37ea7088d508b8766e09892706225a49fad06ad3716d48e0e0&token=2107017276&lang=zh_CN#rd
具体流程
- 核心是通过一层缓存直接隔绝大量无效请求,然后将请求直接串行化,最后实时计算领取到的金额,异步到账实现的
-
发送红包时候分布式事务,主事务先扣钱,从事务发消息+插入红包数据库中一条消息(含有剩余金额和剩余人数)
-
发出后,红包的剩余领取人数将缓存到redis(或者memcache)中
-
抢红包环节,通过redis原子减操作(这部分使用版本乐观锁的机制保证,但是这里涉及可能柔性处理,如果出现冲突可以放行,毕竟这里不涉及真正的金钱,这也是导致进入领红包界面最后没领到的原因),这个部分隔绝了大部分无效的请求,削峰的作用,过了这部分的请求会被扔到队列中串行消费
这部分如果自己上mysql事务锁,大量的请求会因为锁失败,因为高并发的场景下一大堆冲突,如果是在内存中计算会出现程序故障就G了
- 如果拆红包采用乐观锁,那么在并发抢到相同版本号的拆红包请求中,只有一个能拆红包成功,其他的请求将事务回滚并返回失败,给用户报错,用户体验完全不可接受。2. 如果采用乐观锁,将会导致第一时间同时拆红包的用户有一部分直接返回失败,反而那些“手慢”的用户,有可能因为并发减小后拆红包成功,这会带来用户体验上的负面影响。3. 如果采用乐观锁的方式,会带来大数量的无效更新请求、事务回滚,给 DB 造成不必要的额外压力。 总思路是设置多层过滤网,层层筛选,层层减少流量和压力。这个设计最初是因为抢操作是业务层,拆是入账操作,一个操作太重了,而且中断率高。 从接口层面看,第一个接口纯缓存操作,搞压能力强,一个简单查询Cache挡住了绝大部分用户,做了第一道筛选,所以大部分人会看到已经抢完了的提示。 这部分如果缓存失败直接降级成为db操作
-
拆红包环节,通过mysql的事物进行处理,因为请求串行化,基本无锁(但是还是需要直接开启悲观锁),写之后再异步写入cache中领取详情
- 开启主事务
- 查询红包中真正的剩余金额和剩余人数,如果等于0直接返回
- 计算此次请求需要拿的红包金额,这部分直接在内存中计算(实时效率更高,预算才效率低下。预算还要占额外存储)
- 更新数据库记录,余额和人数都减少
- 插入红包流水,用于对账
- 提交主事务
-
到账环节,上面成功之后通过事务中心异步打钱
- 从事务开启
- 插入流水ID
- 钱包加钱
- 提交从事务
其他设计
- 一致性hash将不同的红包请求负载均衡到不同的机器上,同一个红包请求到一台server上
- 数据库分表添加天的维度,用于数据的冷热分离,加上ID,施行双维度分表,而求红包记录储存时间有限,超过时间直接删除
具体来说,就是分库表规则像 db_xx.t_y_dd 设计,其中,xx/y 是红包 ID 的 hash 值后三位,dd 的取值范围在 01~31,代表一个月天数最多 31 天。
对账系统
- 审计的公式:
- 单条流水期初余额 + 交易发生金额 = 期末余额。
- 本条流水期初余额 = 上条流水的期末余额。
- 本条流水的余额版本号 =上条流水的余额版本号 + 1。
实时对账和离线对账
- 实时对账就是一边运行一边对账
- 离线对账是按照天或者小时进行对账
- 单系统核验:核验自己的流水和结果明细是否对的上
- 不同系统对账:核验不同系统的账单是否能对的上(比如扣钱和加钱,有借必有贷,借贷必相等)
优缺点
- 离线对账有零点问题(按照时间对账,时间边缘账单无法对齐),离线对账触发的时候占用大量cpu,一次性处理大量数据,对账问题有延迟,出现性能高峰,因此最好用于单系统本地对账,并且设置对账时间在深夜
- 实时对账系统一直的占用都是类似的,一般不存在性能高峰,但是对账系统会一致占用系统资源,因为不同析用时间可能有偏差,不能用按时间段对账的方法,因此适合不同系统对账,设置一个中心化的分布式实时对账系统,不影响业务的运行
实时对账系统的设计
- 业务写操作之后,启动协程将日志写入mq
- 对账系统拿出记录之后写入数据库,数据库两种表,一种记录单号以及到来的个数,一种记录具体
- 等到某一条单号的记录到来个数到达全部之后(这部分可以设置最长期限,超过进行问询操作),直接开始进行对账,如果对上了直接删除有关这个单号的所有记录,对不上直接报警
离线对账系统的设计
- 设置默认触发时间(比如深夜),开始堆数据库进行查询对账
- 对账方法就是账户明细跟着流水走,康康是否发生跳变
- 零点问题看容忍度,容忍度高的话,零点附近的数据不平认为是零点问题忽略,低容忍度的话,取冗余数据(多取5分钟)对账,如果还是不平就要人工处理
电商秒杀
绝大部分流程和微信红包系统类似
- 通过前置redis的原子减进行流量的初筛
- 然后扔到消息队列里面消费
- mysql进行开启事物悲观锁
- 扣减库存数量
- 添加订单信息(这部分如果不是同一个数据库涉及分布式事务或者mysql进行了热点行的优化就扔到消息队列异步化)
- 返回成功,订单状态为待付款
出现大量库存提速
- 秒杀的速度瓶颈在于mysql的事务,对热点行的频繁上锁 数据库总结 > 如何提高秒杀中数据库瓶颈
- 还有的就是大量流量下p99延迟急剧增加,这种情况下需要进行并发限流 限流算法 > 并发限流