论 大并发 下的 乐观锁定 Redis锁定 和 新时代事务
在 《企业应用架构模式》 中 提到了 乐观锁定,
用 时间戳 来 判定 交易 是否有效, 避免 传统事务 的 表锁定 造成 的 瓶颈 。
在 现在的 大并发 的 大环境下, 传统事务 及其 表锁定 以及 事务带来 的 性能消耗, 确实 不能适应 当今 的 大并发 的 场景 了 。
感觉 传统事务 也就只能用在 办公系统 了, 哈哈哈哈 。
但是 传统事务 的 表锁定 是 合理的, 表锁定 使得 事务中 其它 线程 不能 读写 表 。
不能 写, 这个容易理解, 不能 读 是怎么回事 ?
因为 读取表的结果 会 作为 系统 决策行为 的 依据, 所以 也不能 读 。
比如, 一个商品已经卖出去了, 就不能再卖给其它用户 。
那么, 用 乐观锁定 能解决这些问题吗 ?
乐观锁定 的 提出 是 很有意义的,
但是不太靠得住, 为什么呢 ?
时间戳 通常 取到 毫秒, 但 如果 并发密度 超过了 毫秒级, 达到了 比如 微秒级, 那 ?
当然 理论上 可以随机的取 其中 一个用户 作为 成功者, 其他用户作为 失败者(牺牲者?) 。
不过 理论上, 这还不是一种 完备 的 做法 。
我们再来看看 Redis 锁定,
Redis 提供了 对 数据(对象) 的 锁(Lock), 以及 队列 Queue 等 数据类型, 以及 对 Queue 的 Block 操作 。
我们可以利用 Redis 来锁定 某个 ID 的 业务实体 (某个 订单号 的 订单), 然后 对 这个 业务实体(订单) 进行 相关的操作(事务 / 交易),
这样 达到 多个 线程(用户) 对 同一个 订单 的 操作(交易) 顺序进行 。
这种做法 的 实质 是 行锁定 , 那为什么不通过 数据库 来实现 行锁定 呢 ?
因为 我 不知道 数据库 行锁定 的 语法,,,,,,
其次, 数据库 行锁定 的 实现 会 更加 复杂 和 重量,
Redis 的 对象锁 则 简单 而 轻量 。
事实上, 如果 把 上述过程 简化 下来,
我们 只是 需要 有一个 “Flag” 在 Redis 里 可以 供 多台 Server 的 线程间 同步协作 即可 。
严格的讲, 在 负载均衡 (集群), 即 多台 Server 的 情形下 才需要使用 Redis (分布式缓存) ,
如果 是 单台 Server , 则 使用 进程内 的 变量 来 作为 “Flag” Lock 即可 。
此时, 情况 则 退化为 进程内 多线程 之间的 同步协作 。
同时, 严格的讲, Redis 只需要提供一个 “Flag”, 或者说, Redis 只需要提供一个 锁机制 ,
并不需要 将 具体的 业务数据(对象) 保存 到 Redis,
进行 一笔交易 时, 不需要 到 Redis 查找 业务对象(比如 订单), 如果查不到再到 数据库 查, 更新时 先更新 Redis, 再更新 数据库 ,
没有必要这样 。
Redis 只要提供 “Flag” Lock 来 确保 顺序进入(同步协作), 具体的操作直接 读写 数据库 即可 。
所以, Redis 的 真正意义 在于 共享内存, 而不是 数据缓存 。
可以看看我昨天写的 《论 业务系统 架构 的 简化 (二) 用 关系数据库 作 缓存》 https://www.cnblogs.com/KSongKing/p/9928412.html
除了 锁, 事务 还有 另一方面, 数据完整性 。
比如, 更新 A, B, C 三张表, A, B 成功, C 失败, 于是事务会回滚, A, B 恢复原来的数据 。
要实现 数据完整性, 需要 表锁定 和 事务日志(这会带来 性能消耗),
可见,
数据完整性 同样也会成为 大并发 的 瓶颈 。
所以,我们这里 提出 一个 “乐观事务” 概念,
即 对于 每次交易, 都是 Insert , 而不会 反复 的 去 Update 。
假如一个交易 要 更新 3 个表, A 表 为 主表, B, C 表通过 A 表 的 ID 关联,
那么, 这个交易 对 A,B,C 3 个表都是 Insert 操作, 在 最后 事务成功 时 将 A 表里的 “生效” 栏位 更新 为 “Y”,
以此 表示 事务成功 。
显然, 这种做法 并不是对 所有场合 都适用, 它会让一些 小场景 变得麻烦 。
但是, 对于 前端 海量 用户 海量 并发 的 场景, 可以使用 这种做法 。
P : 我们大概可以把 每秒 100万 ~ 1000万 的 交易量 称为 “海量”, 把 每秒 1000万 以上 的 交易量 称为 “天量” 。
Redis 锁 + 乐观事务 = 新时代事务
我们来看一下 新时代事务 处理 3 个场景 :
1 订单(交易)
2 秒杀
3 商品库存计数
1 订单(交易),
用 Redis 锁 来 锁定, 用 乐观事务 执行 更新数据, 具体的 大家 自己想象 吧
2 秒杀
用 Redis 存一个 对象 记录 被 哪个 用户秒杀, 同时 加上 Redis 锁, 这样 用户 就可以 顺序 的 获取这个对象,
第一个获得这个对象的用户 就 秒杀 成功, 并更新这个 对象 的 状态 表示 秒杀成功 。
3 商品库存计数
同 Redis 存一个 对象 记录 库存量, 用户将 商品 放入 购物车 则 对象.库存量 - 1, 用户将 商品 从 购物车 删除 则 对象.库存量 + 1 ,
通过 对象锁 来 确保 用户顺序 获取对象 查看 和 更新 库存量 。
可以看看我前几天写的 《一个类似 Twitter 雪花算法 的 连续序号 ID 产生器 SeqIDGenerator》 https://www.cnblogs.com/KSongKing/p/9918412.html
通常, 一个实体 会 对应一张表,
我们以 订单 为例,
一笔 订单 对应 订单表 里的 一笔资料,
订单表 会有一张 对应的 订单_Trans 表, 用来记录 发生 在 订单 上的 乐观事务,
订单_Trans 表 会有一个 ID , 表示 Transaction ID ,
订单_Trans 表 同时还包含 订单 需要更新 的 栏位,
这样, 发生 一次 事务 时, 会向 订单_Trans 表 insert 一笔资料, 栏位 的 值 是 订单 本次 更新的 栏位 的 值,
事务 成功 后, 会将 订单 表 里的 “trans” 栏位 更新为 这次 事务 的 ID, 即 订单_Trans 表 的 ID ,
这样, 通过 订单.Trans = 订单_Trans.ID 关联, 可以查询到 订单 在 本次 事务 更新后的 栏位 的 值 。
一笔订单 在 订单_Trans 表 中可以对应 多笔 事务记录, 这些 事务记录, 有成功的, 也有不成功的 。
乐观事务 的 关键 在于 最后只能 更新 一张表 的 (一个)栏位 来 决定 事务是否成功,
如果要 更新 多个 表, 那又 回到 传统事务 了 。
所以, 这就看 针对性 的 设计 。
对于 淘宝 天猫 这样的 海量购物, 应该设计为 单项递增数据 的 架构, 也就是 只 insert , 不 update ,
但问题是 如果 一个 交易 里 要 insert 多个 表 呢 ?
也许可以用 乐观事务 A 表 关联 B 表, B 表 关联 C 表, ……
这样只需要 最后 更新 A 表里的一个 生效 栏位 , 就可以 表示 事务 是否成功 。
因为 可以 根据 A 表 关联 到 B 表 , B 表 关联 到 C 表 ,
或者有一个 Trans 表, 通过 Trans 表 的 ID (Trans ID) 来 关联 A, B, C 表 ,
这样可以 查询出 某一个 事务(Trans ID) 在 A, B, C 表 里 insert 的 资料,
然鹅 。
不过 说不定 这种方法 真的 有人在用 喔 ~!
数据完整性 最好 还有由 数据库 来实现, 这才是合理的 。
关键在于,
数据库 应该使用 行锁定 来 实现 数据完整性 。
就是说, 我们应该让 数据库 用 行锁定 来 执行 事务 。
淘宝 天猫 应该 自己 已经对 数据库 做过这种 优化了 。
也就是说, 淘宝 天猫 的 数据库 的 事务 是用 行锁定 的 方式 执行的 ,
当然, 这只是我的 猜测, 啊哈哈哈哈 。
Redis 锁定 + 数据库 行锁定 事务 (行级事务) + 数据库 使用 固态硬盘
这样 来 应对 大并发, 你觉得呢 ?