MVC方案
多版本并发控制,该策略主要使用 update with condition(更新带条件来防止)来保证多次外部请求调用对系统的影响是一致的。在系统设计的过程中,合理的使用乐观锁,通过 version 或者 updateTime(timestamp)等其他条件,来做乐观锁的判断条件,这样保证更新操作即使在并发的情况下,也不会有太大的问题。例如
select * from tablename where condition=#condition# // 取出要跟新的对象,带有版本 versoin
update tableName set name=#name#,version=version+1 where version=#version#
在更新的过程中利用 version 来防止,其他操作对对象的并发更新,导致更新丢失。为了避免失败,通常需要一定的重试机制。
Token机制,防止页面重复提交
业务要求:页面的数据只能被点击提交一次。
发生原因:由于重复点击或者网络重发,或者 nginx 重发等情况会导致数据被重复提交
解决办法:
集群环境:采用 token 加 redis(redis 单线程的,处理需要排队)
单 JVM 环境:采用 token 加 redis 或 token 加 jvm 内存
处理流程:
数据提交前要向服务的申请 token,token 放到 redis 或 jvm 内存,token 有效时间
提交后后台校验 token,同时删除 token,生成新的 token 返回
token 特点:要申请,一次有效性,可以限流
基于Token方式防止API接口幂等
客户端每次在调用接口的时候,需要在请求头中,传递令牌参数,每次令牌只能用一次。
一旦使用之后,就会被删除,这样可以有效防止重复提交。
步骤:
- 生成令牌接口
- 接口中获取令牌验证
实战教程
要用到aop跟Redis , 所以在pom中添加
<!-- Redis-Jedis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- springboot-aop 技术 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
这是通过注解实现接口幂等性,先写Redis逻辑
@Component
public class BaseRedisService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
public void setString(String key, Object data, Long timeout) {
if (data instanceof String) {
String value = (String) data;
// 往Redis存值
stringRedisTemplate.opsForValue().set(key, value);
}
if (timeout != null) {
// 带时间缓存
stringRedisTemplate.expire(key, timeout, TimeUnit.SECONDS);
}
}
/**
* 查看是否有值
* @param key 值
* @return
*/
public Object getString(String key) {
return stringRedisTemplate.opsForValue().get(key);
}
/**
* 删除Redis
* @param key 值
*/
public void delKey(String key) {
stringRedisTemplate.delete(key);
}
}
然后写怎么生成token,保证每个token只用一次
@Component
public class RedisToken {
@Autowired
private BaseRedisService baseRedisService;
/** 缓存指定时间200秒 */
private static final long TOKENTIMEOUT = 200;
/**
* 生成Token
*/
public String getToken(){
String token = UUID.randomUUID().toString();
// 将token放到Redis中,用UUID保证唯一性
baseRedisService.setString(token, token, TOKENTIMEOUT);
return token;
}
public synchronized boolean findToken(String tokenKey) {
String tokenValue = (String) baseRedisService.getString(tokenKey);
// 如果能够获取该(从redis获取令牌)令牌(将当前令牌删除掉) 就直接执行该访问的业务逻辑
if (StringUtils.isEmpty(tokenValue)) {
return false;
}
// 保证每个接口对应的token 只能访问一次,保证接口幂等性问题,用完直接删掉
baseRedisService.delKey(tokenValue);
return true;
}
}
写一个工具类 请求是通过http请求还是from提交过来的,大部分都是form提交来的
public interface ConstantUtils {
/**
* http 中携带的请求
*/
static final String EXTAPIHEAD = "head";
/**
* from 中提交的请求
*/
static final String EXTAPIFROM = "from";
}
写好了 现在就写我们的注解了,没带参数的是前后端不分离,直接跳页面,获取到token,带参数前后端不分离的
带参数的
@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExtApiIdempotent {
String value();
}
不带参数的
@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExtApiToken {
}
写好这个 要是aop切点,要把注解切入进去
@Aspect
@Component
public class ExtApiAopIdempotent {
@Autowired
private RedisToken redisToken;
// 1.使用AOP环绕通知拦截所有访问(controller)
@Pointcut("execution(public * com.yuyi.controller.*.*(..))")
public void rlAop() {
}
/**
* 封装数据
*/
public HttpServletRequest getRequest() {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
return request;
}
/**
* 前置通知
*/
@Before("rlAop()")
public void before(JoinPoint point) {
// 获取被增强的方法相关信息 - 查看方法上是否有次注解
MethodSignature signature = (MethodSignature) point.getSignature();
ExtApiToken extApiToken = signature.getMethod().getDeclaredAnnotation(ExtApiToken.class);
if (extApiToken != null) {
// 可以放入到AOP代码 前置通知
getRequest().setAttribute("token", redisToken.getToken());
}
}
/**
* 环绕通知
*/
@Around("rlAop()")
public Object doBefore(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
// 获取被增强的方法相关信息 - 查看方法上是否有次注解
MethodSignature methodSignature = (MethodSignature) proceedingJoinPoint.getSignature();
ExtApiIdempotent declaredAnnotation = methodSignature.getMethod().getDeclaredAnnotation(ExtApiIdempotent.class);
if (declaredAnnotation != null) {
String values = declaredAnnotation.value();
String token = null;
HttpServletRequest request = getRequest();
if (values.equals(ConstantUtils.EXTAPIHEAD)) {
token = request.getHeader("token");
} else {
token = request.getParameter("token");
}
// 获取不到token
if (StringUtils.isEmpty(token)) {
return ResultTool.error(ExceptionNume.PARAMETER_ERROR);
}
// 接口获取对应的令牌,如果能够获取该(从redis获取令牌)令牌(将当前令牌删除掉) 就直接执行该访问的业务逻辑
boolean isToken = redisToken.findToken(token);
// 接口获取对应的令牌,如果获取不到该令牌 直接返回请勿重复提交
if (!isToken) {
return ResultTool.error(ExceptionNume.REPEATED_SUBMISSION);
}
}
Object proceed = proceedingJoinPoint.proceed();
return proceed;
}
}
controller层 大家可以测一下
@Autowired
private OrderInfoDAO infoDAO;
@Autowired
private RedisToken token;
// @Autowired
// private RedisTokenUtils redisTokenUtils;
//
// 从redis中获取Token
@RequestMapping("/redisToken")
public String getRedisToken() {
return token.getToken();
}
@RequestMapping("/addOrderExtApiIdempotent")
@ExtApiIdempotent(ConstantUtils.EXTAPIFROM)
public ResultBO<?> addOrderExtApiIdempotent(
@RequestParam String orderName,
@RequestParam String orderDes
) {
int result = infoDAO.addOrderInfo(orderName, orderDes);
return ResultTool.success(result);
}
保证了只能请求一次。前后端没有分离的,@ExtApiToken带上注解会自动吧token携带过去