论 大并发 下的 乐观锁定 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 锁定       +        数据库 行锁定 事务 (行级事务)      +        数据库 使用 固态硬盘

这样 来 应对 大并发,  你觉得呢 ?

原文地址:https://www.cnblogs.com/KSongKing/p/9934722.html