分布式自增序列组件封装的思考
最近工作中完成了分布式自增序列组件的简单封装,也产生了一些思考吧,希望对大家有所帮助~
需要有的考虑
- 1.存储的方式?
存储在Redis中,通过Redis的increment原子命令可以保证获取递增值操作的原子性。
- 2.Redis丢失数据的兜底方案?
这个要针对不同业务,如下面示例的业务通过读取最大记录的数据库值来做的。 不同的业务需要有不同的恢复策略,因此可以用策略模式or职责链模式来完成。
- 3.并发的考虑?
Redis数据正常的情况下,原子命令可以保证Java后端不同线程获取到唯一的递增序列。
Redis数据丢失的情况下,大量操作去执行恢复数据方案时,可以导致递增序列被反复恢复,大量请求将获取到相同递增序列,因此,恢复过程必须加锁。
[第一版:策略模式]实现
主要代码
@Service
@Slf4j
public class SequenceGenerateUtil {
private static final String LOCK_PRE="LOCK_";
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private RedissonClient redissonClient;
public Integer generateIntegerId(BusinessEnum businessEnum) {
Integer typeValue = null;
if ((typeValue = (Integer) redisTemplate.opsForValue().get(businessEnum.getName())) == null) {
//redis丢失了序列号
//此过程一般是需要加锁的!!!因为全局ID是唯一的,如果丢失序列号,同时有大量请求过来,都进入这里,执行到恢复策略,会使得大量业务分配到相同的序列号,导致业务错乱
//采用双检验加锁的方法
RLock lock = redissonClient.getLock(LOCK_PRE+businessEnum.getName());
try {
lock.lock();
if ((typeValue = (Integer) redisTemplate.opsForValue().get(businessEnum.getName())) == null) {
LostSequenceNumberRecoverStrategy recoverStrategy = SpringUtil.getBean(businessEnum.getRecoverStrategy());
if (recoverStrategy == null) {
throw new ServiceException("恢复策略为空,请检查代码!");
}
Object recoverData = recoverStrategy.recover();
redisTemplate.opsForValue().set(businessEnum.getName(), recoverData);
}
} catch (Exception e) {
log.error("错误信息:", e);
} finally {
lock.unlock();
}
}
//生成ID
return redisTemplate.opsForValue().increment(businessEnum.getName(), 1).intValue();
}
}
其中参数为:
@JSONType(writeEnumAsJavaBean = true, deserializer = EnumDeserializer.class)
public enum BusinessEnum {
RESOURCE_POSITION_TYPE(1, "RESOURCE_POSITION_TYPE", PositionTypeCodeSequenceNumberRecoverStrategy.class),
;
private final int type;
private final String name;
private final Class<? extends LostSequenceNumberRecoverStrategy> recoverStrategy;
BusinessEnum(int type, String name, Class<? extends LostSequenceNumberRecoverStrategy> recoverStrategy) {
this.type = type;
this.name = name;
this.recoverStrategy = recoverStrategy;
}
}
恢复策略接口:
public interface LostSequenceNumberRecoverStrategy<T> {
T recover();
}
使用示例
具体策略实现恢复策略接口
@Component
public class PositionTypeCodeSequenceNumberRecoverStrategy implements LostSequenceNumberRecoverStrategy<Integer> {
@Resource
private BrmsBasicResourcesTypeMapper resourcesTypeMapper;
@Override
public Integer recover() {
BrmsBasicResourcesType lastResourceType = resourcesTypeMapper..getMaxTypeCode();
if (lastResourceType == null) {
return 1;
}
return lastResourceType.getTypeCode();
}
}
将具体策略注入业务枚举, 然后获取即可:
Integer sequenceCode = sequenceGenerateUtil.generateIntegerId(BusinessEnum.RESOURCE_POSITION_TYPE);
[第二版:职责链模式]实现
第一版总觉得欠点味道,枚举类定义的第三个参数是类文件,总觉得缺点味道。 联想到SpringMVC的设计,以及在宇道源码看过的代码,这里设计成职责链模式也行更加优雅一点。
主要代码
下面代码将找第一个能够处理当前业务的LostSequenceNumberRecoverStrategy实现类作为恢复策略。
@Service
@Slf4j
public class SequenceGenerateUtil {
private static final String LOCK_PRE="LOCK_";
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private RedissonClient redissonClient;
public Integer generateIntegerId(BusinessEnum businessEnum) {
Integer typeValue = null;
if ((typeValue = (Integer) redisTemplate.opsForValue().get(businessEnum.getName())) == null) {
//通过职责链的方式获取到恢复策略
LostSequenceNumberRecoverStrategy recoverStrategy = getRecoverStrategy(businessEnum);
//redis丢失了序列号
//此过程一般是需要加锁的!!!因为全局ID是唯一的,如果丢失序列号,同时有大量请求过来,都进入这里,执行到恢复策略,会使得大量业务分配到相同的序列号,导致业务错乱
//采用双检验加锁的方法
RLock lock = redissonClient.getLock(LOCK_PRE+businessEnum.getName());
try {
lock.lock();
if ((typeValue = (Integer) redisTemplate.opsForValue().get(businessEnum.getName())) == null) {
Object recoverData = recoverStrategy.recover();
redisTemplate.opsForValue().set(businessEnum.getName(), recoverData);
}
} catch (Exception e) {
log.error("错误信息:", e);
} finally {
lock.unlock();
}
}
//生成ID
return Objects.requireNonNull(redisTemplate.opsForValue().increment(businessEnum.getName(), 1)).intValue();
}
private LostSequenceNumberRecoverStrategy getRecoverStrategy(BusinessEnum businessEnum) {
Collection<LostSequenceNumberRecoverStrategy> strategies = SpringUtil.getApplicationContext().getBeansOfType(LostSequenceNumberRecoverStrategy.class).values();
for (LostSequenceNumberRecoverStrategy strategy : strategies) {
if (strategy.isSupport(businessEnum)) {
return strategy;
}
}
throw new ServiceException("未找到对应的恢复策略,请检查代码!");
}
}
恢复策略接口添加isSupport方法。
public interface LostSequenceNumberRecoverStrategy<T> {
T recover();
boolean isSupport(BusinessEnum businessEnum);
}
枚举删去类名称字段
@JSONType(writeEnumAsJavaBean = true, deserializer = EnumDeserializer.class)
public enum BusinessEnum {
/**
* 资源位置类型
*/
RESOURCE_POSITION_TYPE(1, "SEQUENCE_RESOURCE_POSITION_TYPE"),
;
private final int type;
private final String name;
BusinessEnum(int type, String name) {
this.type = type;
this.name = name;
}
public int getType() {
return type;
}
public String getName() {
return name;
}
}
使用示例
恢复策略实现类示例:
@Component
public class PositionTypeCodeSequenceNumberRecoverStrategy implements LostSequenceNumberRecoverStrategy<Integer> {
@Resource
private BrmsBasicResourcesTypeMapper resourcesTypeMapper;
@Override
public Integer recover() {
BrmsBasicResourcesType lastResourceType = resourcesTypeMapper.getMaxTypeCode();
if (lastResourceType == null) {
return 1;
}
return lastResourceType.getTypeCode();
}
@Override
public boolean isSupport(BusinessEnum businessEnum) {
return BusinessEnum.RESOURCE_POSITION_TYPE.equals(businessEnum);
}
}
使用保持不变:
Integer sequenceCode = sequenceGenerateUtil.generateIntegerId(BusinessEnum.RESOURCE_POSITION_TYPE);