基础工具组件starter-idempotent-redission设计与实现

一、功能描述

基于redis实现分布式锁,幂等,防重复提交,唯一性校验功能,一个注解即可使用全部特性。

二、实现原理

通过自定义注解将不同场景进行编码,同时基于切面即可完成不同场景下的业务特征需求。

2.1 配置说明

这里是单机版,可以按自己集群进行配置调整

2.1.1 服务配置

server:
  port: 80
spring:
  application:
    name: server
  redis:
    #数据库索引
    database: 0
    host: 127.0.0.1
    port: 6379
    password: root
    lettuce:
      pool:
        #最大连接数
        max-active: 8
        #最大阻塞等待时间(负数表示没限制)
        max-wait: -1
        #最大空闲
        max-idle: 8
        #最小空闲
        min-idle: 0
        #连接超时时间
        timeout: 10000

2.1.2 lua脚本

lock.lua

-- Set a lock
--  如果获取锁成功,则返回 1
local key     = KEYS[1]
local content = KEYS[2]
local ttl     = ARGV[1]
local lockSet = redis.call('setnx', key, content)
if lockSet == 1 then
  redis.call('pexpire', key, ttl)
else 
  -- 如果value相同,则认为是同一个线程的请求,则认为重入锁
  local value = redis.call('get', key)
  if(value == content) then
    lockSet = 1;
    redis.call('pexpire', key, ttl)
  end
end
return lockSet

unlock.lua

-- unlock key
local key     = KEYS[1]
local content = KEYS[2]
local value = redis.call('get', key)
if value == content then
--  redis.call('decr', "count")
  return redis.call('del', key);
end
return 0

2.1.3 幂等表mapper配置

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.coderman.common.starter.idempotent.mapper.IdemPotentMapper">
    <resultMap id="BaseResultMap" type="com.coderman.common.starter.idempotent.bean.IdemPotentBean">
        <id column="id" jdbcType="BIGINT" property="id"/>
        <result column="version" jdbcType="INTEGER" property="version"/>
        <result column="project_name" jdbcType="VARCHAR" property="projectName"/>
        <result column="request_code" jdbcType="VARCHAR" property="requestCode"/>
        <result column="request" jdbcType="LONGVARCHAR" property="requestJson"/>
        <result column="response" jdbcType="LONGVARCHAR" property="responseJson"/>
        <result column="date_create" jdbcType="TIMESTAMP" property="createDate"/>
        <result column="date_update" jdbcType="TIMESTAMP" property="updateDate"/>
    </resultMap>
    <sql id="Base_Column_List">
        id, project_name, request_no, request, response,date_create , date_update,version
    </sql>
    <select id="findById" parameterType="java.lang.Long" resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List"/>
        from trade_idempotency
        where id = #{id,jdbcType=INTEGER}
    </select>
    <delete id="delById" parameterType="java.lang.Long">
        delete from trade_idempotency
        where id = #{id,jdbcType=INTEGER}
    </delete>
    <insert id="insert" keyColumn="id" keyProperty="id"
            parameterType="com.coderman.common.starter.idempotent.bean.IdemPotentBean" useGeneratedKeys="true">
        <!--@mbg.generated-->
        insert into trade_idempotency (request_no, request, response,
        date_create, date_update)
        values (#{requestNo,jdbcType=VARCHAR}, #{request,jdbcType=LONGVARCHAR}, #{response,jdbcType=LONGVARCHAR},
        #{dateCreate,jdbcType=TIMESTAMP}, #{dateUpdate,jdbcType=TIMESTAMP})
    </insert>
    <update id="update" parameterType="com.coderman.common.starter.idempotent.bean.IdemPotentBean">
        update trade_idempotency
        set request_no = #{requestNo,jdbcType=VARCHAR},
        request = #{request,jdbcType=LONGVARCHAR},
        response = #{response,jdbcType=LONGVARCHAR},
        date_create = #{dateCreate,jdbcType=TIMESTAMP},
        date_update = #{dateUpdate,jdbcType=TIMESTAMP}
        where id = #{id,jdbcType=INTEGER}
    </update>
    <select id="findOneByRequestNo" resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List"/>
        from trade_idempotency
        where request_code=#{requestCode,jdbcType=VARCHAR}
    </select>
</mapper>

2.2 代码实现

  1. 自定义注解
package com.coderman.common.starter.idempotent.annotations;


import com.coderman.common.starter.idempotent.service.UniqService;

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

/**
 * 幂等注解
 * 功能1:防止重复提交
 * 功能2:幂等
 * 功能3:锁
 * @author fcs
 * key: projectName_prefixKey_fieldArr[i]
 * value:fieldArr[i]
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisLock {

    /**
     * key前缀--功能code,自定义
     * 默认Constant.value_prefixKey
     * @return
     */
    String prefixKey() default "";

    /**
     * 功能类型
     * 0:防止重复提交
     * 1:幂等
     * 2:锁
     * 3:唯一性校验
     * @return
     */
    int funcType() default 0;

    /**
     * 是否需要持久化
     * 持久化则在业务库中新建表
     * 请求参数json,响应json,请求key,
     * @return
     */
    boolean persisit() default false;

    /**
     * 超时时间配置
     * 单位毫秒,默认两百ms
     *
     * @return
     */
    int timeout() default 200;

    /**
     * 要锁的属性名称
     * @return
     * 优先从属性列表中找,然后从对象中找
     */
    String  [] fieldArr() default ("");

    /**
     * 回调服务bean的接口
     * @return
     */
    String uniqCallBackBeanService() default ("");

}
  1. 核心切面类
package com.coderman.common.starter.idempotent.aspect;

import com.coderman.common.starter.idempotent.Constant;
import com.coderman.common.starter.idempotent.annotations.RedisLock;
import com.coderman.common.starter.idempotent.service.DistributedLocker;
import com.coderman.common.starter.idempotent.service.LockService;
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.CodeSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.List;

@Aspect
@Component
@Order(value = 3)
public class RedisLockAspect {
    private Logger logger = LoggerFactory.getLogger(this.getClass());

    @Value("{spring.application.ame}")
    private String serviceName;


    @Autowired
    private LockService lockService;

    @Pointcut("@annotation(com.coderman.common.starter.idempotent.annotations.RedisLock)")
    public void idemPotentiPointCut() {

    }

    @Transactional(rollbackFor = Exception.class)
    @Around("@annotation(idemPotent)")
    public Object aroundIdemPotent(ProceedingJoinPoint joinPoint, RedisLock idemPotent) throws Throwable {
        Object[] paramValues = joinPoint.getArgs();
        String[] paramNames = ((CodeSignature) joinPoint.getSignature()).getParameterNames();
        List<String> keyList = new ArrayList<>();
        String[] fieldArr = idemPotent.fieldArr();
        /**
         * 适用于防重复提交
         * 唯一业务
         */
        String key = "";
        if (fieldArr.length == 0 || fieldArr[0].equals("")) {
            key = getKey(idemPotent.prefixKey());
        } else {
            keyList = KeyBuilder.getKeyList(paramValues, paramNames, fieldArr);
            if (keyList.size() == 0) {
                keyList = KeyBuilder.getKeyListFromObject(paramValues, fieldArr);
            }
            String[] keyarr = new String[keyList.size()];
            keyList.toArray(keyarr);
            key = getKey(idemPotent.prefixKey(), keyarr);
        }

        //防止重复提交
        if (idemPotent.funcType() == 0 || idemPotent.funcType() == 2) {
            boolean b = lockService.getDistributedLocker().lock(key, idemPotent.timeout());
            //加锁失败
            if(!b){

            }
        }
        //幂等
        else if (idemPotent.funcType() == 1) {
            key = getKey(idemPotent.prefixKey());

        }
        //3:唯一性校验
        else if (idemPotent.funcType() == 3) {
            key = getKey(idemPotent.prefixKey());
            StringBuilder value = new StringBuilder();
            keyList.forEach(v->{
                value.append(v+"-");
            });

        }


        return joinPoint.proceed();
    }

    /**
     * @param prefix
     * @param keyList
     * @return
     */
    private String getKey(String prefix, String... keyList) {
        String basicKey = serviceName + "_" + Constant.NO_REPEAT_PRE + prefix + "_";

        if (keyList == null || keyList.length == 0) {
            return basicKey;
        }
        if (keyList.length > 2) {
            logger.error("key params too much.......");
        }

        if (keyList.length == 1) {
            basicKey = basicKey + keyList[0];
        } else {
            basicKey = basicKey + keyList[0] + "_" + keyList[1];
        }
        return basicKey;
    }


    /**
     * 校验基本类型
     *
     * @param typeName typeName
     * @return boolean
     */
    private static Class checkPrimitive(String typeName) {
        if (typeName.equals("java.lang.String")) {
            return String.class;
        }
        if (typeName.equals("java.lang.Boolean")) {
            return Boolean.class;
        }
        if (typeName.equals("java.lang.Character")) {
            return Character.class;
        }
        if (typeName.equals("java.lang.Byte")) {
            return Byte.class;
        }
        if (typeName.equals("java.lang.Short")) {
            return Short.class;
        }
        if (typeName.equals("java.lang.Integer")) {
            return Integer.class;
        }
        if (typeName.equals("java.lang.Long")) {
            return Long.class;
        }
        if (typeName.equals("java.lang.Float")) {
            return Float.class;
        }
        if (typeName.equals("java.lang.Double")) {
            return Double.class;
        }
        return null;
    }

}
  1. 幂等请求bean
package com.coderman.common.starter.idempotent.bean;

import java.util.Date;

/**
 * Description: 幂等请求持久化
 * date: 2020/9/14 3:17 下午
 *
 * @author fanchunshuai
 * @version 1.0.0
 * @since JDK 1.8
 */
public class IdemPotentBean {
    /**
     * 主键ID
     */
    private Long id;
    /**
     * 项目名称
     */
    private String projectName;
    /**
     * 请求业务编码或者/注解配置的key
     */
    private String requestCode;
    /**
     * 请求的json字符串
     */
    private String requestJson;
    /**
     * 请求类型
     */
    private int funcType;
    /**
     * 响应数据
     */
    private String responseJson;

    /**
     * 使用乐观锁的版本号机制存储幂等数据
     */
    private int version;

    private Date createDate;

    private Date updateDate;

    public Date getUpdateDate() {
        return updateDate;
    }

    public void setUpdateDate(Date updateDate) {
        this.updateDate = updateDate;
    }

    public Date getCreateDate() {
        return createDate;
    }

    public void setCreateDate(Date createDate) {
        this.createDate = createDate;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getProjectName() {
        return projectName;
    }

    public void setProjectName(String projectName) {
        this.projectName = projectName;
    }

    public String getRequestCode() {
        return requestCode;
    }

    public void setRequestCode(String requestCode) {
        this.requestCode = requestCode;
    }

    public String getRequestJson() {
        return requestJson;
    }

    public void setRequestJson(String requestJson) {
        this.requestJson = requestJson;
    }

    public int getFuncType() {
        return funcType;
    }

    public void setFuncType(int funcType) {
        this.funcType = funcType;
    }

    public String getResponseJson() {
        return responseJson;
    }

    public void setResponseJson(String responseJson) {
        this.responseJson = responseJson;
    }

    public int getVersion() {
        return version;
    }

    public void setVersion(int version) {
        this.version = version;
    }
}
  1. 分布式锁定义接口
package com.coderman.common.starter.idempotent.service;

/**
 * Description:
 * date: 2020/9/16 4:29 下午
 *
 * @author fanchunshuai
 * @version 1.0.0
 * @since JDK 1.8
 */
public interface DistributedLocker {
    /**
     * 解锁接口
     * @param lockKey
     */
    void unlock(String lockKey) throws Exception;

    /**
     * 带超时时间的加锁接口
     * @param lockKey  加锁的key
     * @param timeout  超时时间 秒
     * @param tryCount 重试次数
     * @param sleepTime 获取锁失败的休息时间 毫秒
     */
    boolean lock(String lockKey, int timeout,int tryCount,int sleepTime) throws Exception;

    /**
     * 带超时时间的加锁接口
     * @param lockKey
     * @param timeout
     */
    boolean lock(String lockKey, int timeout)throws Exception;
}
  1. redis lua实现
package com.coderman.common.starter.idempotent.service.impl;

import com.coderman.common.starter.idempotent.service.DistributedLocker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

/**
 * Description:
 * date: 2020/9/16 4:31 下午
 *
 * @author fanchunshuai
 * @version 1.0.0
 * @since JDK 1.8
 */
@Service(value = "redisLuaScriptLockImpl")
public class RedisLuaScriptLockImpl implements InitializingBean, DistributedLocker {
    private Logger logger = LoggerFactory.getLogger(RedisLuaScriptLockImpl.class);

    @Autowired
    private StringRedisTemplate redisTemplate;

    // 锁脚本
    private DefaultRedisScript<Long> lockScript;
    // 解锁脚本
    private DefaultRedisScript<Long> unlockScript;


    private ThreadLocal<String> threadKeyId = new ThreadLocal<String>() {
        @Override
        protected String initialValue() {
            return UUID.randomUUID().toString().replace("-", "");
        }
    };


    @Override
    public void unlock(String lockKey) throws Exception {
        List<String> keyList = new ArrayList<String>();
        keyList.add(lockKey);
        keyList.add(threadKeyId.get());
        Long info = redisTemplate.execute(unlockScript, keyList);
        if (info == null) {
            logger.error("释放锁失败,lockKey:{}", lockKey);
        } else {
            logger.debug("成功释放锁,lockKey:{}", lockKey);
        }
    }

    @Override
    public boolean lock(final String lockKey, final int timeout, final int tryCount, int sleepTime) throws Exception {
        //默认睡眠200ms
        if (sleepTime == 0) {
            sleepTime = 200;
        }
        List<String> keyList = new ArrayList<String>();
        keyList.add(lockKey);
        keyList.add(threadKeyId.get());
        int sleep = sleepTime;
        //获取锁次数
        int lockTryCount = 0;
        logger.debug("加锁成功,lockKey:{},准备执行业务操作", lockKey);
        while (true) {
            if (tryCount > 0 && lockTryCount > tryCount) {
                //加锁失败超过设定重试次数,抛出异常表示加锁失败
                throw new RuntimeException("access to distributed lock more than retries:" + tryCount);
            }
            if (lockTryCount > 0) {
                logger.debug("重试获取锁:{}操作:{}次", lockKey, lockTryCount);
            }
            boolean result = redisTemplate.execute(lockScript, keyList, String.valueOf(timeout * 1000)) > 0;
            if (result) {
                //返回结果大于0,表示加锁成功
                logger.debug("加锁成功,lockKey:{},准备执行业务操作", lockKey);
                return true;
            } else {
                try {
                    //如果重试失败则睡眠增加50ms,递增累加休眠时间
                    if (lockTryCount > 1) {
                        sleep = sleep + 50;
                    }
                    Thread.sleep(sleep, (int) (Math.random() * 500));
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            lockTryCount++;
        }
    }

    @Override
    public boolean lock(String lockKey, int timeout) {
        List<String> keyList = new ArrayList<String>();
        keyList.add(lockKey);
        keyList.add(threadKeyId.get());
        boolean result = redisTemplate.execute(lockScript, keyList,timeout) > 0;
        if (result) {
            //返回结果大于0,表示加锁成功
            logger.debug("加锁成功,lockKey:{},准备执行业务操作", lockKey);
            return true;
        } else {
            return false;
        }
    }


    /**
     * 生成操作redis的lua脚本操作实例
     */
    private void initialize() {
        //lock script
        lockScript = new DefaultRedisScript<Long>();
        lockScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lock/lock.lua")));
        lockScript.setResultType(Long.class);
        logger.debug("初始化加锁脚本成功:\n:{}", lockScript.getScriptAsString());

        //unlock script
        unlockScript = new DefaultRedisScript<Long>();
        unlockScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lock/unlock.lua")));
        unlockScript.setResultType(Long.class);
        logger.debug("初始化释放锁脚本成功:\n:{}", unlockScript.getScriptAsString());
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        initialize();
    }
}
  1. redisson实现
package com.coderman.common.starter.idempotent.service.impl;

import com.coderman.common.starter.idempotent.service.DistributedLocker;
import org.springframework.stereotype.Service;

/**
 * Description:
 * date: 2020/9/25 4:39 下午
 *
 * @author fanchunshuai
 * @version 1.0.0
 * @since JDK 1.8
 */
@Service(value = "redissionLockImpl")
public class RedissionLockImpl implements DistributedLocker {


    @Override
    public void unlock(String lockKey) throws Exception {

    }

    @Override
    public boolean lock(String lockKey, int timeout, int tryCount, int sleepTime) throws Exception {
        return false;
    }

    @Override
    public boolean lock(String lockKey, int timeout) throws Exception {
        return false;
    }
}
  1. 锁服务
package com.coderman.common.starter.idempotent.service;

import com.coderman.common.starter.idempotent.service.config.RedisProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

/**
 * Description:
 * date: 2020/9/25 5:11 下午
 *
 * @author fanchunshuai
 * @version 1.0.0
 * @since JDK 1.8
 */
@Service
public class LockService {
    @Resource(name = "redissionLockImpl")
    private DistributedLocker redissionLock;

    @Resource(name = "redisLuaScriptLockImpl")
    private DistributedLocker redisLuaScriptLock;

    @Autowired
    private RedisProperties redisProperties;

    /**
     * 根据配置选择分布式锁的实现
     * @return
     */
    public DistributedLocker getDistributedLocker(){
        if(redisProperties.getLockService().equals("redissionLockImpl")){
            return redissionLock;
        }
        return redisLuaScriptLock;
    }

}

我最近整了一个公众号,持续输出原创内容,敬请关注:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值