一个简单的积分系统该如何设计

2,571 阅读5分钟

积分系统

1、单表实现

​ 我们可以设计一张积分详情表*(record_detail)*用于存储用户的每一个积分操作,该表中保存了UID、积分数、操作类型、过期时间、操作类型。

idUIDrecord(积分)type(操作类型、 0:减少 1:增加)create_timeexpire_timeoperation(操作)
1110020220101消费
222012022010120230101每日签到
311012022010120230101每日签到
  • 优点:设计比较简单,通过过滤过期时间求和就可以得出一个用户的积分。

  • 缺点:但是这样的设计将所有的积分全放一起,当数据量大了之后,每次查询都需要过滤大量的数据才能得到准确的积分,查询效率随着数据量变大而降低。

2、引入用户积分表user_record优化

​ 第一种设计中随着数据量增大查询效率降低,仔细思考降低的原因在于想要获取 用户A的积分需要查询出所有符合条件的详情进行运算。于是我们可以考虑提前将积分算好保存下来。这边我们依然保留积分详情表,不过此表的作用是用来提供用户查看积分详情,同时引入一个用户积分表user_record来保存每个用户的总积分,在对积分进行操作时同步更新这张表,多一步更新操作换来更快速的积分查询。

record_detail
idUIDrecord(积分)type(操作类型、 0:减少 1:增加)create_timeexpire_timeoperation(操作)
1110020220101消费
222012022010120230101每日签到
311012022010120230101每日签到
user_record
idUIDrecord(积分)
1110
2220
3110

3、过期积分的处理

在上述的设计中存在一个问题:过期的积分该如何处理?

  1. 利用定时任务每日扫描以便record_detail将过期的积分扣除
  2. 选定某个特定的时间节点进行更新如:查看积分时、登录时

方法1需要每隔一段时间对record_detail进行全表扫描计算好积分,随着积分数量增多,更新速度会越来越慢

方法2虽然在用户触发事件时进行更新且只计算当前用户的积分数可以提升效率但是积分数据存在滞后性,用户不触发特定事件就不会去更新积分。

这时候我们可以结合方法1方法2

方法1的定时任务可以保证时效,方法2减少扫描数据量那么我们可以结合两种,依然采用定时任务,不过我们如何缩小扫描数据大小,那就将可用的积分抽取出来做为一张新表,将过期积分也当作对积分的一种操作,因为该表只存可用积分所以表的大小可控不会无限膨胀,定时任务保证积分的时效性.

现在我们的系统有以下几张表。

record_usable

idUIDrecord(积分)create_timeexpire_time
111020220101
22202022010120230101
31102022010120230101

record_detail

idUIDrecord(积分)type(操作类型、 0:减少 1:增加)create_timeexpire_timeoperation(操作)
1110020220101消费
222012022010120230101每日签到
311012022010120230101每日签到

user_record

idUIDrecord(积分)
1110
2220
3110

​ 积分的新增、删除、过期操作全部记入到record_detail表中,user_record用于保存用户的总积分数量,record_usable保存用户的可用积分。积分增加时只需要在record_detail表中新增具体操作,在record_usable中插入这条新积分(应该不会有人让积分刚新增就过期吧)。积分过期需要利用定时任务去可用积分表中将过期的积分取出来,逐条过期,插入record_detail并更新user_record,该操作可用丢到消息队列中去异步完成。

4、消费积分

​ 表结构已经设计好了,那么剩下消费积分该如何设计。积分的消费首先需要判断当前用户的积分足部足够进行消费,积分足够时要如何选择需要消费积分,比如我现在需要对1号用户消费15积分,我们应该按照过期时间倒序去record_usable中取积分,判断积分是否满足,所以取到积分1,再取积分2时我们还需5积分,积分2为10积分,这时我们需要将积分2进行拆分,拆分成两条5积分的数据,然后进行消费。伪代码如下所示:

int page=10;
int pageSize = 10;
int needRecord = 20;
List<Record> deleteR = new ArrayList<>();
while(true){
    List<Record> l = dao.selectRecordByTime(page,pageSize)//按过期时间升序排列
    if(l==null || l.size()==0){
        //积分不够
        break;
    }
    for(Record r : l){
        if(needRecord<r.recore){
            //拆分积分
            r(needRecord)
            r1(r.record - needRecord)
            //r r1写入数据库
            break;
        }
        needRecord-=r.record;
        deleteR.add(r);
    }
}
//deleteR写入record_detail

5、结语

​ 积分消费解决了,但是又存在一个问题,在用户消费积分时刚好积分过期了该怎么办。我们将积分过期需要的操作使用消息队列进行异步处理,同时我们也可以引入Redis锁住用户的积分,在用户消费积分时利用用户ID锁住用户的积分,这时候消费者应该拒绝任务,并将任务重新加入到消息队列中直到积分过期操作完成。