Ищите «Java Fish Aberdeen» в WeChat, и вы не пропустите ни одного очка знаний каждый день.
Одно очко знаний в день
Что такое идемпотентность интерфейса и как ее реализовать?
(1) концепция идемпотентности
Идемпотентность изначально является математической концепцией, и ее можно понять при использовании в интерфейсах: один и тот же интерфейс, если один и тот же запрос выдается несколько раз, должен гарантировать, что операция будет выполнена только один раз. Когда в вызывающем интерфейсе возникает исключение и повторные попытки всегда будут приводить к потерям, которые система не может вынести, необходимо предотвратить возникновение этого явления. Например, в следующих случаях, если не будет реализована идемпотентность интерфейса, будут серьезные последствия: Платежный интерфейс, повторная оплата приведет к множественным вычетам Интерфейс заказа, один и тот же заказ может быть создан несколько раз.
(2) Идемпотентное решение
уникальный индексИспользование уникального индекса позволяет избежать добавления грязных данных.При вставке повторяющихся данных база данных выдает исключение, чтобы обеспечить уникальность данных.
оптимистическая блокировкаОптимистическая блокировка здесь относится к реализации принципа оптимистической блокировки, добавляя поле версии к полю данных.Когда данные необходимо обновить, сначала перейдите в базу данных, чтобы получить номер версии в это время.
select version from tablename where xxx
При обновлении данных сначала сравните его с номером версии, если он не равен, значит есть другие запросы на обновление данных, и обновление не проходит.
update tablename set count=count+1,version=version+1 where version=#{version}
пессимистический замокОптимистическая блокировка часто может быть достигнута с помощью пессимистической блокировки.Блокировка выполняется при получении данных.При одновременном выполнении нескольких повторяющихся запросов другие запросы не могут быть обработаны.
Распределенная блокировкаСуть идемпотентности заключается в проблеме распределенных блокировок.Распределенные блокировки обычно могут быть реализованы через redis или zookeeper;в распределенной среде глобальный уникальный ресурс блокируется для сериализации запроса, что на самом деле является взаимоисключающей блокировкой для предотвращения дублирования и решения Идемпотент.
жетонный механизмОсновные механизмы токена идеи для каждой операции генерируют учетные данные уникальности, которые являются токенами. Токен на каждом этапе реализации имеет право действовать только один раз, при успешном выполнении сохраняются результаты выполнения. Повторный запрос возвращает тот же результат. Механизм токена приложения очень обширен.
(3) Реализация механизма токенов
Вот случай реализации идемпотентности интерфейса с помощью механизма токенов: самозагрузка в конце статьи на github. Сначала импортируйте необходимые зависимости:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.4</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
3.1 Настройте тело метода и класс перечисления запроса
Сначала настройте общий текст возврата запроса.
public class Response {
private int status;
private String msg;
private Object data;
//省略get、set、toString、无参有参构造方法
}
и код возврата
public enum ResponseCode {
// 通用模块 1xxxx
ILLEGAL_ARGUMENT(10000, "参数不合法"),
REPETITIVE_OPERATION(10001, "请勿重复操作"),
;
ResponseCode(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
private Integer code;
private String msg;
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
3.2 Настройка исключений и настройка глобальных классов исключений
public class ServiceException extends RuntimeException{
private String code;
private String msg;
//省略get、set、toString以及构造方法
}
Настроить глобальный обработчик исключений
@ControllerAdvice
public class MyControllerAdvice {
@ResponseBody
@ExceptionHandler(ServiceException.class)
public Response serviceExceptionHandler(ServiceException exception){
Response response=new Response(Integer.valueOf(exception.getCode()),exception.getMsg(),null);
return response;
}
}
3.3 Напишите интерфейс и класс реализации для создания токена и проверки токена
@Service
public interface TokenService {
public Response createToken();
public Response checkToken(HttpServletRequest request);
}
Конкретный класс реализации, основная бизнес-логика написана в комментариях
@Service
public class TokenServiceImpl implements TokenService {
@Autowired
private RedisTemplate redisTemplate;
@Override
public Response createToken() {
//生成uuid当作token
String token = UUID.randomUUID().toString().replaceAll("-","");
//将生成的token存入redis中
redisTemplate.opsForValue().set(token,token);
//返回正确的结果信息
Response response=new Response(0,token.toString(),null);
return response;
}
@Override
public Response checkToken(HttpServletRequest request) {
//从请求头中获取token
String token=request.getHeader("token");
if (StringUtils.isBlank(token)){
//如果请求头token为空就从参数中获取
token=request.getParameter("token");
//如果都为空抛出参数异常的错误
if (StringUtils.isBlank(token)){
throw new ServiceException(ResponseCode.ILLEGAL_ARGUMENT.getCode().toString(),ResponseCode.ILLEGAL_ARGUMENT.getMsg());
}
}
//如果redis中不包含该token,说明token已经被删除了,抛出请求重复异常
if (!redisTemplate.hasKey(token)){
throw new ServiceException(ResponseCode.REPETITIVE_OPERATION.getCode().toString(),ResponseCode.REPETITIVE_OPERATION.getMsg());
}
//删除token
Boolean del=redisTemplate.delete(token);
//如果删除不成功(已经被其他请求删除),抛出请求重复异常
if (!del){
throw new ServiceException(ResponseCode.REPETITIVE_OPERATION.getCode().toString(),ResponseCode.REPETITIVE_OPERATION.getMsg());
}
return new Response(0,"校验成功",null);
}
}
3.4 Настройка пользовательских аннотаций
Это более важный шаг.Добавьте эту аннотацию в метод, который должен реализовать идемпотентность интерфейса с помощью пользовательской аннотации для реализации проверки токена.
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiIdempotent {
}
перехватчик интерфейса
public class ApiIdempotentInterceptor implements HandlerInterceptor {
@Autowired
private TokenService tokenService;
@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();
ApiIdempotent methodAnnotation=method.getAnnotation(ApiIdempotent.class);
if (methodAnnotation != null){
// 校验通过放行,校验不通过全局异常捕获后输出返回结果
tokenService.checkToken(request);
}
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 {
}
}
3.5 Настройка перехватчиков и Redis
Настроить webConfig, добавить перехватчик
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(apiIdempotentInterceptor());
}
@Bean
public ApiIdempotentInterceptor apiIdempotentInterceptor() {
return new ApiIdempotentInterceptor();
}
}
Настройте Redis, чтобы китайский язык мог нормально передаваться
@Configuration
public class RedisConfig {
//自定义的redistemplate
@Bean(name = "redisTemplate")
public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory){
//创建一个RedisTemplate对象,为了方便返回key为string,value为Object
RedisTemplate<String,Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
//设置json序列化配置
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer=new
Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper=new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance);
//string的序列化
StringRedisSerializer stringRedisSerializer=new StringRedisSerializer();
//key采用string的序列化方式
template.setKeySerializer(stringRedisSerializer);
//value采用jackson的序列化方式
template.setValueSerializer(jackson2JsonRedisSerializer);
//hashkey采用string的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
//hashvalue采用jackson的序列化方式
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
Наконец-то контроллер
@RestController
@RequestMapping("/token")
public class TokenController {
@Autowired
private TokenService tokenService;
@GetMapping
public Response token(){
return tokenService.createToken();
}
@PostMapping("checktoken")
public Response checktoken(HttpServletRequest request){
return tokenService.checkToken(request);
}
}
Остальной код взят из ссылки на github в конце статьи.
(4) Проверка результатов
Для начала создадим токен через интерфейс токена, на данный момент токен есть и в redis.
Выполняя 50 запросов одновременно в jmeter, мы можем наблюдать, что только первый запрос успешно проверен, а последующие запросы не повторяют операцию.
Файл стресс-теста jmeter (Token Plan.jmx) и код загружаются автоматически:гитхаб самовывоз