一、背景
业务上需要一个限额功能,以配置的方式,按日为维度去限制某一家通道的授信、放款额度。这个业务刚刚起步规模不大,日进件笔数万级,并发量不高,服务也刚搭建,已满足业务上的基本功能,目前基建不足。产品以紧急需求提出。
二、设计
1、表设计
CREATE TABLE `quota` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`quota_code` varchar(64) NOT NULL COMMENT '限额code',
`product_code` varchar(32) NOT NULL COMMENT '产品编码',
`trans_type` varchar(16) NOT NULL COMMENT '交易类型(授信CREDIT/放款LOAN/还款REPAY)',
`quota` bigint(20) NOT NULL DEFAULT '0' COMMENT '额度(分)',
`frozen_quota` bigint(20) NOT NULL DEFAULT '0' COMMENT '已冻结额度',
`used_quota` bigint(20) NOT NULL DEFAULT '0' COMMENT '已使用的额度',
`start_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '额度有效期起始',
`end_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '额度有效期结束',
`status` varchar(16) NOT NULL DEFAULT '0' COMMENT '状态 1 生效、0失效',
`remark` varchar(4096) DEFAULT NULL COMMENT '备注',
`creator` varchar(32) NOT NULL DEFAULT 'SYSTEM' COMMENT '创建人',
`modifier` varchar(32) NOT NULL DEFAULT 'SYSTEM' COMMENT '修改人',
`gmt_created` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
`is_deleted` char(1) NOT NULL DEFAULT 'N' COMMENT '是否删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_quota_code` (`quota_code`) USING BTREE,
KEY `idx_product_code_trans_type` (`product_code`,`trans_type`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=171 DEFAULT CHARSET=utf8 COMMENT='限额表';
CREATE TABLE `quota_record` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`quota_code` varchar(64) NOT NULL COMMENT '限额code',
`business_no` varchar(64) NOT NULL COMMENT '交易编号',
`product_code` varchar(32) NOT NULL COMMENT '产品编码',
`trans_type` varchar(16) NOT NULL COMMENT '交易类型(授信CREDIT/放款LOAN/还款REPAY)',
`amount` bigint(20) NOT NULL DEFAULT '0' COMMENT '交易金额(分)',
`original_quota` bigint(20) DEFAULT NULL COMMENT '原使用额度(分)',
`current_quota` bigint(20) DEFAULT NULL COMMENT '现使用额度(分)',
`trans_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '交易时间',
`status` varchar(16) NOT NULL DEFAULT '0' COMMENT '状态 SUCCESS 成功、FAIL 失败、INIT 初始化',
`remark` varchar(4096) DEFAULT NULL COMMENT '备注',
`creator` varchar(32) NOT NULL DEFAULT 'SYSTEM' COMMENT '创建人',
`modifier` varchar(32) NOT NULL DEFAULT 'SYSTEM' COMMENT '修改人',
`gmt_created` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
`is_deleted` char(1) NOT NULL DEFAULT 'N' COMMENT '是否删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_quota_code_business_no` (`quota_code`,`business_no`) USING BTREE,
KEY `idx_gmt_created` (`gmt_created`) USING BTREE,
KEY `idx_business_no_trans_type_status` (`business_no`,`trans_type`,`status`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=139 DEFAULT CHARSET=utf8 COMMENT='限额明细表';
2、系统设计
三、核心代码
QuotaService.java
/**
* 限额服务
* @author majunbo
*/
public interface QuotaService {
/**
* 创建额度
*
* @param quotaDTO
* @return
*/
BaseResp<QuotaDTO> createQuota(QuotaDTO quotaDTO);
/**
* 修改额度
*
* @param quotaDTO
* @return
*/
BaseResp<QuotaDTO> editQuota(QuotaDTO quotaDTO);
/**
* 查询额度配置
*
* @param quotaDTO
* @return
*/
BaseResp<List<QuotaDTO>> query(QuotaDTO quotaDTO);
/**
* 额度冻结
* @param productCode 产品编号
* @param businessNo 业务流水号
* @param transType 交易类型(授信CREDIT、放款LOAN)
* @param amount 交易金额
* @param applyTime 交易时间
* @return
*/
boolean freezeQuota(String productCode, String businessNo, String transType, Long amount, Date applyTime);
/**
* 额度更新
*
* @param businessNo
* @param transType
* @param status
* @return
*/
boolean updateQuota(String businessNo, String transType, String status);
void update(QuotaDO quotaFromDb, QuotaRecordDO quotaRecordFromDb, String status);
void freeze(QuotaDO quotaFromDb, Long amount, String businessNo, String transType);
}
QuotaServiceImpl.java
/**
* 限额服务实现类
* @author majunbo
*/
@Service
@Slf4j
public class QuotaServiceImpl implements QuotaService {
@Resource
private Sequence sequence;
@Resource
private QuotaMapper quotaMapper;
@Resource
private QuotaRecordMapper quotaRecordMapper;
@Resource
private JedisUtil jedisUtil;
private final String REDIS_LOCK_KEY = "quotaLock:";
private final Integer REDIS_EXPIRE_TIME = 30;
private final String STATUS_SUCCESS ="SUCCESS";
private final String STATUS_INIT ="INIT";
private final String STATUS_FAIL ="FAIL";
@Override
public BaseResp<QuotaDTO> createQuota(QuotaDTO quotaDTO) {
log.info("QuotaServiceImpl createQuota begin,quotaDTO={}", quotaDTO);
ValidatorUtil.validate(quotaDTO);
try {
QuotaDO quotaDO = initQuotaDO(quotaDTO);
int result = quotaMapper.insertQuota(quotaDO);
log.info("QuotaServiceImpl createQuota success,quotaDTO={}", quotaDTO);
if (result > 0) {
return BaseResp.success();
}
log.error("QuotaServiceImpl createQuota fail,quotaDTO={}", quotaDTO);
return BaseResp.fail(RespCodeEnum.FAIL.getCode(), "额度配置落库失败");
} catch (Exception e) {
log.error("QuotaServiceImpl createQuota fail,quotaDTO={}", quotaDTO, e);
return BaseResp.fail(RespCodeEnum.FAIL.getCode(), "额度配置落库失败");
}
}
@Override
public BaseResp<QuotaDTO> editQuota(QuotaDTO quotaDTO) {
log.info("QuotaServiceImpl editQuota begin,quotaDTO={}", quotaDTO);
//1.入参校验
ValidatorUtil.validate(quotaDTO);
if (ObjectUtils.isEmpty(quotaDTO.getId()) || ObjectUtils.isEmpty(quotaDTO.getStatus())) {
log.error("QuotaServiceImpl editQuota id、status不能为空,quotaDTO={}", quotaDTO);
return BaseResp.fail(RespCodeEnum.FAIL.getCode(), "id、status不能为空");
}
//2.查本地记录
QuotaDO quotaDO = initQuotaDO(quotaDTO);
QuotaDO quotaFromDb = quotaMapper.selectById(quotaDO.getId());
if (null == quotaFromDb) {
log.error("QuotaServiceImpl editQuota 查无此限额配置,quotaDTO={}", quotaDTO);
return BaseResp.fail(RespCodeEnum.FAIL.getCode(), "查无此限额配置");
}
//3.校验时间是否存在交叉
if (!ObjectUtils.isEmpty(quotaDTO.getStatus()) && "1".equals(quotaDTO.getStatus())) {
checkTimeCross(quotaDO);
}
//4.redis分布式自旋锁
String lockKey = REDIS_LOCK_KEY + quotaFromDb.getQuotaCode();
try {
int times = 0;
while (!jedisUtil.tryLock(lockKey, REDIS_EXPIRE_TIME)) {
Thread.sleep(50);
times++;
if (times > 10) {
log.info("QuotaServiceImpl editQuota 无法获取锁 redisKey:[{}] quotaDTO:[{}]", lockKey, quotaDTO);
return BaseResp.fail(RespCodeEnum.FAIL.getCode(), "无法获取锁,请稍后重试");
}
}
quotaFromDb = quotaMapper.selectById(quotaDO.getId());
//5.校验修改后额度 >= 冻结额度 + 已使用额度
if (quotaDO.getQuota() < quotaFromDb.getUsedQuota() + quotaFromDb.getFrozenQuota()) {
log.error("QuotaServiceImpl editQuota 额度小于冻结额度+已使用额度,quotaDTO={}", quotaDTO);
jedisUtil.releaseLock(lockKey);
return BaseResp.fail(RespCodeEnum.FAIL.getCode(), "额度小于冻结额度+已使用额度");
}
//6.修改限额
quotaDO.setGmtModified(new Date());
int result = quotaMapper.updateByPrimaryKeySelective(quotaDO);
//7.释放锁
jedisUtil.releaseLock(lockKey);
log.info("QuotaServiceImpl editQuota success,quotaDTO={}", quotaDO);
if (result > 0) {
return BaseResp.success();
}
return BaseResp.fail(RespCodeEnum.FAIL.getCode(), "额度修改落库失败");
} catch (Exception e) {
jedisUtil.releaseLock(lockKey);
log.error("QuotaServiceImpl editQuota 额度修改失败,quotaDTO={}", quotaDTO, e);
return BaseResp.fail(RespCodeEnum.FAIL.getCode(), "额度修改失败");
}
}
@Override
public BaseResp<List<QuotaDTO>> query(QuotaDTO quotaDTO) {
QuotaDO quotaDO = new QuotaDO();
BeanUtils.copyProperties(quotaDTO, quotaDO);
List<QuotaDO> quotaDOS = quotaMapper.selectByDO(quotaDO);
if(ObjectUtils.isEmpty(quotaDOS)){
return BaseResp.success(new ArrayList<>());
}
List<QuotaDTO> quotaDTOS = quotaDOS.stream().map(this::toDTO).collect(Collectors.toList());
return BaseResp.success(quotaDTOS);
}
@Override
public boolean freezeQuota(String productCode, String businessNo, String transType, Long amount, Date applyTime) {
log.info("QuotaServiceImpl freezeQuota begin,productCode={},bussinessNo={},transType={},amount={},applyTime={}",
productCode, businessNo, transType, amount, applyTime);
//1.查生效限额配置
QuotaDO quotaFromDb = queryQuota(productCode, transType, applyTime);
if (ObjectUtils.isEmpty(quotaFromDb)) {
log.info("QuotaServiceImpl freezeQuota 没有查到生效的额度配置,默认成功,productCode={},transType={},amount={}", productCode,
transType, amount);
return true;
}
String lockKey = REDIS_LOCK_KEY + quotaFromDb.getQuotaCode();
try {
//2.加锁
int times = 0;
while (!jedisUtil.tryLock(lockKey, REDIS_EXPIRE_TIME)) {
Thread.sleep(50);
times++;
if (times > 10) {
//如果拿不到锁直接通过不限额
log.info("QuotaServiceImpl freezeQuota 无法获取锁 redisKey:[{}] quotaFromDb:[{}]", lockKey, quotaFromDb);
return true;
}
}
//3.校验是否超额
quotaFromDb = quotaMapper.selectById(quotaFromDb.getId());
log.info("QuotaServiceImpl freezeQuota quotaFromDb={}", quotaFromDb);
if (amount <= quotaFromDb.getQuota() - quotaFromDb.getUsedQuota() - quotaFromDb.getFrozenQuota()) {
((QuotaService) AopContext.currentProxy()).freeze(quotaFromDb, amount, businessNo, transType);
jedisUtil.releaseLock(lockKey);
return true;
}
log.info("QuotaServiceImpl freezeQuota 剩余额度不足,amount={},quotaFromDb={}", amount, quotaFromDb);
jedisUtil.releaseLock(lockKey);
return false;
} catch (Exception e) {
log.error("QuotaServiceImpl freezeQuota 额度冻结失败,productCode={},transType={},amount={}", productCode,
transType, amount, e);
jedisUtil.releaseLock(lockKey);
throw new BusinessException(GatewayRespCodeEnum.FAIL.getCode(), "额度冻结失败");
}
}
@Override
public boolean updateQuota(String businessNo, String transType, String status) {
log.info("QuotaServiceImpl updateQuota begin,businessNo={},transType={},status={}", businessNo, transType,
status);
//1.查询限额明细
QuotaRecordDO quotaRecordFromDb = getQuotaRecord(businessNo, transType);
if (null == quotaRecordFromDb) {
log.info("QuotaServiceImpl updateQuota 未查到限额明细,默认成功,businessNo={},transType={}", businessNo, transType);
return true;
}
String lockKey = REDIS_LOCK_KEY + quotaRecordFromDb.getQuotaCode();
try {
//2.分布式锁
int times = 0;
while (!jedisUtil.tryLock(lockKey, REDIS_EXPIRE_TIME)) {
Thread.sleep(50);
times++;
if (times > 10) {
log.info("QuotaServiceImpl updateQuota 无法获取锁 redisKey:[{}] quotaRecordFromDb:[{}]", lockKey,
quotaRecordFromDb);
return false;
}
}
//查询限额配置
QuotaDO quotaFromDb = queryQuota(quotaRecordFromDb.getQuotaCode());
((QuotaService) AopContext.currentProxy()).update(quotaFromDb,quotaRecordFromDb,status);
jedisUtil.releaseLock(lockKey);
return true;
} catch (Exception e) {
log.error("QuotaServiceImpl updateQuota 限额更新失败,businessNo={},transType={},status={}", businessNo, transType,
status,e);
jedisUtil.releaseLock(lockKey);
throw new BusinessException(GatewayRespCodeEnum.FAIL.getCode(), "额度更新失败");
}
}
@Override
@Transactional(rollbackFor = Exception.class, value = "TxMgr")
public void update(QuotaDO quotaFromDb,QuotaRecordDO quotaRecordFromDb,String status){
QuotaRecordDO updateQuotaRecord = new QuotaRecordDO();
updateQuotaRecord.setId(quotaRecordFromDb.getId());
QuotaDO quotaDO = new QuotaDO();
quotaDO.setId(quotaFromDb.getId());
if (STATUS_SUCCESS.equals(status)) {
//放款成功
//3.更新限额明细
updateQuotaRecord.setOriginalQuota(quotaFromDb.getUsedQuota());
updateQuotaRecord.setCurrentQuota(quotaFromDb.getUsedQuota() + quotaRecordFromDb.getAmount());
updateQuotaRecord.setStatus(STATUS_SUCCESS);
updateQuotaRecord.setGmtModified(new Date());
quotaRecordMapper.updateByPrimaryKeySelective(updateQuotaRecord);
//4.更新限额
quotaDO.setFrozenQuota(quotaFromDb.getFrozenQuota() - quotaRecordFromDb.getAmount());
quotaDO.setUsedQuota(quotaFromDb.getUsedQuota() + quotaRecordFromDb.getAmount());
quotaDO.setGmtModified(new Date());
quotaMapper.updateByPrimaryKeySelective(quotaDO);
} else {
//放款失败
//3.更新限额明细
updateQuotaRecord.setStatus(STATUS_FAIL);
updateQuotaRecord.setGmtModified(new Date());
quotaRecordMapper.updateByPrimaryKeySelective(updateQuotaRecord);
//4.更新限额
quotaDO.setFrozenQuota(quotaFromDb.getFrozenQuota() - quotaRecordFromDb.getAmount());
quotaDO.setGmtModified(new Date());
quotaMapper.updateByPrimaryKeySelective(quotaDO);
}
log.info("QuotaServiceImpl updateQuota 限额更新成功,status={},quotaDO={},quotaRecord={}", status, quotaDO,
updateQuotaRecord);
}
@Override
@Transactional(rollbackFor = Exception.class, value = "TxMgr")
public void freeze(QuotaDO quotaFromDb, Long amount, String businessNo, String transType) {
//4.更新限额
QuotaDO updateQuota = new QuotaDO();
updateQuota.setId(quotaFromDb.getId());
updateQuota.setFrozenQuota(quotaFromDb.getFrozenQuota() + amount);
updateQuota.setGmtModified(new Date());
quotaMapper.updateByPrimaryKeySelective(updateQuota);
//5.新增限额记录
QuotaRecordDO quotaRecordDO = initQuotaRecordDO(quotaFromDb, businessNo, transType, amount);
quotaRecordMapper.insertSelective(quotaRecordDO);
log.info("QuotaServiceImpl freezeQuota 冻结成功,updateQuota={}", updateQuota);
}
QuotaDO initQuotaDO(QuotaDTO quotaDTO) {
QuotaDO quotaDO = new QuotaDO();
BeanUtils.copyProperties(quotaDTO, quotaDO);
quotaDO.setStartTime(DateUtil.string2Date(quotaDTO.getStartTime(), DateUtil.DATE_FORMAT_YYYYMMDD_HHMMSS));
quotaDO.setEndTime(DateUtil.string2Date(quotaDTO.getEndTime(), DateUtil.DATE_FORMAT_YYYYMMDD_HHMMSS));
if (ObjectUtils.isEmpty(quotaDTO.getId())) {
quotaDO.setQuotaCode(
quotaDTO.getProductCode() + DateUtil.date2String(new Date(), DateUtil.DATE_FORMAT_YYYYMMDD) + String
.format("%08d", SequenceUtil.getId(sequence)));
}
return quotaDO;
}
void checkTimeCross(QuotaDO quotaDO) {
QuotaDO record = new QuotaDO();
record.setProductCode(quotaDO.getProductCode());
record.setTransType(quotaDO.getTransType());
record.setStatus("1");
List<QuotaDO> records = quotaMapper.selectByDO(record);
boolean timeCross = false;
Date start = quotaDO.getStartTime();
Date end = quotaDO.getEndTime();
if (!ObjectUtils.isEmpty(records)) {
//判断时间是否交叉
//日期不重合场景
//第1种:日期段1 开始日 < 日期段2 开始日,且 日期段1 结束日 < 日期段2 开始日
//第2种:日期段1 开始日 > 日期段2 开始日,且 日期段1 开始日 > 日期段2 结束日
for (QuotaDO qu : records) {
if (!quotaDO.getId().equals(qu.getId())) {
if (start.getTime() < qu.getStartTime().getTime() && end.getTime() <= qu.getStartTime().getTime()) {
continue;
}
if (start.getTime() > qu.getStartTime().getTime() && start.getTime() >= qu.getEndTime().getTime()) {
continue;
}
timeCross = true;
}
}
}
if (timeCross) {
log.error("QuotaServiceImpl editQuota 同一产品时间段内只能存在唯一生效配置,quotaDO={}", quotaDO);
throw new BusinessException(RespCodeEnum.FAIL.getCode(), "同一产品时间段内只能存在唯一生效配置");
}
}
public QuotaDO queryQuota(String productCode, String transType,Date applyTime) {
QuotaDO quotaDO = new QuotaDO();
quotaDO.setProductCode(productCode);
quotaDO.setTransType(transType);
quotaDO.setStatus("1");
quotaDO.setGmtCreated(applyTime);
return quotaMapper.queryQuota(quotaDO);
}
public QuotaDO queryQuota(String quotaCode) {
QuotaDO quotaDO = new QuotaDO();
quotaDO.setQuotaCode(quotaCode);
return quotaMapper.queryQuotaByCode(quotaDO);
}
QuotaRecordDO initQuotaRecordDO(QuotaDO quotaFromDb, String businessNo, String transType, Long amount) {
QuotaRecordDO quotaRecordDO = new QuotaRecordDO();
quotaRecordDO.setQuotaCode(quotaFromDb.getQuotaCode());
quotaRecordDO.setProductCode(quotaFromDb.getProductCode());
quotaRecordDO.setBusinessNo(businessNo);
quotaRecordDO.setTransType(transType);
quotaRecordDO.setAmount(amount);
quotaRecordDO.setOriginalQuota(null);
quotaRecordDO.setCurrentQuota(null);
quotaRecordDO.setTransTime(new Date());
quotaRecordDO.setStatus(STATUS_INIT);
return quotaRecordDO;
}
QuotaRecordDO getQuotaRecord(String businessNo, String transType) {
QuotaRecordDO quotaRecordDO = new QuotaRecordDO();
quotaRecordDO.setBusinessNo(businessNo);
quotaRecordDO.setTransType(transType);
quotaRecordDO.setStatus(STATUS_INIT);
return quotaRecordMapper.selectRecord(quotaRecordDO);
}
private QuotaDTO toDTO(QuotaDO quotaDO) {
QuotaDTO dto = new QuotaDTO();
BeanUtils.copyProperties(quotaDO, dto);
return dto;
}
}
QuotaDO.java
/**
* 限额表
* @author majunbo
* @TableName quota
*/
@Data
public class QuotaDO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
private Long id;
/**
* 限额code
*/
private String quotaCode;
/**
* 产品编码
*/
private String productCode;
/**
* 交易类型(授信CREDIT/放款LOAN)
*/
private String transType;
/**
* 额度(分)
*/
private Long quota;
/**
* 已冻结额度
*/
private Long frozenQuota;
/**
* 已使用的额度
*/
private Long usedQuota;
/**
* 额度有效期起始
*/
private Date startTime;
/**
* 额度有效期结束
*/
private Date endTime;
/**
* 状态 1 生效、0失效
*/
private String status;
/**
* 备注
*/
private String remark;
/**
* 创建人
*/
private String creator;
/**
* 修改人
*/
private String modifier;
/**
* 创建时间
*/
private Date gmtCreated;
/**
* 修改时间
*/
private Date gmtModified;
/**
* 是否删除
*/
private String isDeleted;
}
QuotaDTO.java
/**
* @author majunbo
* @date 2022/9/7 14:14
*/
@Data
public class QuotaDTO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
private Long id;
/**
* 限额code
*/
private String quotaCode;
/**
* 产品编码
*/
@NotEmpty(message = "productCode is empty")
private String productCode;
/**
* 交易类型(授信CREDIT/放款LOAN)
*/
@NotEmpty(message = "transType is empty")
@EnumCodeValid(values = {"CREDIT","LOAN"}, message = "transType param error")
private String transType;
/**
* 额度(分)
*/
@NotNull(message = "quota is empty")
@Range(message = "quota range error")
private Long quota;
/**
* 已冻结额度
*/
private Long frozenQuota;
/**
* 已使用的额度
*/
private Long usedQuota;
/**
* 额度有效期起始
*/
@NotEmpty(message = "startTime is empty")
private String startTime;
/**
* 额度有效期结束
*/
@NotEmpty(message = "endTime is empty")
private String endTime;
/**
* 状态 1 生效、0失效
*/
private String status;
/**
* 备注
*/
private String remark;
/**
* 创建人
*/
private String creator;
/**
* 修改人
*/
private String modifier;
/**
* 创建时间
*/
private Date gmtCreated;
/**
* 修改时间
*/
private Date gmtModified;
/**
* 是否删除
*/
private String isDeleted;
}
QuotaRecordDO.java
/**
* 限额明细表
* @author majunbo
* @TableName quota_record
*/
@Data
public class QuotaRecordDO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
private Long id;
/**
* 限额code
*/
private String quotaCode;
/**
* 交易编号
*/
private String businessNo;
/**
* 产品编码
*/
private String productCode;
/**
* 交易类型(授信CREDIT/放款LOAN)
*/
private String transType;
/**
* 交易金额(分)
*/
private Long amount;
/**
* 原使用额度(分)
*/
private Long originalQuota;
/**
* 现使用额度(分)
*/
private Long currentQuota;
/**
* 交易时间
*/
private Date transTime;
/**
* 状态 SUCCESS 成功、FAIL 失败、INIT 初始化
*/
private String status;
/**
* 备注
*/
private String remark;
/**
* 创建人
*/
private String creator;
/**
* 修改人
*/
private String modifier;
/**
* 创建时间
*/
private Date gmtCreated;
/**
* 修改时间
*/
private Date gmtModified;
/**
* 是否删除
*/
private String isDeleted;
四、个人思考和后记
半夜失眠,起来随便写点东西。
限额这块功能在业务上是正常跑着,因为时间的关系从设计到实现难免有些简陋和粗糙,以微服务架构来说,还是拆出来完善一下形成一个单独的限额服务比较好,但考虑到现有的业务规模、成本、需求、时间,单独做一套限额服务难免有过度设计的嫌疑,目前这样设计已经绰绰有余,如果这业务真能做起来再说罢。
关于冻结额度时无法获取锁的处理,我一开始是设计成无法获取锁就返回系统繁忙请重试的code,正好有这个返回码拿来用一下,让上游重新发起请求就行。最后跟产品和leader过到这块时,交流下来他们的想法是拿不到锁就不限额直接放行,估计是有业务上的考虑,那肯定是跟着需求走了,我只是觉得这里有点不妥,只能注释记一下防止之后踩坑罢。