前言
针对消息重复消费和重复下单等问题,可以从多个层面进行解决,包括消息队列的设计、消费者端的处理逻辑、数据库层面的约束以及系统架构的优化等。以下是一些具体的解决方案:
解决消息重复消费
- 实现消费幂等性:
- 定义幂等性:无论消息被消费多少次,系统的状态都应该保持一致,即不会产生副作用或影响系统的正确性。
- 实现方式:
- 对于消息的消费结果,可以通过唯一标识符(如消息ID)进行判断,确保在重复消费时不会产生副作用。
- 对于需要记录状态的操作,可以使用分布式锁来保证同一时刻只有一个节点可以执行此操作。
- 对于数据库操作,可以使用唯一索引来防止重复插入重复数据。
- 消息去重:
- 在消费者端去重:使用缓存(如Redis)或数据库来记录已经处理过的消息ID,避免重复消费。
- 在消息队列端去重:通过在消息队列中记录已发送的消息ID(如使用map或set结构),避免重复推送相同消息。
- 确保消息确认机制:
- 消费者在成功处理消息后,应及时向消息队列发送确认回执(ack),告知消息已被消费,消息队列可以删除或标记已消费的消息。
- 设置消息有效期:
- 在消息中设置有效期,确保消息在一定时间内被消费,超过有效期的消息将被丢弃,从而避免过期消息被重复消费。
解决重复下单
- 前端控制:
- 按钮置灰:在用户点击下单按钮后,立即将按钮置灰或禁用,防止用户重复点击。
- 提示信息:在用户尝试重复下单时,显示提示信息告知用户订单已提交,避免重复操作。
- 后端控制:
- 请求唯一ID+数据库唯一索引约束:
- 当用户进入订单提交界面时,调用后端获取请求唯一ID,并将ID值存储在页面中。
- 用户点击提交按钮时,后端检查这个唯一ID是否已使用,如果已使用则提示重复提交。
- 将唯一ID存入业务表中,并设置该字段为唯一索引,从数据库层面防止重复提交。
- 分布式锁+token:
- 使用Redis等分布式锁服务,对请求ID在限定时间内尝试加锁。
- 如果加锁成功,则继续后续流程;如果加锁失败,则提示用户重复提交。
- 锁的时间范围应根据实际业务需求设置,避免过长或过短导致的用户体验问题。
- 请求唯一ID+数据库唯一索引约束:
- 接口幂等性设计:
- 设计接口时考虑幂等性,确保无论请求被发送多少次,对系统状态的影响都与执行一次相同。
- 可以通过状态机、业务流水表等方式进行重复操作的判断。
- 系统架构优化:
- 在高并发场景下,优化系统架构以提高处理能力和响应速度,减少因超时重试等原因导致的重复下单问题。
解决方案
token验证策略:
import redis.clients.jedis.Jedis;
import java.util.UUID;
public class RedisToken {
public static void main(String[] args) {
// 连接到Redis服务器
Jedis jedis = new Jedis("localhost");
try {
// 生成一个UUID作为token
String token = UUID.randomUUID().toString();
// 使用setex命令设置token,60*60表示过期时间为1小时(秒为单位)
// 注意:setex的参数需要是字符串类型,并且是用英文的逗号或空格分隔
jedis.setex(token, 60 * 60, "1"); // 假设"1"是用来标记token的值或状态
// 输出token
System.out.println("Token: " + token);
} finally {
// 关闭Jedis连接
jedis.close();
}
}
}
在生成了token之后,后续请求就可以校验这个token是否有效,并且确保只能用一次:
import redis.clients.jedis.Jedis;
public class RedisTokenVerify {
public void verify(String token) {
// 使用try-with-resources语句自动关闭Jedis连接
try (Jedis jedis = new Jedis("localhost")) {
String value = jedis.get(token);
if (value != null) {
// 删除token
jedis.del(token);
// 校验成功,可以添加日志或执行其他操作
System.out.println("Token verified and deleted successfully.");
// 也可以考虑返回一个布尔值或抛出一个自定义的成功异常
} else {
// 校验失败,可以添加日志或执行其他操作
System.out.println("Token verification failed. Token not found.");
// 同样,也可以返回一个布尔值或抛出一个自定义的异常
}
} catch (Exception e) {
// 处理与Redis交互时可能发生的任何异常
e.printStackTrace();
// 可以选择抛出或记录这个异常,或者根据应用需求进行其他处理
}
}
}
这是一个token校验的简单实现,但是这个逻辑存在一个问题,那就是如果高并发场景,可能会导致多个线程同时,导致token同时校验通过。要解决这个问题,有两个办法,第一个是把get和del放到一个事务中,或者用lua脚本,或者用分布式锁也可以。