创建和更新订单时,如何保证数据准确无误?

如何避免重复下单?

幂等性

发号器,先获得唯一订单号,通过订单号唯一键,来做限制

还有一点需要注意的是,如果是因为重复订单导致插入订单表失败,订单服务不要把这个错误 返回给前端页面。否则,就有可能出现这样的情况:用户点击创建订单按钮后,页面提示创建 订单失败,而实际上订单却创建成功了。正确的做法是,遇到这种情况,订单服务直接返回订 单创建成功就可以

如何解决 ABA 问题?

增加版本号,更新的时候带着版本号要

订单系统各种更新订单的服务一样也要具备幂等性。

什么是 ABA 问题呢?我举个例子你就明白了。比如说,订单支付之后,小二要发货,发货完 成后要填个快递单号。假设说,小二填了一个单号 666,刚填完,发现填错了,赶紧再修改成 888。对订单服务来说,这就是 2 个更新订单的请求

ABA 问题怎么解决?这里给你提供一个比较通用的解决方法。给你的订单主表增加一列,列 名可以叫 version,也即是“版本号”的意思。每次查询订单的时候,版本号需要随着订单数 据返回给页面。页面在更新数据的请求中,需要把这个版本号作为更新请求的参数,再带回给 订单更新服务。

1UPDATE orders set tracking_number = 666, version = version + 1 WHERE version = 8 and order_sn =xxx

流量大、数据多的商品详情页系统该如何设计?

基本信息,固定的,用数据库加缓存系统搞定

对于商品参数信息,数据量大、数据结构不统一,使用使用 MongoDB 保存商品参数。

图片和视频只保存url或id

详情静态化,存储到cdn,局部ajax技术

购物车系统,应该如何设计?

购物车是写多读少的场景。

未登录,加购物车,本地记录,登录后,要合并购物车,退出则客户端购物车清空,已经合并到了服务器上。

如何设计“暂存购物车”的存储?

客户端的存储可以选择的不太多:Session、Cookie 和 LocalStorage,其中浏览器的 LocalStorage 和 App 的本地存储是类似的。

存在哪儿最合适?SESSION 是不太合适的,原因是,SESSION 的保留时间短,而且SESSION 的数据实际上还是保存在服务端的。

客户端和服务端的每次 交互,都会自动带着 Cookie 数据往返,这样服务端可以读写客户端 Cookie 中的数据,而LocalStorage 里的数据,只能由客户端来访问。

cookie优点:实现简单,但是只能存储4kb,数据量较少。

LocalStorage 存储,实现相对就复杂一点儿,客户端和服务端都要实现一些业务逻辑。

小型系统用cookie,大型需求加很多商品的,则用LocalStorage。

如何设计“用户购物车”的存储?

redis or mysql 。

redis 用hash 速度快,但是redis可能是会丢数据的,是异步刷盘,但是购物车数据又没那么重要。redis 不太好统计。

mysql 查询方便,支持事务,统计也够方便。

总之更推荐用mysql 。

因为购物车写多读少,这样玩会频繁失效缓存,进而导致大部分读都要击穿到db并多做一步缓存的操作。实则弊大于利。

账户余额总是对不上账,怎么办?

使用事务解决。

账户余额和账户日志要对的上。

可以采用乐观锁的方式,这样无论是rc,还是rr级别都是能一致的。

  1. 我们给账户余额表增加一个 log_id 属性,记录最后一笔交易的流水号。
  2. 首先开启事务,查询并记录当前账户的余额和最后一笔交易的流水号。
  3. 然后写入流水记录。
  4. 再更新账户余额,需要在更新语句的 WHERE 条件中限定,只有流水号等于之前查询出的 流水号时才更新。
  5. 然后检查更新余额的返回值,如果更新成功就提交事务,否则回滚事务。
1 begin;
2select balance, log_id from account_balance where user_id = 0;
3insert into account_log values -> (NULL, 100, NOW(), 1, 1001, NULL, 0, NULL, 0, 0);
4-- 更新余额,注意where条件中,限定了只有流水号等于之前查询出的流水号3时才更新
5
6update account_balance -> set balance = balance + 100, log_id = LAST_INSERT_ID(), timestamp = NOW() -> where user_id = 0 and log_id = 3;
7
8-- 这里需要检查更新结果,只有更新余额成功(Changed: 1)才提交事务,否则回滚事务
9commit or rollback

分布式事务 如何保证多个系统间的数据是一致的?

订单和优惠券,订单系统,促销系统 2个系统 怎么保证数据一致性

分布式事务的解决方案有很多,比如:2PC、3PC、TCC、Saga 和本地消息表等等

2PC:订单与优惠券的数据一致性问题

在我们购物下单时,如果 使用了优惠券,订单系统和优惠券系统都要更新自己的数据,才能完成“在订单中使用优惠券”这个操作。

订单系统需要:

  1. 在“订单优惠券表”中写入订单关联的优惠券数据;
  2. 在“订单表”中写入订单数据。

这种是在同一个系统中,我们只需要用事务保证一致性即可。

促销系统需要操作就比较简单,把刚刚使用的那张优惠券的状态更新成“已使用”就可以了。

我们需要这两个系统的数据更新操作保持一致,要么都更新成功,要么都更新失败

2PC 引入了一个事务协调者的角色,来协调订单系统和促销系统,协调者对客户端提供一个完整的“使用优惠券下单”的服务,在这个服务的内部,协调者再分别调用订单和促销的相应服务。

异常情况下怎么办?

如果准备阶段成功,进入提交阶段,这个时候就“只有华山一条路”,整个分布式事务只能成功,不能失败。

如果发生网络传输失败的情况,需要反复重试,直到提交成功为止。如果这个阶段发生宕机, 包括两个数据库宕机或者订单服务、促销服务所在的节点宕机,还是有可能出现订单库完成了 提交,但促销库因为宕机自动回滚,导致数据不一致的情况。但是,因为提交的过程非常简 单,执行很快,出现这种情况的概率非常小,所以,从实用的角度来说,2PC 这种分布式事务 的方法,实际的数据一致性还是非常好的。

在实现 2PC 的时候,没必要单独启动一个事务协调服务,这个协调服务的工作最好和订单服务或者优惠券服务放在同一个进程里面,这样做有两个好处: 参与分布式事务的进程更少,故障点也就更少,稳定性更好; 减少了一些远程调用,性能也更好一些

2PC 也有很明显的缺陷,整个事务的执行过程需要阻塞服务端的线程和数据库的会话,所以, 2PC 在并发场景下的性能不会很高。并且,协调者是一个单点,一旦过程中协调者宕机,就会 导致订单库或者促销库的事务会话一直卡在等待提交阶段,直到事务超时自动回滚。

只有在需要强一致、并且并发量不大的场景下,才考虑使用 2PC。

本地消息表:订单与购物车的数据一致性问题

结算进入订单页面:

第一,订单系统需要创建一个新订单,订单关联的商品就是购物车中选择的那些商品。

第二,创建订单成功后,购物车系统需要把订单中的这些商品从购物车里删掉。

这也是一个分布式事务问题,创建订单和清空购物车这两个数据更新操作需要保证,要么都成 功,要么都失败。但是,清空购物车这个操作,它对一致性要求就没有扣减优惠券那么高,订 单创建成功后,晚几秒钟再清空购物车,完全是可以接受的。只要保证经过一个小的延迟时间 后,最终订单数据和购物车数据保持一致就可以了

本地消息表非常适合解决这种分布式最终一致性的问题。

本地消息表的实现思路是这样的,订单服务在收到下单请求后,正常使用订单库的事务去更新 订单的数据,并且,在执行这个数据库事务过程中,在本地记录一条消息。这个消息就是一个 日志,内容就是“清空购物车”这个操作。因为这个日志是记录在本地的,这里面没有分布式 的问题,那这就是一个普通的单机事务,那我们就可以让订单库的事务,来保证记录本地消息 和订单库的一致性。完成这一步之后,就可以给客户端返回成功响应了。

就是订单系统中 记录一条这个订单需要清空购物车的标记, 然后用异步服务去消费这个本地消息表,完成则标记记录为已完成清空购物车。

如果操作失败了,可以通过重试来解决。最终,可以保证订单系统和购物车系统它们的数据是一致的。

即使能接受数据最终一致,本地消息表也不是什么场景都可以适用的。它有一个前提条 件就是,异步执行的那部分操作,不能有依赖的资源。比如说,我们下单的时候,除了要清空 购物车以外,还需要锁定库存。

库存系统锁定库存这个操作,虽然可以接受数据最终一致,但是,锁定库存这个操作是有一个 前提的,这个前提是:库存中得有货。这种情况就不适合使用本地消息表,不然就会出现用户 下单成功后,系统的异步任务去锁定库存的时候,因为缺货导致锁定失败。

本地消息表,异步消费(幂等保证),最终一致。

MySQL HA:如何将“删库跑路”的损失降到最低?

如何更安全地做数据备份和恢复?

mysqldump + binlog

1show variables like '%log_bin%';
2show master status;
1# 数据恢复到当天的15点
2$mysqlbinlog --start-datetime "2020-02-20 00:00:00" --stop-datetime "2020-02-20 15:09:00" /usr/local/var/mysql/binlog.000001 | mysql -uroot

配置MySQL HA实现高可用

访问数据库超时

慢sql,缓存

根据 CPU 利用率曲线的规律变化,推断大概是什么场景,或任务, 时间点出了任务。

服务降级。

怎么能避免写出慢SQL?

预估sql遍历的行

应对高并发: 使用缓存保护MySQL

Read/Write Through 并发有问题

Cache Aside 模式 和上面的 Read/Write Through 模式非常像,它们处理读请求的逻辑是完全一样的,唯一的一个小差别就是,Cache Aside 模式在更新数据的时候,并不去尝试更新缓存,而是去删除缓存。

写请求-》更新数据库-》更新成功-》删除缓存-》返回更新成功

构建缓存数据需要的查询时间太长,或者并发量特别大的时候,Cache Aside 或 者是 Read/Write Through 这两种缓存模式都可能出现大量缓存穿透。

可以牺牲缓存的时效性和利用率,缓存所有的数据,放弃 Read Through 策略所有的请求,只读缓存不读数据库,用后台线程来定时更新缓存数据。(曾经在的一家公司,商品数据就是这么干的)

一些好的方案思路:

  • 数据加版本号,写库时自动增一。更新缓存时,只允许高版本数据覆盖低版本数据。

对于Cache aside和read/write through而带来的数据不一致问题,工作中是这样解决:

  • a写线程,b读线程:
  • b线程:读缓存->未命中->上写锁>从db读数据到缓存->释放锁;
  • a线程:上写锁->写db->删除缓存/改缓存->释放锁;
  • 这样来保证a,b线程并发读写缓存带来的脏数据问题;

MySQL如何应对高并发(二):读写分离

  1. 纯手工方式:修改应用程序的 DAO 层代码,定义读写两个数据源,指定每一个数据库请求 的数据源。
  2. 组件方式:也可以使用像 Sharding-JDBC 这种集成在应用中的第三方组件来实现,这些组 件集成在你的应用程序内,代理应用程序的所有数据库请求,自动把请求路由到对应数据库 实例上。
  3. 代理方式:在应用程序和数据库实例之间部署一组数据库代理实例,比如说 Atlas 或者 MaxScale。对应用程序来说,数据库代理把自己伪装成一个单节点的 MySQL 实例,应用 程序的所有数据库请求被发送给代理,代理分离读写请求,然后转发给对应的数据库实例。

推荐第二种

应用程序是一个逻辑非常简单的微服务,简单到只有几个 SQL, 或者是,你的应用程序使用的编程语言没有合适的读写分离组件,那你也可以考虑使用第一种 纯手工的方式来实现读写分离。

代理方式有一个好处是,它对应用程序是完全透明的。所以,只有在不方便修改应用程 序代码这一种情况下,你才需要采用代理方式

主从同步带来延迟等问题, 可以通过应用业务改变方式去实现。

开启事务,是会走主库的

部署了多个从库,推荐用“HAProxy+Keepalived”来做这些从库的负载均衡和高可用,这个方案的好处是简单稳定而且足够灵活,不需要增加额外的服务器部署,便于维护并且不增加故障点。

MySQL主从数据库同步是如何实现的?

主库需要提交事务、更新存储引擎中的数据、把 Binlog 写到磁盘上、给客户端返回响应、把 Binlog 复制到所有从库上、每个从库需要把复制过来的 Binlog 写到暂存日志中、回放这个 Binlog、更新存储引擎中的数据、给主库返回复制成功的响应。

binglog 和 事务提交,先复制binglog再提交,数据肯定一致。

先提交事务后复制binglog, 性能高,但是数据可能存在不一致。

MySQL 主库在收到客户端提交事务的请求之后,会先写入 Binlog,然后再提交事务,更新存 储引擎中的数据,事务提交完成后,给客户端返回操作成功的响应。同时,从库会有一个专门 的复制线程,从主库接收 Binlog,然后把 Binlog 写到一个中继日志里面,再给主库返回复制成功的响应。

从库还有另外一个回放 Binlog 的线程,去读中继日志,然后回放 Binlog 更新存储引擎中的数据。

异步复制它没有办法保证数据能第一时间复制到从库上。

同步复制 :等待数据都复制到所有从库,才会进行响应。

mysql5.7增加半同步,只要复制设定个数到从库,即可返回客户端。

比如说,一主二从的集群,配置成半同步复制,只要数据成功复制到任意一个从库上,主库的事务线程就直接返回了。

配置半同步复制的时候,有一个重要的参数“rpl_semi_sync_master_wait_slave_count”, 含义是:“至少等待数据复制到几个从节点再返回”。这个数量配置的越大,丢数据的风险越 小,但是集群的性能和可用性就越差。最大可以配置成和从节点的数量一样,这样就变成了同步复制。

一般情况下,配成默认值 1 也就够了,这样性能损失最小,可用性也很高,只要还有一个从库活着,就不影响主库读写。丢数据的风险也不大,只有在恰好主库和那个有最新数据的从库一起坏掉的情况下,才有可能丢数据。

另外一个重要的参数是“rpl_semi_sync_master_wait_point”,这个参数控制主库执行事务 的线程,是在提交事务之前(AFTER_SYNC)等待复制,还是在提交事务之后 (AFTER_COMMIT)等待复制。默认是 AFTER_SYNC,也就是先等待复制,再提交事务, 这样完全不会丢数据。AFTER_COMMIT 具有更好的性能,不会长时间锁表,但还是存在宕机 丢数据的风险

复制状态机:所有分布式存储都是这么复制数据的

是全量备份和 Binlog: “快照 + 操作日志”的方法

订单数据越来越多,数据库越来越慢该怎么办

历史归档

1insert into orders_temp 7 select * from orders 8 where timestamp >= SUBDATE(CURDATE(),INTERVAL 3 month);

表改名等

OPTIMIZE TABLE 释放存储空间。对于 InnoDB 来说,执行 OPTIMIZE TABLE 实际上就是把这个表重建一遍,执行过程中会一直锁表

分库分表

能不拆就不拆,能少拆不多拆。原因也很简单,你把数据拆分得越散,开发和维护起来就越麻烦,系统出问题的概率就越大

数据量大,就分表;并发高,就分库。

分片的方法:查表法