优化生成单据编号的逻辑,采用redis分布式锁的方式

This commit is contained in:
jishenghua
2026-01-26 21:41:05 +08:00
parent d3e173fe63
commit e27a424850
4 changed files with 200 additions and 21 deletions

View File

@@ -585,6 +585,17 @@ public class ExceptionConstants {
public static final int REPORT_TWO_MANY_DEPOT_FAILED_CODE = 510; public static final int REPORT_TWO_MANY_DEPOT_FAILED_CODE = 510;
public static final String REPORT_TWO_MANY_DEPOT_FAILED_MSG = "请选择仓库,再进行查询"; 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 int SYSTEM_CONFIG_TEST_USER_CODE = -1;
public static final String SYSTEM_CONFIG_TEST_USER_MSG = "演示用户禁止操作"; public static final String SYSTEM_CONFIG_TEST_USER_MSG = "演示用户禁止操作";

View File

@@ -39,6 +39,7 @@ public class SequenceController {
Map<String, Object> map = new HashMap<String, Object>(); Map<String, Object> map = new HashMap<String, Object>();
try { try {
String number = sequenceService.buildOnlyNumber(); String number = sequenceService.buildOnlyNumber();
logger.info("生成的单据编号:{}", number);
map.put("defaultNumber", number); map.put("defaultNumber", number);
res.code = 200; res.code = 200;
res.data = map; res.data = map;

View File

@@ -2,9 +2,12 @@ package com.jsh.erp.service;
import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSONObject;
import com.jsh.erp.constants.BusinessConstants; import com.jsh.erp.constants.BusinessConstants;
import com.jsh.erp.datasource.entities.*; import com.jsh.erp.constants.ExceptionConstants;
import com.jsh.erp.datasource.mappers.*; import com.jsh.erp.datasource.entities.SerialNumber;
import com.jsh.erp.exception.JshException; 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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -13,6 +16,7 @@ import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource; import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import java.util.List; import java.util.List;
import java.util.UUID;
/** /**
* Description * Description
@@ -27,6 +31,12 @@ public class SequenceService {
@Resource @Resource
private SequenceMapperEx sequenceMapperEx; 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 { public SerialNumber getSequence(long id)throws Exception {
return null; return null;
} }
@@ -64,28 +74,65 @@ public class SequenceService {
} }
/** /**
* 创建一个唯一的序列 * 获取唯一单据编
* */ */
@Transactional(value = "transactionManager", rollbackFor = Exception.class) public String buildOnlyNumber() throws Exception {
public String buildOnlyNumber()throws Exception{ String lockKey = "sequence:lock:" + BusinessConstants.DEPOT_NUMBER_SEQ;
Long buildOnlyNumber=null; String requestId = UUID.randomUUID().toString(); // 唯一请求ID
synchronized (this){ boolean locked = false;
try{ try {
sequenceMapperEx.updateBuildOnlyNumber(); //编号+1 // 尝试获取分布式锁
buildOnlyNumber= sequenceMapperEx.getBuildOnlyNumber(BusinessConstants.DEPOT_NUMBER_SEQ); locked = redisLockUtil.tryLock(
}catch(Exception e){ lockKey,
JshException.writeFail(logger, e); 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);
} }
} }
if(buildOnlyNumber<BusinessConstants.SEQ_TO_STRING_MIN_LENGTH){ }
StringBuffer sb=new StringBuffer(buildOnlyNumber.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); */
private String doGenerateSequence() throws Exception {
try {
// 执行数据库更新
sequenceMapperEx.updateBuildOnlyNumber();
Long number = sequenceMapperEx.getBuildOnlyNumber(BusinessConstants.DEPOT_NUMBER_SEQ);
// 格式化返回
return formatNumber(number);
} catch (Exception e) {
logger.error("生成单据编号失败", e);
throw new BusinessRunTimeException(ExceptionConstants.SEQUENCE_ONLY_BREAK_CODE, ExceptionConstants.SEQUENCE_ONLY_BREAK_MSG);
}
}
/**
* 格式化数字
*/
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(); return sb.toString();
}else{ } else {
return buildOnlyNumber.toString(); return number.toString();
} }
} }
} }

View 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;
}
}