我的积分商城实现 从0开始设计 多表修改并发问题
最近在写公司积分商城实现,我们公司的积分商城,积分有过期时间,积分有回退操作
1.积分商城实现,三张表 积分表 积分日志表 可用积分表 。你问我可用积分表干嘛的? 百度搜一下,答案一大堆,别人都是这么干的,所以我也这么设计,哈哈,好吧,就是因为积分有过期时间,所以需要可用积分表来处理
积分表记录用户id和用户积分余额,积分日志表用来记录用户积分使用情况,比如用户签到,加了多少积分,用户兑换,使用了多少积分,积分过期,减少了多少积分
那为什么还需要可用积分表?因为我们的积分存在一个过期时间,我们使用积分时候需要使用最早获得的那部分积分。
举个例子:
小明1月1号签到得到了3积分,2月2号做任务得到了5积分,在使用积分的时候,要优先使用1月1号签到得到的积分。
这个时候我们首先想到在积分日志表上操作,可是这个积分日志表是用来查询的,前端需要展示,不能被修改,
那就创建一个副本,可用积分表,这张表专门记录用户积分的增日志,也就是获得积分的日志,
使用积分的时候操作这个可用积分表就好了。如 我要使用10积分,那就根据用户id查找可用积分表记录 查询出5条记录
小明 +3 2022.1.1
小明 +10 2022.1.3
小明 +3 2022.1.4
小明 +1 2022.1.5
小明 +2 2022.1.16
这个时候我需要使用6积分,用掉第一条数据3积分 第二条数据有10积分,使用3积分 剩下7积分 这条数据拆成两条处理
数据变成如下
小明 +3 2022.1.1 已使用
小明 +3 2022.1.3 已使用
小明 +7 2022.1.3
小明 +3 2022.1.4
小明 +1 2022.1.5
小明 +2 2022.1.16
看懂了吗?这里为什么要打个标记已经使用? 别人的积分商城都是直接删除就好了??你这设计不怎么样嘛。。。
听我说,之所以打个标记,是因为我们的积分商城有回退操作!!!!别人的都没有,该死的产品,哈哈,不吐槽了(产品挺好的,其实可以沟通去掉积分回退,可是我就是喜欢挑战)。
我们的用户兑换一些比较大的商品是需要审核的,后台如果审核不通过需要把积分退给别人。如果直接删了数据,那没法还原可用积分表了,回退的积分什么时候过期鬼知道!!!
所以先打个标记,然后积分回退的时候把标记修改为未使用就好,一般数据库用0和1表示,你以为就这样完了,噩梦才刚刚开始!!!
首先,分析一波积分过期,我们的积分过期设置的是一年后的月底,也就是如果你 2022年1月1日获得的积分 过期时间是2023年1月31日 , 2022年1月4日获得的积分 过期时间也是2023年1月31日
每个月月底整了个定时器去删除过期积分,具体步骤,1.从可用积分表中查出数据来,然后看哪些可用的积分是过期的,从可用积分表中删除这些记录,然后统计删除了多少积分,去修改积分表,把用户积分减少一下,
这个时候记得存日志,增加积分日志表数据,对吧,你积分过期也是需要展示给用户看的,不然用户今天一看100积分,明天一看 我积分呢??? 如果没个记录给他看,用户不得炸锅啊,所以需要进行批量添加积分修改日志
如 :用户1 积分过期 -100 2022.1.31 用户2 积分过期 -99 2022.1.31 记得是批量添加,别循环一条一条插数据,动态sql拼一下,数据太大拆一下也行,用fork join框架思想,这些基础大家应该都知道,我又废话了。。
来来来,回来,我积分过期需要删除可用积分表数据,那并发情况下怎么办???? 来分析一波使用积分,用户使用积分的时候,我们需要分3步:
1.查询用户积分,判断积分是否足够(这里有个小细节,积分可用被坏人修改,所以这里需要校验积分,防止直接后台数据库修改积分,至于怎么校验??? 我是在数据库存了一个加密字段,这个字段是对积分,时间,用户id的一个加密结果,查询积分的时候需要积分,时间,用户id一起查出来,然后对这些加密,看加密结果和数据库存的的是否一致,如果一致说明积分没有被后台修改)
2.判断积分足够,去可用积分表查数据,看具体用哪条积分,把状态修改为已使用
3.增加积分修改日志,存积分日志表
4.修改用户积分
三步操作,ok,事务开启来,保证同时完成,同时失败。正常没问题,可是如果我在使用积分的时候,后台在处理积分过期呢??
随便一想就有问题,我a线程查出用户10积分,我要用3积分,最后我要修改用户为7积分,我b线程查出用户10积分(为什么是10,因为我这个线程和a线程并发执行,a线程还没修改积分,我查询就是10),然后删除可用积分表几条过期记录,过期了4积分,修改积分为6积分,最后积分余额为6。。用户一看积分明细,我用了3积分,过期了4积分,我还有6积分 哈哈哈,爽歪歪,今晚加个鸡腿
普通用户还好,遇到高手,指着你的bug刷积分,那就凉凉了,这还只是一种情况
我有使用积分,积分过期,积分回退,获得积分,4种操作,一个一个分析???确实,我一个一个分析了,我在获得积分的时候,积分回退了怎么办??我在获得积分的时候,积分过期会有并发问题吗?
获得积分的时候,使用积分了怎么办?嗯?前面两个可以理解,你用户既获得积分,又使用积分是什么鬼???你手速这么快?这边签到完,马上兑换个商品??? 那可不,网络延迟呢,或者恶意刷积分呢?
你不考虑,有bug,别人刷了你背锅呀。。。好吧,我并不怕背锅,也没那么多人刷积分,只是单纯的责任心而已,哈哈,以前搞算法,所以考虑的东西比较多。嗯,不急,慢慢来,一个一个分析
emmm。。。。分析了一波,写个毛,锁表,给我锁,统统串行,管你高并发,管你并发修改,表给我锁住,搞定收工。
什么?锁表?怎么锁,积分表和可用积分表两张表你怎么锁???简单啊,sql语句后面加 for upate 索引锁行,非索引锁表,记得在事务中。
然后呢???两张表,你锁表的sql怎么保证同时锁住两张表?如果锁住积分表,还没锁可用积分表的时候,可用积分表被修改了怎么办,一样并发问题!!!
那就再加一把锁。
开玩笑开玩笑,麻烦把刀收一收。串行是不可能串行的,这辈子都不可能写串行的代码,小小困难,经过我三天三夜的不屑努力,重要找到了解决方案。
自旋锁吧,不用想,数据库并发修改能用乐观锁就不会用悲观锁,分析情况,这里的情况很特殊,因为要操作多张表,多张表的操作又要有原子性,事务只能搞定修改的原子性,
查询的数据会有并发问题。我们在积分表上加个version字段,操作步骤顺序需要记住,先查询积分表,然后去操作别的表,然后修改积分表,修改积分要放最后,修改失败说明有并发情况,这个时候要抛异常,把中间操作的数据回滚
while (!updated) {
try {
//查询出旧积分数据
Points oldPoints = getOneById(userId);
//未找到对应用户
Optional.ofNullable(oldPoints).orElseThrow(() -> new BizException(BizMsgCode.R_FAIL_APP_USERPOINTSISNULL));
//如果积分数据异常
if (!pointsService.chekPoints(oldPoints)) {
//抛出错误 积分异常
throw new BizException(BizMsgCode.R_FAIL_APP_POINTSERROR);
}
//回滚积分
pointsService.fallbackPoints(oldPoints, updatePoints);
//捕获自定义异常,这里的异常不能抛出去
}catch (MYException e) {
log.debug("积分回退时遇到并发修改 userid:{} ",userId);
}
}
正常自旋锁,查旧值,改积分版本号不对,修改失败继续查旧值,我们因为中间对其他表进行了操作,所以版本不对需要抛异常回滚,而这个异常需要在while中捕获掉。
这样写,我a线程查出用户10积分,我要用3积分,最后我要修改用户为7积分,我b线程查出用户10积分,然后删除可用积分表几条过期记录,过期了4积分,修改积分为6积分,这个时候发现version不对,不能把积分修改成6,回滚中间的删除操作,
然后重新查询积分7,然后重新查询可用积分表,最后修改积分为3,搞定。
还有一个问题,自旋锁,在查询旧值的时候,
Points oldPoints = getOneById(userId);
像这个代码,getOneById方法上面我是加了@Transactional(rollbackFor = Exception.class, propagation = Propagation.NOT_SUPPORTED)注解的,
NOT_SUPPORTED是为了在事务外查询,以防有一个大事务在外面,然后调用我们的积分服务,每次查询都是同一个version
分析一波,这个自旋锁其实性能一般了,别人自旋锁操作一张表,不用开事务,纯纯乐观锁,我们这个操作几张表,因为要保证几张表一起完成,所以不得不开事务,修改数据的话,事务会在数据库加一把行锁哦,甚至锁表,
性能就这样下降了,不过问题不大,比悲观锁好多了。
总结: 这里我主要是讲的多表操作并发下的一种解决方案,主要根据一张表为主(加锁),其他表数据回滚来实现,每个积分操作功能都需要把查询放第一,修改积分放最后,这样无论积分回退,积分过期,积分使用,积分获得怎么并发修改都没问题了
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 一文读懂知识蒸馏
· 终于写完轮子一部分:tcp代理 了,记录一下