手寫一個冪等組件分享

前言

最近學了極客時間王爭的《設計模式之美》,感覺以前對設計模式理解的太淺顯了。文章上有一篇設計冪等框架的練習,作者給了簡單的思路和實現,但是沒有給出切面控制,理由是冪等跟業務是高耦合的,我在看之後結合經驗,覺得有必要實現一下功能更強大的功能所以手寫了個小組件。

代碼地址:我的個人組件倉庫 其中的idempotence模塊。

實現功能

  • 基於切面實現註解控制需要實現冪等的接口,冪等號根據方法參數與註解上的spel表達式得到,由業務控制。
  • 判斷冪等號相等後,判斷爲冪等,註解可以指定兩種策略,拋出一個固定的異常或者根據上次執行結果返回執行結果或返回上次執行時產生的異常。
  • 註解可以業務發生異常的時候,哪些異常是不需要刪除冪等號記錄的,默認不刪除冪等號記錄即下一次請求可以正常通過。
  • 如果想在業務中實現冪等,可以應用IdempotenceClient提供的常用的方法。

組件擴展性

  • Storge爲冪等號的存儲對象,根據需要自由選擇不同的存儲實現,比如基於redisson,mogodb什麼的。
  • 提供CacheableStorage,可以注入兩個storge,一個爲緩存用的,一個爲持久化用的。

代碼

代碼結構:

IdempotenceClient 冪等組件,相關方法的統一入口

package com.cman777.springc.idempotence;

import com.cman777.springc.idempotence.storage.Storage;
import org.springframework.beans.factory.annotation.Autowired;

import java.io.Serializable;
import java.util.function.Supplier;

/**
 * @author chenzhicong
 * @time 2020/9/15 10:29
 */
public class IdempotenceClient {
    private Storage storage;
    @Autowired
    public IdempotenceClient(Storage cache) {
        this.storage = cache;
    }
    public  boolean saveIfAbsent(String idempotenceId, Supplier<ResultWrapper> supplier) {
        return storage.setIfAbsent(idempotenceId,supplier);
    }
    public <T extends Serializable> ResultWrapper<T>  getResult(String idempotenceId){
        return storage.getResult(idempotenceId);
    }



    public boolean delete(String idempotenceId) {
        return   storage.delete(idempotenceId);
    }


    public boolean exists(String idempotenceId){
        return  storage.exists(idempotenceId);
    }


}

Storage 提供存儲冪等標識職責

package com.cman777.springc.idempotence.storage;

import com.cman777.springc.idempotence.ResultWrapper;

import java.io.Serializable;
import java.util.function.Supplier;

/**
 * @author chenzhicong
 * @time 2020/9/15 10:30
 */
public interface Storage {
    /**
     * supplier不一定會執行  保證原子性
     */
    boolean setIfAbsent(String key, Supplier<ResultWrapper> supplier);

    /**
     * 得保證冪等性
     */
    boolean setIfAbsent(String key, ResultWrapper value);

    boolean delete(String idempotenceId);


    <T extends Serializable> ResultWrapper<T> getResult(String idempotenceId);


    boolean exists(String key);


}
package com.cman777.springc.idempotence;

import lombok.Getter;
import lombok.Setter;

import java.io.Serializable;

/**
 * @author chenzhicong
 * @time 2020/9/15 18:14
 */
@Getter
@Setter
public class ResultWrapper<T extends Serializable> implements Serializable {
    private Throwable exception;
    private T result;
    private boolean hasException;
}



package com.cman777.springc.idempotence.storage;

import com.cman777.springc.idempotence.ResultWrapper;
import com.cman777.springc.redis.annotation.RedisLock;
import lombok.extern.log4j.Log4j2;

import java.io.Serializable;
import java.util.function.Supplier;

/**
 * @author chenzhicong
 * @time 2020/9/15 10:41
 */
@Log4j2
public class CacheableStorage implements Storage {
    private Storage cacheStorage;
    private Storage persistenceStorage;

    public CacheableStorage(Storage cacheStorage, Storage persistenceStorage) {
        this.cacheStorage = cacheStorage;
        this.persistenceStorage = persistenceStorage;
    }

    @Override
    @RedisLock(fixedPrefix = "Idempoment_CacheableStorage_setIfAbsent",salt = "#key")
    public boolean setIfAbsent(String key, Supplier<ResultWrapper> supplier){
        if(this.exists(key)){
            return false;
        }else{
            return this.setIfAbsent(key,supplier.get());
        }
    }

    @Override
    public boolean setIfAbsent(String key, ResultWrapper resultWrapper) {
        boolean cacheHave = !cacheStorage.setIfAbsent(key,resultWrapper);
        if (cacheHave) {
            log.info("緩存中已存在冪等鍵={}", key);
            return false;
        } else {
            boolean success = persistenceStorage.setIfAbsent(key,resultWrapper);
            if (!success) {
                log.info("持久層中已存在冪等鍵={}", key);
            }
            return success;
        }
    }


    @Override
    public boolean delete(String idempotenceId) {
        try {
            cacheStorage.delete(idempotenceId);
            persistenceStorage.delete(idempotenceId);
            return true;
        } catch (Exception e) {
            log.info("刪除冪等鍵異常");
            log.error(e.getMessage(), e);
            return false;
        }

    }

    @Override
    public <T extends Serializable> ResultWrapper<T> getResult(String idempotenceId) {
        ResultWrapper result = cacheStorage.getResult(idempotenceId);
        if(result == null){
            result =  persistenceStorage.getResult(idempotenceId);
            cacheStorage.setIfAbsent(idempotenceId,result);
        }
        return  result;
    }

    @Override
    public boolean exists(String key) {
        boolean isExists = cacheStorage.exists(key);
        if(!isExists){
            isExists = persistenceStorage.exists(key);
        }
        return isExists;
    }
}
package com.cman777.springc.idempotence.storage;

import com.cman777.springc.idempotence.ResultWrapper;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient;

import java.io.*;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

/**
 * @author chenzhicong
 * @time 2020/9/15 10:55
 */
@Log4j2
public class RedissionStorageAdapter implements Storage {
    private Long cacheTime;
    private RedissonClient redissonClient;

    public RedissionStorageAdapter(RedissonClient redissonClient, Long cacheTime) {
        this.redissonClient = redissonClient;
        this.cacheTime = cacheTime;
    }

    @Override
    public boolean setIfAbsent(String key, Supplier<ResultWrapper> supplier) {
        if (this.exists(key)) {
            return false;
        } else {
            return this.setIfAbsent(key, supplier.get());
        }
    }

    @Override
    public boolean setIfAbsent(String key, ResultWrapper value) {
        String valueStr = null;
        try {
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            ObjectOutputStream out = new ObjectOutputStream(byteArrayOutputStream);
            out.writeObject(value);
            byte[] exceptionBytes = byteArrayOutputStream.toByteArray();
            valueStr = Base64.encodeBase64String(exceptionBytes);
            log.info(valueStr.length());
        } catch (Exception ex) {
            log.error("序列化錯誤", ex);
            return false;
        }
        if (cacheTime == null) {
            return redissonClient.getBucket(key).trySet(valueStr);
        } else {
            return redissonClient.getBucket(key).trySet(valueStr, cacheTime, TimeUnit.SECONDS);
        }

    }

    @Override
    public boolean delete(String idempotenceId) {
        return redissonClient.getBucket(idempotenceId).delete();
    }

    @Override
    public <T extends Serializable> ResultWrapper<T> getResult(String idempotenceId) {
        ResultWrapper<T> resultWrapper = null;
        try {
            String value = String.valueOf(redissonClient.getBucket(idempotenceId).get());
            if(StringUtils.isBlank(value)){
                return null;
            }
            log.info(value.length());
            ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(Base64.decodeBase64(value)));
            resultWrapper = (ResultWrapper<T>) ois.readObject();
        } catch (Exception e) {
            log.error("反序列化錯誤", e);
        }
        return resultWrapper;
    }

    @Override
    public boolean exists(String key) {
        return redissonClient.getBucket(key).isExists();
    }
}

  • RedisLock註解是另外實現了的分佈式鎖切面可以在項目中其他模塊看到。
  • 使用的序列化方式是jdk的序列化,然後base64成字符串,這裏待優化,效率低並且沒法擴展,實際可以將序列化反序列化抽象爲接口。

IdempotenceId 註解與IdempotenceAspect,基於註解實現接口冪等

package com.cman777.springc.idempotence.annotation;

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

/**
 * @author chenzhicong
 * @time 2020/9/15 11:32
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface IdempotenceId {
    /**
     * 與salt共同決定IdempotenceId
     */
    String prefix() default "";
    /**
     * spel表達式
     */
    String salt();
    /**
     * 默認發生異常則刪除冪等鍵,但有些情況不刪除則填入這裏,比如有些異常攜帶了業務含義
     * 就算重試了100次還是同樣的結果,則添加異常類在這裏
     */
    Class<? extends Exception>[] notDeleteForException() default {};

    /**
     * 觸發冪等的策略
     * THROW_EXCEPTION: 拋出一個固定的異常
     * RETURN_RESULT: 拋出原來的異常(添加到notDeleteForException的異常),或原來的返回結果
     */
    Strategy strategy() default Strategy.RETURN_RESULT;

    enum Strategy{

        THROW_EXCEPTION("THROW_EXCEPTION","拋異常"),

        RETURN_RESULT("RETURN_RESULT","返回結果或拋出原異常");
        private String code;
        private String msg;
        Strategy(String code,String msg){
            this.code=code;
            this.msg = msg;
        }

    }
}

IdempotenceConfig 用於在上層註冊好IdempotenceClient後自動注入切面到容器

package com.cman777.springc.idempotence.config;

import com.cman777.springc.idempotence.IdempotenceClient;
import com.cman777.springc.idempotence.IdempotenceAspect;
import com.cman777.springc.redis.config.RedisConfig;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author chenzhicong
 * @time 2020/9/15 11:31
 */
@Configuration
@AutoConfigureAfter(RedisConfig.class)
public class IdempotenceConfig {


    @Bean
    @ConditionalOnBean(IdempotenceClient.class)
    @SuppressWarnings("all")
    public IdempotenceAspect idempotenceAspect(IdempotenceClient idempotenceClient){
        return new IdempotenceAspect(idempotenceClient);
    }

}

以上就是組件包的所有代碼,沒有提供持久層存儲的Storge實現,需要上層業務端自己實現,然後註冊IdempotenceClient纔會生效。

業務端引用

MybatisPlusStorageAdapter:mybatisPlus的存儲實現

package com.cman777.springc.sample.config.idempotence;
import com.cman777.springc.idempotence.ResultWrapper;
import com.cman777.springc.idempotence.storage.Storage;
import com.cman777.springc.redis.annotation.RedisLock;
import com.cman777.springc.sample.bean.po.Idempotence;
import com.cman777.springc.sample.service.IdempotenceService;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.codec.binary.Base64;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.io.*;
import java.util.function.Supplier;

/**
 * @author chenzhicong
 * @time 2020/9/15 14:16
 */
@Component
@Log4j2
public class MybatisPlusStorageAdapter implements Storage {
    @Autowired
    private IdempotenceService idempotenceService;


    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class)
    @RedisLock(entity = Idempotence.class,salt = "#key")
    public boolean setIfAbsent(String key, Supplier<ResultWrapper> supplier) {
        if(this.exists(key)){
            return false;
        }else{
            return this.setIfAbsent(key,supplier.get());
        }
    }

    @Override
    @RedisLock(entity = Idempotence.class,salt = "#key")
    @Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class)
    public boolean setIfAbsent(String key, ResultWrapper value) {
        Idempotence idempotence = idempotenceService.selectByIdempotentceId(key);
        if (idempotence != null) {
            return false;
        } else {
            String valueStr = null;
            try {
                ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
                ObjectOutputStream out = new ObjectOutputStream(byteArrayOutputStream);
                out.writeObject(value);
                byte[] exceptionBytes = byteArrayOutputStream.toByteArray();
                valueStr = Base64.encodeBase64String(exceptionBytes);
            } catch (Exception ex) {
                log.error("序列化錯誤", ex);
                return false;
            }
            Idempotence idempotenceNew = new Idempotence();
            idempotenceNew.setIdempotenceId(key);
            idempotenceNew.setValue(valueStr);
            idempotenceService.save(idempotenceNew);
            return true;
        }
    }

    @Override
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public boolean delete(String idempotenceId) {
        idempotenceService.deleteByIdempotentceId(idempotenceId);
        return true;
    }

    @Override
    public <T extends Serializable> ResultWrapper<T> getResult(String idempotenceId) {
        Idempotence idempotence = idempotenceService.selectByIdempotentceId(idempotenceId);
        ResultWrapper<T> resultWrapper = null;
        try {
            String value = idempotence.getValue();
            ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(Base64.decodeBase64(value)));
            resultWrapper = (ResultWrapper<T>) ois.readObject();
        } catch (Exception e) {
            log.error("反序列化錯誤", e);
        }
        return resultWrapper;
    }



    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class)
    public boolean exists(String key) {
        Idempotence idempotence = idempotenceService.selectByIdempotentceId(key);
        if (idempotence != null) {
            return true;
        } else {
            return false;
        }
    }
}

IdempotenceConfig 註冊IdempotenceClient與CacheableStorage

package com.cman777.springc.sample.config.idempotence;

import com.cman777.springc.idempotence.IdempotenceClient;
import com.cman777.springc.idempotence.storage.CacheableStorage;
import com.cman777.springc.idempotence.storage.RedissionStorageAdapter;

import org.redisson.api.RedissonClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author chenzhicong
 * @time 2020/9/15 14:14
 */
@Configuration
public class IdempotenceConfig {

    private Long cacheSeconds = 60L;

    @Bean
    public CacheableStorage cacheableStorage(RedissonClient redissonClient, MybatisPlusStorageAdapter mybatisPlusStorageAdapter){
        return new CacheableStorage(new RedissionStorageAdapter(redissonClient,cacheSeconds),mybatisPlusStorageAdapter);
    }
    @Bean
    public IdempotenceClient idempotenceClient(CacheableStorage cacheableStorage){
        return new IdempotenceClient(cacheableStorage);
    }
}

爲什麼要單獨註冊cacheableStorage是因爲需要應用cacheableStorage的分佈式鎖切面,爲了讓springboot自己生成代理類。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章