MYSQL 高并发下,记录变动后余额怎么统计才能更准确?

156 天前
 Aluhao
-- 积分总表
CREATE TABLE `api_credits` (
`uid` bigint unsigned NOT NULL COMMENT '用户 ID',
`names` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户姓名',
`credits1` decimal(15,2) NOT NULL DEFAULT '0.00' COMMENT '积分 1 余额',
`credits2` decimal(15,2) NOT NULL DEFAULT '0.00' COMMENT '积分 2 余额',
`credits3` decimal(15,2) NOT NULL DEFAULT '0.00' COMMENT '积分 3 余额',
`time` int NOT NULL DEFAULT '0' COMMENT '更新时间',
PRIMARY KEY (`uid`)
) ENGINE=InnoDB COMMENT='积分总表';

-- 积分记录表
CREATE TABLE `api_credits_log` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '记录 ID',
`uid` bigint unsigned NOT NULL DEFAULT '0' COMMENT '用户 ID',
`credits` decimal(15,2) NOT NULL DEFAULT '0.00' COMMENT '积分变动',
`balance` decimal(15,2) NOT NULL DEFAULT '0.00' COMMENT '变动后余额',
`cid` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '操作类型',
`time` int unsigned NOT NULL DEFAULT '0' COMMENT '记录时间',
PRIMARY KEY (`id`),
KEY `uid_time` (`uid`,`time`)
) ENGINE=InnoDB COMMENT='积分记录表';

消费 100 积分,向 积分总表 api_credits 减去用户总积分,并向积分记录表 api_credits_log 写入一条记录,
balance 用户余额计算是 api_credits 表中 credits1 - 100 (即 80000 - 100 = 79900 )

消费事务会产生 SQL 数据:
SELECT * FROM `api_credits` WHERE `uid`='22' LIMIT 1
UPDATE `api_credits` SET `credits1`=`credits1`-'100' WHERE `uid`='22' AND `credits1`>='100'
INSERT INTO `api_credits_log` SET `uid`='22', `cid`='3', `credits`='100', `balance`='79900', `time`='1701001020'

但是在高并发场景下 SELECT 读出来的值不是最新,如,在同一时间段这个用户同时几条消费记录,这个变动后余额统计的就不是这么准确了,有没有什么好点的解决方案?

系统用了主从架构,读写分离,但是在这条事务中 SELECT 查的是主库

欢迎大家一起探讨一下。
5948 次点击
所在节点    MySQL
61 条回复
miaotaizi
156 天前
同一个用户还能高并发?

这是谁的问题
kanepan19
156 天前
update 放上面,第一步就行锁了, 再 select 的余额就是对了。

UPDATE `api_credits` SET `credits1`=`credits1`-'100' WHERE `uid`='22' AND `credits1`>='100'
SELECT * FROM `api_credits` WHERE `uid`='22' LIMIT 1
INSERT INTO `api_credits_log` SET `uid`='22', `cid`='3', `credits`='100', `balance`='79900', `time`='1701001020'
coderxy
156 天前
单用户加分布式锁? 正常情况下单个用户的操作可以去串行处理
Aluhao
156 天前
@miaotaizi 有 API 串接给用户用,用户又是对接它的用户用。
Aluhao
156 天前
@coderxy 不是单用户,在当前系统,这个用户 ID 是 22 ,可以在用户它的系统,可能它的用户有很多,他们要操作积分变动都是走的 22 这个用户的总积分。
looo
156 天前
1. 你这个查询:SELECT * FROM `api_credits` WHERE `uid`='22' LIMIT 1 肯定要限制住拿到的是没有更新过的,可以采用数据库行锁:SELECT * FROM `api_credits` WHERE `uid`='22' LIMIT 1 FOR UPDATE; 记得加要加事务。

2. 建议你以后更新数据有并发的,可以增加 WHERE 条件,条件就是更新的值必须是你查询出来的值,然后在去 SET 更新。
looo
156 天前
MySQL 默认存储引擎( InnoDB ) UPDATE 会默认加行锁🔐,有时候巧妙的去利用 UPDATE 来避免资源竞争。
cxk0
156 天前
如果仅有增加不会减少,类似等级,可以用记录消化;
如果是有增有减,SQL 级别的 select for update;
bitmin
156 天前
有个麻烦的做法

第一次插入 api_credits_log 表不记录 balance

另外做个任务队列串行更新 balance ,当前 balance 为上一条的 balance - 当前 credits
k9982874
156 天前
1. 楼上已经说了加 where 条件,加行锁
2. 使用消息队列,保证 update 按顺序执行
Aluhao
156 天前
@cxk0 有增加也有减少的记录。
looo
156 天前
还有这种减的操作,一定要先减、多次校验然后在去做业务。
Aluhao
156 天前
@bitmin 还有一个关键的问题就是用户 API 请求过来,需要把最新的余额 balance 返回过去。
looo
156 天前
不建议引入第三方工具来解决,实在没办法在采取。你这个还没有达到高并发,属于同时竞争资源导致的。
Aluhao
156 天前
@looo 感谢建议!
leaves615
156 天前
配置事物隔离级别:read on uncommit
Aluhao
156 天前
@leaves615 可以试试,但这级别并发安全性较差
nothingistrue
156 天前
不要让数据库做业务的事,这事 mysql 干不了。



你的业务逻辑本事是有问题的,属于性能优化事故。
既然第一步查出来了,那么后面 UPDATE `api_credits` 跟 INSERT INTO `api_credits_log` 时候的 `balance` ,都要依赖查出来的值,不能一个用查出来的,一个用底层存储实时的——绝大多数事务隔离级别下,这俩不是一个值。
bitmin
156 天前
@Aluhao #13 你最新的余额 balance 不是有其他表记吗,这个值不需要从 log 表取
happyxhw101
156 天前
这其实是一个 update return 类似的问题,如果是 pg 直接 update xxx return api_credits 就可以了,如果是 mysql 那就是大家说的先 update 再 select ,反正先 select 再 update 并不是一个合理的方法

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/997702

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX