文章目录
- 1.前言
- 2.解决方案
-
- 2.1BigDecimal分布式原子操作实现
- 2.2注意事项
- 2.3其他方案
-
- 2.3.1基于数据库的解决方案
- 2.3.2使用分布式协调服务
- 2.3.3分布式计算框架
- [2.3.4 其它方案比较](#2.3.4 其它方案比较)
- 3.总结
1.前言
在微服务分布式大行其道的今天,在mysql数据库频繁使用的今天,假设让你设计一个账户系统,账户充值扣款消费退款等相关的需求,还是使用传统的mysql数据库的表来实现吗?首先说一下这种设计:如果是在最终一致性的调用下这种实现借助MQ消息队列的顺序单一消费来最终达到账户数据的最终一致性是没有啥问题的,但是如果使用的是强调用,调用接口立马就操作账户,这种会有一个什么样的问题,如果使用账户的人多了之后,并发量一上来,就算是加了分布式锁,但是也会由于myslq的事务延迟导致,账户的查询不是最新的数据导致账户数据不对,这也是mysql表设计账户会存在的巨大隐患,之前在搞乐企开票的时候就吃过这个大亏了,发票号码的计算在myslq的表中来搞的,结果就导致发票号码生成重复的问题,这个问题也是由于mysql的事务延迟导致的,在高并发下就会有问题。
2.解决方案
采用一个分布式可以原子操作BigDecimal的结构来将账户表中的账户余额数据加载到这个结构上分布式多节点下原子操作
2.1BigDecimal分布式原子操作实现
lua
local key = KEYS[1]
local delta = tonumber(ARGV[1])
local initValue = ARGV[2]
-- 获取当前值
local current = redis.call('GET', key)
if not current and initValue and initValue ~= "" then
-- 如果 key 不存在,使用传入的初始值设置
initValue = tonumber(ARGV[2])
redis.call('SET', key, tostring(initValue))
current = initValue
else
-- 如果 key 不存在,返回 nil
if not current then
return nil
end
current = tonumber(current)
end
-- 计算新值
local newValue = current + delta
-- 判断是否余额不足
if newValue < 0 then
return "insufficient_balance"
end
-- 设置key的值并返回结果
redis.call('SET', key, tostring(newValue))
return string.format("%.2f", newValue)
这里只分享这个lua脚本了,至于集成使用也很简单,这里就不做过多的讲解了。
2.2注意事项
在使用这个方案的时候需要注意因为业务接口调用可能会有异常导致mysql数据库中的账户表事务回滚了,但是redis中的账户key已经变动了,所以还需要给redis中的账户key对应做一个回退补偿,加了需要减回去,减了需要加回来,加/减几次需要逆向做一个补偿,保持redis中的key的数据和mysql表中的数据是一致的,然后还需要配合上分布式锁,直接锁账户id,这种这个账户下所有人操作都是安全。
2.3其他方案
2.3.1基于数据库的解决方案
如果你的应用已经基于某个关系型数据库或NoSQL数据库,那么可以直接利用数据库提供的事务管理功能来实现BigDecimal
的原子加减。大多数现代数据库都支持ACID属性,能够确保并发情况下的数据一致性。
-
在SQL数据库中,可以通过
UPDATE
语句直接对字段进行增加或减少操作,例如:UPDATE account SET balance = balance + ? WHERE id = ?
。 -
对于一些NoSQL数据库,如MongoDB,也提供了类似的原子更新操作符,比如
$inc
操作符用于递增或递减数值。
可以使用mybatisPlus的乐观锁来实现:
在SQL数据库中,可以通过UPDATE
语句直接对字段进行增加或减少操作,例如:
sql
UPDATE account SET balance = balance + ? WHERE id = ?
可以使用mybatisPlus的乐观锁来实现,刚一想,之前的姿势不对,使用数据库的乐观锁的姿势不对,更新是要去写upate的接口来更新的,不要去把数据查出来在去算了更新进去,这种你查出来的数据在高并发下mysql事务延迟,查询的数据不是最新的,所以这种姿势是错误的,终于又想通了之前是哪里姿势有问题了。
2.3.2使用分布式协调服务
一种常见的方式是使用像Apache ZooKeeper、etcd或者Consul这样的分布式协调服务。这些工具提供了诸如锁机制(Locks)、选举(Leader Election)等功能,可以帮助你安全地执行原子操作。
- ZooKeeper : 可以创建一个持久节点代表你的共享资源(如一个存储了
BigDecimal
值的节点),然后利用ZooKeeper的API提供的原子操作或通过获取分布式锁来对这个值进行修改。 - etcd/Consul: 类似地,你可以使用事务特性来确保读取和更新值的过程是原子性的。比如etcd支持的Compare-And-Swap (CAS) 操作可以用来实现这一点。
2.3.3分布式计算框架
如果是在大数据处理场景下,考虑使用如Apache Spark这样的分布式计算框架,它提供了强大的RDD(Resilient Distributed Datasets)或DataFrame API,可以轻松地对大规模数据集执行聚合操作,包括对BigDecimal
类型的加法和减法运算。
2.3.4 其它方案比较
方案 | 描述 | 是否推荐 |
---|---|---|
**ZooKeeper CAS 重试机制 | 性能差、吞吐量低 | ❌ |
基于数据库乐观锁 | 使用版本号字段,在更新时检查 | ✅ |
ETCD Compare-and-Swap (CAS) | 支持事务和原子操作,适合云原生系统 | ✅ |
Hazelcast 分布式 Map | 内存级一致性,提供原子更新方法 | ✅ |
3.总结
上面这种思路其实还可以拓展一下,把账户相关的数据使用JSON的方式使用lua脚本在redis中执行,lua脚本解析JSON原子处理账户的各种相关的操作,去除mysql表数据存储账户数据,这种就可以不用逆向补偿操作了,但是如果lua脚本过长还是会影响性能,lua解析JSON需要集成一些开源的库到redis上,需要重启redis服务,所以这个就比较麻烦,redis唯一一个自带解析JSON的库是cjson可以不用安装其他JSON解析的库来实现,写也比较复杂,但是有必要还是可以尝试一下这种进阶的方案的,本次分享到此结束,希望我的分享对你有所启发和帮助,请一键三连,么么么哒!