一个简单的灰度方案设计

简简单单写一个简简单单的灰度方案,然后发现自己真的不会写文档😭

Posted by zhi on February 28, 2020

概述

灰度方案是一种让开发在黑白之间平滑过度的一种发布方案,让部分用户使用稳定的旧方案,让另一部分用户使用新方案,如果对于新方案没有问题,那么就把灰度开关开到最大使用新逻辑。灰度方案可以整体系统的稳定性,并且可以在灰度期间发现问题、解决问题、调整问题,降低出现问题的损失,保证上线进度。

实现思路

大体实现逻辑 699e59b86191ecb2212b062776a52550.png

本方案灰度逻辑实现在代码层面

具体实现

image

废话不多说 直接贴代码

DB 设计

表主要涵盖灰度id、灰度名称、灰度开关、白名单、黑名单、灰度区间几个字段

  • 灰度名称 声明该灰度是干嘛地
  • 灰度开关 可以直接关闭灰度开关走旧逻辑,也可以直接关闭灰度走新逻辑
  • 白名单 用‘,’ 分割的字符串,直接走新逻辑的一部分id
  • 黑名单 用‘.’ 分割的字符串,直接走就逻辑的一部分id
  • 灰度区间 用‘,’ 分割的字符串, 类似 ‘20,80’ 意味着id尾数落在该区间内就是走新逻辑

具体的表结构如下

-- auto-generated definition
create table gray_feature
(
    id            int auto_increment comment '主键id'
        primary key,
    gray_id       int                                   null comment '灰度id',
    gray_name     varchar(32) default ''                not null comment '灰度名称',
    gray_switch   tinyint     default 1                 null comment '0 灰度关闭 直接返回false 走老逻辑
1 灰度开启 进入灰度判断
2 灰度关闭 直接返回true 走新逻辑',
    white_list    varchar(64) default ''                not null comment '白名单',
    black_list    varchar(64) default ''                not null comment '黑名单 优先级高于白名单',
    gray_interval varchar(64) default '0,1'             not null comment '灰度区间 左闭右开',
    create_time   datetime    default CURRENT_TIMESTAMP null comment '创建时间',
    constraint gray_feature_gray_id_uindex
        unique (gray_id)
)
    comment '灰度特性';

代码逻辑设计

因为是个简单的灰度逻辑,主要用于后台开发一些重构、迁移项目刚上线防止出问题的简易策略,因此无论越简单越好,响应越快越好。 灰度判断的核心就是从DB拿到灰度配置,然后去校验对应的id,为了响应足够快,这里把灰度配置缓存在内存中一段时间。。。直接贴代码吧。。。


/**
 * 灰度相关
 *
 * @author zhi
 * @date 2020-02-28 12:50
 */
@Service
public class GrayServiceImpl implements GrayService {

    @Resource
    ApplicationContext context;

    /**
     * 获取灰度信息
     *
     * @param grayId 灰度id
     * @return 灰度信息
     */
    private GrayFeatureDO getGrayFeature(Long grayId) {
        ILoadingCacheService<Long, GrayFeatureDO> grayFeatureCache = context.getBean("GRAY_FEATURE_CACHE", ILoadingCacheService.class);
        return grayFeatureCache.getValue(grayId);
    }

    @Override
    public boolean checkInGray(GrayCheckRequestVO requestVO) {

        if (null == requestVO || null == requestVO.getGrayId() || null == requestVO.getUid()) {
            return false;
        }

        GrayFeatureDO grayFeature = getGrayFeature(requestVO.getGrayId());
        if (grayFeature == null) {
            return false;
        }
        GrayFeatureDetail detail = convertGrayFeatureDetail(grayFeature);
        return checkGrayFeatureDetail(detail, requestVO.getUid());

    }


    private static final Function<String, List<String>> SPLIT_TO_LIST = str -> Splitter.on(",").omitEmptyStrings().splitToList(str);

    /**
     * 灰度开关关闭
     */
    private static final int CLOSE_SWITCH = 0;

    /**
     * 灰度关闭 全量true
     */
    private static final int TRUE_SWITCH = 2;

    private GrayFeatureDetail convertGrayFeatureDetail(GrayFeatureDO grayFeature) {

        GrayFeatureDetail grayFeatureDetail = new GrayFeatureDetail();

        grayFeatureDetail.setGrayId(grayFeature.getGrayId());
        grayFeatureDetail.setGrayName(grayFeature.getGrayName());
        grayFeatureDetail.setGraySwitch(grayFeature.getGraySwitch());

        List<Long> whiteList = SPLIT_TO_LIST.apply(grayFeature.getWhiteList()).stream().map(Long::new).collect(Collectors.toList());
        grayFeatureDetail.setWhiteList(whiteList);

        List<Long> blackList = SPLIT_TO_LIST.apply(grayFeature.getBlackList()).stream().map(Long::new).collect(Collectors.toList());
        grayFeatureDetail.setBlackList(blackList);

        String[] grayInterval = grayFeature.getGrayInterval().split(",");
        grayFeatureDetail.setGrayIntervalFloor(Integer.valueOf(grayInterval[0]));
        grayFeatureDetail.setGrayIntervalCell(Integer.valueOf(grayInterval[1]));
        grayFeatureDetail.setCreateTime(grayFeature.getCreateTime());

        return grayFeatureDetail;

    }

    private boolean checkGrayFeatureDetail(GrayFeatureDetail detail, Long uid) {
        if (uid == null) {
            return false;
        }
        // 灰度开关关闭
        if (detail.getGraySwitch() == CLOSE_SWITCH) {
            return false;
        }
        // 灰度全量
        if (detail.getGraySwitch() == TRUE_SWITCH) {
            return true;
        }
        // 黑名单
        if (!CollectionUtils.isEmpty(detail.getBlackList()) && detail.getBlackList().contains(uid)) {
            return false;
        }
        // 白名单
        if (!CollectionUtils.isEmpty(detail.getWhiteList()) && detail.getWhiteList().contains(uid)) {
            return true;
        }

        if (detail.getGrayIntervalCell() != null && detail.getGrayIntervalFloor() != null) {
            long mod = uid % 100;
            return detail.getGrayIntervalFloor() <= mod && mod < detail.getGrayIntervalCell();
        }

        return false;
    }
}

缓存这块的逻辑

/**
 * 缓存管理
 *
 * @author zhi
 * @date 2020-02-28 14:17
 */
@Configuration
@Slf4j
public class CacheConfig {

    @Bean(name = "grayFeatureCache")
    public Cache<Long, GrayFeatureDO> grayFeatureCache() {
        // 这里用的是 caffine 缓存他个10分钟 缓存久了 就么得意义了
        Cache<Long, GrayFeatureDO> grayFeatureCache = Caffeine.newBuilder().expireAfterWrite(600, TimeUnit.SECONDS).build();
        log.debug("载入 grayFeatureCache");
        return grayFeatureCache;
    }
}

/**
 * 缓存的实现类 
 * ILoadingCacheService 就是个接口 我就不列出来了
 * 
 * @author zhi
 * @date 2020-02-28 14:24
 */
@Service("GRAY_FEATURE_CACHE")
@Slf4j
public class GrayFeatureCacheImpl implements ILoadingCacheService<Long, GrayFeatureDO> {


    @Resource
    @Qualifier("grayFeatureCache")
    Cache<Long, GrayFeatureDO> grayFeatureCache;

    @Resource
    GrayDAO grayDAO;

    @Override
    public GrayFeatureDO getValue(Long grayId) {
        return grayFeatureCache.get(grayId, key -> grayDAO.getGrayFeatureByGrayId(key));
    }

    @Override
    public void clear() {

    }

    @Override
    public void saveValue(Long integer, GrayFeatureDO grayFeatureDO) {

    }

    @Override
    public long size() {
        return 0;
    }
}

就此就完成了所有灰度的逻辑 简单易懂

https://github.com/YinglishZhi/something/tree/master

本人真的真的不善于写文档。。。