知识星球Lottery分布式抽奖系统项目-Note-问题描述/解决方案/Note/Question

本文介绍了在Java微服务开发中,如何处理数据分库分表、数据库路由策略、事务管理,以及使用MapStruct进行对象转换和Kafka消息队列的使用。重点讨论了SpringBoot集成Mybatis的事务处理、Dubbo服务间的调用以及Kafka的监听和错误处理。
摘要由CSDN通过智能技术生成

问题描述

Idea使用Maven Install打包会报错(第一次打大包)


原因分析:

Maven模块间不能循环依赖

Q: 什么是循环依赖?
循环依赖就是循环引用:两个或多个bean相互持有对方
ps:和循环调用的区别,循环调用是方法之间的环调用,且无法解决,除非有终结条件,否则就是死循环,最终导致内存溢出


Q:为什么不能循环依赖
比如构造器的循环依赖,创建TestA类时发现构造器需要TestB类,然后去创建TestB,创建时又发现要创建TestA,又跑回A,然后形成一个环


Q:Spring如何解决循环依赖
对于构造器的循环依赖:解决不了
对于setter循环依赖:setter注入造成的依赖是通过一个 “Spring容器提前暴露刚完成构造器注入但还没有完成其它步骤” 的bean完成的,而且只能解决单例bean的循环依赖
通过提前暴露一个单例工厂方法,从而使其它bean能引用该bean
步骤:首先根据无参构造器创建bean,并且暴露一个ObjectFactory 用于返回一个提前暴露一个创建中的bean,并将它的标识符放到当前创建的的bean池,然后进行setter注入别的bean


问题描述

运行test会报错(小册第三个分支)

java.lang.IllegalStateException: Failed to load ApplicationContext

在这里插入图片描述


解决方案:

Failed to load ApplicationContext一般就是配置文件写错了
以及需要有mybatis-config.xml的配置文件以及操作数据库的mapper映射文件
通过application.yml交给springboot管理:

mybatis:
  config-location:  classpath:/mybatis/config/mybatis-config.xml
  mapper-locations: classpath:/mybatis/mapper/*.xml

yml文件记录这两个配置的路径
在这里插入图片描述
然后表的列名和实体类的类名不是完全一致,因为根据规则,一个是下划线,一个是驼峰,所以需要打开一个设置:
mapUnderscoreToCamelCase
在这里插入图片描述
在这里插入图片描述
在官网里有介绍
官网
是这样描述的:是否开启驼峰命名自动映射,即从经典数据库列名 A_COLUMN 映射到经典 Java 属性名 aColumn。

但我是这样写的:

    <select id="queryActivityById" parameterType="java.lang.Long" resultType="cn.itedus.lottery.infrastructure.po.Activity">
        SELECT activityId, activityName, activityDesc,beginDateTime, endDateTime,
               stockCount, takeCount, state, creator, createTime, updateTime
        FROM activity
        WHERE activityId = #{activityId}
    </select>

显然在学mybatis就没有搞懂这个对应
改成这样:

<select id="queryActivityById" parameterType="java.lang.Long" resultType="cn.itedus.lottery.infrastructure.po.Activity">
        SELECT activity_Id , activity_Name , activity_Desc ,begin_Date_Time , end_Date_Time ,
               stock_Count , take_Count , state, creator, create_Time , update_Time
        FROM activity
        WHERE activity_Id = #{activityId}
    </select>

对应上了
以及需要注意的是

通过springboot的配置文件配置mybatis的设置,则不能够再使用mybatis的配置文件,例如:下边代码中标红的两个设置不能同时存在,要么使用config-location指定mybatis的配置文件,在通过mybatis的配置文件配置相关设置,要么通过springboot配置文件的mybatis.configuration进行相关设置,二者只能选其一,否则会报错
意思就是对驼峰的配置不能也放在.yml文件,否则就会报错:java.lang.IllegalStateException: Failed to load ApplicationContext


问题描述

Dubbo广播模式出错:什么client remoting


解决方案

我一直以为是广播连接出错,更改了.yml都没有效果
然后发现,我没有开启服务端
在这里插入图片描述
我就说Issues怎么没有和我一样报错的…
在这里插入图片描述
然后开启之后报这个错误:
Invoke remote method timeout :method:queryActivityById()
然后我看了看这个方法在rpc包里
我的包结构:
在这里插入图片描述
在这里插入图片描述
然后再根据Dubbo官网的SpringBoot使用说明:官网

发现是Lottery-Test配置文件配置出错了,provider和customer的配置文件都应该加入interface的依赖,这里的interface是rpc包下的IActivityBooth
然后写错了写成依赖了interfaces包,改成rpc就可以成功运行了

在这里插入图片描述


思考:所以啥是Dubbo,为啥要用它


问题描述

配置文件各种爆红,依赖失败,插件爆红等等。eg.spring-boot-maven-plugin/maven-compiler-plugin等


解决方案

更改maven的home path:
在这里插入图片描述

然后加version标签(这个没加version不会爆红,虽然我也不知道为什么,但是ctrl +F查看到了它的版本号
在这里插入图片描述
在这里插入图片描述
添加到另外一个项目的配置文件后终于不红了


问题描述


解决方案


疑惑

  1. 为啥用String[ ]存放散列结果
  2. 为啥使用ConcurrentHashMap
package cn.itedus.lottery.domain.strategy.services.algorithm;
 // 存放概率与奖品对应的散列结果,strategyId -> rateTuple
    protected Map<Long,String[]> rateTupleMap = new ConcurrentHashMap<>();

解答

HashMap :非线程安全
HashTable:线程安全(因为方法上都加了synchronized锁,但锁粒度大,性能低)
ConcurrentHashMap:支持检索的完全并发和更新的高预期并发性。 遵循与Hashtable相同的功能规范,并包括与Hashtable每个方法对应的方法版本(但是,即使所有操作都是线程安全的,检索操作也不需要锁定,并且不支持以阻止所有访问的方式锁定整个表)此类与Hashtable完全可互操作,像Hashtable但不像HashMap ,这个类不允许 null用作键或值。


sk

抽奖算法是为了让奖品均匀分散在不同位置,除了斐波那契索引还有别的方法,比如二维数组取模就可以实现,不想连续就shuffle


sk

为什么公共算法类要包装为抽象类
在这里插入图片描述


sk

总体概率算法的实现 解释

... ...
int cursorVal = 0;
        for (AwardRateInfo awardRateInfo : differenceAwardRateList) {
            int rateVal = awardRateInfo.getAwardRate().divide(differenceDenominator, 2, BigDecimal.ROUND_UP).multiply(new BigDecimal(100)).intValue();
            if (randomVal <= (cursorVal + rateVal)) {
                awardId = awardRateInfo.getAwardId();
                break;
            }
            cursorVal += rateVal;
        }

Note

抽奖模板模式各类和接口的关系
在这里插入图片描述

Note

关于接口
from:

package cn.itedus.lottery.domain.strategy.service.draw;

import ... ...
public class DrawConfig {
    @Resource
    private IDrawAlgorithm defaultRateRandomDrawAlgorithm;// 接口的变量

    @Resource
    private IDrawAlgorithm singleRateRandomDrawAlgorithm;// 接口的变量
... ...
}

接口的特性
尽管不能构造接口的对象,但是可以声明接口的变量
接口的变量必须引用接口的类对象

Comparable x;
x = new Employee(...);// Employee implements Comparable

接口可以被扩展,一个接口extends另外一个接口
接口不能包含实例和静态方法,但可以包含常数
一个类只能扩展一个类,但能实现很多个接口


q

为什么奖品id的数据类型不是List

package cn.itedus.lottery.infrastructure.po;
public class StrategyDetail {
 ... ...
    // 奖品ID
    private String awardId;
    ... ...
}

a


q

Repository包,为什么要创建抽奖策略仓库:IStrategyRepository

a

Repository其实不是仓库的意思(翻译来说确实是),(其实也是仓库的意思:仓库服务(活动表、奖品表、策略表、策略明细表))这里的Repository指的是@Repository,这个是@Component的特殊形式,代表注解DAO,也就是持久层
查看接口(为什么搞这个接口可以用AOP理解我觉得)、接口有如下几种方法

void initRateTable(Long strategyId,List<AwardRateInfo> awardRateInfoList);
//根据抽奖策略id和需要排除的奖品,返回结果
String randomDraw(Long strategyId, List<String> excludeAwardIds);
boolean isExistRateTuple(Long strategyId);

在它的实现类StrategyRepository,调用了DTO层的接口,使用了DTO层方法(和数据库进行消息传递)

@Component
public class StrategyRepository implements IStrategyRepository {

    @Resource
    private IStrategyDao strategyDao;

    @Resource
    private IStrategyDetailDao strategyDetailDao;

    @Resource
    private IAwardDao awardDao;
    ... ...
    }

上面@Component和@Resource这个结构也很好理解:@Component能标识表示一个bean的类,而一个bean也会有对其它bean的引用
BeanDefinition描述一个Bean的实例,它定义了对其他Bean的引用,这样Bean之间可以协作和依赖,也就是@Resource


Note

模板模式
模板模式
在这里插入图片描述
和工厂模式的区别?
在这里插入图片描述

关于模版模式的核心点在于由抽象类定义抽象方法执行策略,也就是说父类规定了好一系列的执行标准,这些标准的串联成一整套业务流程
遇到适合的场景使用这样的设计模式也是非常方便的,因为他可以控制整套逻辑的执行顺序和统一的输入、输出,而对于实现方只需要关心好自己的业务逻辑即可。


使用模板模式之前的结构:
在这里插入图片描述


q

为什么要创建接口IStrategyRepository,它经常被拿来创建接口变量

public class DrawStrategySupport extends DrawConfig{
    @Resource
    protected IStrategyRepository strategyRepository;
}

接口:

public interface IStrategyRepository {
    //策略详细信息
    StrategyRich queryStrategyRich(Long strategyId);
    //查询奖品
    Award queryAwardInfo(String awardId);

}

为啥不能直接用一个类

a

Note

关于抽象类
包含一个或多个抽象类的方法的类必须声明为抽象类
一个抽象类里面可以包含字段和具体方法

Note

package cn.itedus.lottery.common;
/**
 * 枚举信息定义
 */
public class Constants 

关于枚举类型和枚举类

//枚举类型
enum Size {S,M,L,XL};
Size s = Size.M;
//枚举类
public enum Size
{

}

在这里插入图片描述


Q

question in

package cn.itedus.lottery.domain.strategy.services.draw;
public abstract class AbstractDrawBase extends DrawStrategySupport implements IDrawExec{
...
 	/**
     * 执行抽奖算法
     */
    protected abstract String drawAlgorithm(Long strategyId, IDrawAlgorithm drawAlgorithm, List<String> excludeAwardIds);
...
// 4. 执行抽奖算法
this.drawAlgorithm(req.getStrategyId(),...);
...
}

第二个参数 IDrawAlgorithm drawAlgorithm ,怎么获取 ,为什么要设置这个参数

A


Q

为什么DrawResult里存放奖品信息的对象不能之间使用AwardRateInfo类,还要搞个DrawAward类
在这里插入图片描述
他俩的区别:

public class AwardRateInfo {
    private String awardId;
    private BigDecimal awardRate;
    ... ...
}
public class DrawAwardInfo {
    private String rewardId;
    private String awardName;
    ...
    }

A

一个有奖品概率,一个是奖品名字,更适合返回抽奖结果


T

在模板模式中新添加了:

package cn.itedus.lottery.domain.strategy.repository;
public interface IStrategyRepository {
 List<String> queryNoStockStrategyAwardList(Long strategyId);//这个
    boolean deductStock(Long strategyId, String awardId);//这个
}
package cn.itedus.lottery.domain.strategy.services.algorithm;
public abstract class BaseAlgorithm implements IDrawAlgorithm{
    /**
     * 生成百位随机抽奖码
     *
     * @return 随机值
     */
    protected int generateSecureRandomIntCode(int bound){
        return new SecureRandom().nextInt(bound) + 1;
    	}
    }
package cn.itedus.lottery.infrastructure.dao;
@Mapper
public interface IStrategyDetailDao {
    List<StrategyDetail> queryStrategyDetailList(Long strategyId);
    /**
     * 查询无库存策略奖品ID
     * @param strategyId 策略ID
     * @return           返回结果
     */
    List<String> queryNoStockStrategyAwardList(Long strategyId);//这个
    /**
     * 扣减库存
     * @param strategyDetailReq 策略ID、奖品ID
     * @return                  返回结果
     */
    int deductStock(StrategyDetail strategyDetailReq);//这个
}

以及上面这个接口的实现类

muddled

package cn.itedus.lottery.domain.strategy.services.draw.impl;
@Service("drawExec")
public class DrawExecImpl extends AbstractDrawBase {
 @Override
    protected String drawAlgorithm(Long strategyId, IDrawAlgorithm drawAlgorithm, List<String> excludeAwardIds) {
        //执行抽奖
        String awardId = drawAlgorithm.randomDraw(strategyId,excludeAwardIds);
}
}

randomDraw是什么,从哪来忘记了
找了一会,注意抽象类implements一个接口,类里并不需要实现(存在)那个方法

//在domain的service包里
public interface IDrawAlgorithm {
    public void initRateTable(Long strategyId,List<AwardRateInfo> awardRateInfoList);
    //根据抽奖策略id和需要排除的奖品,返回结果
    String randomDraw(Long strategyId, List<String> excludeAwardIds);
    boolean isExistRateTuple(Long strategyId);
}
public abstract class BaseAlgorithm implements IDrawAlgorithm

这个抽象类里面是没有randomDraw的
一个实现子类extends了这个抽象类,就要实现接口的方法

@Component("DefaultRateRandomDrawAlgorithm")
public class DefaultRateRandomDrawAlgorithm extends BaseAlgorithm {

    @Override
    public String randomDraw(Long strategyId, List<String> excludeAwardIds) {
    }
}

所以randomDraw在这里


问题描述

Maven Compule、Package lottery-domain包出错,报错是:找不到Award、strategy、strategyDetail

import cn.itedus.lottery.infrastructure.po.Award;
import cn.itedus.lottery.infrastructure.po.Strategy;
import cn.itedus.lottery.infrastructure.po.StrategyDetail;

可以发现他们都在infrastructure包下,说明访问不到这个包
但是配置文件明明已经引入了依赖

 <dependency>
            <groupId>cn.itedus.lottery</groupId>
            <artifactId>lottery-infrastructure</artifactId>
            <version>1.0-SNAPSHOT</version>
 </dependency>

解决方案

mvn打包时出现一个子模块找不到另一个子模块的情况

在项目的根目录下执行 mvn clean install,然后再执行mvn dependency:tree
在这里插入图片描述
点m图标进入命令端


问题描述

Test运行报错:

Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name ‘drawExec’: Injection of resource dependencies failed; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type ‘cn.itedus.lottery.domain.strategy.services.algorithm.IDrawAlgorithm’ available: expected single matching bean but found 2:

在这里插入图片描述


解决方案

“expected single matching bean but found 2:” 接口IDrawAlgorithm被两个类implements,但是在Test使用时已经确定获取哪个

public class DrawAlgorithmTest {
    @Resource(name = "singleRateRandomDrawAlgorithm")
    private IDrawAlgorithm randomDrawAlgorithm;
    }

所以和这个无关
查看发现没有写IStrategyRepository的实现类StrategyRepository
IStrategyRepository接口中有下面三个方法

//查询策略详细信息
StrategyRich queryStrategyRich(Long strategyId);
//查询奖品
Award queryAwardInfo(String awardId);
//查询无库存的奖品列表
List<String> queryNoStockStrategyAwardList(Long strategyId);
//扣减库存

Award queryAwardInfo(String awardId)方法还出现在DAO层,又mapper.xml绑定接口来完成方法的实现
如何实现这些方法对数据库进行查询

 	@Resource
    private IStrategyDao strategyDao;

    @Resource
    private IStrategyDetailDao strategyDetailDao;

    @Resource
    private IAwardDao awardDao;

通过调用Dao层接口来获取并返回
写完StrategyRepository后仍然报错

更改Service的value后成功
在这里插入图片描述
这个之前注释掉了,但还是不行
在这里插入图片描述
所以是为什么
做了个测试,把drawExecSB改成drawExec,然后在main方法里查看当前的service bean 发现名字仍然是SB,说明配置进去的bean其实没有被改变,执行maven compile后才变成drawExec,所以最可能的原因就是一开始进去的bean是DrawExecImplOld且新的没有被配置进去,因为没有compile
补充一条查看 当前bean的方法:
打印所有Spring boot载入的bean

ApplicationContext  ctx =  SpringApplication.run(ApiCoreApp.class,args);
       String[] beanNames =  ctx.getBeanDefinitionNames();
       System.out.println("所以beanNames个数:"+beanNames.length);
       for(Stringbn:beanNames){
           System.out.println(bn);
       }

---------
ApplicationContext  ctx =  SpringApplication.run(ApiCoreApp.class,args);

       String[] beanNames =  ctx.getBeanNamesForAnnotation(Service.class);

       System.out.println("Service注解beanNames个数:"+beanNames.length);

       for(Stringbn:beanNames){

           System.out.println(bn);

       }


问题描述


解决方案


Note

facade包
在这里插入图片描述


A

为什么 package cn.itedus.lottery.domain.award.services.factory的DistributionGoodsFactory要添加注解@Service

Q

不加的话,方法

package cn.itedus.lottery.test;
@RunWith(SpringRunner.class)
@SpringBootTest
public void SpringRunnerTest {
//根据awardType从抽奖工厂中获取对应的发奖服务
        IDistributionGoods distributionGoodsService = DistributionGoodsFactory.getDistributionGoodsService(drawAwardInfo.getAwardType());
        }

会报错:

Non-static method ‘getDistributionGoodsService’ cannot be referenced from a static context

所以说在方法中添加

 @Resource
    private DistributionGoodsFactory distributionGoodsFactory;

@Service
public class DistributionGoodsFactory extends GoodsConfig{...}

关于报错的解释wdzka

想要解决上面的报错,我们首先需要了解什么叫做static method(静态方法)。
静态方法为类所有,一般情况下我们通过类来使用(而对于不加static的实例方法我们则只能通过对象的来调用)。


问题描述

No qualifying bean of type ‘cn.itedus.lottery.domain.award.repository.IAwardRepository’ available: expected at least 1 bean which qualifies as autowire candidat
在这里插入图片描述

解决方案

上次是两个bean,这次是找不到bean,搞了半天,maven compile了主包和domain都不行,然后test是在interface包,compile了interface包后执行成功
在这里插入图片描述

package cn.itedus.lottery.domain.strategy.services.draw;
public abstract class AbstractDrawBase extends DrawStrategySupport implements IDrawExec{
private DrawResult buildDrawResult(String uId, Long strategyId, String awardId){
DrawAwardInfo drawAwardInfo =  new DrawAwardInfo(award.getAwardId(), award.getAwardName());//问题在这里
}

问题描述

发现只要中奖就会报null错误
在这里插入图片描述

java.lang.NullPointerException
at java.util.concurrent.ConcurrentHashMap.get(ConcurrentHashMap.java:936)
at cn.itedus.lottery.domain.award.services.factory.DistributionGoodsFactory.getDistributionGoodsService(DistributionGoodsFactory.java:13)
at cn.itedus.lottery.test.SpringRunnerTest.test_award(SpringRunnerTest.java:68)

解决方案

DrawAwardInfo drawAwardInfo = drawResult.getDrawAwardInfo();
在这里插入图片描述
在这里插入图片描述
awardType是null
所以问题在这里:

package cn.itedus.lottery.domain.strategy.services.draw;
public abstract class AbstractDrawBase extends DrawStrategySupport implements IDrawExec{
	private DrawResult buildDrawResult(String uId, Long strategyId, String awardId){
	//一开始没有添加award.getAwardType()
	 DrawAwardInfo drawAwardInfo = new DrawAwardInfo(award.getAwardId(), award.getAwardType(), award.getAwardName(), award.getAwardContent());
	}
}

问题描述

java.lang.NullPointerException
at cn.itedus.lottery.test.SpringRunnerTest.test_award(SpringRunnerTest.java:69)

在这里插入图片描述

解决方案

可以看到是distributionGoodsService为空,根据debug
在这里插入图片描述

package cn.itedus.lottery.domain.award.services.factory;
@Service
public class DistributionGoodsFactory extends GoodsConfig{
 public IDistributionGoods getDistributionGoodsService(Integer awardType){
        return goodsMap.get(awardType);}}

这条方法返回为null
那就是枚举类中的typeId和表中的不匹配所以找不到
一开始数据库里的awardtype为10001,而枚举类中只有1-4,更改后测试成功
在这里插入图片描述


第八节

Note

Mybatis的 mapper配置文件
1. select元素的属性
常见的有:

属性描述
id唯一标识符,与Mapper接口中的某个方法名对应
parameterType从接口中传入的类名或别名
resultType返回结果的类名或别名

参数传递:首先要定义Mapper接口,没有使用springboot就要使用@Param()声明参数

public interface IUserMapper{
	public User login(@Param("sno")String sno,@Param("pwd") String pwd) throw Exception;
					
}

然后就是映射SQL,在xml映射文件中使用 #{参数名} 形式来接受参数

<select id = "login" resultTyoe="User">
	select * from t_user where sno= #{sno} and pwd= #{pwd}
</select>

2. 插入、更新和删除元素的属性
常见的有:

属性描述
id唯一标识符,与Mapper接口中的某个方法名对应
parameterType从接口中传入的类名或别名

Q

为什么有重复的
在这里插入图片描述

Note

@PostConstruct注解
@PostConstruct是Java自带的注解,在方法上加该注解会在项目启动的时候执行该方法,也可以理解为在spring容器初始化的时候执行该方法

Q

class AbstractState 和 interface IStateHandler 里面的方法一样,为什么

Note

使用Spring的事务管理
Spring FrameWork支持编程式和声明式两种事务管理方式。在编程式事务管理中,Spring的事务管理抽象于显示启动,结束和提交事务。在声明式事务中,可以使用Spring的@Transactional注解来注解在事务中执行的方法
假设要防止两种方法的失败导致系统处于不一致的状态,就要必须使这两种方法在事务中执行


问题描述

在这里插入图片描述

解决方案

一开始我以为是activityDeployImpl没有配置进去,但是@SpringBootApplication那是有的
在这里插入图片描述
问题在这里:
在这里插入图片描述
意思是它@Resource(引用的这个bean不行),说明是

在这里插入图片描述
IActivityRepository的问题,但它只是个接口,所以得找他的实现类,实现类也是要作为bean添加进去管理

在这里插入图片描述
结果发现,是我忘记写这个实现类了… …


T

domain层不再使用infrastructure层(应该可以这么说吧),原来在domain包提供得repository服务转移到infrastructure层实现了
在这里插入图片描述
之前使用的都要修改,比如在AbstractDrawBase,StrategyRich,DrawStrategySupport等

@Component
public class StrategyRepository implements IStrategyRepository

StrategyRepository 在infrastructure层 implements domain层的接口

//原方法
@Override
    public StrategyRich queryStrategyRich(Long strategyId) {
        Strategy strategy = strategyDao.queryStrategy(strategyId);
        List<StrategyDetail> strategyDetailList = strategyDetailDao.queryStrategyDetailList(strategyId);
        return new StrategyRich(strategyId, strategy, strategyDetailList);
    }

改成

@Override
    public StrategyRich queryStrategyRich(Long strategyId) {
        Strategy strategy = strategyDao.queryStrategy(strategyId);
        List<StrategyDetail> strategyDetailList = strategyDetailDao.queryStrategyDetailList(strategyId);

        StrategyBriefVO strategyBriefVO = new StrategyBriefVO();
        BeanUtils.copyProperties(strategy, strategyBriefVO);

        List<StrategyDetailBriefVO> strategyDetailBriefVOList = new ArrayList<>();
        for (StrategyDetail strategyDetail : strategyDetailList) {
            StrategyDetailBriefVO strategyDetailBriefVO = new StrategyDetailBriefVO();
            BeanUtils.copyProperties(strategyDetail, strategyDetailBriefVO);
            strategyDetailBriefVOList.add(strategyDetailBriefVO);
        }

        return new StrategyRich(strategyId, strategyBriefVO, strategyDetailBriefVOList);
    }

就是原本返回的 StrategyRich(strategyId, strategy, strategyDetailList); 第三个参数变了,是StrategyDetailBriefVOList 类对象
StrategyDetailBriefVOList 和他名字一样,是简要信息,所以要通过strategyDetailList获取并赋值
以及这个方法不再是返回dao层获取的数据,因为要返回简要信息

public Award queryAwardInfo(String awardId) {
        return awardDao.queryAwardInfo(awardId);
    }

同理也是通过获取awardDao.queryAwardInfo(awardId) 然后对AwardBriefVO awardBriefVO 赋值

Note

关于BeanUtils
BeanUtils简介
BeanUtils提供对Java反射和自省API的包装,其主要目的是利用反射机制对JavaBean的属性进行处理,简化JavaBean封装数据的操作
BeanUtils工具类的实现主要包含: Apache commons包实现的BeanUtils工具类,以及spring-beans实现的BeanUtils的工具类

为什么要使用BeanUtils
在我们实际项目开发过程中,我们经常需要将不同的两个对象实例进行属性复制,从而基于源对象的属性信息进行后续操作,而不改变源对象的属性信息。
比如DTO数据传输对象和数据对象DO,我们需要将DO对象进行属性复制到DTO,但是对象格式又不一样,所以我们需要编写映射代码将对象中的属性值从一种类型转换成另一种类型。
通常会调用其set/get方法,有些时候,但是我们很不喜欢写很多几长的b.setF1(a.getF10)这的代码,于是我们需要简化对象拷贝方式
为了解决这一痛点,就诞生了一些方便的BeanUtils类库,比如: 常用的有 apache的BeanUtils,spring的 BeanUtils等拷贝工具类
来自这里:111

Q

<insert id="insertList" parameterType="java.util.List">
        INSERT INTO strategy_detail(strategy_id, award_id, award_name, award_count, award_surplus_count,
        award_rate, create_time, update_time)
        VALUES
        <foreach collection="list" item="item" index="index" separator=",">
            (
            #{item.strategyId},
            #{item.awardId},
            #{item.awardName},
            #{item.awardCount},
            #{item.awardSurplusCount},
            #{item.awardRate},
            NOW(),
            NOW()
            )
        </foreach>
    </insert>
 <foreach collection="list" item="item" index="index" separator=","></foreach>

第九节

Q

啥是雪花算法

A

雪花算法是推特开源的分布式ID生成算法,用于在不同的机器上生成唯一的ID的算法。
该算法生成一个64bit的数字作为分布式ID,保证这个ID自增并且全局唯一

服务基本是分布式、微服务形式的,而且大数据量也导致分库分表的产生,对于水平分表就需要保证表中 id 的全局唯一性。

Note

@Configuration
用编程的方式配置bean和Spring容器也称为“基于Java的容器配置”
Java配置的核心:@Configuration 和 @Bean注解
使用@Configuration 注解一个类则表示该类包含一个或多个使用@Bean注解的方法,这些方法可以创建并返回bean实例。由@Bean注解的方法返回的bean实例由Spring容器管理

Note

策略模式
链接
一个类的行为或其算法可以在运行时更改。这种类型的设计模式属于行为型模式

复述一下就是要写一个接口,比如 搞一个实现a (运算符 ) b的策略,有±*/这些,那参数就是a,b

然后每种操作都有一个实现类,implements 这个接口

再写一个 Context 类,里面有private 接口变量,以及Context的初始化方法,初始化接口变量

然后写一个方法传入a,b两个参数,调用接口变量的方法

部分代码:


public class Context {
   private Strategy strategy;
 
   public Context(Strategy strategy){
      this.strategy = strategy;
   }
 
   public int executeStrategy(int num1, int num2){
      return strategy.doOperation(num1, num2);
   }
}


public class StrategyPatternDemo {
   public static void main(String[] args) {
      Context context = new Context(new OperationAdd());    
      System.out.println("10 + 5 = " + context.executeStrategy(10, 5));
 
      context = new Context(new OperationSubtract());      
      System.out.println("10 - 5 = " + context.executeStrategy(10, 5));
 
      context = new Context(new OperationMultiply());    
      System.out.println("10 * 5 = " + context.executeStrategy(10, 5));
   }
}

关于方法: Context(Strategy strategy) 中 Strategy参数的获取:
在这里插入图片描述
接口变量引用实现了这个接口的类对象


问题描述

导入依赖的时候,xml文件没有报错

		<dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>

加了version也不行,在父xml中添加了也不行,
在这里插入图片描述都没有显示
但是Lottery下的dependencies是有的,后面在Lottery的dependencies都放入dependenciesManeger管理后就不报错了,以后看为什么,后面注释掉想看看什么原因,但是dependece还在,还是可以使用,估计要重新build才能更新?


问题描述

还是配置bean出错,但是和之前有点区别,忘记截图了,大概是这样:Error creating bean with name 'sqlSessionFactory、UnsatisfiedDependencyException

解决方案

然后发现是配置文件没导入mysql…因为之前在父依赖配置添加了DependencyManager

第10节

Note:分库分表

为什么要用分库分表
由于业务体量较大,数据增长较快,所以需要把用户数据拆分到不同的库表中去,减轻数据库压力。
分库分表操作主要有垂直拆分和水平拆分:
垂直拆分按照业务将表进行分类,分布到不同的数据库上,这样也就将数据的压力分担到不同的库上面。最终一个数据库由很多表的构成,每个表对应着不同的业务,也就是专库专用。
水平拆分如果垂直拆分后遇到单机瓶颈,可以使用水平拆分。相对于垂直拆分的区别是:垂直拆分是把不同的表拆到不同的数据库中,而本章节需要实现的水平拆分,是把同一个表拆到不同的数据库中。如:user_001、user_002

分库分表的两种方式
一种是代理方法,代理在应用和数据层之间,向上接收数据然后进行数据分离
还有一种就是使用组件(这里用的是组件),基于jar包的

Note:散列

散列码概念的理解
最简单的散列过程:
add(key,value)
index = h(key)
hashTable [index] =value
理想情况下每个信息都对应hastTable中唯一一个元素(这也叫完美散列函数),就可以这样,但一般不是,因为大部分散列表填不满且很稀疏,且很多时候要查找的键不是一个整数
所以:
getHashIndex(key)
i = key
return i % tableSize
这个计算查找键转换的整数结果就是散列码
还要将散列码压缩到散列表的下标范围

计算散列码
hashCode方法用来返回一个整数的散列码,然后每个类都继承了这个方法

因为hashCode方法来自Java的基类Object,每个类都是Object的子类,所以都继承了这个方法

但是一个类应该根据原则定义自己的hashCode方法

因为该方法会根据调用该方法的对象的 内存地址 返回一个int值,这就导致了相等但是不同的对象会有不同的散列码,在实际使用中散列必须将相等的对象映射为散列表中同一个位置

什么原则

注:用于方法hashCode的原则
●如果类重写了方法 equals,它也应该重写 hashCode。
如果方法 equals 认为两个对象相等,则 hashCode 必须对两个对象返回相同的值。
●如果在程序运行期间,多次调用对象的 hashCode, 且如果此时对象的数据保持不
变,则 hashCode 必须返回相同的散列码。
●在程序的一次执行期间,某个对象的散列码可以不同于同一程序的另一次执行期间的散列码。

热知识:现实世界里数据分布是不均匀的

概况一下就是字符串可以根据字符所在的位置让其Unicode值乘一个因子,再将这些乘积相加得到散列码
用于基本类型的散列码:int可以用它自身,byte、short或char可以转为int。其他的基本类型可以处理它们的二进制表示

⭐将散列码压缩为散列表的下标

将一个整数缩放到某个给定范围内的最常见的方法: (c : 正的散列码,n代表有n个位置)c%n
按理来说n应该等于散列表的长度,但是c%n与c有着相同的奇偶性,所以说
n是素数时,c%n 得到0~n-1之间均匀分布的值

解决冲突
两种方法:
1.使用散列表中的另一个位置
2.改变散列表的结构,让每个数组元素可以表示多个值
寻找散列表中一个未用的或开放的位置称为开放地址法
开放地址的线性探查:
寻找散列表中的开放位置称为探查
线性探查就是在冲突发生的那个位置继续查看后面的位置是否可用,直到找到下一个可用的位置为止(如果探寻到了最后一个位置,就会回到表头)
基本聚集:使用线性探查解决冲突,导致散列表中一组组连续的位置被占用,每个组称为一个簇,这种现象称为基本聚集
二次探查:考虑地址为k+j^2
二次探查可以避免基本聚集,但是可能导致二级聚集(与表中已有项发生冲突的项会使用相同的探查序列)

开放地址双散列
线性探查的增量是1,二次探查的增量是j^2,而双散列使用第二个散列函数以依赖关键字的方式来计算这个增量

拉链法(改变散列表的结构)
每个位置表示多个值,这样的位置称为桶

多路由配置时的application.yml文件

(spring:?)从:

spring:
  datasource:
    username: root
    password: 
    url: jdbc:mysql://127.0.0.1:3306/lotterybase?useUnicode=true&serverTimezone=Asia/Shanghai
    driver-class-name: com.mysql.cj.jdbc.Driver

改为:

# 多数据源路由配置
mini-db-router:
  jdbc:
    datasource:
      dbCount: 2
      tbCount: 4
      default: db00
      routerKey: uId
      list: db01,db02
      db00:
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/lotterybase?useUnicode=true&serverTimezone=Asia/Shanghai
        username: root
        password: 1234
      db01:
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/lotterybase?useUnicode=true&serverTimezone=Asia/Shanghai
        username: root
        password: 1234
      db02:
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/lotterybase?useUnicode=true&serverTimezone=Asia/Shanghai
        username: root
        password: 1234

question:mini-db-router是哪里来的

Q

插件里面的:
数据源配置提取

@Configuration
public class DataSourceAutoConfig implements EnvironmentAware {
...
@Override
    public void setEnvironment(Environment environment) {
        String prefix = "mini-db-router.jdbc.datasource.";
/**
prefix,是数据源配置的开头信息,你可以自定义需要的开头内容。
dbCount、tbCount、dataSources、dataSourceProps,都是对配置信息的提取,并存放到 dataSourceMap 中便于后续使用
*/
        dbCount = Integer.valueOf(environment.getProperty(prefix + "dbCount"));
        tbCount = Integer.valueOf(environment.getProperty(prefix + "tbCount"));
        routerKey = environment.getProperty(prefix + "routerKey");

        // 分库分表数据源
        String dataSources = environment.getProperty(prefix + "list");
        assert dataSources != null;
        for (String dbInfo : dataSources.split(",")) {
            Map<String, Object> dataSourceProps = PropertyUtil.handle(environment, prefix + dbInfo, Map.class);
            dataSourceMap.put(dbInfo, dataSourceProps);
        }

        // 默认数据源
        String defaultData = environment.getProperty(prefix + "default");
        defaultDataSourceConfig = PropertyUtil.handle(environment, prefix + defaultData, Map.class);

    }
...
}

“prefix,是数据源配置的开头信息,你可以自定义需要的开头内”
String prefix = “mini-db-router.jdbc.datasource.”; 这是什么


A

是这个:
在这里插入图片描述

Q

结合 SpringBoot 开发的 Starter 中,需要提供一个 DataSource 的实例化对象(why),那么这个对象我们就放在 DataSourceAutoConfig 来实现,并且这里提供的数据源是可以动态变换的,也就是支持动态切换数据源
配置数据源:


    /**
     * 数据源配置组
     */
    private Map<String, Map<String, Object>> dataSourceMap = new HashMap<>();
    /**
    这个方法在setEnvironment(Environment environment)进行初始化:(节选)
    String dataSources = environment.getProperty(prefix + "list");
    for (String dbInfo : dataSources.split(",")) {
            Map<String, Object> dataSourceProps = PropertyUtil.handle(environment, prefix + dbInfo, Map.class);
            dataSourceMap.put(dbInfo, dataSourceProps);
        }
	*/
    ...
    
 @Bean
    public DataSource dataSource() {
        // 创建数据源
        Map<Object, Object> targetDataSources = new HashMap<>();
        for (String dbInfo : dataSourceMap.keySet()) {
            Map<String, Object> objMap = dataSourceMap.get(dbInfo);
            targetDataSources.put(dbInfo, new DriverManagerDataSource(objMap.get("url").toString(), objMap.get("username").toString(), objMap.get("password").toString()));
        }

        // 设置数据源
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        dynamicDataSource.setTargetDataSources(targetDataSources);
        dynamicDataSource.setDefaultTargetDataSource(new DriverManagerDataSource(defaultDataSourceConfig.get("url").toString(), defaultDataSourceConfig.get("username").toString(), defaultDataSourceConfig.get("password").toString()));

        return dynamicDataSource;
    }
    /**
    这里是一个简化的创建案例,把基于从配置信息中读取到的数据源信息,进行实例化创建。
    */

AOP切面

为什么使用AOP

在 AOP 的切面拦截中需要完成;数据库路由计算、扰动函数加强散列、计算库表索引、设置到 ThreadLocal 传递数据源

了解AOP之前先了解代理模式
一个类代表另一个类的功能
比如一个代理类:public class ProxyImage implements Image
一个被代理的类:public class RealImage implements Image
然后在代理类里面创建那个类的对象
private RealImage realImage;

相关概念
①横切关注点
附加功能设置横切关注点
②通知
每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知方法。

  • 前置通知:在被代理的目标方法前执行
  • 返回通知:在被代理的目标方法成功结束后执行(寿终正寝)
  • 异常通知:在被代理的目标方法异常结束后执行(死于非命)
  • 后置通知:在被代理的目标方法最终结束后执行(盖棺定论)
  • 环绕通知:使用try…catch…finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置

③切面
封装通知方法的类。

AOP注解

  • @Aspect //标注增强处理类(切面类)表示这个类是一个切面类
  • @Component //交由Spring容器管理
  • @Order(0) //设置优先级,值越低优先级越高
  • @Pointcut (value = “” ) //可定义切点的位置
    针对不同切点,方法上的@Around()可以这样写ex:@Around(value = “methodPointcut() && args(…)”)
  • @Around // 环绕通知,pointcut连接点使用@annotation(xxx)进行定义
    eg.@Around(value = “@annotation(around)”) //around 与 下面参数名around对应

value参数填写切入点表达式
来自尚硅谷课件

语法细节
号代替“权限修饰符”和“返回值”部分表示“权限修饰符”和“返回值”不限
在包名的部分,一个“
”号只能代表包的层次结构中的一层,表示这一层是任意的。
例如:.Hello匹配com.Hello,不匹配com.atguigu.Hello
在包名的部分,使用“
…”表示包名任意、包的层次深度任意
在类名的部分,类名部分整体用号代替,表示类名任意
在类名的部分,可以使用
号代替类名的一部分
例如:Service匹配所有名称以Service结尾的类或接口
在方法名部分,可以使用
号表示方法名任意
在方法名部分,可以使用号代替方法名的一部分
例如:Operation匹配所有方法名以Operation结尾的方法
在方法参数列表部分,使用(…)表示参数列表任意
在方法参数列表部分,使用(int,…)表示参数列表以一个int类型的参数开头
在方法参数列表部分,基本数据类型和对应的包装类型是不一样的
切入点表达式中使用 int 和实际方法中 Integer 是不匹配的
在方法返回值部分,如果想要明确指定一个返回值类型,那么必须同时写明权限修饰符
例如:execution(public int …Service.
(…, int)) 正确
例如:execution(
int …Service.*(…, int)) 错误

在这里插入图片描述

每个都写那么长的表达式会比较麻烦,所以说PointCut可以重用
重用切入点表达式
①声明

@Pointcut("execution(* com.atguigu.aop.annotation.*.*(..))")
public void pointCut(){}

②在同一个切面中使用
直接用

@Before("pointCut()")

③在不同切面中使用
要写包路径

Q:

cn.bugstack.middleware.db.router.dynamic.DynamicMybatisPlugin 设计的目的

//作用:实现 Interceptor 接口的 intercept 方法,获取StatementHandler、通过自定义注解判断是否进行分表操作、获取SQL并替换SQL表名 USER 为 USER_03、最后通过反射修改SQL语句
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class DynamicMybatisPlugin implements Interceptor {
/** Pattern是啥:
Regular expression modifier values.(正则表达式的修饰符)  Instead of being passed as
     * arguments, they can also be passed as inline modifiers.
     * For example, the following statements have the same effect.
     *  <pre>
     * RegExp r1 = RegExp.compile("abc", Pattern.I|Pattern.M);
     * RegExp r2 = RegExp.compile("(?im)abc", 0);
     * </pre>
*/
    private Pattern pattern = Pattern.compile("(from|into|update)[\\s]{1,}(\\w{1,})", Pattern.CASE_INSENSITIVE);

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 获取StatementHandler
        /**
        处理Statement方法的接口,有准备更新查找等
        那啥是Statement:
        在jar.sql包下的一个接口,注解里是这样说的:
        用于执行静态 SQL 语句并返回其生成的结果的对象。
		默认情况下,每个语句对象只能同时打开一个 ResultSet 对象。因此,如果一个 ResultSet 对象的读取与另一个对象的读取交错,则每个对象都必				须由不同的语句对象生成。语句接口中的所有执行方法都隐式关闭语句的当前 ResultSet 对象(如果存在打开的对象)。
*/
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        MetaObject metaObject = MetaObject.forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY, SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY, new DefaultReflectorFactory());
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");

        // 获取自定义注解判断是否进行分表操作
        String id = mappedStatement.getId();
        String className = id.substring(0, id.lastIndexOf("."));
        Class<?> clazz = Class.forName(className);
        DBRouterStrategy dbRouterStrategy = clazz.getAnnotation(DBRouterStrategy.class);
        if (null == dbRouterStrategy || !dbRouterStrategy.splitTable()){
            return invocation.proceed();
        }

        // 获取SQL
        BoundSql boundSql = statementHandler.getBoundSql();
        String sql = boundSql.getSql();

        // 替换SQL表名 USER 为 USER_03
        Matcher matcher = pattern.matcher(sql);
        String tableName = null;
        if (matcher.find()) {
            tableName = matcher.group().trim();
        }
        assert null != tableName;
        String replaceSql = matcher.replaceAll(tableName + "_" + DBContextHolder.getTBKey());

        // 通过反射修改SQL语句
        /**
        *Field
        *字段提供有关类或接口的单个字段的信息和动态访问。反射字段可以是类(静态)字段或实例字段。
		*字段允许在获取或设置访问操作期间进行加宽转换
        */
        Field field = boundSql.getClass().getDeclaredField("sql");
        field.setAccessible(true);
        field.set(boundSql, replaceSql);

        return invocation.proceed();
    }
}

关于注解:
Mybatis拦截器注解@Intercepts与@Signature注解属性说明

@Signature 注解参数说明:
type:就是指定拦截器类型(ParameterHandler ,StatementHandler,ResultSetHandler )
method:是拦截器类型中的方法,不是自己写的方法
args:是method中方法的入参

A:

分库分表设计mybatis的插入数据语句,虽然说可以这样插入:
直接在Mybatis对应的表 INSERT INTO user_strategy_export_${tbIdx} 添加字段的方式处理分表
但是也还可以更优雅一些
可以基于 Mybatis 拦截器进行处理,通过拦截 SQL 语句动态修改添加分表信息,再设置回 Mybatis 执行 SQL 中

Q:分库分表,实现上怎么找库和表

分库不分表

自定义注解

/** 该注解表明被注解信息是否被添加在Javadoc中 */
@Documented		

/** 这个注解规定了我们自定义注解的生命周期,里面有个属性叫RetentionPolicy,关于这个属性有三个值:
RetentionPolicy.SOURCE 	编译后即丢弃,自定义注解在编译结束后就不再产生意义,因此不会写入到Class文件中
RetentionPolicy.CLASS 	类加载时丢弃,编译后保留(在Class文件中存在,但是JVM运行时将会忽略),默认使用这种方式
RetentionPolicy.RUNTIME 	运行期保留,在Class文件中存在,JVM运行时保留,可以通过反射机制读取该注解的信息*/
@Retention(RetentionPolicy.RUNTIME)

/** 这个注解就是规定了我们自定义的注解所使用的的范围 
*	ElementType.TYPE 	类,接口(包括注解类型),enum声明
*	ElementType.METHOD 	方法
*/
@Target({ElementType.TYPE, ElementType.METHOD})

/** interface,也就是我们自定义注解的注解名 */
public @interface DBRouter {

    /** 分库分表字段 */
    String key() default "";

}

再来看实现:

// @Aspect表示这个类是一个切面类
@Aspect
public class DBRouterJoinPoint {
	private DBRouterConfig dbRouterConfig;		//里面是分库,分表的数量以及路由字段
    private IDBRouterStrategy dbRouterStrategy;	//里面有路由计算,获取分库分表数量,手动设置分库分表路由的方法
	
	//重用切入点表达式
	//@annotation:用于匹配当前执行方法持有指定注解的方法
	 @Pointcut("@annotation(cn.bugstack.middleware.db.router.annotation.DBRouter)")
    public void aopPoint() {
    }
    /**
    *环绕通知:使用@Around注解标识,使用try...catch...finally结构围绕整个被代理的目标方法,包
	*括四种通知(前置通知:在被代理的目标方法前执行、返回通知:在被代理的目标方法成功结束后执行、
	*异常通知:在被代理的目标方法异常结束后执行、后置通知:在被代理的目标方法最终结束后执行
	*/
     @Around("aopPoint() && @annotation(dbRouter)")
    public Object doRouter(ProceedingJoinPoint jp, DBRouter dbRouter) throws Throwable {
    
    String dbKey = dbRouter.key();//应该是处理uid,但是怎么传的还不知道⭐⭐⭐
    /** 在getAttrValue方法里实现 根据数据库路由字段,从入参中读取出对应的值。比如路由 key 是 uId,那么就从入参对象 Obj 中获取到 uId 的值
*/
    
    	// 路由属性
    	/**(省略版)
    	* 方法和他的参数:String getAttrValue(String attr, Object[] args)
    	* 返回:String filedValue
    	* 对filedValue:filedValue = BeanUtils.getProperty(arg, attr);或(该方法使用的是这个)filedValue = String.valueOf(this.getValueByName(arg, attr));
		*/
        String dbKeyAttr = getAttrValue(dbKey, jp.getArgs());
        // 路由策略
        dbRouterStrategy.doRouter(dbKeyAttr);//⭐??doRouter怎么实现的
        // 返回结果
        try {
            return jp.proceed();  //目标对象方法的执行
        } finally {
           dbRouterStrategy.clear();
        }
    ... 
    
    
}

对于刚才的问题:dbRouterStrategy.doRouter(dbKeyAttr);//⭐??doRouter怎么实现的
这样实现的

public class DBRouterStrategyHashCode implements IDBRouterStrategy
{
 @Override
    public void doRouter(String dbKeyAttr) {
        int size = dbRouterConfig.getDbCount() * dbRouterConfig.getTbCount();

        // 扰动函数;在 JDK 的 HashMap 中,对于一个元素的存放,需要进行哈希散列。而为了让散列更加均匀,所以添加了扰动函数。扩展学习;https://mp.weixin.qq.com/s/CySTVqEDK9-K1MRUwBKRCg
        int idx = (size - 1) & (dbKeyAttr.hashCode() ^ (dbKeyAttr.hashCode() >>> 16));

        // 库表索引;相当于是把一个长条的桶,切割成段,对应分库分表中的库编号和表编号
        int dbIdx = idx / dbRouterConfig.getTbCount() + 1;
        int tbIdx = idx - dbRouterConfig.getTbCount() * (dbIdx - 1);

        // 设置到 ThreadLocal;关于 ThreadLocal 的使用场景和源码介绍;
        DBContextHolder.setDBKey(String.format("%02d", dbIdx));
        DBContextHolder.setTBKey(String.format("%03d", tbIdx));
        logger.debug("数据库路由 dbIdx:{} tbIdx:{}",  dbIdx, tbIdx);
    }
}

这一切是从insert()开始的,也就是在insert进数据库之前进行分库操作,

@Mapper
public interface IUserTakeActivityDao {
    /**
     * 插入用户领取活动信息
     * @param userTakeActivity 入参
     */
    //在需要使用数据库路由的DAO方法上加入注解
    //@DBRouter(key = "uId") key 是入参对象中的属性(意思是uID是从userTakeActivity中搞来的),用于提取作为分库分表路由字段使用
    @DBRouter(key = "uId")
    void insert(UserTakeActivity userTakeActivity);
}

insert实现直接在mapper.xml,那关键就是它的选择(在yaml写了几个去处),它是怎么拦截那个uid然后去选择的❓
可以看出主要是这个 ThreadLocal,在DBContextHolder有这个类型ThreadLocal,进行set、get、clear操作

//省略版,只看set方法
 private static final ThreadLocal<String> dbKey = new ThreadLocal<String>();
 private static final ThreadLocal<String> tbKey = new ThreadLocal<String>();

    public static void setDBKey(String dbKeyIdx){
        dbKey.set(dbKeyIdx);
    }

调用的是ThreadLocals的set方法
在java.lang包里的,在注解里是这么说的:

/**
*ThreadLocals 依赖于附加到每个线程的每线程线性探测哈希映射(Thread.threadLocals 和 inheritableThreadLocals)。ThreadLocal 对象充当*键,通过 threadLocalHashCode 进行搜索。这是一个自定义哈希代码
*/
//它的set方法:
public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

所以说在doRouter里把得到的库表索引放到那个ThreadLocal dbKey、tdKey里
关于ThreadLocal

附加:PointCut使用参数
ProceedingJoinPoint和它的process方法

Q: @DBRouter(key = “uId”)uid是怎么传的

上面的那个问题

@Mapper
public interface IUserTakeActivityDao {
    /**
     * 插入用户领取活动信息
     * @param userTakeActivity 入参
     */
    @DBRouter(key = "uId")
    void insert(UserTakeActivity userTakeActivity);
}

A

用的是类DBRouterJoinPoint里的这个方法:

public String getAttrValue(String attr, Object[] args) {
        if (1 == args.length) {
            Object arg = args[0];
            if (arg instanceof String) {
                return arg.toString();
            }
        }

        String filedValue = null;
        for (Object arg : args) {
            try {
                if (StringUtils.isNotBlank(filedValue)) {
                    break;
                }
                // filedValue = BeanUtils.getProperty(arg, attr);
                // fix: 使用lombok时,uId这种字段的get方法与idea生成的get方法不同,会导致获取不到属性值,改成反射获取解决
                filedValue = String.valueOf(this.getValueByName(arg, attr));
            } catch (Exception e) {
                logger.error("获取路由属性值失败 attr:{}", attr, e);
            }
        }
        return filedValue;
    }

dbRouter.key() 确定根据哪个字段进行路由 2. getAttrValue
根据数据库路由字段,从入参中读取出对应的值。比如路由 key 是 uId,那么就从入参对象 Obj 中获取到 uId 的值

第11节

简要概括:改善上一节那个简单的路由组件

问题:如果一个场景需要在同一个事务下,连续操作不同的DAO操作,那么就会涉及到在 DAO 上使用注解 @DBRouter(key = “uId”) 反复切换路由的操作。虽然都是一个数据源,但这样切换后,事务就没法处理了。

解决:这里选择了一个较低的成本的解决方案,就是把数据源的切换放在事务处理前,而事务操作也通过编程式编码进行处理。具体可以参考 db-router-spring-boot-starter 源码

配置事务处理对象

  • 创建路由策略对象,便于切面和硬编码注入使用。
@Bean
public IDBRouterStrategy dbRouterStrategy(DBRouterConfig dbRouterConfig) {
    return new DBRouterStrategyHashCode(dbRouterConfig);//这个什么HashCode里面有一个doRouter的方法计算路由
}

  • 创建事务对象,用于编程式事务引入(❓有点不懂)
@Bean
public TransactionTemplate transactionTemplate(DataSource dataSource) {
    DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
    dataSourceTransactionManager.setDataSource(dataSource);
    TransactionTemplate transactionTemplate = new TransactionTemplate();
    transactionTemplate.setTransactionManager(dataSourceTransactionManager);
    transactionTemplate.setPropagationBehaviorName("PROPAGATION_REQUIRED");
    return transactionTemplate;
}

Q:创建事务对象,用于编程式事务引入(❓有点不懂)是什么

活动领取模板抽象类

BaseActivityPartake extends ActivityPartakeSupport implements IActivityPartake

这个类包含查询活动账单的方法(通过符类调用queryxxx方法)
活动信息的校验处理(通过Constants枚举类判断checkResult和成功Code是否相等)
领取完就要扣减活动库存,目前为直接对配置库中的 lottery.activity 直接操作表扣减库存,后续优化为Redis扣减
领取活动信息(个人用户把活动信息写入用户表)
封装结果,返回策略ID,继续完成抽奖步骤

抽象类 BaseActivityPartake 继承数据支撑类并实现接口方法 IActivityPartake#doPartake
在领取活动 doPartake 方法中,先是通过父类提供的数据服务,获取到活动账单,再定义三个抽象方法:活动信息校验处理、扣减活动库存、领取活动,依次顺序解决活动的领取操作。

处理的是活动,所以在domain的activity包下
partake 参加
新建了一个partake包
新建一个vo对象,ActivityBill:活动账单【库存、状态、日期、个人参与次数】
用户参与活动会发生参与请求,返回活动参与的结果,结果就是策略Id
添加了ActivityBillVO后也要把它添加进Activity的数据仓储cn.itedus.lottery.domain.activity.model.vo.ActivityBillVO和把方法加入接口

在ActivityPartakeImpl实现在Base抽象类定义的检查校验方法、扣减和领取活动方法

  • Result checkActivityBill(PartakeReq partake, ActivityBillVO bill)
    通过equal方法讲活动的当前状态、时间范围、剩余库存、以及个人活动领取的最大范围是不是可用的,来返回result
  • Result subtractionActivityStock(PartakeReq req)
    扣减活动库存要调用ActivityRepository里的扣减活动方法(在Impl是通过接口对象调用的,实际方法的实现类在 ActivityRepository下
return activityDao.subtractionActivityStock(activityId);

就是这一句,调用的是Dao层的方法,所以得在xml映射添加这一句UPDATE操作

 <update id="subtractionActivityStock" parameterType="java.lang.Long">
        UPDATE activity SET stock_surplus_count = stock_surplus_count - 1
        WHERE activity_id = #{activityId} AND stock_surplus_count > 0
 </update>
  • 活动的获取:Result grabActivity(PartakeReq partake, ActivityBillVO bill)
    是个环绕通知
    嵌套try try

Q:class TransactionTemplate?

A:

简化编程事务划分和事务异常处理的模板类,允许编写使用资源(如 JDBC 数据源)但本身不具有事务感知能力的低级数据访问对象

T:baba grabActivity方法,它的trytry和transcationTemplate->

try {
            dbRouter.doRouter(partake.getuId());
            return transactionTemplate.execute(status -> {
                try {
                    // 扣减个人已参与次数
                    int updateCount = userTakeActivityRepository.subtractionLeftCount(bill.getActivityId(), bill.getActivityName(), bill.getTakeCount(), bill.getUserTakeLeftCount(), partake.getuId(), partake.getPartakeDate());
                    if (0 == updateCount) {
                        status.setRollbackOnly();
                        logger.error("领取活动,扣减个人已参与次数失败 activityId:{} uId:{}", partake.getActivityId(), partake.getuId());
                        return Result.buildResult(Constants.ResponseCode.NO_UPDATE);
                    }

                    // 插入领取活动信息
                    Long takeId = idGeneratorMap.get(Constants.Ids.SnowFlake).nextId();
                    userTakeActivityRepository.takeActivity(bill.getActivityId(), bill.getActivityName(), bill.getTakeCount(), bill.getUserTakeLeftCount(), partake.getuId(), partake.getPartakeDate(), takeId);
                } catch (DuplicateKeyException e) {
                    status.setRollbackOnly();
                    logger.error("领取活动,唯一索引冲突 activityId:{} uId:{}", partake.getActivityId(), partake.getuId(), e);
                    return Result.buildResult(Constants.ResponseCode.INDEX_DUP);
                }
                return Result.buildSuccessResult();
            });
        } finally {
            dbRouter.clear();
        }

课件解释:dbRouter.doRouter(partake.getuId()); 是编程式处理分库分表,如果在不需要使用事务的场景下,直接使用注解配置到DAO方法上即可。两个方式不能混用
transactionTemplate.execute 是编程式事务,用的就是路由中间件提供的事务对象,通过这样的方式也可以更加方便的处理细节的回滚,而不需要抛异常处理。

Q:领取活动领域开发是

基于模板模式开发领取活动领域,因为在领取活动中需要进行活动的日期、库存、状态等校验,并处理扣减库存、添加用户领取信息、封装结果等一系列流程操作,因此使用抽象类定义模板模式更为妥当

之前完成了抽奖策略领域的开发(中奖和不中奖,中了什么奖品,返回什么的),活动领域的开发(不同的抽奖活动,注册活动,然后状态),现在是参与活动的领取活动的开发

理一理
首先从这个出发:

public abstract class BaseActivityPartake extends ActivityPartakeSupport implements IActivityPartake{}
  • IActivityPartake:对外提供接口
  • ActivityPartakeSupport: 提供数据支持(查询)
@Resource
    protected IActivityRepository activityRepository;
//仓库服务接口,在domain层(优雅的分法)
public interface IActivityRepository

它的实现类(记得配置成bean),在infrastructure层,调用DAO接口,搞数据库

@Component
public class ActivityRepository implements IActivityRepository
  • BaseActivityPartake :basebase
    课件这么说的:

抽象类 BaseActivityPartake 继承数据支撑类并实现接口方法 IActivityPartake#doPartake
在领取活动 doPartake 方法中,先是通过父类提供的数据服务,获取到活动账单,再定义三个抽象方法:活动信息校验处理、扣减活动库存、领取活动,依次顺序解决活动的领取操作

在抽象类实现(可以这么说吗?还是说写了的方法)就是doPartake方法,在里面调用了三个定义了的抽象方法(但这三个方法是在实现类实现的),返回的是各个执行结果Result,相当于一种剥(语文退化了)

okok,别的就是注意表的更改vo包的更改mapper的更改等等了
11节over!

第12节

Q: 在编排抽奖过程中要在‘用户领取活动表’中加入 活动单使用状态state

A:

写入中奖信息到 user_strategy_export_000~003 表中时候,两个表可以做一个幂等性的事务。同时还需要加入 strategy_id 策略ID字段,用于处理领取了活动单但执行抽奖失败时,可以继续获取到此抽奖单继续执行抽奖,而不需要重新领取活动。其实领取活动就像是一种活动镜像信息,可以在控制幂等反复使用

process 包用于流程编排,其实它也是 service 服务包是对领域功能的封装,很薄的一层,一般这一层的处理可以使用可视化的流程编排工具通过拖拽的方式,处理这部分代码的逻辑

抽奖整个活动过程的流程编排,主要包括:对活动的领取、对抽奖的操作、对中奖结果的存放,以及如何处理发奖
对于每一个流程节点编排的内容,都是在领域层开发完成的,而应用层只是做最为简单的且很薄的一层

do:
领取活动增加判断和返回领取单ID(之前领取活动的方法返回的是Result,这次返回PartakeResult)
具体的:

  • 增加了查询流程
    查询是否存在未执行抽奖领取活动单 user_take_activity 存在 state = 0,领取了但抽奖过程失败的,可以直接返回领取结果继续抽奖
  • 修改返回takeId
    插入领取活动信息【个人用户把活动信息写入到用户表】

用户领取活动时候,新增记录:strategy_id、state 两个字段,这两个字段就是为了处理用户对领取镜像记录的二次处理未执行抽奖的领取单,以及state状态控制事务操作的幂等性

在xml文件添加状态state的修改以及查询是否存在未执行…的SQL语句

建一个新的类:奖品单 DrawOrderVO
:找了一会这个类在哪个包(不知道draw是奖品单的时候),但是在UserTakeActivityRepository方法中使用它作为参数,按ddd架构来说应该在同一包下,所以在activity的vo层

活动结果public class PartakeResult extends Result要添加活动领取Id :private Long takeId;

插入领取活动结果在抽象类里面又在原本的方法里加了这条:

Long takeId = idGeneratorMap.get(Constants.Ids.SnowFlake).nextId();

封装结果方法单独分出去,不再在doPartake方法里写具体实现
return buildPartakeResult(activityBillVO.getStrategyId(), takeId);

修改完domain层就要编写application层,编排抽奖流程
为什么说这是薄薄的一层
因为在ActivityProcessImpl主要需要@Resource 抽奖活动参与接口:IActivityPartake 以及 抽奖执行接口:IDrawExec
参与抽奖活动的实现类ActivityPartakeImpl @Resource了:用户参与活动仓储接口IUserTakeActivityRepository 以及路由策略接口:IDBRouterStrategy

在这里插入图片描述
回答一下上面的那个问题,VO在domain包(层),那里使用就使用VO,但是在infrastructure层通过数据仓储实现两个数据的对接,通过BeanUtil
在这里插入图片描述til


Q:本节在public interface IUserTakeActivityDao再次使用了@DBRouter注解,上面说的一种编程式使用和注解式使用不能同时存在具体是哪一种情况

A


Q:忘记这个Long takeId = idGeneratorMap.get(Constants.Ids.SnowFlake).nextId();

A:

没有忘,这个是新加的,我没写

@Resource
//< Ids 生成策略枚举 ,定义生成ID的策略接口 >
    private Map<Constants.Ids, IIdGenerator> idGeneratorMap;

Q: return transactionTemplate.execute(status -> {

Q:领取活动,唯一索引冲突 是什么情况

uuid唯一

A:在这里插入图片描述

catch (DuplicateKeyException e) {
                    status.setRollbackOnly();
                    logger.error("领取活动,唯一索引冲突 activityId:{} uId:{}", partake.getActivityId(), partake.getuId(), e);

DuplicateKeyException e

Exception thrown when an attempt to insert or update data results in violation of an primary key or unique constraint. Note that this is not necessarily a purely relational concept; unique primary keys are required by most database types.尝试插入或更新数据导致违反主键或唯一约束时引发的异常。请注意,这不一定是一个纯粹的关系概念;大多数数据库类型都需要唯一的主键。

在这里插入图片描述
在这里插入图片描述

13节

Note组合模式

组合模式
意图:将对象组合成树形结构以表示"部分-整体"的层次结构

都把Config class做成:?

Double.parseDouble(“xxxx”)

返回初始化为由指定 String 表示的值的新双精度值,由类 Double 的 valueOf 方法执行。
参数:
s – 要解析的字符串。
返回:
字符串参数表示的双精度值。

决策物料

物料是年龄性别是否首单balabla,放入map来进行映射

/**
 * 决策物料
 */
public class DecisionMatterReq {
    /** 规则树ID */
    private Long treeId;
    /** 用户ID */
    private String userId;
    /** 决策值 */
    private Map<String, Object> valMap;

T: 引擎决策Maker:TreeNodeVO engineDecisionMaker(TreeRuleRich treeRuleRich,DecisionMatterReq matter)

规则信息仓储服务的实现:class RuleRepository

在这里插入图片描述

before:

在这里插入图片描述

在这里插入图片描述

发现表没加100002的
user take activity 有人要改
在这里插入图片描述
为false 为什么

14节

@MapperConfig

来自属性映射工具——MapStruct
来自:MapStruct

放一个:官网

对于不同领域层使用不同JavaBean对象传输数据,避免相互影响。比如传输对象DTO、业务普通封装对象BO、数据库映射对象DO等

在之前数据交换使用了BeanUtil的equal、使用了getter and setter
但是

调用getter/setter方法进行属性赋值:一大堆‘巨简单’的代码,不美观
调用BeanUtil.copyPropertie进行反射属性赋值:坑巨多,比如sources与target写反,难以定位某个字段在哪里进行的赋值,不利于debug,同时因为用到反射,导致性能也不佳

HOW TO USE :

在这里插入图片描述
在这里插入图片描述

常用注解解释

@Mapper——表示该接口作为映射接口,编译时MapStruct处理器的入口

1)uese:外部引入的转换类;

2)componentModel:就是依赖注入,类似于在spring的servie层用@servie注入,那么在其他地方可以使用@Autowired取到值。该属性可取的值为

a)默认:这个就是经常使用的 xxxMapper.INSTANCE.xxx;

b)cdi:使用该属性,则在其他地方可以使用@Inject取到值;

c)spring:使用该属性,则在其他地方可以使用@Autowired取到值;

d)jsr330/Singleton:使用者两个属性,可以再其他地方使用@Inject取到值;

@Mappings——一组映射关系,值为一个数组,元素为@Mapping

@Mapping——一对映射关系

1)target:目标属性,赋值的过程是把“源属性”赋值给“目标属性”;

2)source:源属性,赋值的过程是把“源属性”赋值给“目标属性”;

3)dateFormat:用于源属性是Date,转化为String;

4)numberFormat:用户数值类型与String类型之间的转化;

5)constant:不管源属性,直接将“目标属性”置为常亮;

6)expression:使用表达式进行属性之间的转化;

7)ignore:忽略某个属性的赋值;

8)qualifiedByName:根据自定义的方法进行赋值;

9)defaultValue:默认值;

@MappingTarget——用在方法参数的前面。使用此注解,源对象同时也会作为目标对象,用于更新。

@InheritConfiguration——指定映射方法

@InheritInverseConfiguration——表示方法继承相应的反向方法的反向配置

@Named——定义类/方法的名称

另一个帖子的补充:

MapStruct提供的一些处理器选项配置 MapStruct为我们提供了 @Mapper 注解,并提供了一些属性配置。
如:我们常用的两种componentModel和unmappedTargetPolicy
java复制代码@Mapper(componentModel = “spring”, unmappedTargetPolicy =
ReportingPolicy.IGNORE) public interface StudentMapStruct { }

1.componentModel = “XX” 四个属性值

default: 默认的情况,mapstruct不使用任何组件类型,
可以通过Mappers.getMapper(Class)方式获取自动生成的实例对象。 cdi: the generated mapper
is an application-scoped CDI bean and can be retrieved via @Inject
spring(经常使用): 生成的实现类上面会自动添加一个@Component注解,可以通过Spring的 @Autowired方式进行注入
jsr330: 生成的实现类上会添加@javax.inject.Named 和@Singleton注解,可以通过 @Inject注解获取。

  1. unmappedTargetPolicy=ReportingPolicy.XX 三个属性值 在未使用source值填充映射方法的target的属性的情况下要应用的默认报告策略
  • IGNORE 将被忽略
  • WARN 构建时引起警告
  • ERROR 映射代码生成失败

作者:SeeYou 来源:稀土掘金

拓展 check一下MapStruct源码


Q: why implements Serializable

A:

来自官方注解的翻译:

类的可序列化性由实现 java.io.Serializable 接口的类启用。未实现此接口的类将不会序列化或反序列化其任何状态。可序列化类的所有子类型本身都是可序列化的。序列化接口没有方法或字段,仅用于标识可序列化的语义。
为了允许序列化不可序列化类的子类型,子类型可能负责保存和恢复超类型的公共、受保护和(如果可访问)包字段的状态。仅当子类型扩展的类具有可访问的 no-arg 构造函数来初始化类的状态时,子类型才能承担此责任。如果不是这种情况,则声明类可序列化是错误的。将在运行时检测到该错误。
在反序列化期间,不可序列化类的字段将使用类的公共或受保护的 no-arg 构造函数进行初始化。无参数构造函数必须可供可序列化的子类访问。可序列化子类的字段将从流中恢复。
遍历图形时,可能会遇到不支持可序列化接口的对象。在这种情况下,将引发 NotSerializableException,并将标识不可序列化对象的类。


报错:there is no default constructor available in

原因:

指路
子类继承的父类没有默认无参构造函数,那子类要写父类的有参来构造super(…)

浅扒一下这个:drawProcessResult.getCode()

问题描述 抽奖测试出错

userTakeLeftCount 为0
在这里插入图片描述

在这里插入图片描述

kafka教程

今天按照教程在idea的terminal输入命令后 弹窗无反应,jps查看进程发现没有开启zookeeper和kafka
发现是命令用错了
指路:kafka在windows中的使用

在这里插入图片描述

⭐ 一个 topic 对应的多个 partition 分散存储到集群中的多个 broker 上,存储方式是一个 partition 对应一个文件,每个 broker 负责存储在自己机器上的 partition 中的消息读写。

zookeeper官网

16节

java.util.Optional

可能包含也可能不包含非 null 值的容器对象。如果存在一个值,isPresent() 将返回 true,get() 将返回该值。
ofNullable方法:
返回描述指定值的 Optional(如果非 null),否则返回空 Optional。
参数:
值 – 要描述的可能为空值
类型参数:
– 值的类
返回:
a 可选,如果指定的值为非

Q: com.alibaba.fastjson的JSON和JSON方法

//1.转化对象(中奖物品发货单)
InvoiceVO invoiceVO = JSON.parseObject((String) message.get(), InvoiceVO.class);//InvoiceVO是自己构造的类

JSON:

通常将这两个方法调用为 JSONString(Object) 和 parseObject(String, Class)。
下面是如何将 fastjson 用于简单类的示例:
模型模型 = 新模型();
String json = JSON.toJSONString(model);将模型序列化为 Json
Model model2 = JSON.parseObject(json, Model.class);将 JSON 反序列化为模型 2

arseObject(String text, Class< T > clazz)

public static <T> T parseObject(String text, Class<T> clazz) {
        return parseObject(text, clazz, new Feature[0]);
    }
/**
此方法将指定的 Json 反序列化为指定类的对象。如果指定的类是泛型类型,则不适合使用它,因为由于 Java 的类型擦除功能,它不会具有泛型类型信息。因此,如果所需的类型是泛型类型,则不应使用此方法
参数:
JSON – 要从中反序列化对象的字符串
class – T 类
功能 – 解析器功能
返回:
字符串 classOfT 中的 T 类型的对象
*/
 public static <T> T parseObject(String json, Class<T> clazz, Feature... features) {
        return (T) parseObject(json, (Type) clazz, ParserConfig.global, null, DEFAULT_PARSER_FEATURE, features);
    }

JAVA序列化

hutool

抽奖活动是怎么解耦的

Q:

16节在解耦抽奖活动时,对上一节进行了修改
对ActivityProcessImpl类下的doDrawProcess方法的 3. 结果落库进行了修改
原本:

// activityPartake:抽奖活动参与接口 的接口对象
// recordDrawOrde: 保存奖品单
	// @param drawOrder 奖品单
activityPartake.recordDrawOrder(buildDrawOrderVO(req, strategyId, takeId, drawAwardVO));

① 添加 DrawOrderVO 奖品单、保存recordDrawOrder返回的result值
② 添加“发送MQ,触发发奖流程”
添加中奖物品发货单 InvoiceVO
插入一个关于分层的笔记
IActivityPartake (抽奖活动参与接口),添加新的方法:updateInvoiceMqState
😀 在实现类中调用的是domain层repository包的接口的方法:

userTakeActivityRepository.updateInvoiceMqState(uId, orderId, mqState)

😀 调用的是domain层 repository包下 接口( IUserTakeActivityRepository)的变量的方法

😀 infrastructure层的 实现类 UserTakeActivityRepository 调用的是 接口变量userStrategyExportDao的方法

③ Constants枚举类添加 消息发送状态MQState

		 // 3. 结果落库
        DrawOrderVO drawOrderVO = buildDrawOrderVO(req, strategyId, takeId, drawAwardVO);
        Result recordResult = activityPartake.recordDrawOrder(drawOrderVO);
        if (!Constants.ResponseCode.SUCCESS.getCode().equals(recordResult.getCode())) {
            return new DrawProcessResult(recordResult.getCode(), recordResult.getInfo());
        }

        // 4. 发送MQ,触发发奖流程
        InvoiceVO invoiceVO = buildInvoiceVO(drawOrderVO);
        ListenableFuture<SendResult<String, Object>> future = kafkaProducer.sendLotteryInvoice(invoiceVO);
        future.addCallback(new ListenableFutureCallback<SendResult<String, Object>>() {

            @Override
            public void onSuccess(SendResult<String, Object> stringObjectSendResult) {
                // 4.1 MQ 消息发送完成,更新数据库表 user_strategy_export.mq_state = 1
                activityPartake.updateInvoiceMqState(invoiceVO.getuId(), invoiceVO.getOrderId(), Constants.MQState.COMPLETE.getCode());
            }

            @Override
            public void onFailure(Throwable throwable) {
                // 4.2 MQ 消息发送失败,更新数据库表 user_strategy_export.mq_state = 2 【等待定时任务扫码补偿MQ消息】
                activityPartake.updateInvoiceMqState(invoiceVO.getuId(), invoiceVO.getOrderId(), Constants.MQState.FAIL.getCode());
            }

总结:在 ActivityProcessImpl#doDrawProcess 方法中,主要补全的就是关于 MQ 的处理,这里我们会调动 kafkaProducer.sendLotteryInvoice 发送一个中奖结果的发货单。

Kafka的addCallback方法


问题描述

报错:MessageConversionException
反序列化出错

org.springframework.kafka.listener.ListenerExecutionFailedException:
invokeHandler Failed; nested exception is java.lang.IllegalStateException:
No Acknowledgment available as an argument, the listener container must have a MANUAL AckMode to populate the Acknowledgment.; nested exception is java.lang.IllegalStateException: No Acknowledgment available as an argument, the listener container must have a MANUAL AckMode to populate the Acknowledgment.

如果没有可用的Acknowledgment参数,那么监听容器必须具有手动AckMode才能填充Acknowledgment

Caused by: java.lang.IllegalStateException: No Acknowledgment
available as an argument, the listener container must have a MANUAL AckMode to populate the Acknowledgment.

Caused by: org.springframework.messaging.converter.MessageConversionException: Cannot convert from [java.lang.String] to [org.springframework.kafka.support.Acknowledgment] for GenericMessage

解决方案

一个监听方法的举例:

@KafkaListener(topics = "myTopic", ackMode = "MANUAL")
public void receiveMessage(String message, Acknowledgment acknowledgment) {
    System.out.println("Received message: " + message);
    acknowledgment.acknowledge();
}

我的监听方法:

 @KafkaListener(topics = "lottery_invoice",groupId = "lottery")
    public void onMessage(ConsumerRecord<?,?>record ,Acknowledgment ack, @Header(KafkaHeaders.RECEIVED_TOPIC) String topic){
    }

问题出在:没有设置手动提交:ackMode = “MANUAL”
我以为在配置文件配置就可以了:

enable-auto-commit: false
          ack-mode: manual_immediate

但是,使用的Kafka版本低, @KafkaListener没有ackMode参数
在这里插入图片描述
解决:在监听容器工厂中设置手动AckMode
详细:先创建一个Spring Boot配置类,取名KafkaConfig

在这里插入图片描述

@Configuration
@EnableKafka
public class KafkaConfig {

    @Autowired
    private KafkaProperties kafkaProperties;

    @Bean
    public ConcurrentKafkaListenerContainerFactory<String, String> kafkaListenerContainerFactory() {
        ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
        factory.setConsumerFactory(consumerFactory());
        factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL);
        return factory;
    }

    @Bean
    public ConsumerFactory<String, String> consumerFactory() {
        return new DefaultKafkaConsumerFactory<>(kafkaProperties.buildConsumerProperties());
    }

}

@EnableKafka注释启用Kafka支持。kafkaListenerContainerFactory()方法返回一个ConcurrentKafkaListenerContainerFactory对象,它用于创建Kafka监听容器。setConsumerFactory()方法用于设置消费者工厂,它将用于创建Kafka消费者。getContainerProperties()方法返回一个ContainerProperties对象,它用于设置监听容器的各种属性,例如AckMode。setAckMode()方法用于设置监听容器的AckMode属性,以便手动提交偏移量。然后手动调用Acknowledgment对象的acknowledge()方法来提交偏移量。

16节结束,撒花!★,°:.☆( ̄▽ ̄)/$:.°★

test:

    @Test
    public void test_send() throws InterruptedException {
        InvoiceVO invoice = new InvoiceVO();
        invoice.setuId("fustack");
        invoice.setOrderId(1444540456057864192L);
        invoice.setAwardId("3");
        invoice.setAwardType(Constants.AwardType.DESC.getCode());
        invoice.setAwardName("Code");
        invoice.setAwardContent("苹果电脑");
        invoice.setShippingAddress(null);
        invoice.setExtInfo(null);
        kafkaProducer.sendLotteryInvoice(invoice);

        while (true){
            Thread.sleep(10000);
        }

    }

运行结果:

在这里插入图片描述
在这里插入图片描述

test :
(加入MQ后再走一次流程)

@Test
    public void test_doDrawProcess(){
        DrawProcessReq req = new DrawProcessReq();
        req.setuId("fustack");
        req.setActivityId(100001L);
        DrawProcessResult drawProcessResult = activityProcess.doDrawProcess(req);

        logger.info("请求入参:{}", JSON.toJSONString(req));
        logger.info("测试结果:{}", JSON.toJSONString(drawProcessResult));
    }

在这里插入图片描述
次数消耗完了没改,不重要


Note

ask:SeekToCurrentErrorHandler : Backoff none exhausted for ConsumerRecord,这是什么错误
这个错误是由于在Kafka消费者中没有正确使用Acknowledgment对象导致的。在Kafka消费者中,你可以使用@KafkaListener注释来指定消费者应该监听的主题,并使用Acknowledgment对象来提交偏移量。如果你的消费者没有正确使用Acknowledgment对象,就会出现“java.lang.IllegalStateException: No Acknowledgment available as an argument, the listener container must have a MANUAL AckMode to populate the Acknowledgment”的错误。
要解决这个错误,你需要确保你的消费者方法签名正确,并正确地使用Acknowledgment对象来提交偏移量。以下是一个示例代码:

关于Kafka 监听类的
Acknowledgment:表示消费者已经成功处理了一条消息并准备提交偏移量
偏移量是一个整数值,它表示消费者在一个特定分区中已经处理的消息数量
当消费者从Kafka中读取消息时,它们会跟踪它们已经处理的消息的偏移量。这样,如果消费者需要重新启动或者发生故障,它们可以从它们上次处理的偏移量处继续处理消息,而不是从头开始。这种机制确保了消息的可靠性和一致性,并且使得消费者可以在任何时候恢复到正确的位置。在Kafka中,偏移量是由消费者组维护的

SeekToCurrentErrorHandler是什么
SeekToCurrentErrorHandler是Kafka客户端API中的一个类,它是一个错误处理程序,用于处理在消费消息时发生的错误。当消费者在处理消息时遇到错误时,SeekToCurrentErrorHandler将尝试将消费者的偏移量重置为当前位置,并重新处理消息。如果重试失败,则SeekToCurrentErrorHandler将将消息发送到错误处理程序以进行进一步处理。
SeekToCurrentErrorHandler的作用是确保消费者可以在发生错误时恢复到正确的位置,并尽可能多地处理消息。它可以帮助消费者在处理消息时更加健壮,并提高消费者的可靠性和一致性。

//下面几个问题是debug部分出现了null,有关跳转的类
PayloadMethodArgumentResolver是什么
PayloadMethodArgumentResolver是Spring框架中的一个类,它是用于处理Kafka消息负载的参数解析器。当使用@KafkaListener注释定义Kafka消费者方法时,可以使用PayloadMethodArgumentResolver来将消息负载转换为方法参数。

Payload是什么
Payload是指Kafka消息中的负载部分,也称为消息内容。在Kafka中,消息由一个键和一个值组成,其中键和值都是字节数组。负载是指消息的值部分,它可以是任何字节数组,通常是序列化的对象或字符串。

说说getParameterAnnotation方法
getParameterAnnotation方法是Java反射API中的一个方法,它用于获取方法参数上的注释。在使用注释来配置Kafka消费者方法时,可以使用getParameterAnnotation方法来获取@Header注释或其他自定义注释。

@KafkaListener(topics = "myTopic")
public void receiveMessage(@Header("myHeader") String headerValue)

headerValue是什么:
headervalue是一个方法参数上的注释,它使用了@Header注释来指定应该从消息中提取的标头值。具体来说,它是用于从Kafka消息中提取KafkaHeaders.RECEIVED_TOPIC标头值的。在你的消费者方法中,headervalue参数将被注入KafkaHeaders.RECEIVED_TOPIC标头的值,以便你可以在方法中使用它。

@Header是什么
@Header是一个注释,用于从Kafka消息中提取标头值。标头值是指Kafka消息中的元数据,它包含有关消息的信息,例如主题、分区和偏移量。从消息中提取标头值可以帮助你更好地理解消息的来源和内容,并在处理消息时做出更好的决策。简而言之,@Header注释允许你从Kafka消息中提取元数据,并将其作为方法参数使用。

Windows使用kafka命令(高版)

创建主题(高版本Kafka)(windows-server变成了bootstrap-server)
.\bin\windows\kafka-topics.bat --create --topic zhangphil_demo --bootstrap-server localhost:9092
查看主题:
.\bin\windows\kafka-topics.bat -list -bootstrap-server localhost:2181

17节

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
抽奖系统是一种常见的应用,在设计抽奖系统时,使用基于领域驱动设计(Domain-Driven Design,DDD)的四层架构可以提供更好的架构实践。 在四层架构中,首先是用户界面层(User Interface Layer),用户界面层负责向用户展示抽奖界面,并接收用户的输入请求。用户界面层可以采用Web页面、移动应用等形式实现。通过使用领域驱动设计,用户界面层可以更加贴近用户需求,提供更好的用户体验。 接下来是应用层(Application Layer),应用层是整个抽奖系统的核心。应用层负责处理用户请求,协调各个领域对象之间的交互,并调用相应的领域服务和聚合根进行业务逻辑的处理。应用层在领域驱动设计中起到了承上启下的作用,通过定义和实现各种用例和操作,实现了系统的功能。 在领域层(Domain Layer)中,定义了抽奖系统中的核心业务逻辑。领域层包含了各种实体、值对象、聚合根等领域对象,通过这些领域对象的交互实现了系统的业务流程。在抽奖系统中,可以定义抽奖活动、参与者、奖品等领域对象,并在领域层中定义它们的行为和属性,从而满足系统的各项业务需求。 最后是基础设施层(Infrastructure Layer),基础设施层提供了抽奖系统运行所需的各种支持服务,包括数据库、缓存、消息队列等。在抽奖系统中,基础设施层可以提供参与者信息的持久化存储、抽奖结果的发送等功能。通过将基础设施逻辑与领域逻辑相分离,可以提高系统的可维护性和可扩展性。 综上所述,基于领域驱动设计的四层架构可以有效地设计和实现抽奖系统。通过将系统的核心业务逻辑与界面、应用和基础设施进行分离,可以实现系统的高内聚、低耦合,提供更好的扩展性和可维护性。同时,领域驱动设计还能够更好地满足用户需求,提供更好的用户体验。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值