我们在面试过程中,hr常问及的问题便是幂等性问题产生的原因、如何解决、运用的场景等。今天小编就带领大家一同看看什么是幂等性问题,常见的幂等性问题发生场景及解决方案有哪一些,如果有哪些地方写的不够周到的也请大家在评论区补充说明!
什么是幂等性
幂等性是指在计算机系统中,一个操作无论执行多少次,产生的结果都是一样的,不会因为多次点击而产生副作用。比如在我们日常生活中进行银行的转账操作,如果你转账100元,且重复操作,账户会被多扣100元,导致不同的结果,这就没有保证接口幂等性。因此,设计一个幂等的转账操作尤为重要,无论你点击多少次“转账”,系统只会进行一次扣款和一次转账,避免多次转账的情况。
幂等性产生的原因
幂等性产生的原因主要是为了应对分布式系统、网络通信、并发操作中的不确定性和故障情况。具体原因包括以下几个方面:
1. 网络不可靠性
网络通信可能会出现各种问题,比如请求丢失、超时、重复发送等。为了保证在这种情况下系统的稳定性和一致性,操作需要设计成幂等的。这样,即使请求被重复发送,系统也能保证最终结果的一致性。
2. 分布式系统中的冗余和重复
在分布式系统中,同一操作可能会被多个服务节点处理,或者因为某个节点失败后重新发送请求。这种情况下,幂等性确保重复操作不会导致数据错误或系统异常。
3. 并发操作
多线程或多进程环境下,多个操作可能同时对同一资源进行访问和修改。幂等性可以防止并发操作导致的不一致问题,确保无论多少次操作结果都相同。
4. 事务管理和故障恢复
在处理事务时,如果事务执行失败,系统需要重试操作。幂等性确保重试不会导致事务被多次应用,保证数据的一致性和完整性。
5. 外部系统调用
调用外部系统的API或服务时,如果外部系统未能及时响应,调用方可能会重复发送请求。幂等性确保重复调用不会产生副作用或错误结果。
6. 数据一致性要求
为了保证数据的一致性和完整性,尤其在金融、支付等对数据准确性要求极高的领域,幂等性可以防止因重复操作导致的数据错误或不一致。
什么场景需要进行幂等设计?
1.在线支付:当用户发起支付请求时,避免重复扣款
2.银行交易:确保同一笔交易不会因为网络重试等原因被多次执行操作
3.票务系统:在线购票平台在用户购票时,会进行检查用户所选位置是否被重复预订
4.通信服务:如短信或通话服务,系统会检查是否已为相同内容的请求计费
5.任务调度:在定时任务或批处理系统中,确保不会因为任务的重启或重试而重复执行相同的操作
6.库存扣减:确保库存扣减操作无论被执行多少次,只会扣减一次库存
幂等性的解决方案
常见的幂等性解决方案有乐观锁、去重表、redis分布式锁、幂等性token等等。
1.去重表
去重表的设计结构:
- 唯一标识符字段(Primary Key):ID 唯一标识每条记录,可以为UUID、请求ID、事务ID、等等,类型通常为字符串或者整形,根据唯一标识符的生成方式决定
- 状态字段(Status):记录操作的状态,如处理中,已完成、失败等
- 操作符字段(Operation Type):记录操作的类型,如常见的创建、更新、删除等操作
- 其他字段(Optional Fields):根据具体的业务需求按需添加即可
具体实现操作:
在执行具体业务操作之前,系统会先检查去重表是否已经存在该唯一标识的记录,如果存在,则说明该记录已经执行过,可以直接返回结果、避免重复的添加执行。
如果不存在该唯一标识符记录,则会进行插入操作,标记该操作正在处理中,这样可以防止并发操作导致的重复执行。
然后再执行具体的业务操作,如处理支付请求、消费消息等等。
再根据业务逻辑执行的结果,更新去重表中的状态字段,标记操作已完成或者失败
2.Token令牌机制
利用AOP注解+拦截器+Token+redis 通过UUID生成唯一键值的 token,将token 存放到redis中,在需要进行幂等性操作的业务方法上添加aop注解
TokenService代码
拦截器代码
@Component
public class IdemponentInterceptor implements HandlerInterceptor {
@Autowired
private TokenService tokenService;
@Override
public boolean preHandle(HttpServletRequest request,HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod){
HandlerMethod hm = (HandlerMethod) handler;
// 获取接口方法上的注解
AutoIdempoent idempoent = hm.getMethodAnnotation(AutoIdempoent.class);
if (idempoent == null){
// 说明不存在幂等性的注解 即这个接口不需要幂等性处理
return true;
}else {
// 说明存在幂等性的注解 即这个接口需要幂等性处理
return tokenService.chectoken(request);
}
}
return HandlerInterceptor.super.preHandle(request, response, handler);
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView moderAndView) throws Exception {
HandlerInterceptor.super.postHandle(request, response, handler, moderAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}
当请求进来时 方法会来到拦截器获取接口方法上的注解判断是否需要进行幂等性处理,若需要则会先进行查询token令牌是否存在,如果存在lua脚本则删除token 不存在 则返回重复请求,进行相应的业务操作
RedisService代码
@Service
public class RedisService {
@Resource
private RedisTemplate redisTemplate;
public boolean exists(String token) {
return redisTemplate.hasKey(token);
}
public boolean delete(String token) {
if (exists(token)){
return redisTemplate.delete(token);
}
return false;
}
//保存token
public void saveToken(String token) {
redisTemplate.opsForValue().set(token, token);
}
}