优化生成单据编号的逻辑,采用redis分布式锁的方式
This commit is contained in:
@@ -585,6 +585,17 @@ public class ExceptionConstants {
|
||||
public static final int REPORT_TWO_MANY_DEPOT_FAILED_CODE = 510;
|
||||
public static final String REPORT_TWO_MANY_DEPOT_FAILED_MSG = "请选择仓库,再进行查询";
|
||||
|
||||
/**
|
||||
* 生成单据编号
|
||||
* type = 120
|
||||
* */
|
||||
//获取唯一单据编号失败
|
||||
public static final int SEQUENCE_ONLY_FAILED_CODE = 12000001;
|
||||
public static final String SEQUENCE_ONLY_FAILED_MSG = "获取唯一单据编号失败,请稍后重试";
|
||||
//获取唯一单据编号操作被中断
|
||||
public static final int SEQUENCE_ONLY_BREAK_CODE = 12000002;
|
||||
public static final String SEQUENCE_ONLY_BREAK_MSG = "获取唯一单据编号操作被中断";
|
||||
|
||||
//演示用户禁止操作
|
||||
public static final int SYSTEM_CONFIG_TEST_USER_CODE = -1;
|
||||
public static final String SYSTEM_CONFIG_TEST_USER_MSG = "演示用户禁止操作";
|
||||
|
||||
@@ -39,6 +39,7 @@ public class SequenceController {
|
||||
Map<String, Object> map = new HashMap<String, Object>();
|
||||
try {
|
||||
String number = sequenceService.buildOnlyNumber();
|
||||
logger.info("生成的单据编号:{}", number);
|
||||
map.put("defaultNumber", number);
|
||||
res.code = 200;
|
||||
res.data = map;
|
||||
|
||||
@@ -2,9 +2,12 @@ package com.jsh.erp.service;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.jsh.erp.constants.BusinessConstants;
|
||||
import com.jsh.erp.datasource.entities.*;
|
||||
import com.jsh.erp.datasource.mappers.*;
|
||||
import com.jsh.erp.exception.JshException;
|
||||
import com.jsh.erp.constants.ExceptionConstants;
|
||||
import com.jsh.erp.datasource.entities.SerialNumber;
|
||||
import com.jsh.erp.datasource.entities.SerialNumberEx;
|
||||
import com.jsh.erp.datasource.mappers.SequenceMapperEx;
|
||||
import com.jsh.erp.exception.BusinessRunTimeException;
|
||||
import com.jsh.erp.utils.RedisLockUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -13,6 +16,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Description
|
||||
@@ -27,6 +31,12 @@ public class SequenceService {
|
||||
@Resource
|
||||
private SequenceMapperEx sequenceMapperEx;
|
||||
|
||||
@Resource
|
||||
private RedisLockUtil redisLockUtil;
|
||||
|
||||
private static final long LOCK_EXPIRE_TIME = 3000; // 锁有效期3秒
|
||||
private static final long LOCK_WAIT_TIME = 100; // 等待锁时间100ms
|
||||
|
||||
public SerialNumber getSequence(long id)throws Exception {
|
||||
return null;
|
||||
}
|
||||
@@ -64,28 +74,65 @@ public class SequenceService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个唯一的序列号
|
||||
* */
|
||||
@Transactional(value = "transactionManager", rollbackFor = Exception.class)
|
||||
* 获取唯一单据编号
|
||||
*/
|
||||
public String buildOnlyNumber() throws Exception {
|
||||
Long buildOnlyNumber=null;
|
||||
synchronized (this){
|
||||
String lockKey = "sequence:lock:" + BusinessConstants.DEPOT_NUMBER_SEQ;
|
||||
String requestId = UUID.randomUUID().toString(); // 唯一请求ID
|
||||
boolean locked = false;
|
||||
try {
|
||||
sequenceMapperEx.updateBuildOnlyNumber(); //编号+1
|
||||
buildOnlyNumber= sequenceMapperEx.getBuildOnlyNumber(BusinessConstants.DEPOT_NUMBER_SEQ);
|
||||
// 尝试获取分布式锁
|
||||
locked = redisLockUtil.tryLock(
|
||||
lockKey,
|
||||
requestId,
|
||||
LOCK_EXPIRE_TIME,
|
||||
LOCK_WAIT_TIME
|
||||
);
|
||||
if (!locked) {
|
||||
throw new BusinessRunTimeException(ExceptionConstants.SEQUENCE_ONLY_FAILED_CODE, ExceptionConstants.SEQUENCE_ONLY_FAILED_MSG);
|
||||
}
|
||||
// 执行业务逻辑
|
||||
return doGenerateSequence();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new BusinessRunTimeException(ExceptionConstants.SEQUENCE_ONLY_BREAK_CODE, ExceptionConstants.SEQUENCE_ONLY_BREAK_MSG);
|
||||
} finally {
|
||||
// 释放锁
|
||||
if (locked) {
|
||||
redisLockUtil.unlock(lockKey, requestId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 实际生成单据编号
|
||||
*/
|
||||
private String doGenerateSequence() throws Exception {
|
||||
try {
|
||||
// 执行数据库更新
|
||||
sequenceMapperEx.updateBuildOnlyNumber();
|
||||
Long number = sequenceMapperEx.getBuildOnlyNumber(BusinessConstants.DEPOT_NUMBER_SEQ);
|
||||
// 格式化返回
|
||||
return formatNumber(number);
|
||||
} catch (Exception e) {
|
||||
JshException.writeFail(logger, e);
|
||||
logger.error("生成单据编号失败", e);
|
||||
throw new BusinessRunTimeException(ExceptionConstants.SEQUENCE_ONLY_BREAK_CODE, ExceptionConstants.SEQUENCE_ONLY_BREAK_MSG);
|
||||
}
|
||||
}
|
||||
if(buildOnlyNumber<BusinessConstants.SEQ_TO_STRING_MIN_LENGTH){
|
||||
StringBuffer sb=new StringBuffer(buildOnlyNumber.toString());
|
||||
|
||||
/**
|
||||
* 格式化数字
|
||||
*/
|
||||
private String formatNumber(Long number) {
|
||||
if (number < BusinessConstants.SEQ_TO_STRING_MIN_LENGTH) {
|
||||
StringBuffer sb = new StringBuffer(number.toString());
|
||||
int len = BusinessConstants.SEQ_TO_STRING_MIN_LENGTH.toString().length() - sb.length();
|
||||
for (int i = 0; i < len; i++) {
|
||||
sb.insert(0, BusinessConstants.SEQ_TO_STRING_LESS_INSERT);
|
||||
}
|
||||
return sb.toString();
|
||||
} else {
|
||||
return buildOnlyNumber.toString();
|
||||
return number.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
120
jshERP-boot/src/main/java/com/jsh/erp/utils/RedisLockUtil.java
Normal file
120
jshERP-boot/src/main/java/com/jsh/erp/utils/RedisLockUtil.java
Normal file
@@ -0,0 +1,120 @@
|
||||
package com.jsh.erp.utils;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.script.DefaultRedisScript;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class RedisLockUtil {
|
||||
|
||||
@Autowired
|
||||
private RedisTemplate<String, String> redisTemplate;
|
||||
|
||||
private static final String UNLOCK_LUA_SCRIPT =
|
||||
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
|
||||
" return redis.call('del', KEYS[1]) " +
|
||||
"else " +
|
||||
" return 0 " +
|
||||
"end";
|
||||
|
||||
private static final String LOCK_LUA_SCRIPT =
|
||||
"local key = KEYS[1] " +
|
||||
"local value = ARGV[1] " +
|
||||
"local expire = ARGV[2] " +
|
||||
"local result = redis.call('setnx', key, value) " +
|
||||
"if result == 1 then " +
|
||||
" redis.call('pexpire', key, expire) " +
|
||||
" return 1 " +
|
||||
"else " +
|
||||
" local currentValue = redis.call('get', key) " +
|
||||
" if currentValue == value then " +
|
||||
" redis.call('pexpire', key, expire) " +
|
||||
" return 1 " +
|
||||
" else " +
|
||||
" return 0 " +
|
||||
" end " +
|
||||
"end";
|
||||
|
||||
/**
|
||||
* 尝试获取分布式锁(支持锁重入)
|
||||
* @param lockKey 锁的key
|
||||
* @param requestId 请求ID(可使用UUID)
|
||||
* @param expireMillis 锁的过期时间(毫秒)
|
||||
* @param waitMillis 等待时间(毫秒)
|
||||
* @return 是否获取成功
|
||||
*/
|
||||
public boolean tryLock(String lockKey, String requestId,
|
||||
long expireMillis, long waitMillis)
|
||||
throws InterruptedException {
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
while (System.currentTimeMillis() - startTime < waitMillis) {
|
||||
// 使用Lua脚本保证原子性
|
||||
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
|
||||
script.setScriptText(LOCK_LUA_SCRIPT);
|
||||
script.setResultType(Long.class);
|
||||
|
||||
Long result = redisTemplate.execute(
|
||||
script,
|
||||
Collections.singletonList(lockKey),
|
||||
requestId,
|
||||
String.valueOf(expireMillis)
|
||||
);
|
||||
|
||||
if (result != null && result == 1L) {
|
||||
return true; // 获取锁成功
|
||||
}
|
||||
|
||||
// 短暂休眠后重试
|
||||
Thread.sleep(10);
|
||||
}
|
||||
|
||||
return false; // 获取锁失败
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放分布式锁
|
||||
*/
|
||||
public boolean unlock(String lockKey, String requestId) {
|
||||
try {
|
||||
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
|
||||
script.setScriptText(UNLOCK_LUA_SCRIPT);
|
||||
script.setResultType(Long.class);
|
||||
|
||||
Long result = redisTemplate.execute(
|
||||
script,
|
||||
Collections.singletonList(lockKey),
|
||||
requestId
|
||||
);
|
||||
|
||||
return result != null && result == 1L;
|
||||
} catch (Exception e) {
|
||||
log.error("释放锁失败, lockKey: {}", lockKey, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速获取锁(立即返回,不等待)
|
||||
*/
|
||||
public boolean lockFast(String lockKey, String requestId, long expireMillis) {
|
||||
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
|
||||
script.setScriptText(LOCK_LUA_SCRIPT);
|
||||
script.setResultType(Long.class);
|
||||
|
||||
Long result = redisTemplate.execute(
|
||||
script,
|
||||
Collections.singletonList(lockKey),
|
||||
requestId,
|
||||
String.valueOf(expireMillis)
|
||||
);
|
||||
|
||||
return result != null && result == 1L;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user