一、功能描述
基于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 代码实现
- 自定义注解
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 ("");
}
- 核心切面类
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;
}
}
- 幂等请求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;
}
}
- 分布式锁定义接口
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;
}
- 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();
}
}
- 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;
}
}
- 锁服务
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;
}
}
我最近整了一个公众号,持续输出原创内容,敬请关注: