前段时间一直忙着面试、写论文,这段时间稍微空闲下来,写一下做的一个比较有特点的项目,主要是处理高并发的一套流程,主要是相关思路并且会帖出相关代码实现。
1:项目简单介绍+问题产生原因
在研一到研二期间实验室开发小组为某区疾控中心做的一个预约管理系统,这个项目具有以下的一个业务流程:每天的上午九点钟,疾控中心的工作人员会放出一批体检预约号,总量大概在200-300左右,号码有限,且体检通过会获得疾控中心发放的健康证(此证存在用于上岗的硬性要求),于是会导致大量餐饮或者其他第三产业服务者用户蹲点抢号,造成了一个短时间的高并发场景,一方面是对于用户的响应不及时,影响用户使用体验,一方面大量请求集中打在数据库上,对于数据库也造成了一定的压力,于是我们开发了一套先消费的处理逻辑解决这个问题。
2:解决流程思路分享
2.1: 总体流程思路
具体流程如下,在疾控中心的操作人员发放预约号时,会同时将这一批预约号信息(包括预约时间、预约号库存等信息)放入到redis缓存中,用户调用取号接口时,首先通过redis分布式锁进行互斥访问缓存中的预约号信息,成功获取到库存号的用户才允许发送消费消息到rabbitMQ(实质上是发送自己的相关信息、预约号的相关信息整合到一个对象类,在数据库中生成一条预约记录),成功发送消息到消息队列上的用户,可以立即返回消费成功的提示,以此减少用户响应时间,同时进行了一个流量削峰的操作,缓解了数据库压力。
2.2:方案存在的问题和解决方案
(1)redis分布式锁取库存号实现代码
首先是上redis分布式锁,以下代码都是逻辑简单表示代码,不是项目中具体使用的代码,可便于大家理解。
![]()
上锁之后执行具体的取库存操作以及库存--,上面代码设计了过期时间10s,但是一般会在10s内自动解锁。解锁操作也是原子性的,具体使用rua脚本的解锁命令如下
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
Long result = redisTemplate.execute(redisScript, Collections.singletonList(key), value);
分布式锁的具体实现网上有很多代码案例,大家可以取一种自己理解的方案进行具体实施。
(2)发送消息后确保消息不会丢失且能被正确消费
这一步主要是分为三个阶段,即用户发送消息到消息队列失败的问题、消息在消息队列上丢失问题以及消息消费失败的问题。
首先是消息发送问题,用户拿到库存后发送消息到消息队列上会有两个回调函数,即returnCallback和confirmCallback函数,结合使用可以判断消息是否成功发送到具体的队列上,同时对于消息从拿库存到发送消息这一个过程的方法做一个事务处理,保证消息发送失败后库存能恢复,若消息成功发送到队列上,可马上返回用户预约成功提示。
其次是消息在消息队列上的问题,存在消息队列可能宕机导致消息丢失和消息本身没做持久化的话即使消息队列重启也会丢失这两个问题,对于第一个问题我们声明一个持久化的消息队列,即声明队列时durable设置为true,同时发送的消息的delivery_mode 设置为2,表示此消息也是持久化消息,需要写入磁盘。
最后是消息消费失败问题,针对这一问题,在消息监听处理方法中,我们使用一个try catch把具体的消费操作(实质上就是在数据库中插入一条预约记录)环绕,同时消费后使用chanel.basicAck()方法告知消息队列本条消息消费成功,如果在消费过程中产生了异常或者别的问题,我们可以捕获这个异常,并且在其中使用chanek.basicNack()方法进行消息的重新入队,重新消费操作。
除上述问题外,还存在消息一致性的问题,即保证我们的消息只被消费一次,针对这个问题我们可以在消息头部取得消息的唯一标识id,每次消费前先判断该消息唯一标识id是否已存在,若已存在则拒绝消费,不存在则进行消费,消费成功后将此唯一标识id放入hashSet中存入redis,上述的判断则是在redis中拿到这个hashSet判断是否已存在此消息唯一标识id。
920



被折叠的 条评论
为什么被折叠?



