分布式自增序列组件封装的思考

193 阅读3分钟

分布式自增序列组件封装的思考

最近工作中完成了分布式自增序列组件的简单封装,也产生了一些思考吧,希望对大家有所帮助~

需要有的考虑

  • 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);