Spring Boot 实现幂等性

spring boot 实现幂等性
背景

在分布式服务中,业务在高并发的情况下会消费者出现多次请求的情况.这个时候如果执行插入的业务操作,则数据库中出现多条数据.造成脏数据的产生.而且也是对资源的浪费.此时我们需要做的就是阻止多页业务的处理操作.

执行方案

实现接口的幂等性,让请求只成功一次.这里需要保存一个唯一标示,在下一个请求执行时获取标示如果重复提交则阻止执行.

代码实现

创建一个自定义异常,阻止接口的下一步执行

/**
 * 自定义异常类
 */
public class IdempotentException extends RuntimeException{

    private static final long serialVersionUID = 1L;

    public IdempotentException(String message){
        super(message); 
    }

    @Override
    public String getMessage() {
        return super.getMessage();
    }

}

生成key值工具类

import java.lang.reflect.Method;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import com.alibaba.fastjson.JSON;

/**
 * IdempotentKeyUtil
 * 生成key值工具类
 */
public class IdempotentKeyUtil {

    /**
     * 对接口的参数进行处理生成固定key
     * @param method
     * @param args
     * @return
     */
    public static String  generate(Method method,Object... args) {
        StringBuilder stringBuilder = new StringBuilder(method.toString());
        for (Object  arg : args) {
            stringBuilder.append(toStrinhg(arg));
        }
        //进行md5等长加密
        return md5(stringBuilder.toString());
    }   
    /**
     * 使用jsonObject对数据进行toString,(保持数据一致性)
     * @param object
     * @return
     */
    public static String toStrinhg(Object obj){
        if( obj == null ){
            return "-";
        }
        return JSON.toJSONString(obj);
    }
    
    /**
     * 对数据进行MD5等长加密
     * @param str
     * @return
     */
    public static String md5(String str){
        StringBuilder stringBuilder = new StringBuilder();
        try {
            //选择MD5作为加密方式
            MessageDigest mDigest = MessageDigest.getInstance("MD5");
            mDigest.update(str.getBytes());
            byte b[] =  mDigest.digest();
            int j = 0;
            for (int i = 0,max = b.length; i < max; i++) {
                j = b[i];
                if(j < 0 ){
                    i += 256;
                }else if(j < 16){
                    stringBuilder.append(0);
                }
                stringBuilder.append(Integer.toHexString(j));
            }
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
		}
        return stringBuilder.toString();
    }
}

自定义注解

import java.lang.annotation.Documented;
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)
@Documented
public @interface Idempotent{
    //注解自定义redis的key的一部分
    String key();
    //过期时间
    long expirMillis();
}

AOP对我们自定义注解进行拦截处理

import java.lang.reflect.Method;
import java.util.Objects;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import redis.clients.jedis.JedisCommands;


/**
 * 自定义切点
 */
@Component
@Aspect
@ConditionalOnClass(RedisTemplate.class)
public class IdempotentAspect{
    private static final String KEY_TEMPLATE = "idempotent_%S";
    
    @Autowired
    private RedisTemplate<String,String> redisTemplate;

    /**
     * 切点(自定义注解)
     */
    @Pointcut("@annotation(com.idempotent.core.Idempotent)")
    public void executeIdempotent(){
        
    }

    /**
     * 切点业务
     * 
     * @throws Throwable
     */
    @Around("executeIdempotent()")
    public Object arountd(ProceedingJoinPoint jPoint) throws Throwable {
        //获取当前方法信息
        Method method = ((MethodSignature)jPoint.getSignature()).getMethod();
        //获取注解
        Idempotent idempotent = method.getAnnotation(Idempotent.class);
        //生成Key
        String key = String.format(KEY_TEMPLATE,idempotent.key()+"_"+IdempotentKeyUtil.generate(method, jPoint.getArgs()));
        //https://segmentfault.com/a/1190000002870317 -- JedisCommands接口的分析
        //nxxx的值只能取NX或者XX,如果取NX,则只有当key不存在是才进行set,如果取XX,则只有当key已经存在时才进行set
        //expx expx的值只能取EX或者PX,代表数据过期时间的单位,EX代表秒,PX代表毫秒
        // key value nxxx(set规则) expx(取值规则) time(过期时间)
        String redisRes =  redisTemplate.execute((RedisCallback<String>)conn ->((JedisCommands)conn.getNativeConnection()).set(key, key,"NX","EX",idempotent.expirMillis()));
        // Jedis jedis = new Jedis("127.0.0.1",6379);
        // jedis.auth("xuzz");
        // jedis.select(0);
        // String redisRes = jedis.set(key, key,"NX","EX",idempotent.expirMillis());
        if(Objects.equals("OK", redisRes)){
            return jPoint.proceed();
        }else{
            //throw new IdempotentException("sorry!! Interface duplicates requests, violating idempotency.");
            System.err.println("数据错误");
            return null;
        }
       
    }

}

service 接口

import com.alibaba.fastjson.JSONObject;

/**
 * TestService
 */
public interface TestService {
    /**
     * 数据测试
     */
    public void print(JSONObject params);
}

service 实现类

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.idempotent.core.Idempotent;

import org.springframework.stereotype.Service;

/**
 * TestServiceImpl
 */
@Service
public class TestServiceImpl implements TestService {

    /**
     * 
     */
    @Idempotent(key = "com.idempotent.controller.IdempotentController",expirMillis = 100)
    @Override
    public void print(JSONObject params) {
        System.err.println(JSON.toJSONString(params));
	} 
}

controller

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.idempotent.core.TestService;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

/**
 * IdempotentController
 */
@RestController
public class IdempotentController {
    @Autowired
    private TestService testService;

    /**
     * 测试push
     */
    //@Idempotent(key = "com.idempotent.controller.IdempotentController",expirMillis = 100)
    @PostMapping("/push/test")
    public String pushTest(@RequestBody JSONObject params){
        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                try {
                    testService.print(params);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }
        return JSON.toJSONString(params);
    }
    
}

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.1.7.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.idempotent</groupId>
	<artifactId>idempotent-dome</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>demo</name>
	<description>Demo project for Spring Boot</description>

	<properties>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<!-- <dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
		</dependency> -->
		
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
			<exclusions>
				<exclusion>
					<groupId>io.lettuce</groupId>
					<artifactId>lettuce-core</artifactId>
				</exclusion>
			</exclusions>
			<version>2.1.7.RELEASE</version>
		</dependency>
		<dependency>
			<groupId>redis.clients</groupId>
			<artifactId>jedis</artifactId>
			<version>2.10.2</version>
		</dependency>
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>fastjson</artifactId>
			<version>1.2.59</version>
		</dependency>
		<dependency>
			<groupId>org.aspectj</groupId>
			<artifactId>aspectjweaver</artifactId>
			<version>1.9.4</version>
		</dependency>
		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-pool2</artifactId>
			<version>2.6.0</version>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>

application.yml

server:
  port: 7001
spring:
  redis:
    database: 0
    host: 127.0.0.1
    port: 6379
    password: xxxx
    timeout: 1000
    pool:
      max-wait: 10
      max-active: 1
      max-idle: 2
      min-idle: 50

环境

jdk1.8,docker-redis

发布了65 篇原创文章 · 获赞 85 · 访问量 27万+

猜你喜欢

转载自blog.csdn.net/u013521220/article/details/103700813