遇到了一个重要的需求,需要在支付场景上做一个大的改动,改动量较大,虽然已经对技术方案做了很多的review,大量的自测,QA测试,但还是觉得应该逐步放量,如果有问题,及时回切,避免造成过大的影响。
网上搜了一下,方案还是很多的,但是感觉都稍微有点复杂,对于灰度的需求是越简单越好,毕竟主要精力还是在业务逻辑上。最终实现了一版灰度方案,虽然看着简单,但经过生产验证,效果不错,实现了快速切量功能以及部分白名单、黑名单功能,这个涉及支付的需求最终也成功上线。下面把这套方案分享一下。直接上代码了。
public class CallbackGrayRelease extends BaseGrayRelease {
/**
* 灰度开关,默认打开
*/
private String open = "true";
/**
* 三方白名单,格式:127,119,104。命中该名单的三方订单,走老逻辑
*/
private String thirdPartnerBlackStr = "";
/**
* 灰度白名单,格式:aaa,bbb,ccc。命中该名单的订单,走新逻辑
*/
private String phoneWhiteStr = "";
/**
* 灰度发布的总份数,默认值10000份
*/
private Integer totalNumber = 10000;
/**
* 灰度发布最小比例,totalNumber>= minRate >= 1
*/
private Integer minRate = 1;
/**
* 灰度发布最大比例,,totalNumber>= maxRate >= minRate
*/
private Integer maxRate = 10000;
/**
* 判断是否命中灰度
*/
public boolean hitGray(String orderId) {
try {
if (StringUtils.equals(this.open, "false")) {
log.info("灰度关闭,orderCode:{}", orderId);
return false;
}
// 判断当前请求是否命中三方黑名单,命中供应商黑名单的走老逻辑
if (hitThirdPartnerWhiteList(orderId, this.thirdPartnerBlackStr)) {
return false;
}
// 判断当前请求是否命中手机号白名单,命中手机号白名单的走新逻辑
if (hitPhoneWhiteList(orderId, this.phoneWhiteStr)) {
return true;
}
// 获取订单hash值,判断是否命中灰度
int hashValue = generateHashByOrderId(orderId, this.totalNumber);
//解释一下minRate和maxRate。minRate是发布最小比例,后者是最大发布比例。比如:minRate是1000,maxRate是5000,那就是只有命中这个范围的hash值才能命中灰度,我们可以自由控制灰度的范围。为什么需要这样实现呢?比如:我们用手机号作为生产hash值的依据,因为每个人的手机号是固定的,就会导致命中灰度和未能命中灰度的人群就定了,实际上我们想要达到的效果是:每次让不同的人群命中灰度
boolean isHitGray = hashValue >= this.minRate - 1 && hashValue <= this.maxRate - 1;
log.info("灰度逻辑。当前订单,灰度范围:{},orderCode:{},hashValue:{},isHitGray:{}", String.format("%d ~ %d", this.minRate, this.maxRate), orderId, hashValue, isHitGray);
return isHitGray;
} catch (Exception e) {
log.error("灰度逻辑异常,走老逻辑。orderCode:{},", orderId, e);
return false;
}
}
}
/**
* 类 描 述:灰度基类
* 创建时间:2023/6/2 15:22
* 创 建 人:test
*/
@Component
@Slf4j
public class BaseGrayRelease {
//hitThirdPartnerWhiteList和hitPhoneWhiteList的方法实现就不贴了
//就是一个业务黑白名单而已,自己看着实现一下就行了。
//根据订单号生成hash值
protected int generateHashByOrderId(String orderId,Integer totalNumber) {
try {
// & Integer.MAX_VALUE的目的是避免出现负数,一个二进制操作。
//Integer.MAX_VALUE的十进制是2147483647,对应的二进制是:01111111 11111111 11111111 11111111,首位是0,我们知道二进制的首位是符号位,代表正负,负数的二进制首位是1,正数的二进制首位是0。所以这个与操作会将负数变成正数,保证我们拿到的hash值是一个正数
return (Objects.hashCode(orderId) & Integer.MAX_VALUE) % totalNumber;
} catch (Exception e) {
log.error("灰度逻辑生成订单hash值异常,走老逻辑,orderCode:{}",orderId,e);
return Integer.MAX_VALUE;
}
}
}
以上逻辑,隐藏了业务逻辑,但是主要的技术实现都有了。说明一下,上述代码经过生产验证,是可用的,但是每个业务的情况不一样,不要直接粘代码到生产上使用,而是吸收方案的思想,进而改造成适合自己业务的方案。如果有问题,可以随时留言沟通。