文章目录
前言
- 在传统的OA办公自动化、CRM客户关系管理、WMS仓储管理系统中,使用的技术栈比较老,在日常开发中,很多时间都在做业务开发,很难发掘技术亮点,并写到简历上。在撰写简历时,即使面对传统且技术栈较老的OA、CRM、WMS系统,依然可以提炼出技术亮点,本文将总结一些可以直接套用在项目中的技术亮点,并写到简历中;
项目亮点一
- 基于Redis、自定义注解、Token机制实现接口幂等,解决XXX模块接口的数据重复提交问题
场景描述
- 在OA系统中,一个常见的复杂场景是处理员工的请假申请。由于网络延迟、用户误操作或前端重复提交等原因,员工可能不小心提交了重复的请假申请。这不仅会给HR审批带来困扰,还可能影响考勤统计的准确性。为了解决这个问题,我们可以使用Redis结合自定义注解来实现接口幂等性,确保同一个请假申请在同一时间段内不会被重复处理。
- 员工A通过OA系统提交了一个请假申请,该申请包括请假类型、起始时间、结束时间等信息。由于某种原因(如网络问题或用户快速点击提交按钮多次),该请求被发送了多次到服务器。服务器需要确保即使接收到多个相同的请求,也只处理一次。
实现思路
- 对于服务的提供者来说,我们需要在接口中做好幂等控制,来避免因为重复而导致的脏数据
- 对于重复提交的场景,我们可以使用token作为这个幂等号 来保障唯一性,当用户每一次访问页面的时候,都向后端接口请求获取一个token,然后在之后本页面的操作中,都需要把这个token带过来。如果页面没有刷新,这个token应该是不变的。
- 基于token机制,每次接口请求前先获取一个token,然后再下次请求的时候在请求的header体中加上这个token,后台进行验证,如果验证通过删除token,下次请求再次判断token
- 有了这个token就可以用它来做防重控制,并且还能避免有人恶意的刷我们的接口。
- 在消息或者下单场景中,有了唯一的幂等字段之后,就可以基于下面三个步骤来进行幂等控制了
- 一锁:第一步,先加锁。可以加分布式锁、或者悲观锁都可以。但是一定要是一个互斥锁!
- 二判:第二步,进行幂等性判断。可以基于状态机、流水表、唯一性索引等等进行重复操作的判断
- 三更新:第三步,进行数据的更新,将数据进行持久化。
redis实现自动幂等的原理图:
一:搭建redis的服务Api
1:首先是搭建redis服务器。
2:引入springboot中到的redis的stater,或者Spring封装的jedis也可以,后面主要用到的api就是它的set方法和exists方法,这里我们使用springboot的封装好的redisTemplate
/**
* redis工具类
*/
@Component
public class RedisService {
@Autowired
private RedisTemplate redisTemplate;
/**
* 写入缓存
* @param key
* @param value
* @return
*/
public boolean set(final String key, Object value) {
boolean result = false;
try {
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
operations.set(key, value);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/**
* 写入缓存设置时效时间
* @param key
* @param value
* @return
*/
public boolean setEx(final String key, Object value, Long expireTime) {
boolean result = false;
try {
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
operations.set(key, value);
redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/**
* 判断缓存中是否有对应的value
* @param key
* @return
*/
public boolean exists(final String key) {
return redisTemplate.hasKey(key);
}
/**
* 读取缓存
* @param key
* @return
*/
public Object get(final String key) {
Object result = null;
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
result = operations.get(key);
return result;
}
/**
* 删除对应的value
* @param key
*/
public boolean remove(final String key) {
if (exists(key)) {
Boolean delete = redisTemplate.delete(key);
return delete;
}
return false;
}
}
二:自定义注解AutoIdempotent
- 自定义一个注解,定义此注解的主要目的是把它添加在需要实现幂等的方法上,凡是某个方法注解了它,都会实现自动幂等。后台利用反射如果扫描到这个注解,就会处理这个方法实现自动幂等,使用元注解ElementType.METHOD表示它只能放在方法上,etentionPolicy.RUNTIME表示它在运行时
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoIdempotent {
}
三:token创建和检验
1:token服务接口
我们新建一个接口,创建token服务,里面主要是两个方法,一个用来创建token,一个用来验证token。创建token主要产生的是一个字符串,检验token的话主要是传达request对象,为什么要传request对象呢?主要作用就是获取header里面的token,然后检验,通过抛出的Exception来获取具体的报错信息返回给前端
public interface TokenService {
/**
* 创建token
* @return
*/
public String createToken();
/**
* 检验token
* @param request
* @return
*/
public boolean checkToken(HttpServletRequest request) throws Exception;
}
2:token的服务实现类
token引用了redis服务,创建token采用随机算法工具类生成随机uuid字符串,然后放入到redis中(为了防止数据的冗余保留,这里设置过期时间为10000秒,具体可视业务而定),如果放入成功,最后返回这个token值。checkToken方法就是从header中获取token到值(如果header中拿不到,就从paramter中获取),如若不存在,直接抛出异常。这个异常信息可以被拦截器捕捉到,然后返回给前端。
@Service
public class TokenServiceImpl implements TokenService {
@Autowired
private RedisService redisService;
/**
* 创建token
*
* @return
*/
@Override
public String createToken() {
String str = RandomUtil.randomUUID();
StrBuilder token = new StrBuilder();
try {
token.append(Constant.Redis.TOKEN_PREFIX).append(str);
redisService.setEx(token.toString(), token.toString(),10000L);
boolean notEmpty = StrUtil.isNotEmpty(token.toString());
if (notEmpty) {
return token.toString();
}
}catch (Exception ex){
ex.printStackTrace();
}
return null;
}
/**
* 检验token
*
* @param request
* @return
*/
@Override
public boolean checkToken(HttpServletRequest request) throws Exception {
String token = request.getHeader(Constant.TOKEN_NAME);
if (StrUtil.isBlank(token)) {// header中不存在token
token = request.getParameter(Constant.TOKEN_NAME);
if (StrUtil.isBlank(token)) {// parameter中也不存在token
throw new ServiceException(Constant.ResponseCode.ILLEGAL_ARGUMENT, 100);
}
}
if (!redisService.exists(token)) {
throw new ServiceException(Constant.ResponseCode.REPETITIVE_OPERATION, 200);
}
boolean remove = redisService.remove(token);
if (!remove) {
throw new ServiceException(Constant.ResponseCode.REPETITIVE_OPERATION, 200);
}
return true;
}
}
四:拦截器的配置
1:web配置类,实现WebMvcConfigurerAdapter,主要作用就是添加autoIdempotentInterceptor到配置类中,这样我们到拦截器才能生效,注意使用@Configuration注解,这样在容器启动是时候就可以添加进入context中
@Configuration
public class WebConfiguration extends WebMvcConfigurerAdapter {
@Resource
private AutoIdempotentInterceptor autoIdempotentInterceptor;
/**
* 添加拦截器
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(autoIdempotentInterceptor);
super.addInterceptors(registry);
}
}
2:拦截处理器:主要的功能是拦截扫描到AutoIdempotent到注解到方法,然后调用tokenService的checkToken()方法校验token是否正确,如果捕捉到异常就将异常信息渲染成json返回给前端
/**
* 拦截器
*/
@Component
public class AutoIdempotentInterceptor implements HandlerInterceptor {
@Autowired
private TokenService tokenService;
/**
* 预处理
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
//被ApiIdempotment标记的扫描
AutoIdempotent methodAnnotation = method.getAnnotation(AutoIdempotent.class);
if (methodAnnotation != null) {
try {
return tokenService.checkToken(request);// 幂等性校验, 校验通过则放行, 校验失败则抛出异常, 并通过统一异常处理返回友好提示
}catch (Exception ex){
ResultVo failedResult = ResultVo.getFailedResult(101, ex.getMessage());
writeReturnJson(response, JSONUtil.toJsonStr(failedResult));
throw ex;
}
}
//必须返回true,否则会被拦截一切请求
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
/**
* 返回的json值
* @param response
* @param json
* @throws Exception
*/
private void writeReturnJson(HttpServletResponse response, String json) throws Exception{
PrintWriter writer = null;
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html; charset=utf-8");
try {
writer = response.getWriter();
writer.print(json);
} catch (IOException e) {
} finally {
if (writer != null)
writer.close();
}
}
}
五:保障业务数据的幂等性
- 解决接口幂等问题,只需要记住一句口令"一锁、二判、三更新”,只要严格遵守这个过程,那么就可以解决并发问题。
- 一锁:第一步,先加锁。可以加分布式锁、或者悲观锁都可以。但是一定要是一个互斥锁!
- 二判:第二步,进行幂等性判断。可以基于状态机、流水表、唯一性索引等等进行重复操作的判断.
- 三更新:第三步,进行数据的更新,将数据进行持久化。
注意事项:
- 三步需要严格控制顺序,确保加锁成功后进行数据查询和判断,幂等性判断通过后再更新,更新结束后释放锁。
- 以上操作需要有一个前提,那就是第一步加锁、和第二步判断的时候,需要有一个依据,这个就是幂等号了,通常需要和上游约定一个唯一ID作为幂等号。然后通过对幂等号加锁,再通过幂等号进行幂等判断即可。
- 锁这个过程,建议使用Redis实现分布式锁,因为他是非阻塞的高效率的互斥锁。非常适合在幂等控制场景中。
- 二判这个过程,如果有操作流水,建议基于操作流水做幂等,并将幂等号作为唯一性约束,确保唯一性。如果没有流水,那么基于状态机也是可以的。
- 但是不管怎么样,数据库的唯一性约束都要加好,这是系统的最后一道防线。万一前面的锁失效了,这里也能控制
得住不会产生脏数据。
总结
- 本次使用springboot和拦截器、redis来优雅的实现接口幂等,对于幂等在实际的开发过程中是十分重要的,因为一个接口可能会被无数的客户端调用,如何保证其不影响后台的业务处理,如何保证其只影响数据一次是非常重要的,它可以防止产生脏数据或者乱数据,也可以减少并发量,实乃十分有益的一件事。而传统的做法是每次判断数据,这种做法不够智能化和自动化,比较麻烦。而今天的这种自动化处理也可以提升程序的伸缩性。