谷粒商城分布式基础篇
谷粒商城分布式高级篇(上)
谷粒商城分布式高级篇(中)
谷粒商城分布式高级篇(下)
开启线程的四种方式
以后的业务代码,将所有的多线程异步任务都交给线程池执行
给线程池直接提交任务 创建都是返回一个ExecutorService
public static ExecutorService service = Executors.newFixedThreadPool(10);
线程池的原生创建:ThreadPoolExecutor executor = new ThreadPoolExecutor();
线程池的七大参数
七大参数
in corePoolSize
核心线程数;线程池创建好以后就准备就绪的线程数量,等待接受异步任务执行int maximumPoolSize
最大线程数量;控制资源long keepAliveTime
存活事件。如果当前的线程数量大于corePoolSize
数量,释放空闲的线程(超出核心线程数量的部分)条件:超出核心线程数量的线程空闲时间超过long keepAliveTime
就会被释放TimeUnit unit
时间单位BlockingQueue workQueue
阻塞队列。如果任务有很多,超过最大线程数量maximumPoolSize
ThreadFactory threadFactory
线程的创建工厂RejectedExecutionHandler handler
如果队列满了,按照我们指定的拒绝策略拒绝执行任务
3,4,5都需要获取1的返回结果,CompletableFuture
实现了Future
接口
runXxx是都没有返回值的,supplyXxx 都是可以获得返回值的
public static ExecutorService executor = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println("main...start...");
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 2;
System.out.println("运行结果:" + i);
}, executor);
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 2;
System.out.println("运行结果:" + i);
return i;
}, executor);
Integer integer = future.get();
System.out.println("main...end..."+integer );
}
whenComplete感知异常
exceptionally 修改返回结果
两个参数,返回值和异常
1、thenRun:不能获取到上一步的执行结果,无返回值
2、thenAcceptAsync能接收上一步结果,但是无返回值
3、thenApplyAsync能接收上一步结果,有返回值,.get()
为阻塞式方法,等到俩个线程都结束并发回结果了才调用
两个任务,只要有一个完成,我们就执行任务3
runAfterEitherAsync:不感知结果,自身无返回值
future01.runAfterEitherAsync(future02,()->{
//不感知结果,自身无返回值
System.out.println("任务3开始...之前的结果");
},executor);
acceptEitherAsync: 感知结果,自身无返回值
future01.acceptEitherAsync(future02,(res)->{
System.out.println("任务3开始...之前的结果"+res);
},executor);
applyToEitherAsync: 感知结果,自身有返回值
CompletableFuture<String> fu = future01.applyToEitherAsync(future02, (res) -> {
System.out.println("任务3开始...之前的结果"+res);
return res + "haha";
}, executor);
System.out.println("main...end..."+fu.get());
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println("main...start...");
CompletableFuture<String> futureImg = CompletableFuture.supplyAsync(() -> {
System.out.println("查询图片信息");
return "hello.jpg";
}, executor);
CompletableFuture<String> futureAttr = CompletableFuture.supplyAsync(() -> {
System.out.println("查询商品的属性");
return "黑色+256G";
}, executor);
CompletableFuture<String> futureDesc = CompletableFuture.supplyAsync(() -> {
System.out.println("查询商品介绍");
return "华为";
}, executor);
// CompletableFuture allOf = CompletableFuture.allOf(futureImg, futureAttr, futureDesc);
// allOf.get();//等待所有结果完成
// System.out.println("main...end..."+futureImg.get()+"---"+futureAttr.get()+"---"+futureDesc.get());
CompletableFuture<Object> anyOf = CompletableFuture.anyOf(futureImg, futureAttr, futureDesc);
System.out.println("main...end..."+anyOf.get());//获得了最先完成的结果
}
分析业务可知接口返回一个页面模型 SkuItemVo item = skuInfoService.item(skuId);
在模型中封装
联合查询凑齐所需要的数据,并使用自定义封装,注意自定义封装时,不能使用内部类的方式返回封装对象,所以降之前的内部类设计改为单独类,注意sql
语句中返回的字段值不能起别名,否则自定义resultMap
无法封装到
<resultMap id="spuItemAttrGroupVo" type="com.atguigu.gulimall.product.vo.SpuItemAttrGroupVo">
<result property="groupName" column="attr_group_name"/>
<collection property="attrs" ofType="com.atguigu.gulimall.product.vo.product.Attr">
<result property="attrName" column="attr_name"/>
<result property="attrValue" column="attr_value"/>
collection>
resultMap>
<select id="getAttrGroupWithAttrsBySpuId"
resultMap="spuItemAttrGroupVo">
SELECT
ag.attr_group_name,
ag.attr_group_id,
aar.attr_id,
attr.attr_name ,
pav.attr_value ,
pav.spu_id attr
FROM
`pms_attr_group` ag
LEFT JOIN pms_attr_attrgroup_relation aar ON aar.attr_group_id = ag.attr_group_id
LEFT JOIN pms_attr attr ON attr.attr_id = aar.attr_id
LEFT JOIN pms_product_attr_value pav ON attr.attr_id = pav.attr_id
WHERE
ag.catelog_id = #{catalogId} and pav.spu_id=#{spuId}
select>
SELECT ssav.attr_id attrId,
ssav.attr_name attrName,
GROUP_CONCAT(DISTINCT ssav.attr_value ) attrValues
FROM pms_sku_info info
LEFT JOIN pms_sku_sale_attr_value ssav ON ssav.sku_id = info.sku_id
WHERE info.spu_id = #{spuId}
GROUP BY ssav.attr_id,
ssav.attr_name
有用到 thymelef 函数 ${#strings.listSplit(item.desc.decript,',')}
分隔函数
sku
的聚合sql
查询
SELECT
ssav.attr_id attrId,
ssav.attr_name attrName,
ssav.attr_value,
GROUP_CONCAT(DISTINCT info.sku_id) sku_ids
FROM
pms_sku_info info
LEFT JOIN pms_sku_sale_attr_value ssav ON ssav.sku_id = info.sku_id
WHERE
info.spu_id = 5
GROUP BY ssav.attr_id,ssav.attr_name,ssav.attr_value
attr_value的交集sku_ids 为一个sku
js交集判断跳转
主要通过jq拼接实现
thymeleaf 添加 skus 属性
jq 得到属性的值,并用fiter 函数得到 数组的交集
线程池配置完毕
异步编排开始
@Override
public SkuItemVo item(Long skuId) throws ExecutionException, InterruptedException {
SkuItemVo skuItemVo = new SkuItemVo();
CompletableFuture<SkuInfoEntity> infoFuture = CompletableFuture.supplyAsync(() -> {
//1、sku基本信息获取 pms_sku_info
SkuInfoEntity info = getById(skuId);
Long spuId = info.getSpuId();
Long catalogId = info.getCatalogId();
skuItemVo.setInfo(info);
return info;
}, executor);
CompletableFuture<Void> saleAttrFuture = infoFuture.thenAcceptAsync((res) -> {
//3、spu的销售属性组合。
List<SkuItemSaleAttrsVo> saleAttrsVos = skuSaleAttrValueService.getSaleAttrsBySpuId(res.getSpuId());
skuItemVo.setSaleAttrsVos(saleAttrsVos);
}, executor);
CompletableFuture<Void> descFuture = infoFuture.thenAcceptAsync((res) -> {
//4、获取spu的介绍(商品介绍)
SpuInfoDescEntity desc = spuInfoDescService.getById(res.getSpuId());
skuItemVo.setDesc(desc);
}, executor);
CompletableFuture<Void> baseAttrFuture = infoFuture.thenAcceptAsync((res) -> {
//5、获取spu的规格参数信息(规格与包装)
List<SpuItemAttrGroupVo> attrGroupVos = attrGroupService.getAttrGroupWithAttrsBySpuId(res.getSpuId(), res.getCatalogId());
skuItemVo.setGroupAttrs(attrGroupVos);
}, executor);
CompletableFuture<Void> imgFuture = CompletableFuture.runAsync(() -> {
//2、sku的图片信息 pms_sku_images
List<SkuImagesEntity> images = imagesService.getImagesBySkuId(skuId);
skuItemVo.setImages(images);
}, executor);
//阻塞等到所有任务都完成
CompletableFuture.allOf(infoFuture,saleAttrFuture,
descFuture,baseAttrFuture,imgFuture).get();
return skuItemVo;
}
导入 登录页和注册页的html文件,并nginx动静分离,nginx站点文件,网关路由配置
点击倒计时
$(function () {
$("#sendCode").click(function () {
//1、给指定手机号发送验证码
//2、倒计时
if ($(this).hasClass("disabled")) {
} else {
timeoutChangeStyle();
}
})
})
var num = 3
function timeoutChangeStyle() {
$("#sendCode").attr("class", "disabled")
if (num == 0) {
$("#sendCode").text("发送验证码")
$("#sendCode").attr("class", "")
} else {
var str = num + "s 后再次发送"
$("#sendCode").text(str)
setTimeout("timeoutChangeStyle()", 1000)
}
num--;
}
SpringMvc viewController 直接映射请求到页面
@Configuration
public class GulimallWebConfig implements WebMvcConfigurer {
/**
* 视图映射
* @param registry
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
/**
* @GetMapping("/login.html")
* public String loginPage(){
* return "login";
* }
* @param registry
*/
registry.addViewController("/login.html").setViewName("login");
registry.addViewController("/reg.html").setViewName("reg");
}
}
将验证码发送代码作为组件整体封装在第三方服务模块中
gulimall-auth-server
模块远程调用 gulimall-third-party
短信服务
创建短信发送控制器,能返回json数据,基准路径为 /sms
发送验证码时,将手机作为键值存入redis,并设置过期时间,和存入时的系统时间,发送发送验证码时先在redis中查找这个值,判断是否过期
Request method 'POST' not supported
POST 不支持,
原因: 用户注册----》 /register[post]
--》return "forward:/reg.html";
做了路径映射 路径映射默认是get方式访问
解决办法直接渲染,不做转发 return "reg.html";
为防止刷新表单重复提交,不要转发至页面,应该使用重定向,但是重定向会获取不到请求域中的数据,将Model
改为 RedirectAttributes
解决问题
重定向携带数据,利用session原理,将数据放在session中,只要跳到下一个页面去除这个数据就会删掉
验证码验证完成后,调用远程接口注册,gulimall-member
服务模块注册,微服务之间都是 http 加 json 进行调用,所以远服务都返回和接收json,则 接口 都会加上 @RequestBody
注解,SpringMvc 会自动将请求体里的对象转为json
远程接口检验并处理数据,接口要给调用者返回各种数据的校验异常结果,所以应该使用异常机制来返回 检验结果,
//检查用户名 和 手机号是否唯一 为了让controller感知异常,异常机制
checkPhoneUnique(vo.getPhone());
@Override
public void checkPhoneUnique(String phone) throws PhoneExistException{
Integer count = this.baseMapper.selectCount(new QueryWrapper<MemberEntity>().eq("mobile", phone));
if (count > 0) {
//说明数据库有这个手机号
throw new PhoneExistException();
}
//否则什么都不做 检查通过 业务继续进行注册
}
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
//生成加密结果
String encode = encoder.encode("123456");
//比对
boolean matches = encoder.matches("123456", "$2a$10$l.3r9fHIrJFvgbxp7nw2LuFACxMYnEOnXdAEYDv4q.Gx/4iGIp0T.");
System.out.println(encode + "=>" + matches);
回到gulimall-auth-server
模块,验证码校验成功,调用远程接口完成注册
简单的登录流程
提交input中的值至auth模块的 /login
地址 @PostMapping("/login")
public String login(UserRegisterVo vo,RedirectAttributes redirectAttributes){
//远程登录
R r = memberFeignService.login(vo);
@PostMapping("/login")
public R login(@RequestBody MemberLoginVo vo)
code只能用一次,同一个用户的accessToken一段时间是不会变化的,即使多次获取
<a href="https://api.weibo.com/oauth2/authorize?client_id=4074626065&response_type=code&redirect_uri=http://auth.gulimall.com/oauth2.0/weibo/success">授权页地址a>
http://auth.gulimall.com/oauth2.0/weibo/success/?code=CODE
@GetMapping("/oauth2.0/weibo/success")
public String weibo(@RequestParam("code") String code)
Map<String, String> map = new HashMap<>();
map.put("client_id", "4074626065");//和login.html的要保持一致
map.put("client_secret", "163a1200816d951ed4bf88735469c573");
map.put("grant_type", "authorization_code");
map.put("redirect_uri", "http://auth.gulimall.com/oauth2.0/weibo/success");
map.put("code", code);
//1 根据 code 换取 access_token 能获取则成功
HttpResponse response = HttpUtils.doPost("https://api.weibo.com", "/oauth2/access_token", "post", new HashMap<>(), map, new HashMap<>());
获得的token数据,注意 code 只能使用一次,而access_token能用多次
{
"access_token": "2.008kSAYFP4ik8Ecd802dd8f0h5eOeE",
"remind_in": "157679999",
"expires_in": 157679999,
"uid": "5083131655",
"isRealName": "true"
}
//查询当前社交用户的社交账号(昵称,性别等)
Map<String, String> query = new HashMap<>();
query.put("access_token", vo.getAccess_token());
query.put("uid", vo.getUid());
HttpResponse response = HttpUtils.doGet("https://api.weibo.com/", "/2/users/show.json", "get", new HashMap<String, String>(), query);
解决域名之间cookie不能共享,设置cookie时就让 cookie的 domain 为主域名,这样子域名设置的cookie主域名也能访问
spring.session.store-type=redis
server.servlet.session.timeout=30m
Serializable
gulimall-product
也如上整合spring-session,再次启动发现报错序列化异常SerializationException
这一次的序列化异常是因为 gulimall-product
要取存入的MemberRespVo
而 此模块中没有这个类,所以解决办法是将这个类放入公共模块中,让两个模块都能访问到创建配置类,解决子域共享问题
@Configuration
public class GulimallSessionConfig {
/**
* 自定义session作用域:整个网站
* 使用一样的session配置,能保证全网站共享一样的session
*/
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer defaultCookieSerializer = new DefaultCookieSerializer();
defaultCookieSerializer.setDomainName("gulimall.com");
defaultCookieSerializer.setCookieName("GULISESSION");
return defaultCookieSerializer;
}
/**
* 序列化机制
*/
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer(){
return new GenericJackson2JsonRedisSerializer();
}
}
给 search模块加入session
spring:
redis:
host: 192.168.56.10
session:
store-type: redis
@EnableRedisHttpSession
@Configuration
public class GulimallSessionConfig {
/**
* 自定义session作用域:整个网站
* 使用一样的session配置,能保证全网站共享一样的session
*/
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer defaultCookieSerializer = new DefaultCookieSerializer();
defaultCookieSerializer.setDomainName("gulimall.com");
defaultCookieSerializer.setCookieName("GULISESSION");
return defaultCookieSerializer;
}
/**
* 序列化机制
*/
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer(){
return new GenericJackson2JsonRedisSerializer();
}
}
下载 gitee 许雪里 / xxl-sso
框架
更改项目中的redis地址和配置的域名
添加本机测试域名
127.0.0.1 ssoserver.com
127.0.0.1 client1.com
127.0.0.1 client2.com
在此框架根目录先清理再打包并跳过测试
mvn clean package -Dmaven.skip.test=true
启动服务 xxl-sso-server
访问 http://ssoserver.com:8080/xxl-sso-server/login
启动 两个web 测试模块 xxl-sso-web-sample-springboot
分别是 8081和8082端口
访问 http://client1.com:8081/xxl-sso-web-sample-springboot/
可以测试一处登录处处登录了
单独打包一个模块时发生错误。无法解析xxl-sso-core 这个包
解决:将这个包安装到仓库
再打包还是没解决问题,因为还得安装所有所依赖的父项目,那就干脆在根目录全部重新打
项目中的测试
加入 web和lombok
加入web Lombok 和 thyme leaf
redirect_url
值 并随页面提交一起提交client1
登录成功创建购物车服务
上传静态资源至nginx并修改html中的资源地址
cart服务引入common模块,配置nacos地址服务名,端口号,启动类加入@EnableFeignClients
开启服务注册发现,和开启远程调用 @EnableFeignClients
server.port=30000
spring.application.name=gulimall-cart
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
启动网关和cart 就可访问cart了
/**
* 整个购物车
* 需要计算的属性,都需要重写get方法
*/
public class Cart {
List<CartItem> items;
//全部sku的总数 3+3=6 商品数量
private Integer countNum;
//共有多少种类型
private Integer countType;
//所有sku总价
private BigDecimal totalAmount;
//优惠价格
private BigDecimal reduce = new BigDecimal("0");
public Integer getCountNum() {
int count = 0;
if (items != null && items.size() > 0) {
for (CartItem item : items) {
count += item.getCount();
}
}
return count;
}
public Integer getCountType() {
int countType = 0;
if (items != null && items.size() > 0) {
for (CartItem item : items) {
countType += 1;
}
}
return countType;
}
public BigDecimal getTotalAmount() {
BigDecimal amount = new BigDecimal("0");
//计算购物项总价
if (items != null && items.size() > 0) {
for (CartItem item : items) {
BigDecimal totalPrice = item.getTotalPrice();
amount = amount.add(totalPrice);
}
}
//减去优惠总价
BigDecimal subtract = amount.subtract(getReduce());
return subtract;
}
}
还是整合spring-session 加入依赖和配置类,判断是否登录只用从session获取当前用户
* 浏览器有一个cookie,user-key用来标识临时用户信息,一个月过期
* 如果第一次使用jd的购物车功能,都会给一个临时的用户身份
* 浏览器保存以后,每次访问都会带上
京东的判断登录逻辑
登录:session有
没登录:安照cookie李米娜带来的user-key来做
第一次:如果没有临时用户,帮忙创建一个临时用户
登录了有id,没登录有user-key
编写一个拦截器,在执行目标方法之前,判断用户的登录状态,并封装传递给controller目标请求
编写的方法,创建一个类,实现 spring-mvc
的 HandlerInterceptor
, 关键 的方法实现preHandle
public class CartInterceptor implements HandlerInterceptor {
/**
* 目标方法执行之前
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession();
MemberRespVo member = (MemberRespVo) session.getAttribute(AuthServerConstant.LOGIN_USER);
if (member != null){
//用户登录
}else {
//用户没登录
}
return false;
}
注册这个拦截器需要创建一个配置类 并添加需要拦截的路径地址,这里拦截所有请求
@Configuration
public class GulimallWebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new CartInterceptor()).addPathPatterns("/**");
}
}
同一个线程共享数据
用 ThreadLocal
快速获取用户信息
在拦截器中注入一个 ThreadLocal
的线程,里面存的 UserInfo
目标方法返回之前,将信息存入 ThreadLocal
Controller
或其他任何方法的获取,因为是静态的
//1、快速得到用户信息。登录了有id,没登录有user-key
UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
前端页面拼接地址,携带catId和数量
location.href = "http://cart.gulimall.com/addToCart?skuId="+skuId+"&num="+val;
controller封装了一个redis的查询操作,返回当前用户存储在reids 的购物车数据
/**
* 获取我们要操作的购物车
* @return
*/
private BoundHashOperations<String, Object, Object> getCartOps() {
UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
//1、
String cartKey = "";
if (userInfoTo.getUserId() != null) {
//gulimall:cart:1
cartKey = CART_PREFIX + userInfoTo.getUserId();
} else {
cartKey = CART_PREFIX + userInfoTo.getUserKey();
}
return redisTemplate.boundHashOps(cartKey);
}
远程查询商品信息和商品属性的组合信息
并做好这两个查询的异步编排
导入了两个线程池的配置
加入购物车时报错数字转化异常
debug发现购物车对象完全为空,原因为异步问题,异步还没执行完就运行结束了所以对象为空,要等到异步的查询完成后再装入对象
等到getSkuSaleAttrValues
和getSkuInfoTask
这两个异步任务都完成后再往redis添加数据
避免刷新导致重复添加,添加购物车,和添加成功分为两个页面操作
点击我的购物车后,主要做判断,登录和未登录,未登录就查询临时购物车数据,登录了,就需要合并临时和用户购物车,合并后清除临时购物车
点击勾选后跳转至处理请求,处理完成后重定向回购物车列表
@GetMapping("/checkItem")
public String checkItem(@RequestParam("skuId") Long skuId,@RequestParam("check") Integer check){
cartService.checkItem(skuId,check);
return "redirect:http://cart.gulimall.com/cart.html";
}
同上
同上
下载并启动这个容器
docker run -d --name rabbitmq -p 5671:5671 -p 5672:5672 -p 4369:4369 -p 25672:25672 -p 15671:15671 -p 15672:15672 rabbitmq:management
容器自启
docker update rabbitmq --restart=always
访问虚拟机的15672端口就可打开rabbitmq的web端 http://192.168.56.10:15672/
默认账号密码 guest
点对点 直接类型交换机
创建4个实验队列
创建一个direct交换机
依次绑定刚创建的队列
发送一则消息
获取消息,第一种获取后不消失,第二种获取后消失
扇出类型交换机
绑定四个测试队列
发送一个路由键为 emps的,结果四个队列都收到了
而且不写路由键,绑定的路由键也都能收到
主题类型交换机
实现这种绑定效果,分别是 atguigu.#
和 *.news
发送一个 atguigu.news
路由键的消息,四个队列都接收到了,因为 atguigu.news
满足了两种绑定效果
若发送 hello.news
路由键的消息,就只有 一个满足了调教的接收到了
AmqpAdmin
创建交换机,declareExchange
方法声明一个交换机,传入一个 Exchange
接口类型的类,Exchange
接口的具体实现有这几种,就是之前之前客户创建时可选的那几个类型,创建一个 简单的 DirectExchange
类型 ,他有三种构造函数,全参的就是之前客户端创建时的几种选项,String name, boolean durable, boolean autoDelete, Map arguments
分别是 名字,是否持久化(关闭RebbitMq后再打开还存在),是否自动删除,指定的参数@Autowired
AmqpAdmin amqpAdmin;
@Test
public void createExchange() {
//String name, boolean durable, boolean autoDelete, Map arguments
DirectExchange directExchange = new DirectExchange("hello-java-exchange",true,false);
amqpAdmin.declareExchange(directExchange);
System.out.println("Exchange[hello-java-exchange]创建成功");
}
全参构造器,String name, boolean durable, boolean exclusive, boolean autoDelete, Map
名字,持久化,是否排他,自动删除,运行成功后 客户端可查看 创建成功
3. 创建绑定
Binding 类只有一个构造函数
客户端查看绑定成功
客户端可看到
还可以发送对象,但是对象必须实现序列化接口
可以将对象转换成json发送,需要一个转化配置类,配置好转化配置类则自动转化为json了
监听消息:使用@RabbitListener
必须有 @EnableRabbit
@RabbitListener
标注在业务逻辑组件上,且组件在容器中
在任意一个 service实现中 编写一个监听器,用Object 接收这个消息获取他的类型,发现类型为class org.springframework.amqp.core.Message
就可以将接收 Message 类型的数据,还可以直接将其中的对象直接写在参数上,spring回自动转换
还可以传入当前通道
测试多个服务监听这个队列,启动多个相同的服务,发送多个消息
发现一个消息只能被一个客户端接收
接收消息时休眠此线程,测试是否能正常接收,发现只有一个消息完全处理完,方法运行结束才可以接收下一个消息
@RabbitListener
能标注在类和方法上(监听哪些队列)
@RabbitHandler
能标注在方法上 (重载区分不同类型的消息)
直接重载了
设置发送端消息抵达Broker的确认
在配置类中定制RabbitTemplate
设置ConfirmCallback
@Configuration
public class MyRabbitConfig {
@Autowired
RabbitTemplate rabbitTemplate;
/**
* 定制 RabbitTemplate
* 1、服务器收到消息就回调
* 1、spring.rabbitmq.publisher-confirms=true
* 2、设置确认回调 ConfirmCallback
* 2、消息正确抵达队列
* 1、spring.rabbitmq.publisher-returns=true
* spring.rabbitmq.template.mandatory=true
* 2、设置确认回调 ReturnCallback
*
* @PostConstruct MyRabbitConfig对象创建完成后执行这个方法
*/
@PostConstruct
public void initRabbitTemplate() {
//设置消息抵达Broker的确认回调
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
/**
* 1、只要消息抵达Broker ack就为true 不管是否有消费者都会回调
* @param correlationData 当前消息的唯一关联数据(这个是消息的唯一id)
* @param ack 消息是否成功收到
* @param cause 失败的原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
System.out.println("confirm...correlationData[" + correlationData + "],=>b[" + ack + "],=>s[" + cause + "]");
}
});
correlationData 消息的唯一id如何获取。在消息发送时可以携带这个类,类中的值指定一个字符串为唯一id
//1、发送消息
rabbitTemplate.convertAndSend("hello-java-exchange", "hello.java", res, new CorrelationData(UUID.randomUUID().toString()));
设置发送端抵达队列的确认
在配置类中定制RabbitTemplate
设置ReturnCallback
//设置消息抵达队列的确认回调
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
/**
* 只有消息没有投递给指定的队列,才会触发这个失败回调
* @param message 投递失败的消息详细信息
* @param replyCode 恢复的状态码
* @param replyText 恢复的文本内容
* @param exchange 当时发给哪个交换机
* @param routingKey 当时用的路由键
*/
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.out.println("Fail message[" + message + "] replyCode[" + replyCode + "] replyText[" + replyText + "] exchange[" + exchange + "] routingKey[" + routingKey + "]");
}
});
测试失败投递,最简单的失败在投递消息时错误指定路由键,这种错误信息回复码为312,NOT_ROUTE
ack是自动的,默认是自动确认的,只要消息被接收到,客户端会自动确认,服务端就会移除这个消息
这种自动确认的问题:debug启动,发送5个消息,在接收这个消息的地方断点,模拟宕机断点运行到这里并处理完一个消息后就结束这个服务,发现消息5个消息都被自动回复了,发生了消息丢失
解决方法设置开启手动确认模式,只要没有明确告诉MQ消息被签收。没有ACK,消息就一直是unacked状态。即使Consumer 消费端宕机。消息也不会丢失,重置为Ready状态,下一次有新的Consumer连接进来就发给他
#手动ack消息
spring.rabbitmq.listener.simple.acknowledge-mode=manual
如何签收这个手动模式的消息
Channel 通道中有一个方法 channel.basicAck();
确认签收方法,此方法传入一个 long deliveryTag
和 布尔值,deliveryTag
则在 message
中 message.getMessageProperties().getDeliveryTag();
获取,每一个传入的消息都有一个deliveryTag
是自增的 ,布尔值则表示是否批量签收
重新设计了签收逻辑,保证部分消息没有签收到
就发现,队列中没有被签收的消息还在Unacked状态等待签收
关闭服务模拟宕机后消息回到 Ready 准备状态 没有消失
也能调用channel.basicNack(deliveryTag,false,false);
手动拒绝签收消息,第三个参数为是否重新入队,重新入队后会回到队列中当即消费,若不重新入队则直接丢弃,服务器也不会再等待接收
总结
业务成功完成就应该签收 channel.basicAck(deliveryTag,false);
业务失败就拒签channel.basicNack(deliveryTag,false,false);