第四十七章 【重点】海量数据下-流量包更新维护解决方案
第1集 中间件设计思想-Redis过期Key淘汰策略回顾
简介:中间件设计思想-Redis过期Key淘汰策略回顾
-
背景
- redis的key配置了过期时间,这个是怎么被删除的
- redis数据明明过期了,怎么还占用着内存?
-
如果是你怎么设计
- 初级工程师
- 对于如何清除过期的 key ,很自然的可以想到就是可以给每个 key 加一个定时器
- 这样当时间到达过期时间的时候就自动删除 key,这种定时策略叫 主动策略
- 每个带有过期时间的 key 都需要一个定时器,对 CPU 是不友好的,会占用很多的 CPU,这种方式是一种主动的行为
- 高级工程师
- 也可以不用定时器,采取被动的方式,在访问一个 key 的时候去判断这个 key 是否到达过期时间了,过期了就删除掉,这种叫做惰性策略
- 初级工程师
key过期是否立即从内存中删除?
- Redis key过期策略: 定期删除+惰性删除
- 不会立即从内存中删除,当过期key未被客户端调用且未达到执行主动策略的时间,此key依旧存在内存中
Redis如何淘汰过期的keys, set name xdclass 3600(每个设置了过期时间的 key 放入到一个独立的容器中)
- 定期删除:
- 隔一段时间,就随机抽取一些设置了过期时间的 key,检查其是否过期,如果过期就删除,
- 定期删除可能会导致很多过期 key 到了时间并没有被删除掉
- 官方文档:https://www.redis.io/commands/expire
Specifically this is what Redis does 10 times per second:
Test 20 random keys from the set of keys with an associated expire.
Delete all the keys found expired.
If more than 25% of keys were expired, start again from step 1.
-
Redis 会每秒进行十次过期扫描,过期扫描不会遍历容器中所有的 key,而是采用一种特殊策略
- 从容器中随机 20 个 key;
- 删除这 20 个 key 中已经过期的 key;
- 如果过期的 key 比率超过 1/4,那就重复步骤 1;
-
-
惰性删除 :
- 当某个客户端试图访问key时,发现该key已超时会把此key从内存中删除
-
-
如果Redis是主从复制
- 从节点不会让key过期,而是主节点的key过期删除后,成为del命令传输到从节点进行删除
- 主库在 key 到期时,会在 AOF 文件里增加一条 del 指令,同步到所有的从库,从库通过执行这条 del 指令来删除过期的 key
- 指令同步是异步进行的,所以主库过期的 key 的 del 指令没有及时同步到从库的话,会出现主从数据的不一致,主库没有的数据在从库里还存在
-
Redis服务器实际使用的是惰性删除和定期删除两种策略
-
通过配合使用这两种删除策略,服务器可以很好地在合理使用CPU时间和避免浪费内存空间之间取得平衡。
-
问题
-
如果定期删除漏掉了很多过期 key,然后你也没及时去查,也就没走惰性删除,此时会怎么样?
-
如果大量过期 key 堆积在内存里,导致 redis 内存块耗尽了,就需要走内存淘汰机制
-
-
-
看到这里,你有什么思考?有哪些设计思想可以应用到我们短链平台-流量包模块?
第2集 惰性策略思想在主流中间件架构中的应用讲解
简介:惰性策略思想在主流中间件架构中的应用讲解
-
设计模式-单例创建
- 懒汉方式:就是所谓的懒加载,延迟创建对象
- 优点:前期不占据应用内存,用时创建
- 缺点: 初次创建对象有延迟
- 饿汉方式:提前创建好对象
- 优点:实现简单,使用时没延迟
- 缺点:不管有没使用,instance对象一直占着这段内存
- 如何选择:
- 如果对象不大,且创建不复杂,直接用饿汉的方式即可
- 其他情况则采用懒汉实现方式
- 懒汉方式:就是所谓的懒加载,延迟创建对象
-
Mybatis
- 懒加载
- 按需加载,先从单表查询,需要时再从关联表去关联查询,能大大提高数据库性能, 并不是所有场景下使用懒加载都能提高效率
- ResultMap里面的association、collection有延迟加载功能
- 懒加载
-
Redis过期可以淘汰
- 定期删除 : 隔一段时间,就随机抽取一些设置了过期时间的 key,检查其是否过期,如果过期就删除,
- 惰性删除: 当某个客户端试图访问key时,发现该key已超时会把此key从内存中删除
-
Spring中bean创建懒加载(延迟加载)
- 单实例 bean : 默认在容器启动的时候创建对象;
- 懒加载: 容器启动不创建对象。第一次使用的时候也就是获取bean创建对象,并且初始化,使用@lazy注解
第3集 海量数据下流量包更新-惰性策略解决方案讲解
** 海量数据下流量包更新-惰性策略解决方案讲解**
-
流量包更新维护需求
- 付费流量包:通过购买,然后每天都是有一定的使用次数
- 免费流量包:业务为了拉新,鼓励新用户注册,赠送一个免费流量包,每天允许有一定次免费创建短链的次数
-
采用惰性策略解决方案
- 不用每天更新全部流量包,用的时候再更新即可
- 好处
- 只要用户有使用,流量包都是可以得到更新
- 没使用的用户流量包不会去更新,避免了海量数据下更新维护的问题
- 如果采用定时更新,几千万用户更新记录都是会有不少时间的延迟
- 更进阶的作业
- 可以定时 + 惰性
-
大体步骤
- 查询用户全部可用流量包
- 遍历用户可用流量包
- 判断是否更新-用日期判断(要么都更新过,要么都没更新,根据gmt_modified)
- 没更新的流量包后加入【待更新集合】中
- 增加【今天剩余可用总次数】
- 已经更新的判断是否超过当天使用次数
- 如果没超过则增加【今天剩余可用总次数】
- 超过则忽略
- 没更新的流量包后加入【待更新集合】中
- 判断是否更新-用日期判断(要么都更新过,要么都没更新,根据gmt_modified)
- 更新用户今日流量包相关数据
- 扣减使用的某个流量包使用次数
`id` bigint unsigned NOT NULL AUTO_INCREMENT, `day_limit` int DEFAULT NULL COMMENT '每天限制多少条,短链', `day_used` int DEFAULT NULL COMMENT '当天用了多少条,短链', `expired_date` date DEFAULT NULL COMMENT '过期日期', `gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
第4集 海量数据下流量包更新-惰性策略编码开发实战《上》
简介:海量数据下流量包更新-惰性策略编码开发实战《上》
- 伪代码
- 查询用户全部可用流量包
- 遍历用户可用流量包
- 判断是否更新-用日期判断
- 没更新的流量包后加入【待更新集合】中
- 增加【今天剩余可用总次数】
- 已经更新的判断是否超过当天使用次数
- 如果没超过则增加【今天剩余可用总次数】
- 超过则忽略
- 没更新的流量包后加入【待更新集合】中
- 判断是否更新-用日期判断
- 更新用户今日流量包相关数据
- 扣减使用的某个流量包使用次数
- 开发流量包相关的Manager层
/**
* 查找可用的短链流量包(未过期),包括免费流量包
* @param accountNo
* @return
*/
List<TrafficDO> selectAvailableTraffics( Long accountNo);
/**
* 给某个流量包增加使用次数
*
* @param currentTrafficId
* @param accountNo
* @param usedTimes
* @return
*/
int addDayUsedTimes(Long accountNo, Long trafficId, Integer usedTimes) ;
/**
* 恢复流量包使用当天次数
* @param accountNo
* @param trafficId
* @param useTimes
*/
int releaseUsedTimes(long accountNo, Long trafficId, Integer useTimes);
/**
* 批量更新流量包使用次数为0
* @param accountNo
* @param unUpdatedTrafficIds
*/
int batchUpdateUsedTimes(Long accountNo, List<Long> unUpdatedTrafficIds);
第5集 流量包模块Manager层单元测试和Bug修复
简介:流量包模块Manager层单元测试和Bug修复
-
大家可能遇到的问题
- sql边界值问题 或 逻辑问题
- MybatisPlus异常:Invalid bound statement (not found)
-
查找用户全部流量包(付费和免费)
public List<TrafficDO> selectAvailableTraffics(Long accountNo) {
String today = TimeUtil.format(new Date(),"yyyy-MM-dd");
//SELECT * FROM traffic_0 WHERE (account_no = ? AND (expired_date >= ? OR out_trade_no = ?))
QueryWrapper<TrafficDO> queryWrapper = new QueryWrapper<TrafficDO>();
queryWrapper.eq("account_no", accountNo);
queryWrapper.and(wrapper -> wrapper.ge("expired_date", today).or().eq("out_trade_no","free_init"));
List<TrafficDO> trafficDOList = trafficMapper.selectList(queryWrapper);
return trafficDOList;
}
- 给某个流量包增加天使用次数
<!--给某个流量包增加天使用次数-->
<update id="addDayUsedTimes">
update traffic set day_used = day_used + #{usedTimes}
where id = #{trafficId} and account_no = #{accountNo}
and (day_limit - day_used) >= #{usedTimes} limit 1
</update>
- 恢复流量包
<!--恢复流量包-->
<update id="releaseUsedTimes">
update traffic set day_used = day_used - #{usedTimes}
where id = #{trafficId} and account_no = #{accountNo}
and (day_used - #{usedTimes}) >= 0 limit 1;
</update>
- 单元测试
@Test
public void testSelectAvailableTraffics() {
List<TrafficDO> list = trafficManager.selectAvailableTraffics(693100647796441088L);
list.stream().forEach(obj -> {
log.info(obj.toString());
});
}
@Test
public void testAddDayUsedTimes() {
int rows = trafficManager.addDayUsedTimes(693100647796441088L,1486221880318595076L,1);
log.info("rows={}",rows);
}
@Test
public void testReleaseUsedTimes() {
int rows = trafficManager.releaseUsedTimes(693100647796441088L,1486221880318595076L,1);
log.info("rows={}",rows);
}
@Test
public void testBatchUpdateUsedTimes() {
List<Long> ids = new ArrayList<>();
ids.add(1486221880318595073L);
ids.add(1486221880318595076L);
trafficManager.batchUpdateUsedTimes(693100647796441088L,ids);
}
第6集 海量数据下流量包更新-惰性策略编码开发实战《下》
简介:海量数据下流量包更新-惰性策略编码开发实战《下》
- 伪代码
- 查询用户全部可用流量包
- 遍历用户可用流量包
- 判断是否更新-用日期判断
- 没更新的流量包后加入【待更新集合】中
- 增加【用总次数】
- 已经更新的判断是否超过当天使用次数
- 增加今日可用总次数(多流量包累加)
- 超过则忽略
- 没更新的流量包后加入【待更新集合】中
- 判断是否更新-用日期判断
- 更新用户今日流量包相关数据
- 扣减使用的某个流量包使用次数
- 组装业务逻辑
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UseTrafficVO {
/**
* 天剩余可用总次数 = 总次数-已用
*/
private Integer dayTotalLeftTimes;
/**
* 当前使用流量包
*/
private TrafficDO currentTrafficDO ;
/**
* 没过期,且今天没更新的流量包
*/
private List<Long> unUpdatedTrafficIds = new ArrayList<>();
}
@Override
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
public JsonData useTraffic(UseTrafficRequest trafficRequest) {
Long accountNo = trafficRequest.getAccountNo();
//处理流量包,筛选未更新流量包、当前使用流量包
UseTrafficVO useTrafficVO = processTrafficList(accountNo);
log.info("今天可用总次数:{}, 当前使用的流量包:{}",useTrafficVO.getDayTotalLeftTimes(),useTrafficVO.getCurrentTrafficDO());
//如果当前流量包为空,则没有可用流量包
if(useTrafficVO.getCurrentTrafficDO() == null){
return JsonData.buildResult(BizCodeEnum.TRAFFIC_REDUCE_FAIL);
}
log.info("待更新流量包列表:{}",useTrafficVO.getUnUpdatedTrafficIds());
if(useTrafficVO.getUnUpdatedTrafficIds().size() >0) {
//更新今日流量包
trafficManager.batchUpdateUsedTimes(accountNo, useTrafficVO.getUnUpdatedTrafficIds());
}
//先更新,再增加此次流量包扣减
int rows = trafficManager.addDayUsedTimes( accountNo, useTrafficVO.getCurrentTrafficDO().getId(),1);
if(rows !=1){
throw new BizException(BizCodeEnum.TRAFFIC_REDUCE_FAIL);
}
return JsonData.buildSuccess();
}
/**
* 处理流量包,筛选未更新流量包、当前使用流量包
* @param accountNo
*/
private UseTrafficVO processTrafficList(Long accountNo){
//全部流量包
List<TrafficDO> list = trafficManager.selectAvailableTraffics(accountNo);
if (list == null || list.size() == 0) { throw new BizException(BizCodeEnum.TRAFFIC_EXCEPTION);}
//天剩余可用总次数 = 总次数-已用
Integer dayTotalLeftTimes = 0;
//当前使用流量包
TrafficDO currentTrafficDO = null;
//没过期,且今天没更新的流量包
List<Long> unUpdatedTrafficIds = new ArrayList<>();
//今天日期
String todayStr = TimeUtil.format(new Date(),"yyyy-MM-dd");
for(TrafficDO trafficDO : list){
//判断是否更新,用日期判断,不能用时间
String trafficUpdateDate = TimeUtil.format(trafficDO.getGmtModified(),"yyyy-MM-dd");
if(todayStr.equalsIgnoreCase(trafficUpdateDate)){
//已经更新 剩余可用 = 天总次数-已用次数
int dayLeftTimes = trafficDO.getDayLimit()-trafficDO.getDayUsed();
dayTotalLeftTimes = dayTotalLeftTimes + dayLeftTimes;
//选取 当次流量包
if(dayLeftTimes>0 && currentTrafficDO == null){
currentTrafficDO = trafficDO;
}
}else {
//未更新
dayTotalLeftTimes = dayTotalLeftTimes + trafficDO.getDayLimit();
//记录未更新流量包 剩余可用 = 天总次数
unUpdatedTrafficIds.add(trafficDO.getId());
//选取 当次流量包
if(currentTrafficDO == null){
currentTrafficDO = trafficDO;
}
}
}
UseTrafficVO useTrafficVO =
new UseTrafficVO(dayTotalLeftTimes,currentTrafficDO,unUpdatedTrafficIds);
return useTrafficVO;
}
第7集 海量数据下流量包更新-惰性策略编码测试
简介:海量数据下流量包更新-惰性策略编码测试
- 流量包更新-惰性策略编码测试
第四十四章 【重点】短链服务+流量包业务联动架构设计
第1集 短链服务和流量包业务联动RPC调用链路性能问题
简介:短链服务和流量包业务联动RPC调用性能问题优化
- 性能需求分析
- 创建短链->扣减流量包(RPC)->发送MQ消息
第2集 高并发下扣减流量包解决方案讲解-通用秒杀
简介:高并发下扣减流量包解决方案讲解-通用秒杀
- 扣减流量包性能优化解决方案
第3集 高并发下-创建短链和流量包业务联动开发
简介:短链服务-创建短链和流量包业务联动开发
-
需求
- 创建短链
- 扣减流量包
- 发送MQ
-
编码实战
- 鉴权增加token(多种方式)
#用于rpc调用token验证 rpc.token=class.net168
- 短链服务编码
@Data @AllArgsConstructor @NoArgsConstructor @Builder public class UseTrafficRequest { /** * 账号 */ private Long accountNo; /** * 业务id, 短链码 */ private String bizId; } @FeignClient(name = "dcloud-account-service") public interface TrafficFeignService { /** * 使用流量包 * * @param shortLinkCode 业务id,短链则是短链码 * @return */ @PostMapping(value = "/api/traffic/v1/reduce", headers = {"rpc-token=${rpc.token}"}) JsonData useTraffic(@RequestBody UseTrafficRequest request); }
- C端消费者-扣减流量包
//先判断是否短链码被占用 ShortLinkDO shortLinCodeDOInDB = shortLinkManager.findByShortLinCode(shortLinkCode); if (shortLinCodeDOInDB == null) { boolean reduceFlag = reduceTraffic(eventMessage,shortLinkCode); if (reduceFlag) { ShortLinkDO shortLinkDO = ShortLinkDO.builder().accountNo(accountNo).code(shortLinkCode) .title(addRequest.getTitle()).originalUrl(addRequest.getOriginalUrl()) .domain(domainDO.getValue()).groupId(linkGroupDO.getId()) .expired(addRequest.getExpired()).sign(originalUrlDigest) .state(ShortLinkStateEnum.ACTIVE.name()).del(0).build(); shortLinkManager.addShortLink(shortLinkDO); return true; } } /** * 扣减流量包 * * @param eventMessage * @return */ private boolean reduceTraffic(EventMessage eventMessage,String shortLinkCode) { //处理流量包扣减 //根据短链类型,检查是否有足够多的流量包 分布式事务问题 UseTrafficRequest request = UseTrafficRequest.builder() .accountNo(eventMessage.getAccountNo()) .bizId(shortLinkCode) .build(); JsonData jsonData = trafficFeignService.useTraffic(request); //使用流量包 if (jsonData.getCode() != 0) { log.error("流量包不足,扣减失败操:{}", eventMessage); return false; } return true; }
- 账号服务编码
@Value("${rpc.token}") private String rpcToken; @PostMapping("reduce") public JsonData useTraffic(@RequestBody UseTrafficRequest useTrafficRequest, HttpServletRequest request){ //具体使用流量包逻辑 String requestToken = request.getHeader("rpc-token"); if (rpcToken.equalsIgnoreCase(requestToken)) { JsonData jsonData = trafficService.reduce(useTrafficRequest); return jsonData; } else { return JsonData.buildError("非法访问"); } }
拦截器配置 "/api/traffic/*/reduce" 不进行登录拦截
第4集 高并发下扣减流量包-日剩余流量包计算存储Redis
简介:高并发下扣减流量包-日剩余流量包计算存储Redis
- 日剩余流量包计算存储
/**
* 1天总的流量包
*/
public static final String DAY_TOTAL_TRAFFIC = "lock:traffic:day_total:%s";
//先更新,再扣减当前使用的流量包
int rows = trafficManager.addDayUsedTimes(accountNo,useTrafficVO.getCurrentTrafficDO().getId(),1);
if(rows != 1){throw new BizException(BizCodeEnum.TRAFFIC_REDUCE_FAIL);}
//往Redis设置流量包总次数,短链服务那边递减次数,如果有新增流量包,则删除这个key TODO
long leftSeconds = TimeUtil.getRemainSecondsOneDay(new Date());
String totalKey = String.format(RedisKey.DAY_TOTAL_TRAFFIC, accountNo);
//减少1是减去此次的流量次数,redis 库存-1,有可能库存为零 ,这个需要额外判断下。
redisTemplate.opsForValue().set(totalKey, useTrafficVO.getDayTotalLeftTimes() - 1, leftSeconds, TimeUnit.SECONDS);
return JsonData.buildSuccess();
/** TimeUtil
* 获取当天剩余的秒数,用于流量包过期配置
* @param currentDate
* @return
*/
public static Integer getRemainSecondsOneDay(Date currentDate) {
LocalDateTime midnight = LocalDateTime.ofInstant(currentDate.toInstant(),
ZoneId.systemDefault()).plusDays(1).withHour(0).withMinute(0)
.withSecond(0).withNano(0);
LocalDateTime currentDateTime = LocalDateTime.ofInstant(currentDate.toInstant(),
ZoneId.systemDefault());
long seconds = ChronoUnit.SECONDS.between(currentDateTime, midnight);
return (int) seconds;
}
第5集 高并发下创建短链-预扣减流量包Lua脚本开发
简介:高并发下创建短链-预扣减流量包Lua脚本开发
- 预扣减流量包Lua脚本开发
//需要预先检查下是否有足够多的可以进行创建
String cacheKey = String.format(RedisKey.DAY_TOTAL_TRAFFIC, accountNo);
//检查key是否存在,然后递减,是否大于等于0,使用lua脚本
// 如果key不存在,则未使用过,lua返回值是0; 新增流量包的时候,不用重新计算次数,直接删除key,消费的时候回计算更新
String script = "if redis.call('get',KEYS[1]) then return redis.call('decr',KEYS[1]) else return 0 end";
Long leftTimes = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(cacheKey), "");
log.info("今日流量包剩余次数:{}", leftTimes);
if (leftTimes >= 0) {
String newOriginalUrl = CommonUtil.addUrlPrefix(request.getOriginalUrl());
request.setOriginalUrl(newOriginalUrl);
EventMessage eventMessage = EventMessage.builder().accountNo(accountNo)
.content(JsonUtil.obj2Json(request))
.messageId(IDUtil.geneSnowFlakeID().toString())
.eventMessageType(EventMessageType.SHORT_LINK_ADD.name())
.build();
rabbitTemplate.convertAndSend(rabbitMQConfig.getShortLinkEventExchange(), rabbitMQConfig.getShortLinkAddRoutingKey(), eventMessage);
} else {
return JsonData.buildResult(BizCodeEnum.TRAFFIC_REDUCE_FAIL);
}
第6集 高并发下创建短链-预扣减流量包链路测试
简介:高并发下创建短链-预扣减流量包链路测试
- 测试
- 流量包扣减
- 流量包更新维护
- 亮点
- 惰性策略更新维护流量包-海量数据下
- 高并发扣减流量包-预扣减机制
第四十五章 微服务必备技术分布式事务介绍和Base理论案例
第1集 关于分布式事务的由来你知识多少
简介:分布式事务介绍和产生原因
-
什么是分布式事务
-
事务
- 事务指的就是一个操作单元,在这个操作单元中的所有操作最终要保持一致的行为,要么所有操作都成功,要么所有的操作都被撤销
- 分两种
- 一个是本地事务:本地事物其实可以认为是数据库提供的事务机
- 一个是分布式事务
-
分布式事务
指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。 简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用 分布式事务需要保证这些小操作要么全部成功,要么全部失败。 本质上来说,分布式事务就是为了保证不同数据库的数据一致性
-
-
产生的原因
- 业务发展,数据库的拆分-分库分表
- SOA和微服务架构的使用
- 多个微服务之间调用异常
- 网络异常、请求超时、数据库异常、程序宕机等
基本是每个微服务架构项目都离不开的难题
第2集 分布式事务的常见解决方案概览
简介:讲解分布式事务常见解决方案概览
- 常见分布式事务解决方案
- 2PC 和 3PC
- 两阶段提交, 基于XA协议
- TCC
- Try、Confirm、Cancel
- 事务消息
- 最大努力通知型
- 2PC 和 3PC
- 分布式事务分类
- 刚性事务:遵循ACID
- 柔性事务:遵循BASE理论
- 分布式事务框架
- TX-LCN:支持2PC、TCC等多种模式
- https://github.com/codingapi/tx-lcn
- 更新慢(个人感觉处于停滞状态)
- Seata:支持 AT、TCC、SAGA 和 XA 多种模式
- https://github.com/seata/seata
- 背靠阿里,专门团队推广
- 阿里云商业化产品GTS
- https://www.aliyun.com/aliware/txc
- RocketMq:自带事务消息解决分布式事务
- https://github.com/apache/rocketmq
- TX-LCN:支持2PC、TCC等多种模式
第3集 分布式事务下数据最终一致性-CAP的权衡结果 BASE理论
简介:分布式事务下数据最终一致性-BASE理论介绍
- 什么是Base理论
CAP 中的一致性和可用性进行一个权衡的结果,核心思想就是:我们无法做到强一致,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性, 来自 ebay 的架构师提出
-
Basically Available(基本可用)
- 假设系统,出现了不可预知的故障,但还是能用, 可能会有性能或者功能上的影响,比如RT是10ms,变成50ms
-
Soft state(软状态)
- 允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性,即允许系统在多个不同节点的数据副本存在数据延时
-
Eventually consistent(最终一致性)
- 系统能够保证在没有其他新的更新操作的情况下,数据最终一定能够达到一致的状态,因此所有客户端对系统的数据访问最终都能够获取到最新的值
-
关于数据一致性
- 强一致:操作后的能立马一致且可以访问
- 弱一致:容忍部分或者全部访问不到
- 最终一致:弱一致性经过多一段时间后,都一致且正常
第4集 BASE理论-分布式事务的解决方案之一事务消息
简介:BASE理论-讲解分布式事务的解决方案之一事务消息
- 事务消息
- 消息队列提供类似Open XA的分布式事务功能,通过消息队列事务消息能达到分布式事务的最终一致
- 半事务消息
- 暂不能投递的消息,发送方已经成功地将消息发送到了消息队列服务端,但是服务端未收到生产者对该消息的二次确认,此时该消息被标记成“暂不能投递”状态,处于该种状态下的消息即半事务消息。
- 消息回查
- 由于网络闪断、生产者应用重启等原因,导致某条事务消息的二次确认丢失,消息队列服务端通过扫描发现某条消息长期处于“半事务消息”时,需要主动向消息生产者询问该消息的最终状态(Commit或是Rollback),该询问过程即消息回查
- 交互图(来源rocketmq官方文档)
- 目前较为主流的MQ,比如ActiveMQ、RabbitMQ、Kafka、RocketMQ等,只有RocketMQ支持事务消息
- 如果其他队列需要事务消息,可以开发个消息服务,自行实现半消息和回查功能
- 好处
- 事务消息不仅可以实现应用之间的解耦,又能保证数据的最终一致性
- 同时将传统的大事务可以被拆分为小事务,能提升效率
- 不会因为某一个关联应用的不可用导致整体回滚,从而最大限度保证核心系统的可用性
- 缺点
- 不能实时保证数据一致性
- 极端情况下需要人工补偿,比如 假如生产者成功处理本地业务,消费者始终消费不成功
第5集 最终一致性的体现-第三方支付平台和微服务之间的通讯
简介:讲解第三方支付平台和微服务之间的交互
- 支付业务
- 支付宝支付
- 微信支付
- 其他支付
- 多个服务之间通信,怎么保证分布式事务呢?
- 利用最终一致性思想,也叫柔性事务解决方案
- 关于数据一致性
- 强一致:操作后的能立马一致且可以访问
- 弱一致:容忍部分或者全部访问不到
- 最终一致:弱一致性经过多一段时间后,都一致且正常
第四十六章 高并发下短链服务和流量包服务分布式事务解决方案
第1集 高并发下-创建短链和扣减流量包的分布式事务设计
简介: 高并发下-创建短链和扣减流量包的分布式事务设计
- 当前交互链路
- 常规分布式事务
- TCC、2PC等方案性能差,普遍都是需要加锁
- Seata框架也是,适合常规的管理后台或者并发量不高的场景解决分布事务,一个注解搞定
- 常规分布式事务
第2集 创建短链和扣减流量包的分布式事务RabbitMQ解决方案
简介: 创建短链和扣减流量包的分布式事务RabbitMQ解决方案
- 分布式调度定时检测
- XXL-JOB分布式定时任务
- 延迟队列
- 延迟队列
- 一种带有延迟功能的消息队列,Producer 将消息发送到消息 队列 服务端,但并不期望这条消息立⻢投递,而是推迟到在 当前时间点之后的某一个时间投递到 Consumer 进行消费
- 该消息即定时消息
- 业界的一些实现方式
- 定时任务高精度轮训
- 采用RocketMQ自带延迟消息功能
- RabbitMQ本身是不支持延迟队列的,怎么办? 结合死信队列的特性,就可以做到延迟消息
第3集 流量包锁定任务表设计和创建讲解
简介: 流量包锁定任务表设计和创建讲解
- 流量包锁定任务表
CREATE TABLE `traffic_task` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`account_no` bigint DEFAULT NULL,
`traffic_id` bigint DEFAULT NULL,
`use_times` int DEFAULT NULL,
`lock_state` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '锁定状态锁定LOCK 完成FINISH-取消CANCEL',
`biz_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '唯一标识',
`gmt_create` datetime DEFAULT CURRENT_TIMESTAMP,
`gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_biz_id` (`biz_id`) USING BTREE,
KEY `idx_release` (`account_no`,`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
改动
- 不叫messageId,存在误解改为bizId(存储短链码,检查是否创建短链成功)
- POJO类改动
- 创建LOCK状态枚举类
public enum TaskStateEnum {
/**
* 锁定
*/
LOCK,
/**
* 完成
*/
FINISH,
/**
* 取消,释放库存
*/
CANCEL;
}
第4集 流量包任务表Manager层相关开发
简介: 流量包任务表Manager层相关开发
- 编码
@Component
@Slf4j
public class TrafficTaskManagerImpl implements TrafficTaskManager {
@Autowired
private TrafficTaskMapper trafficTaskMapper;
@Override
public int add(TrafficTaskDO trafficTaskDO) {
return trafficTaskMapper.insert(trafficTaskDO);
}
@Override
public TrafficTaskDO findByIdAndAccountNo(Long id, Long accountNo) {
TrafficTaskDO taskDO = trafficTaskMapper.selectOne(new QueryWrapper<TrafficTaskDO>()
.eq("id", id).eq("account_no", accountNo));
return taskDO;
}
@Override
public int deleteByIdAndAccountNo(Long id, Long accountNo) {
return trafficTaskMapper.delete(new QueryWrapper<TrafficTaskDO>()
.eq("id", id).eq("account_no", accountNo));
}
}
第5集 创建短链扣减流量包整合RabbitMQ死信队列配置
简介: 创建短链扣减流量包整合RabbitMQ死信队列配置
- 死信交换机配置
//================流量包扣减,创建短链死信队列配置==================================
// 发送锁定流量包消息-》延迟exchange-》lock.queue-》死信exchange-》release.queue 延迟队列,不能被监听消费
/**
* 第一个队列延迟队列,
*/
private String trafficReleaseDelayQueue = "traffic.release.delay.queue";
/**
* 第一个队列的路由key
* 进入队列的路由key
*/
private String trafficReleaseDelayRoutingKey = "traffic.release.delay.routing.key";
/**
* 第二个队列,被监听恢复流量包的队列
*/
private String trafficReleaseQueue = "traffic.release.queue";
/**
* 第二个队列的路由key
*
* 即进入死信队列的路由key
*/
private String trafficReleaseRoutingKey="traffic.release.routing.key";
/**
* 过期时间,毫秒,1分钟
*/
private Integer ttl = 60000;
/**
* 延迟队列
*/
@Bean
public Queue trafficReleaseDelayQueue(){
Map<String,Object> args = new HashMap<>(3);
args.put("x-message-ttl",ttl);
args.put("x-dead-letter-exchange", trafficEventExchange);
args.put("x-dead-letter-routing-key",trafficReleaseRoutingKey);
return new Queue(trafficReleaseDelayQueue,true,false,false,args);
}
/**
* 死信队列,普通队列,用于被监听
*/
@Bean
public Queue trafficReleaseQueue(){
return new Queue(trafficReleaseQueue,true,false,false);
}
/**
* 第一个队列,即延迟队列的绑定关系建立
* @return
*/
@Bean
public Binding trafficReleaseDelayBinding(){
return new Binding(trafficReleaseDelayQueue,Binding.DestinationType.QUEUE, trafficEventExchange,trafficReleaseDelayRoutingKey,null);
}
/**
* 死信队列绑定关系建立
* @return
*/
@Bean
public Binding trafficReleaseBinding(){
return new Binding(trafficReleaseQueue,Binding.DestinationType.QUEUE, trafficEventExchange,trafficReleaseRoutingKey,null);
}
第四十七章 创建短链-扣减流量包数据最终一致性编码实战
第1集 创建短链-扣减流量包整合RabbitMQ死信队列实战《上》
简介: 创建短链-扣减流量包整合RabbitMQ死信队列实战
-
编码实战
- 保存Task
//先更新,再扣减当前使用的流量包 int rows = trafficManager.addDayUsedTimes(accountNo,useTrafficVO.getCurrentTrafficDO().getId(),1); TrafficTaskDO trafficTaskDO = TrafficTaskDO.builder().accountNo(accountNo) .bizId(trafficRequest.getBizId()) .useTimes(1).trafficId(useTrafficVO.getCurrentTrafficDO().getId()) .lockState(TaskStateEnum.LOCK.name()).build(); trafficTaskManager.add(trafficTaskDO);
- 发送MQ消息
EventMessage usedTrafficEventMessage = EventMessage.builder() .accountNo(accountNo) .bizId(trafficTaskDO.getId() + "") .eventMessageType(EventMessageType.TRAFFIC_USED.name()) .build(); //发送延迟信息,用于异常回滚,数据最终一致性 rabbitTemplate.convertAndSend(rabbitMQConfig.getTrafficEventExchange(), rabbitMQConfig.getTrafficReleaseDelayRoutingKey(), usedTrafficEventMessage); return JsonData.buildSuccess();
-
发送测试
第2集 创建短链-扣减流量包整合RabbitMQ死信队列实战《下》
简介: 创建短链-扣减流量包整合RabbitMQ死信队列实战
- feign远程调用
@FeignClient(name = "dcloud-link-service")
public interface ShortLinkFeignService {
/**
* 检查短链是否存在
*
* @param shortLinkCode 短链码
* @return
*/
@GetMapping(value = "/api/link/v1/check", headers = {"rpc-token=${rpc.token}"})
JsonData simpleDetail(@RequestParam("shortLinkCode") String shortLinkCode);
}
@Value("${rpc.token}")
private String rpcToken;
/**
* rpc调用获取短链信息
*
* @return
*/
@GetMapping("check")
public JsonData simpleDetail(@RequestParam("shortLinkCode") String shortLinkCode, HttpServletRequest request) {
String requestToken = request.getHeader("rpc-token");
if (rpcToken.equalsIgnoreCase(requestToken)) {
ShortLinkVO shortLinkVO = shortLinkService.parseShortLinkCode(shortLinkCode);
return shortLinkVO == null ? JsonData.buildError("不存在") : JsonData.buildSuccess();
} else {
return JsonData.buildError("非法访问");
}
}
- 消费者开发
else if(EventMessageType.TRAFFIC_USED.name().equalsIgnoreCase(messageType)){
//流量包使用,检查是否成功使用
//检查task是否存在
//检查短链是否成功
//如果不成功则恢复流量包,删除缓存key
//删除task(也可以更新状态,定时删除也行)
Long trafficTaskId = Long.valueOf(eventMessage.getBizId());
TrafficTaskDO trafficTaskDO = trafficTaskManager.findByIdAndAccountNo(trafficTaskId, accountNo);
//非空 且 是锁定状态
if(trafficTaskDO!=null && trafficTaskDO.getLockState()
.equalsIgnoreCase(TaskStateEnum.LOCK.name())){
JsonData jsonData = shortLinkFeignService.check(trafficTaskDO.getBizId());
if(jsonData.getCode()!=0){
log.error("创建短链失败,流量包回滚");
trafficManager.releaseUsedTimes(accountNo,trafficTaskDO.getTrafficId(),trafficTaskDO.getUseTimes());
}
trafficTaskManager.deleteByIdAndAccountNo(trafficTaskId,accountNo);
}
}
第3集 创建短链失败-流量包数据跨日期恢复问题修复
简介: 创建短链失败-流量包数据跨日期恢复问题修复
- 恢复流量包跨日期问题
- 1月1号晚上11点59分创建,然后失败了,1月2号凌晨恢复了次日流量包
- 解决方法:增加恢复流量包的日期
<!--恢复流量包-->
<update id="releaseUsedTimes">
update traffic set day_used = day_used - #{usedTimes}
where id = #{trafficId} and account_no = #{accountNo}
and (day_used - #{usedTimes}) >= 0 and date_format(gmt_modified, '%Y-%m-%d')=#{useDateStr} limit 1;
</update>
- 流量包重新计算(可以先删除key,再恢复流量包)
//恢复流量包,应该删除这个key
String totalTrafficTimesKey = String.format(RedisKey.DAY_TOTAL_TRAFFIC,accountNo);
redisTemplate.delete(totalTrafficTimesKey);
第4集 创建短链-扣减流量包数据最终一致性链路测试实战
简介: 创建短链-扣减流量包数据最终一致性链路测试实战
- 链路测试
- 创建短链成功,流量包扣减成功,缓存-数据库一致
- 创建短链失败,流量包扣减回滚,缓存-数据库一致
ate()
.equalsIgnoreCase(TaskStateEnum.LOCK.name())){
JsonData jsonData = shortLinkFeignService.check(trafficTaskDO.getBizId());
if(jsonData.getCode()!=0){
log.error("创建短链失败,流量包回滚");
trafficManager.releaseUsedTimes(accountNo,trafficTaskDO.getTrafficId(),trafficTaskDO.getUseTimes());
}
trafficTaskManager.deleteByIdAndAccountNo(trafficTaskId,accountNo);
}
}
#### 第3集 创建短链失败-流量包数据跨日期恢复问题修复
**简介: 创建短链失败-流量包数据跨日期恢复问题修复**
- 恢复流量包跨日期问题
- 1月1号晚上11点59分创建,然后失败了,1月2号凌晨恢复了次日流量包
- 解决方法:增加恢复流量包的日期
```java
<!--恢复流量包-->
<update id="releaseUsedTimes">
update traffic set day_used = day_used - #{usedTimes}
where id = #{trafficId} and account_no = #{accountNo}
and (day_used - #{usedTimes}) >= 0 and date_format(gmt_modified, '%Y-%m-%d')=#{useDateStr} limit 1;
</update>
- 流量包重新计算(可以先删除key,再恢复流量包)
//恢复流量包,应该删除这个key
String totalTrafficTimesKey = String.format(RedisKey.DAY_TOTAL_TRAFFIC,accountNo);
redisTemplate.delete(totalTrafficTimesKey);
第4集 创建短链-扣减流量包数据最终一致性链路测试实战
简介: 创建短链-扣减流量包数据最终一致性链路测试实战
- 链路测试
- 创建短链成功,流量包扣减成功,缓存-数据库一致
- 创建短链失败,流量包扣减回滚,缓存-数据库一致