SpringBoot 之使用 Redis 实现接口幂等性

幂等概念

在实际的开发项目中,一个对外暴露的接口往往会面临,瞬间大量的重复的请求提交,如果想过滤掉重复请求造成对业务的伤害,那就需要实现幂等!

幂等的概念:
幂等性,就是一个接口, 多次发起同一个请求,,必须保证操作只能执行一次,最终的含义就是对数据库的影响只能是一次性的,不能重复处理。

哪些情况需要防止:

  • 订单接口,不能多次创建订单。
  • 支付接口,重复支付同一笔订单只能扣一次钱。
  • 普通表单提交接口, 因为网络超时等原因多次点击提交, 只能成功一次。
  • 微服务互相调用,由于网络问题,导致请求失败。feign 触发重试机制。

以 SQL 为例,有些操作是天然幂等的。
SELECT * FROM table WHER id=?,无论执行多少次都不会改变状态,是天然的幂等。
UPDATE tab1 SET col1=1 WHERE col2=2,无论执行成功多少次状态都是一致的,也是幂等操作。
delete from user where userid=1,多次操作,结果一样,具备幂等性。
insert into user(userid,name) values(1,‘a’) 如 userid 为唯一主键,即重复操作上面的业务,只 会插入一条用户数据,具备幂等性。

UPDATE tab1 SET col1=col1+1 WHERE col2=2,每次执行的结果都会发生变化,不是幂等的。
insert into user(userid,name) values(1,‘a’) 如 userid 不是主键,可以重复,那上面业务多次操 作,数据都会新增多条,不具备幂等性。

幂等解决方案

常见解决方案:

  • 唯一索引 – 防止新增脏数据
  • token机制 – 防止页面重复提交
  • 悲观锁 – 获取数据的时候加锁(锁表或锁行)
  • 乐观锁 – 基于版本号version实现,在更新数据那一刻校验数据
  • 分布式锁 – redis(jedis、redisson)或zookeeper实现
  • 状态机 – 状态变更, 更新数据时判断状态

token 机制

1、服务端提供了发送 token 的接口。我们在分析业务的时候,哪些业务是存在幂等问题的, 就必须在执行业务前,先去获取 token,服务器会把 token 保存到 redis 中。
2、然后调用业务接口请求时,把 token 携带过去,一般放在请求头部。
3、服务器判断 token 是否存在 redis 中,存在表示第一次请求,然后删除 token,继续执行业 务。
4、如果判断 token 不存在 redis 中,就表示是重复操作,直接返回重复标记给 client,这样 就保证了业务代码,不被重复执行。

危险性:
1、先删除 token 还是后删除 token;
(1) 先删除可能导致,业务确实没有执行,重试还带上之前 token,由于防重设计导致, 请求还是不能执行。
(2) 后删除可能导致,业务处理成功,但是服务闪断,出现超时,没有删除 token,别 人继续重试,导致业务被执行两次。
(3) 我们最好设计为先删除 token,如果业务调用失败,就重新获取 token 再次请求。

2、Token 获取、比较和删除必须是原子性
(1) redis.get(token) 、token.equals、redis.del(token)如果这两个操作不是原子,可能导致,高并发下,都 get 到同样的数据,判断都成功,继续业务并发执行 。
(2) 可以在 redis 使用 lua 脚本完成这个操作。

if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end

各种锁机制

1、数据库悲观锁
select * from xxxx where id = 1 for update。

2、数据库乐观锁
这种方法适合在更新的场景中, update t_goods set count = count -1 , version = version + 1 where good_id=2 and version = 1

3、业务层分布式锁

各种唯一约束

1、数据库唯一约束
插入数据,应该按照唯一索引进行插入,比如订单号,相同的订单就不可能有两条记录插入。

全局请求唯一 id

调用接口时,生成一个唯一 id,redis 将数据保存到集合中(去重),存在即处理过。 可以使用 nginx 设置每一个请求的唯一 id; proxy_set_header X-Request-Id $request_id;

本文实现

通过Redis+Token机制实现接口幂等性校验。
原理图:
在这里插入图片描述

实现思路

为需要保证幂等性,每一次请求创建一个唯一标识token,先获取token,并将此token存入Redis,请求接口时,将此token放到header或者作为请求参数请求接口,后端接口判断Redis中是否存在此token:

  • 如果存在,正常处理业务逻辑, 并从redis中删除此token, 那么,如果是重复请求, 由于token已被删除,则不能通过校验,返回请勿重复操作提示。
  • 如果不存在,说明参数不合法或者是重复请求,返回提示即可。

代码实现

@AutoIdempotent 注解 + 拦截器对请求进行拦截。
@ControllerAdvice 全局异常处理

封装一个操作Redis的API工具类,使用RedisTemplate进行封装,需引入Redis的stater:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;

import java.io.Serializable;
import java.util.concurrent.TimeUnit;

@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表示它在运行时。

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoIdempotent {
}

token服务接口,token创建和检验:
createToken创建token采用随机算法工具类生成随机uuid字符串,然后放入到redis中(为了防止数据的冗余保留,这里设置过期时间为10000秒,具体可视业务而定),如果放入成功,返回这个token值。
checkToken方法就是从header中获取token到值(如果header中拿不到,就从paramter中获取),如若不存在,直接抛出异常。这个异常信息可以被拦截器捕捉到,然后返回给前端。

import javax.servlet.http.HttpServletRequest;

public interface TokenService {
    // 创建token
    String createToken();
    // 检验token
    void checkToken(HttpServletRequest request);
}


import com.demo.exception.ServiceException;
import com.demo.service.RedisService;
import com.demo.service.TokenService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.util.UUID;

@Component
public class TokenServiceImpl implements TokenService {

    private static final String TOKEN_NAME = "token";

    @Autowired
    private RedisService redisService;

    @Override
    public String createToken() {
        String token = UUID.randomUUID().toString();
        try {
            redisService.setEx(token, token, 10000L);
            return token;
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return null;
    }

    @Override
    public void checkToken(HttpServletRequest request) {
        String token = request.getHeader(TOKEN_NAME);
        if (StringUtils.isBlank(token)) {
            // header中不存在token
            token = request.getParameter(TOKEN_NAME);
            if (StringUtils.isBlank(token)) {
                // parameter中也不存在token
                throw new ServiceException("参数不合法,必须带token参数");
            }
        }
        if (!redisService.exists(token)) {
            throw new ServiceException("请勿重复操作");
        }
        boolean remove = redisService.remove(token);
        // 必须再次判断是否移除成功,因为可能多个请求同时执行上面移除的代码,但是最终只有一个返回移除成功的,如果不判断是否移除成功,就会失去幂等性的
        if (!remove) {
            throw new ServiceException("请勿重复操作");
        }
    }
}

在这里插入图片描述

拦截器处理幂等:
主要的功能是拦截扫描到AutoIdempotent到注解到方法,然后调用tokenService的checkToken()方法校验token是否正确,如果捕捉到异常就将异常信息渲染成json返回给前端。

import com.demo.annotation.AutoIdempotent;
import com.demo.service.TokenService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;

/**
 * 接口幂等性拦截器
 */
@Component
public class AutoIdempotentInterceptor implements HandlerInterceptor {

    @Autowired
    private TokenService tokenService;

    /**
     * 预处理
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        AutoIdempotent methodAnnotation = method.getAnnotation(AutoIdempotent.class);
        if (methodAnnotation != null) {
            tokenService.checkToken(request);
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
    }

    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
    }
}

配置拦截器:
继承WebMvcConfigurationSupport,主要作用就是添加autoIdempotentInterceptor到配置类中,这样我们到拦截器才能生效,注意使用@Configuration注解,这样在容器启动是时候就可以添加进入context中。

import com.demo.interceptor.AutoIdempotentInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

import javax.annotation.Resource;

@Configuration
public class WebConfiguration extends WebMvcConfigurationSupport {

    @Resource
    private AutoIdempotentInterceptor autoIdempotentInterceptor;

    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(autoIdempotentInterceptor);
        super.addInterceptors(registry);
    }
}

业务请求类:
通过/get/token路径通过getToken()方法去获取具体的token。
调用test/idempotence方法,这个方法上面注解了@AutoIdempotent,拦截器会拦截所有的请求,当判断到处理的方法上面有该注解的时候,就会调用TokenService中的checkToken()方法,如果捕获到异常会将异常抛出调用者。

import com.demo.annotation.AutoIdempotent;
import com.demo.service.TokenService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
public class BusinessController {

    @Resource
    private TokenService tokenService;

    @GetMapping("/get/token")
    public String getToken() {
        String token = tokenService.createToken();
        return token;
    }


    @AutoIdempotent
    @GetMapping("/test/idempotence")
    public String testIdempotence() {
        return "ok";
    }
}

获取Token:
在这里插入图片描述
第一次请求:
在这里插入图片描述
第二次请求:
在这里插入图片描述
参考:
瞬间几千次的重复提交,我用 SpringBoot+Redis 扛住了
Sprinig Boot + Redis 实现接口幂等性,写得太好了!

  • 2
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
Spring Boot可以使用Redis实现延时队列,可以通过以下步骤实现: 1. 在pom.xml中添加Redis和Jedis的依赖: ``` <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency> ``` 2. 创建一个RedisTemplate对象,用于操作Redis: ``` @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(redisConnectionFactory); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); return template; } ``` 3. 创建一个队列类,用于存储延时任务: ``` public class DelayedTaskQueue { private RedisTemplate<String, Object> redisTemplate; public DelayedTaskQueue(RedisTemplate<String, Object> redisTemplate) { this.redisTemplate = redisTemplate; } public void add(String key, Object value, long delay) { redisTemplate.opsForZSet().add(key, value, System.currentTimeMillis() + delay); } public void remove(String key, Object value) { redisTemplate.opsForZSet().remove(key, value); } public Set<Object> range(String key, long start, long end) { return redisTemplate.opsForZSet().range(key, start, end); } } ``` 4. 创建一个延时任务处理类: ``` @Component public class DelayedTaskProcessor { @Autowired private RedisTemplate<String, Object> redisTemplate; @Scheduled(fixedRate = 1000) public void process() { DelayedTaskQueue queue = new DelayedTaskQueue(redisTemplate); Set<Object> tasks = queue.range("delayed_queue", 0, System.currentTimeMillis()); for (Object task : tasks) { // 处理延时任务 queue.remove("delayed_queue", task); } } } ``` 5. 在需要添加延时任务的地方,调用DelayedTaskQueue的add方法: ``` @Autowired private RedisTemplate<String, Object> redisTemplate; public void addDelayedTask(Object task, long delay) { DelayedTaskQueue queue = new DelayedTaskQueue(redisTemplate); queue.add("delayed_queue", task, delay); } ``` 这样,就可以使用Redis实现延时队列了。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值