第11章 订单 角色 : 订单模块,后端开发工程师。
课程内容 完成订单结算页渲染
完成用户下单实现
完成库存变更实现
1 订单结算页 image-20211205120105277 1.1 收件地址分析 1展示 打开前台 order.html
image-20211205115918039 收件地址分析 td_address表。表结构分析:
image-20211205120007359
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 CREATE TABLE `tb_address` ( `id` int (11 ) NOT NULL AUTO_INCREMENT, `username` varchar (50 ) DEFAULT NULL COMMENT '用户名' , `provinceid` varchar (20 ) DEFAULT NULL COMMENT '省' , `cityid` varchar (20 ) DEFAULT NULL COMMENT '市' , `areaid` varchar (20 ) DEFAULT NULL COMMENT '县/区' , `phone` varchar (20 ) DEFAULT NULL COMMENT '电话' , `address` varchar (200 ) DEFAULT NULL COMMENT '详细地址' , `contact` varchar (50 ) DEFAULT NULL COMMENT '联系人' , `is_default` varchar (1 ) DEFAULT NULL COMMENT '是否是默认 1默认 0否' , `alias` varchar (50 ) DEFAULT NULL COMMENT '别名' , PRIMARY KEY (`id`) ) ENGINE= InnoDB AUTO_INCREMENT= 66 DEFAULT CHARSET= utf8;
我们可以根据用户登录名去tb_address表中查询对应的数据。
1.2 实现用户收件地址查询 1.2.1 代码实现 ydles-service-user 1需改com.ydles.user.service.AddressService接口,添加根据用户名字查询用户收件地址信息,代码如下:
From: 元动力 1 2 public List<Address> list(String username);
2业务层接口实现类
修改com.ydles.user.service.impl.AddressServiceImpl类,添加根据用户查询用户收件地址信息实现方法,如下代码:
From: 元动力 1 2 3 4 5 6 7 8 9 @Override public List<Address> list(String username) { Address address=new Address (); address.setUsername(username); List<Address> addressList = addressMapper.select(address); return addressList; }
3控制层
修改com.ydles.user.controller.AddressController,添加根据用户名查询用户收件信息方法,代码如下:
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 @Autowired TokenDecode tokenDecode;@GetMapping("/list") public List<Address> list(){ String username = tokenDecode.getUserInfo().get("username" ); List<Address> addressList = addressService.list(username); return addressList; }
4创建TokenDecode
com.ydles.UserApplication中创建TokenDecode,代码如下:
From: 元动力 1 2 3 4 @Bean public TokenDecode tokenDecode () { return new TokenDecode (); }
测试:通过网关访问,修改网关配置信息
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 routes: - id: ydles_goods_route uri: lb://goods predicates: - Path=/api/album/**,/api/brand/**,/api/cache/**,/api/categoryBrand/**,/api/category/**,/api/para/**,/api/pref/**,/api/sku/**,/api/spec/**,/api/spu/**,/api/stockBack/**,/api/template/** filters: - StripPrefix=1 - id: ydles_user_route uri: lb://user predicates: - Path=/api/user/**,/api/address/**,/api/areas/**,/api/cities/**,/api/provinces/** filters: - StripPrefix=1 - id: ydles_oauth_user uri: lb://user-auth predicates: - Path=/api/oauth/** filters: - StripPrefix=1 - id: ydles_order_route uri: lb://order predicates: - Path=/api/cart/**,/api/categoryReport/**,/api/orderConfig/**,/api/order/**,/api/orderItem/**,/api/orderLog/**,/api/preferential/**,/api/returnCause/**,/api/returnOrder/**,/api/returnOrderItem/** filters: - StripPrefix=1 - id: ydles_order_web_route uri: lb://order-web predicates: - Path=/api/wcart/**,/api/worder/** filters: - StripPrefix=1
先登录 Postman访问 http://localhost:8001/api/oauth/loginopen in new window
再请求数据 Postman访问 http://localhost:8001/api/address/listopen in new window
image-20211205121817519 1.3 页面模板渲染 image-20211205120105277 购物车这块也使用的是模板渲染,用户先请求经过微服务网关,微服务网关转发到订单购物车模板渲染服务,模板渲染服务条用用户微服务和订单购物车微服务查询用户收件地址和购物车清单,然后到页面显示。
1.3.1 准备工作 (1)静态资源导入
将资料中的order.html
拷贝到ydles-web-order
工程的templates中。
image-20211206091251118 (2)页面跳转实现
在ydles-web-order中创建com.ydles.order.controller.OrderController
实现页面跳转,代码如下:
From: 元动力 1 2 3 4 5 6 7 8 @Controller @RequestMapping("/worder") public class OrderController { @RequestMapping("/ready/order") public String readyOrder (Model model) { return "order" ; } }
(3)网关配置
修改ydles-gateway-web的application.yml文件,将订单的路由过滤地址添加上去,代码如下:
image-20211206091646934
From: 元动力 1 2 3 4 5 6 7 - id: ydles_order_web_route uri: lb://order-web predicates: - Path=/api/wcart/**,/api/worder/** filters: - StripPrefix=1
同时不要忘了把该地址添加到登录过滤地址中,修改com.ydles.filter.URLFilter
,在orderFilterPath里添加/api/worder/**
过滤,代码如下:
image-20211206091746759 1.3.2 信息查询-重点 需求:
因为一会儿要调用ydles-service-user查询用户的收件地址信息,调用ydles-service-order查询购物车清单信息,所以我们需要创建Feign。购物车的Feign之前已经创建过了,所以只需要创建用户地址相关的即可。
(1)用户地址信息查询
在ydles-service-user-api中创建AddressFeign,代码如下:
From: 元动力 1 2 3 4 5 6 7 8 9 10 @FeignClient(name="user") public interface AddressFeign { @GetMapping(value = "/list") public Result<List<Address>> list(); }
(2) ydles_web_order项目启动类OrderWebApplication加入调用的feign包
From: 元动力 1 2 3 4 5 < dependency> < groupId> com.ydles< /groupId> < artifactId> ydles_service_user_api< /artifactId> < version> 1.0-SNAPSHOT< /version> < /dependency>
From: 元动力 1 2 3 4 @SpringBootApplication @EnableEurekaClient @EnableFeignClients(basePackages = {"com.ydles.order.feign","com.ydles.user.feign"}) public class OrderWebApplication {
(3)查询购物车和用户收件地址信息
修改ydles-web-order中的com.ydles.order.controller.OrderController
的readyOrder方法,在该方法中,使用feign调用查询收件地址信息和用户购物车信息,代码如下:
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @Controller @RequestMapping("/worder") public class OrderController { @Autowired private AddressFeign addressFeign; @Autowired private CartFeign cartFeign; @RequestMapping("/ready/order") public String readyOrder (Model model) { List<Address> addressList = addressFeign.list().getData(); model.addAttribute("address" ,addressList); Map map = cartFeign.list(); List<OrderItem> orderItemList = (List<OrderItem>) map.get("orderItemList" ); Integer totalMoney = (Integer) map.get("totalMoney" ); Integer totalNum = (Integer) map.get("totalNum" ); model.addAttribute("carts" ,orderItemList); model.addAttribute("totalMoney" ,totalMoney); model.addAttribute("totalNum" ,totalNum); return "order" ; } }
(3)数据回显-了解
修改order.html,与静态原型中的order.html打开对比。
From: 元动力 1 < html xmlns:th="http://www.thymeleaf.org">
From: 元动力 1 < div class="cart py-container" id="app">
From: 元动力 1 2 3 4 5 6 7 8 9 < div class="choose-address" th:each="addr:${address}"> < div class="con name " th:@click="|chooseAddr('${addr.contact}','${addr.phone}','${addr.address}')|" th:classappend="${addr.isDefault}==1?'selected':''"> < a href="javascript:;" > < em th:text="${addr.contact}"> < /em> < span title="点击取消选择"> < /span> < /a> < /div> < div class="con address"> < span class="place"> < em th:text="${addr.address}"> < /em> < /span> < span class="phone"> < em th:text="${addr.phone}"> < /em> < /span> < span class="base" th:if="${addr.isDefault}==1"> 默认地址< /span> < /div> < div class="clearfix"> < /div> < /div>
From: 元动力 1 2 3 4 < ul class="payType"> < li class="selected" th:@click="|order.payType=1|"> 在线支付< span title="点击取消选择"> < /span> < /li> < li th:@click="|order.payType=0|"> 货到付款< span title="点击取消选择"> < /span> < /li> < /ul>
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 < ul class="yui3-g" th:each="cart,cartsList:${carts}"> < li class="yui3-u-1-9"> < span> < img th:src="${cart.image}"/> < /span> < /li> < li class="yui3-u-5-12"> < div class="desc" th:text="${cart.name}"> < /div> < div class="seven"> 7天无理由退货< /div> < /li> < li class="yui3-u-1-12"> < div class="price" th:text="${cart.price}"> < /div> < /li> < li class="yui3-u-1-12"> < div class="num" th:text="${cart.num}"> < /div> < /li> < li class="yui3-u-1-12"> < div class="num" th:text="${cart.num}*${cart.price}"> < /div> < /li> < li class="yui3-u-1-12"> < div class="exit"> 有货< /div> < /li> < /ul>
From: 元动力 1 2 3 4 5 6 < div class="fc-receiverInfo"> 寄送至: < span id="receive-address"> {{order.receiveAddress}}< /span> 收货人:< span id="receive-name"> {{order.receiveContact}}< /span> < span id="receive-phone"> {{order.receiveMobile}}< /span> < /div>
From: 元动力 1 2 3 4 < div class="list"> < span> < i class="number"> < em th:text="${totalNum}"> < /em> < /i> 件商品,商品总金额< /span> < em class="allprice"> ¥< em th:text="${totalMoney}"> < /em> < /em> < /div>
From: 元动力 1 2 3 < div class="clearfix trade"> < div class="fc-price"> 应付金额: < span class="final-price"> ¥< em th:text="${totalMoney}"> < /em> < /span> < /div> < /div>
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 < script th:inline="javascript"> var app = new Vue({ el:"#app", data:{ order:{ 'receiveContact': [[${deAddr.contact}]], 'receiveMobile': [[${deAddr.phone}]], 'receiveAddress': [[${deAddr.address}]], 'payType':1 } }, methods:{ chooseAddr: function (contact,mobile,address){ app.$set(app.order,'receiveContact',contact); app.$set(app.order,'receiveMobile',mobile); app.$set(app.order,'receiveAddress',address); } } })< /script>
测试:先登录,再访问
http://localhost:8001/api/worder/ready/orderopen in new window
image-20211206114046064 1.3.3 记录选中收件人-了解 收件人动态展示
From: 元动力 1 73 <div class="cart py-container" id="app">
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 < script th:inline="javascript"> var app = new Vue({ el:"#app", data:{ order:{'receiveContact':[[${deAddr.contact}]],'receiveMobile':[[${deAddr.phone}]],'receiveAddress':[[${deAddr.address}]],'payType':1} }, methods:{ chooseAddr:function (contact,mobile,address) { app.$set(app.order,'receiveContact',contact); app.$set(app.order,'receiveMobile',mobile); app.$set(app.order,'receiveAddress',address); }, add:function () { axios.post('/api/worder/add',this.order).then(function (response) { if (response.data.flag){ //添加订单成功 alert("添加订单成功"); } else{ alert("添加订单失败"); } }) } } })< /script>
From: 元动力 1 2 3 4 5 6 7 8 9 < div class="clearfix trade"> < div class="fc-price"> 应付金额: < span class="price"> ¥< em th:text="${totalMoney}"> < /em> < /span> < /div> < div class="fc-receiverInfo"> 寄送至: < span id="receive-address"> {{order.receiveAddress}}< /span> 收货人:< span id="receive-name"> {{order.receiveContact}}< /span> < span id="receive-phone"> {{order.receiveMobile}}< /span> < /div> < /div>
后端设置默认收件人
From: 元动力 1 2 3 4 5 6 7 8 for (Address address : addressList) { if ("1" .equals(address.getIsDefault())){ model.addAttribute("deAddr" ,address); break ; } }
前端:
From: 元动力 1 order:{'receiveContact':[[${deAddr.contact}]],'receiveMobile':[[${deAddr.phone}]],'receiveAddress':[[${deAddr.address}]],'payType':1}
支付方式
From: 元动力 1 2 3 4 5 6 < div class="step-cont"> < ul class="payType"> < li class="selected" th:@click="|order.payType=1|"> 在线支付< span title="点击取消选择"> < /span> < /li> < li th:@click="|order.payType=0|"> 货到付款< span title="点击取消选择"> < /span> < /li> < /ul> < /div>
购物车跳转结算页面
cart.html
From: 元动力 1 < a class="sum-btn" href="/api/worder/ready/order" target="_blank"> 结算< /a>
2 下单-重点 2.1 业务分析 点击提交订单的时候,会立即创建订单数据,创建订单数据会将数据存入到2张表中,分别是订单表和订单明细表,此处还需要修改商品对应的库存数量。
image-20211206121803107 订单表结构如下:
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 CREATE TABLE `tb_order` ( `id` varchar (50 ) COLLATE utf8_bin NOT NULL COMMENT '订单id' , `total_num` int (11 ) DEFAULT NULL COMMENT '数量合计' , `total_money` int (11 ) DEFAULT NULL COMMENT '金额合计' , `pre_money` int (11 ) DEFAULT NULL COMMENT '优惠金额' , `post_fee` int (11 ) DEFAULT NULL COMMENT '邮费' , `pay_money` int (11 ) DEFAULT NULL COMMENT '实付金额' , `pay_type` varchar (1 ) COLLATE utf8_bin DEFAULT NULL COMMENT '支付类型,1、在线支付、0 货到付款' , `create_time` datetime DEFAULT NULL COMMENT '订单创建时间' , `update_time` datetime DEFAULT NULL COMMENT '订单更新时间' , `pay_time` datetime DEFAULT NULL COMMENT '付款时间' , `consign_time` datetime DEFAULT NULL COMMENT '发货时间' , `end_time` datetime DEFAULT NULL COMMENT '交易完成时间' , `close_time` datetime DEFAULT NULL COMMENT '交易关闭时间' , `shipping_name` varchar (20 ) COLLATE utf8_bin DEFAULT NULL COMMENT '物流名称' , `shipping_code` varchar (20 ) COLLATE utf8_bin DEFAULT NULL COMMENT '物流单号' , `username` varchar (50 ) COLLATE utf8_bin DEFAULT NULL COMMENT '用户名称' , `buyer_message` varchar (1000 ) COLLATE utf8_bin DEFAULT NULL COMMENT '买家留言' , `buyer_rate` char (1 ) COLLATE utf8_bin DEFAULT NULL COMMENT '是否评价' , `receiver_contact` varchar (50 ) COLLATE utf8_bin DEFAULT NULL COMMENT '收货人' , `receiver_mobile` varchar (12 ) COLLATE utf8_bin DEFAULT NULL COMMENT '收货人手机' , `receiver_address` varchar (200 ) COLLATE utf8_bin DEFAULT NULL COMMENT '收货人地址' , `source_type` char (1 ) COLLATE utf8_bin DEFAULT NULL COMMENT '订单来源:1:web,2:app,3:微信公众号,4:微信小程序 5 H5手机页面' , `transaction_id` varchar (30 ) COLLATE utf8_bin DEFAULT NULL COMMENT '交易流水号' , `order_status` char (1 ) COLLATE utf8_bin DEFAULT NULL COMMENT '订单状态,0:未完成,1:已完成,2:已退货' , `pay_status` char (1 ) COLLATE utf8_bin DEFAULT NULL COMMENT '支付状态,0:未支付,1:已支付,2:支付失败' , `consign_status` char (1 ) COLLATE utf8_bin DEFAULT NULL COMMENT '发货状态,0:未发货,1:已发货,2:已收货' , `is_delete` char (1 ) COLLATE utf8_bin DEFAULT NULL COMMENT '是否删除' , PRIMARY KEY (`id`), KEY `create_time` (`create_time`), KEY `status` (`order_status`), KEY `payment_type` (`pay_type`) ) ENGINE= InnoDB DEFAULT CHARSET= utf8 COLLATE = utf8_bin;
订单明细表结构如下:
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 CREATE TABLE `tb_order_item` ( `id` varchar (50 ) COLLATE utf8_bin NOT NULL COMMENT 'ID' , `category_id1` int (11 ) DEFAULT NULL COMMENT '1级分类' , `category_id2` int (11 ) DEFAULT NULL COMMENT '2级分类' , `category_id3` int (11 ) DEFAULT NULL COMMENT '3级分类' , `spu_id` varchar (20 ) COLLATE utf8_bin DEFAULT NULL COMMENT 'SPU_ID' , `sku_id` bigint (20 ) NOT NULL COMMENT 'SKU_ID' , `order_id` bigint (20 ) NOT NULL COMMENT '订单ID' , `name` varchar (200 ) COLLATE utf8_bin DEFAULT NULL COMMENT '商品名称' , `price` int (20 ) DEFAULT NULL COMMENT '单价' , `num` int (10 ) DEFAULT NULL COMMENT '数量' , `money` int (20 ) DEFAULT NULL COMMENT '总金额' , `pay_money` int (11 ) DEFAULT NULL COMMENT '实付金额' , `image` varchar (200 ) COLLATE utf8_bin DEFAULT NULL COMMENT '图片地址' , `weight` int (11 ) DEFAULT NULL COMMENT '重量' , `post_fee` int (11 ) DEFAULT NULL COMMENT '运费' , `is_return` char (1 ) COLLATE utf8_bin DEFAULT NULL COMMENT '是否退货,0:未退货,1:已退货' , PRIMARY KEY (`id`), KEY `item_id` (`sku_id`), KEY `order_id` (`order_id`) ) ENGINE= InnoDB DEFAULT CHARSET= utf8 COLLATE = utf8_bin;
2.2 下单实现 下单的时候,先往tb_order表中增加数据,再往tb_order_item表中增加数据。
2.2.1 代码实现 这里先修改ydles-service-order微服务,实现下单操作,这里会生成订单号,我们首先需要在启动类中创建一个IdWorker对象。
在com.ydles.OrderApplication
中创建IdWorker,代码如下:
From: 元动力 1 2 3 4 @Bean public IdWorker idWorker () { return new IdWorker (workerId, datacenterId); }
(1)业务层
实现逻辑:
1)获取所有购物车
2)统计计算:总金额,总支付金额,总数量
3)填充订单数据并保存
4)获取每一个购物项保存到orderItem
5)删除购物车中数据
修改订单微服务添加com.ydles.order.service.impl.OrderServiceImpl,代码如下:
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 @Override public boolean add (Order order) { Map cartMap = cartService.list(order.getUsername()); Integer totalNum = (Integer) cartMap.get("totalNum" ); Integer totalMoney = (Integer) cartMap.get("totalMoney" ); String orderId = idWorker.nextId()+"" ; order.setId(orderId); order.setTotalNum(totalNum); order.setTotalMoney(totalMoney); order.setPayMoney(totalMoney); order.setCreateTime(new Date ()); order.setUpdateTime(new Date ()); order.setBuyerRate("0" ); order.setSourceType("1" ); order.setOrderStatus("0" ); order.setPayStatus("0" ); order.setConsignStatus("0" ); order.setIsDelete("0" ); orderMapper.insertSelective(order); List<OrderItem> orderItemList = (List<OrderItem>) cartMap.get("orderItemList" ); for (OrderItem orderItem : orderItemList) { orderItem.setOrderId(orderId); orderItem.setPostFee(0 ); orderItem.setIsReturn("0" ); orderItemMapper.insertSelective(orderItem); } stringRedisTemplate.delete(CART+order.getUsername()); return true ; }
(2)控制层
修改ydles-service-order微服务,修改com.ydles.order.controller.OrderController类,代码如下:
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Autowired TokenDecode tokenDecode;@PostMapping public Result add (@RequestBody Order order) { String username = tokenDecode.getUserInfo().get("username" ); order.setUsername(username); orderService.add(order); return new Result (true ,StatusCode.OK,"添加成功" ); }
2.2.2 渲染服务对接 image-20211206142024505 我们需要在模板渲染端调用订单微服务实现下单操作,下单操作需要调用订单微服务,所以需要创建对应的Feign。
(1)Feign创建
修改ydles-service-order-api,添加OrderFeign,代码如下:
From: 元动力 1 2 3 4 5 6 @FeignClient(name = "order") public interface OrderFeign { @PostMapping("/order") public Result add (@RequestBody Order order) ; }
(2)下单调用
修改ydles-web-order的com.ydles.order.controller.OrderController
添加下单方法,代码如下:
From: 元动力 1 2 3 4 5 6 7 8 9 @Autowired private OrderFeign orderFeign;@PostMapping("/add") @ResponseBody public Result add (@RequestBody Order order) { Result result = orderFeign.add(order); return result; }
(3)页面调用
order.html
From: 元动力 1 2 3 4 5 6 7 8 9 10 add :function ( ) { axios.post ('/api/worder/add' ,this .order ).then (function (response ) { if (response.data .flag ){ alert ("添加订单成功" ); } else { alert ("添加订单失败" ); } }) }
From: 元动力 1 < a class="sui-btn btn-danger btn-xlarge" href="javascript:void(0)" @click="add()"> 提交订单< /a>
点击提交订单调用
保存订单测试,表数据变化如下:
tb_order表数据:
image-20211206143109177 tb_order_item表数据:
image-20211206143122426 2.3 库存变更 2.3.1 业务分析 上面操作只实现了下单操作,但对应的库存还没跟着一起减少,我们在下单之后,应该调用商品微服务,将下单的商品库存减少,销量增加。每次订单微服务只需要将用户名传到商品微服务,商品微服务通过用户名到Redis中查询对应的购物车数据,然后执行库存减少,库存减少需要控制当前商品库存>=销售数量。
如何控制库存数量>=购买数量呢?其实可以通过SQL语句实现,每次减少数量之前,加个条件判断。
where num>=#{num}
即可。
商品服务需要查询购物车数据,所以需要引入订单的api,在pom.xml中添加如下依赖:
From: 元动力 1 2 3 4 5 6 < !--order api 依赖--> < dependency> < groupId> com.ydles< /groupId> < artifactId> ydles_service_order_api< /artifactId> < version> 1.0-SNAPSHOT< /version> < /dependency>
2.3.2 代码实现 要调用其他微服务,需要将头文件中的令牌数据携带到其他微服务中取,所以我们不能使用hystrix的多线程模式,修改ydles-service-order的applicatin.yml配置,代码如下:
From: 元动力 1 2 3 4 5 6 7 8 9 hystrix : command : default : execution : isolation : thread : timeoutInMilliseconds : 10000 strategy : SEMAPHORE
每次还需要使用拦截器添加头文件信息,添加拦截器,代码如下:
(1)Dao层
修改ydles-service-goods微服务的com.ydles.goods.dao.SkuMapper
接口,增加库存递减方法,代码如下:
From: 元动力 1 2 3 4 5 6 7 @Update("UPDATE tb_sku SET num=num-#{num},sale_num=sale_num+#{num} WHERE id=#{skuId} AND num>=#{num}") int decrCount (OrderItem orderItem) ;
(2)业务层
修改ydles-service-goods微服务的com.ydles.goods.service.SkuService
接口,添加如下方法:
From: 元动力 1 2 3 4 5 void decrCount (String username) ;
修改ydles-service-goods微服务的com.ydles.goods.service.impl.SkuServiceImpl
实现类,添加一个实现方法,代码如下:
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Autowired private RedisTemplate redisTemplate;@Override public void decrCount (String username) { List<OrderItem> orderItems = redisTemplate.boundHashOps("Cart_" + username).values(); for (OrderItem orderItem : orderItems) { int count = skuMapper.decrCount(orderItem); if (count<=0 ){ throw new RuntimeException ("库存不足,递减失败!" ); } } }
(3)控制层
修改ydles-service-goods的com.ydles.goods.controller.SkuController
类,添加库存递减方法,代码如下:
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 @PostMapping(value = "/decr/count") public Result decrCount (@RequestParam("username") String username) { skuService.decrCount(username); return new Result (true ,StatusCode.OK,"库存递减成功!" ); }
(4)创建feign
同时在ydles-service-goods-api工程添加com.ydles.goods.feign.SkuFeign
的实现,代码如下:
From: 元动力 1 2 3 4 5 6 7 @PostMapping(value = "/decr/count") Result decrCount (@RequestParam(value = "username") String username) ;
2.3.3 调用库存递减 需求:
ydles_service_order服务调用 ydles_service_goods服务, ydles_web_order服务的Application启动类
都需要添加下面拦截器
From: 元动力 1 2 3 4 @Bean public FeignInterceptor feignInterceptor () { return new FeignInterceptor (); }
ydles_service_goods加入配置信息
From: 元动力 1 2 3 4 5 spring: application: name: goods redis: host: 192.168 .200 .128
调用库存递减
修改ydles-service-order微服务的com.ydles.order.service.impl.OrderServiceImpl类的add方法,增加库存递减的调用。
先注入SkuFeign
From: 元动力 1 2 @Autowired private SkuFeign skuFeign;
再调用库存递减方法
From: 元动力 1 2 skuFeign.decrCount(order.getUsername());
完整代码如下:
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 @Override public Boolean add (Order order) { Map orderItemMap = cartService.list(order.getUsername()); Integer totalMoney = Integer.parseInt(String.valueOf(orderItemMap.get("totalPrice" ))); Integer num = Integer.parseInt(String.valueOf(orderItemMap.get("totalNum" ))); order.setTotalNum(num); order.setTotalMoney(totalMoney); order.setPayMoney(totalMoney); order.setPreMoney(totalMoney); order.setCreateTime(new Date ()); order.setUpdateTime(order.getCreateTime()); order.setBuyerRate("0" ); order.setSourceType("1" ); order.setOrderStatus("0" ); order.setPayStatus("0" ); order.setConsignStatus("0" ); order.setId(idWorker.nextId()); int count = orderMapper.insertSelective(order); redisTemplate.boundValueOps(Constants.ORDER_PAY + order.getUsername()).set(order); List<OrderItem> orderItemList = (List<OrderItem>)orderItemMap.get("orderItemList" ); for (OrderItem orderItem : orderItemList) { orderItem.setId(idWorker.nextId()); orderItem.setIsReturn("0" ); orderItem.setOrderId(order.getId()); orderItemMapper.insertSelective(orderItem); } skuFeign.decrCount(order.getUsername()); redisTemplate.delete("Cart_" +order.getUsername()); return true ; }
测试 1购物车添加商品
http://localhost:8001/api/wcart/add?skuId=1450862568724758528&num=5open in new window
image-20211206152606093 2查看购物车
http://localhost:8001/api/wcart/listopen in new window
image-20211206152627024 3点击结算
image-20211206152646505 image-20211206152654362 4点击提交订单
image-20211206152743660 tb_order 有数据
image-20211206152848108 tb_order_item也有数据
image-20211206152906228 tb_sku表查看库存和销量是否更改
image-20211206152831101 库存减少前,查询数据库Sku数据如下:95库存,5销量 库存94,销量6
image-20211206152957078 2.4 增加积分(学员练习) tb_user表
From: 元动力 1 `points` int(11) DEFAULT NULL COMMENT '积分',
需求:
在增加订单的时候,同时添加用户积分。与减库存道理相同。
(1)dao层
修改ydles-service-user微服务的com.ydles.user.dao.UserMapper
接口,增加用户积分方法,代码如下:
From: 元动力 1 2 3 4 5 6 7 8 @Update("UPDATE tb_user SET points=points+#{point} WHERE username=#{username}") int addUserPoints (@Param("username") String username, @Param("point") Integer pint) ;
(2)业务层
修改ydles-service-user微服务的com.ydles.user.service.UserService
接口,代码如下:
From: 元动力 1 2 3 4 5 6 7 int addUserPoints (String username,Integer pint) ;
修改ydles-service-user微服务的com.ydles.user.service.impl.UserServiceImpl
,增加添加积分方法实现,代码如下:
From: 元动力 1 2 3 4 5 6 7 8 9 10 @Override public int addUserPoints (String username, Integer pint) { return userMapper.addUserPoints(username,pint); }
(3)控制层
修改ydles-service-user微服务的com.ydles.user.controller.UserController
,添加增加用户积分方法,代码如下:
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Autowired private TokenDecode tokenDecode;@GetMapping(value = "/points/add") public Result addPoints (Integer points) { Map<String, String> userMap = tokenDecode.getUserInfo(); String username = userMap.get("username" ); userService.addUserPoints(username,points); return new Result (true ,StatusCode.OK,"添加积分成功!" ); }
(4)Feign添加
修改ydles-service-user-api工程,修改com.ydles.user.feign.UserFeign
,添加增加用户积分方法,代码如下:
From: 元动力 1 2 3 4 5 6 7 @GetMapping(value = "/points/add") Result addPoints (@RequestParam(value = "points") Integer points) ;
4.4.2 增加积分调用 修改ydles-service-order,添加ydles-service-user-api的依赖,修改pom.xml,添加如下依赖:
From: 元动力 1 2 3 4 5 6 < !--user api 依赖--> < dependency> < groupId> com.ydles< /groupId> < artifactId> ydles-service-user-api< /artifactId> < version> 1.0-SNAPSHOT< /version> < /dependency>
在加订单的时候,同时添加用户积分,修改ydles-service-order微服务的com.ydles.order.service.impl.OrderServiceImpl
下单方法,增加调用添加积分方法
修改ydles-service-order的启动类com.ydles.OrderApplication
,添加feign的包路径
总结:
1订单结算页
image-20211206153405594 image-20211206153523449 image-20211206153555752 2下单
image-20211206153608375 image-20211206153617157 第12章 分布式事务解决方案 角色 :架构师
学习目标: 能够说出cap定理 能够说出BASE定理 能够说出常见的分布式事务解决方案 能够说出seata框架如何在项目中实现分布式事务 1.分布式事务解决方案 刚才我们编写的扣减库存与保存订单是在两个服务中存在的,如果扣减库存后订单保存失败了是不会回滚的,这样就会造成数据不一致的情况,这其实就是我们所说的分布式事务的问题,接下来我们来学习分布式事务的解决方案。
1.1 本地事务与分布式事务 1.1.1 事务 数据库事务(简称:事务,Transaction)是指数据库执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。
事务拥有以下四个特性 ,习惯上被称为ACID 特性:
原子性(Atomicity) :事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。
一致性(Consistency) :事务应确保数据库的状态从一个一致状态转变为另一个一致状态。一致状态是指数据库中的数据应满足完整性约束。除此之外,一致性还有另外一层语义,就是事务的中间状态不能被观察到(这层语义也有说应该属于原子性)。
隔离性(Isolation) :多个事务并发执行时,一个事务的执行不应影响其他事务的执行,如同只有这一个操作在被数据库所执行一样。
持久性(Durability) :已被提交的事务对数据库的修改应该永久保存在数据库中。在事务结束时,此操作将不可逆转。
image-20211208154640726 作业:隔离级别 传播机制
1.1.2 本地事务 起初,事务仅限于对单一数据库资源的访问控制,架构服务化以后,事务的概念延伸到了服务中。倘若将一个单一的服务操作作为一个事务,那么整个服务操作只能涉及一个单一的数据库资源,这类基于单个服务单一数据库资源访问的事务,被称为本地事务(Local Transaction)。
解决方案:@Transactional
image-20211208155006738 1.1.3 分布式事务 分布式事务指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上,且属于不同的应用,分布式事务需要保证这些操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。
最早的分布式事务应用架构很简单,不涉及服务间的访问调用,仅仅是服务内操作涉及到对多个数据库资源的访问。
image-20211208155238640 当一个服务操作访问不同的数据库资源,又希望对它们的访问具有事务特性时,就需要采用分布式事务来协调所有的事务参与者。
对于上面介绍的分布式事务应用架构,尽管一个服务操作会访问多个数据库资源,但是毕竟整个事务还是控制在单一服务的内部。如果一个服务操作需要调用另外一个服务,这时的事务就需要跨越多个服务了。在这种情况下,起始于某个服务的事务在调用另外一个服务的时候,需要以某种机制流转到另外一个服务,从而使被调用的服务访问的资源也自动加入到该事务当中来。下图反映了这样一个跨越多个服务的分布式事务:
image-20211208155459473 如果将上面这两种场景(一个服务可以调用多个数据库资源,也可以调用其他服务)结合在一起,对此进行延伸,整个分布式事务的参与者将会组成如下图所示的树形拓扑结构。在一个跨服务的分布式事务中,事务的发起者和提交均系同一个,它可以是整个调用的客户端,也可以是客户端最先调用的那个服务。
image-20211208155810435 较之基于单一数据库资源访问的本地事务,分布式事务的应用架构更为复杂。在不同的分布式应用架构下,实现一个分布式事务要考虑的问题并不完全一样,比如对多资源的协调、事务的跨服务传播等,实现机制也是复杂多变。
只要是涉及到多个微服务之间远程调用的话,那就回涉及到分布式事务。
分布式事务的作用:
1.2 分布式事务相关理论 1.2.1 CAP定理-重点 CAP定理是在 1998年加州大学的计算机科学家 Eric Brewer (埃里克.布鲁尔)提出,分布式系统有三个指标
Consistency 强一致性 Availability 可用性 Partition tolerance 分区容错 它们的第一个字母分别是 C、A、P。Eric Brewer 说,这三个指标不可能同时做到。这个结论就叫做 CAP 定理。
因为:强一致性和可用性 互斥!
image-20211208164142661 真实情况 :
1 ac 传统项目。ssm管理系统,一个程序,一个数据库。
2 cp 信息重要的场景。手机银行转账。转圈圈,在做数据同步,这段时间内,可用性是没有的。
3 ap 互联网。 放弃强一致性,慢慢数据同步。
image-20211208164958537 分区容错 Partition tolerance 理解: 分布式系统集群中, 一个机器坏掉不应该影响其他机器
大多数分布式系统都分布在多个子网络。每个子网络就叫做一个区(partition)。分区容错的意思是,区间通信可能失败。比如,一台服务器放在中国,另一台服务器放在美国,这就是两个区,它们之间可能无法通信。
上图中,G1 和 G2 是两台跨区的服务器。G1 向 G2 发送一条消息,G2 可能无法收到。系统设计的时候,必须考虑到这种情况。
一般来说,分区容错无法避免,因此可以认为 CAP 的 P 总是成立。CAP 定理告诉我们,剩下的 C 和 A 无法同时做到。
可用性 Availability 理解: 一个请求, 必须返回一个响应
Availability 中文叫做"可用性",意思是只要收到用户的请求,服务器就必须给出回应。
用户可以选择向 G1 或 G2 发起读操作。不管是哪台服务器,只要收到请求,就必须告诉用户,到底是 v0 还是 v1,否则就不满足可用性。
一致性 Consistency 理解: 一定能读取到最新的数据
Consistency 中文叫做"一致性"。意思是,写操作之后的读操作,必须返回该值。
举例来说,某条记录是 v0,用户向 G1 发起一个写操作,将其改为 v1。
问题是,用户有可能向 G2 发起读操作,由于 G2 的值没有发生变化,因此返回的是 v0。G1 和 G2 读操作的结果不一致,这就不满足一致性了。
为了让 G2 也能变为 v1,就要在 G1 写操作的时候,让 G1 向 G2 发送一条消息,要求 G2 也改成 v1。
一致性和可用性的矛盾 一致性(C)和可用性(A),为什么不可能同时成立?答案很简单,因为可能通信失败(即出现分区容错)。
如果保证 G2 的一致性,那么 G1 必须在写操作时,锁定 G2 的读操作和写操作。只有数据同步后,才能重新开放读写。锁定期间,G2 不能读写,没有可用性(CP)。
如果保证 G2 的可用性,那么势必不能锁定 G2,所以一致性不成立(AP)。
综上所述,G2 无法同时做到一致性和可用性。系统设计时只能选择一个目标。如果追求一致性,那么无法保证所有节点的可用性;如果追求所有节点的可用性,那就没法做到一致性。
1.2.2 BASE理论 BASE:全称:Basically Available(基本可用),Soft state(软状态),和 Eventually consistent(最终一致性)三个短语的缩写,来自 ebay 的架构师提出。BASE 理论是对 CAP 中一致性和可用性权衡的结果,其来源于对大型互联网分布式实践的总结,是基于 CAP 定理逐步演化而来的。其核心思想是:
既是无法做到强一致性(Strong consistency),但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)。
Basically Available(基本可用) 理解: 允许服务降级或者允许响应时间受到一定损失
什么是基本可用呢?假设系统,出现了不可预知的故障,但还是能用,相比较正常的系统而言:
响应时间上的损失:正常情况下的搜索引擎 0.5 秒即返回给用户结果,而基本可用 的搜索引擎可以在 1 秒作用返回结果。 功能上的损失:在一个电商网站上,正常情况下,用户可以顺利完成每一笔订单,但是到了大促期间,为了保护购物系统的稳定性,部分消费者可能会被引导到一个降级页面。 Soft state(软状态) 理解: 允许同步数据的时候出现一定时间延迟
什么是软状态呢?相对于原子性而言,要求多个节点的数据副本都是一致的,这是一种 “硬状态”。
软状态指的是:允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性,即允许系统在多个不同节点的数据副本存在数据延时。
Eventually consistent(最终一致性) **理解: 经过一段时间的同步数据之后,最终都能够达到一个一致的状态 **
系统能够保证在没有其他新的更新操作的情况下,数据最终一定能够达到一致的状态,因此所有客户端对系统的数据访问最终都能够获取到最新的值。
1.3 分布式事务解决方案-面试 1.XA两段提交(低效率)-2PC
2.TCC三段提交(3段,高效率[不推荐(补偿代码)])
3.本地消息表(MQ+Table)
4.事务消息(RocketMQ[alibaba])
5.Seata(alibaba)
6.RabbitMQ的ACK机制实现分布式事务(作业)
1.3.1 基于XA协议的两阶段提交 2pc 首先我们来简要看下分布式事务处理的XA规范 :
可知XA规范中分布式事务有AP,RM,TM组成:
其中应用程序(Application Program ,简称AP):AP定义事务边界(定义事务开始和结束)并访问事务边界内的资源。
资源管理器(Resource Manager,简称RM):Rm管理计算机共享的资源,许多软件都可以去访问这些资源,资源包含比如数据库 、文件系统、打印机服务器等。
事务管理器(Transaction Manager ,简称TM):负责管理全局事务,分配事务唯一标识,监控事务的执行进度,并负责事务的提交、回滚、失败恢复等。
二阶段协议:
第一阶段 TM要求所有的RM准备提交对应的事务分支,询问RM是否有能力保证成功的提交事务分支,RM根据自己的情况,如果判断自己进行的工作可以被提交,那就对工作内容进行持久化,并给TM回执OK;否者给TM的回执NO。RM在发送了否定答复并回滚了已经的工作后,就可以丢弃这个事务分支信息了。
第二阶段 TM根据阶段1各个RM prepare的结果,决定是提交还是回滚事务。如果所有的RM都prepare成功,那么TM通知所有的RM进行提交;如果有RM prepare回执NO的话,则TM通知所有RM回滚自己的事务分支。
也就是TM与RM之间是通过两阶段提交协议进行交互的.
优点: 尽量保证了数据的强一致,适合对数据强一致要求很高的关键领域。(其实也不能100%保证强一致)
缺点: 实现复杂,牺牲了可用性,对性能影响较大,不适合高并发高性能场景。
流程看我的图 :
mysql
image-20211208174923396 悲观锁 mysql行级锁 oracle表级锁
两阶段提交协议(Two Phase Commitment Protocol)中,涉及到两种角色
一个事务协调者(coordinator):负责协调多个参与者进行事务投票及提交(回滚) 多个事务参与者(participants):即本地事务执行者
总共处理步骤有两个 (1)投票阶段 (voting phase):协调者将通知事务参与者准备提交或取消事务,然后进入表决过程。参与者将告知协调者自己的决策:同意(事务参与者本地事务执行成功,但未提交)或取消(本地事务执行故障); (2)提交阶段 (commit phase):收到参与者的通知后,协调者再向参与者发出通知,根据反馈情况决定各参与者是否要提交还是回滚;
1.3.2 TCC补偿机制 TCC 其实就是采用的补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。它分为三个阶段:
Try 阶段主要是对业务系统做检测及资源预留 Confirm 阶段主要是对业务系统做确认提交,Try阶段执行成功并开始执行 Confirm阶段时,默认 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。 Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。 例如: A要向 B 转账,思路大概是:
From: 元动力 1 2 3 4 我们有一个本地方法,里面依次调用 1、首先在 Try 阶段,要先调用远程接口把 B和 A的钱给冻结起来。 2、在 Confirm 阶段,执行远程调用的转账的操作,转账成功进行解冻。 3、如果第2步执行成功,那么转账成功,如果第二步执行失败,则调用远程冻结接口对应的解冻方法 (Cancel)。
优点: 相比两阶段提交,可用性比较强
缺点: 数据的一致性要差一些。TCC属于应用层的一种补偿方式,所以需要程序员在实现的时候多写很多补偿的代码,在一些场景中,一些业务流程可能用TCC不太好定义及处理。
流程看我的图:
image-20211208180127771 1.3.3 消息最终一致性-重点 消息最终一致性应该是业界使用最多的,其核心思想是将分布式事务拆分成本地事务进行处理,这种思路是来源于ebay。我们可以从下面的流程图中看出其中的一些细节:
基本思路就是:
消息生产方,需要额外建一个消息表,并记录消息发送状态。消息表和业务数据要在一个事务里提交,也就是说他们要在一个数据库里面。然后消息会经过MQ发送到消息的消费方。如果消息发送失败,会进行重试发送。
消息消费方,需要处理这个消息,并完成自己的业务逻辑。此时如果本地事务处理成功,表明已经处理成功了,如果处理失败,那么就会重试执行。如果是业务上面的失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚等操作。
生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。如果有靠谱的自动对账补账逻辑,这种方案还是非常实用的。
优点: 一种非常经典的实现,避免了分布式事务,实现了最终一致性。
缺点: 消息表会耦合到业务系统中,如果没有封装好的解决方案,会有很多杂活需要处理。
流程看我的图:
需求:下单同时,减库存
参与者A 订单服务
image-20211208221414338 1、订单服务
1.1 订单表插入数据,订单消息表添加数据101。本地事务,可以控制住。
1.2 定时任务:扫描订单消息表---》mq发一条消息,订单信息
2、商品服务
2.1监听消息队列,收到消息订单详情。
2.2sku 减库存,接受到商品订单的消息表。本地事务,可以控制住。
2.3 定时任务:扫描接受到商品订单的消息表---》mq发一条消息,我已经减库存了
2.4 mq发一条消息 接受到商品订单的消息表 101 staus 2 已经发了消息了
3、订单服务
3.1监听消息队列(已经减库存了) 订单消息表 删除
2. 分布式事务框架seata 2.1 seata简介 Seata(原名Fescar) 是阿里18年开源的分布式事务的框架。Fescar的开源对分布式事务框架领域影响很大。作为开源大户,Fescar来自阿里的GTS,经历了好几次双十一的考验,一经开源便颇受关注。后来Fescar改名为Seata。 https://github.com/seata/seataopen in new window
Fescar虽然是二阶段提交协议的分布式事务,但是其解决了XA的一些缺点:
单点问题:虽然目前Fescar(0.4.2)还是单server的,但是Fescar官方预计将会在0.5.x中推出HA-Cluster,到时候就可以解决单点问题。 同步阻塞:Fescar的二阶段,其再第一阶段的时候本地事务 就已经提交释放资源了,不会像XA会再两个prepare和commit阶段资源都锁住,并且Fescar,commit是异步操作 ,也是提升性能的一大关键。 数据不一致:如果出现部分commit失败,那么fescar-server会根据当前的事务模式和分支事务的返回状态的结果来进行不同的重试策略。并且fescar的本地事务会在一阶段的时候进行提交,其实单看数据库来说在commit的时候数据库已经是一致的了。 只能用于单一数据库: Fescar提供了三种模式,AT和TCC和混合模式 。在AT模式下事务资源可以是任何支持ACID的数据库,在TCC模式下事务资源没有限制,可以是缓存,可以是文件,可以是其他的等等。当然这两个模式也可以混用。 同时Fescar也保留了接近0业务入侵的优点,只需要简单的配置Fescar的数据代理和加个注解,加一个Undolog表,就可以达到我们想要的目的。
2.2 实现原理 Fescar将一个本地事务做为一个分布式事务分支,所以若干个分布在不同微服务中的本地事务共同组成了一个全局事务,结构如下。
官帮助文档: https://seata.io/zh-cn/index.htmlopen in new window
image-20200304154454364 TM:全局事务管理器,在标注开启fescar分布式事务的服务端开启,并将全局事务发送到TC事务控制端管理
TC:事务控制中心,控制全局事务的提交或者回滚。这个组件需要独立部署维护,目前只支持单机版本,后续迭代计划会有集群版本
RM:资源管理器,主要负责分支事务的上报,本地事务的管理
一段话简述其实现过程:服务起始方发起全局事务并注册到TC。在调用协同服务时,协同服务的事务分支事务会先完成阶段一的事务提交或回滚,并生成事务回滚的undo_log日志,同时注册当前协同服务到TC并上报其事务状态,归并到同一个业务的全局事务中。此时若没有问题继续下一个协同服务的调用,期间任何协同服务的分支事务回滚,都会通知到TC,TC在通知全局事务包含的所有已完成一阶段提交的分支事务回滚。如果所有分支事务都正常,最后回到全局事务发起方时,也会通知到TC,TC在通知全局事务包含的所有分支删除回滚日志。在这个过程中为了解决写隔离和度隔离的问题会涉及到TC管理的全局锁。
(增加订单(branchId=901),减库存(branchId=109)) xid=101
2.3 Fescar模式 Fescar对分布式事务的实现提供了3种模式,AT模式和TCC模式、saga模式:
2.3.1 AT模式 AT模式 :主要关注多 DB 访问的数据一致性,实现起来比较简单,对业务的侵入较小,但性能没有TCC高,这种模式推荐大家使用。
AT模式部分代码如下:不需要关注执行状态,对业务代码侵入较小。
From: 元动力 1 2 3 4 5 6 7 8 9 10 @GlobalTransactional(timeoutMills = 300000, name = "dubbo-demo-tx") public void purchase (String userId, String commodityCode, int orderCount) { LOGGER.info("purchase begin ... xid: " + RootContext.getXID()); storageService.deduct(commodityCode, orderCount); orderService.create(userId, commodityCode, orderCount); throw new RuntimeException ("AT 模式发生异常,回滚事务" ); }
AT模式的核心是对业务无侵入,是一种改进后的两阶段提交,其设计思路如图:
第一阶段:
核心在于对业务sql进行解析,转换成undolog,两阶段提交往往对资源的锁定需要持续到第二阶段实际的提交或者回滚操作,而有了回滚日志之后,可以在第一阶段释放对资源的锁定,降低了锁范围,提高效率,即使第二阶段发生异常需要回滚,只需找对undolog中对应数据并反解析成sql来达到回滚目的。Seata通过代理数据源将业务sql的执行解析成undolog来与业务数据的更新同时入库,达到了对业务无侵入的效果。
第二阶段:
如果决议是全局提交,此时分支事务此时已经完成提交,不需要同步协调处理(只需要异步清理回滚日志),Phase2 可以非常快速地完成。
如果决议是全局回滚,RM 收到协调器发来的回滚请求,通过 XID 和 Branch ID 找到相应的回滚日志记录,通过回滚记录生成反向的更新 SQL 并执行 ,以完成分支的回滚。
2.3.3 TCC模式 TCC模式 :TCC补偿机制,对代码造成一定的侵入,实现难度较大,这种方式不推荐,不过TCC模式的特点是性能高。
TCC模式部分代码如下:可以看到执行事务回滚,都需要根据不同阶段执行的状态判断,侵入了业务代码。
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @Override @GlobalTransactional public boolean transfer (final String from, final String to, final double amount) { boolean ret = firstTccAction.prepareMinus(null , from, amount); if (!ret){ throw new RuntimeException ("账号:[" +from+"] 预扣款失败" ); } ret = secondTccAction.prepareAdd(null , to, amount); if (!ret){ throw new RuntimeException ("账号:[" +to+"] 预收款失败" ); } System.out.println(String.format("transfer amount[%s] from [%s] to [%s] finish." , String.valueOf(amount), from, to)); return true ; }
2.3.3 Saga模式 详见 https://seata.io/zh-cn/docs/dev/mode/saga-mode.htmlopen in new window
3 Seata案例 3.1准备工作 1导入资料中的ydles_common_fescar。注意总父工程中加入模块
From: 元动力 1 <module>ydles_common_fescar</module>
2观察模块中的三个类。
3数据库ydles_order中的undo_log表为记录相关操作的表
image-20211209000144424 4资料中的fescar-server-0.4.2解压,bin目录中双击fescar-server.bat。注意 :这是fescar的服务,并且放到一个短目录才能执行。
image-20211209000404624 3.2分布式事务错误演示 1order服务 com.ydles.order.service.impl 的 add方法中增加一个错误 本地事务控制注解增加@Transactional
From: 元动力 1 2 3 4 5 6 7 8 //减库存 skuFeign.decrCount(order.getUsername()); int i=1/0; // 5)删除购物车中数据 redisTemplate.delete("cart_"+order.getUsername());
2goods 服务 com.ydles.goods.service.impl decrCount减库存方法上增加本地事务控制注解@Transactional
3查看数据库 tb_order tb_order_item中没数据。
image-20211209001159153 image-20211209001206890 tb_sku查看一条数据库存量 100
4登陆购物车并登陆 http://localhost:8001/api/wcart/listopen in new window
5往购物车添加数据 http://localhost:8001/api/wcart/add?skuId=1450862568724758528&num=5open in new window
6购物车页面点击结算到订单页面
image-20211209001242965 7点击提交订单
image-20211209001233900 8代码中 order服务报错
image-20211209001252770 9观察数据库
tb_order tb_order_item没变
image-20211209001159153 image-20211209001206890 tb_sku 1450862568724758528商品库存减少了5
image-20211209001328489 10 为什么
order goods服务是两个服务,即使加上@Transactional也是本地事务控制。
image-20211209001403817 3.3 分布式事务正确演示 1 goods order 增加fescar依赖
From: 元动力 1 2 3 4 5 <dependency> <groupId>com.ydles</groupId> <artifactId>ydles_common_fescar</artifactId> <version>1.0-SNAPSHOT</version> </dependency>
2在订单微服务的OrderServiceImpl的add方法上增加@GlobalTransactional(name = "order_add")注解
image-20211209001555985 我的购物车
image-20211209001721316 到结算页
image-20211209001737111 下单失败了
image-20211209001813785 3order 服务重启 观察 fescar控制台输出
4goods 服务重启 观察 fescar控制台输出
5重新提交订单 依然失败 但库存不扣减了
image-20211209001946242 4消息队列实现分布式事务---整个项目的重点 1业务流程-重点 需求:(一次下单---》用户积分增加一次) 事务
image-20211209011831937 1 order
1.1 order task.本地事务控制。
1.2 spring-task扫表 task-----》mq发消息
2user
2.1监听消息 6
2.2 8相当于锁 11释放锁
2.3 mq(另一个队列) task 积分加上了
3order
3.1监听消息 task_his 加上,后期查看,task 删除
2代码实现 2.1准备 1order中添加两张表 tb_task 任务表
tb_task_his 历史任务表
2order-api中添加对应的实体类
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 package com.ydles.order.pojo;import javax.persistence.Column;import javax.persistence.Id;import javax.persistence.Table;import java.util.Date;@Table(name = "tb_task") public class Task { @Id private Long id; @Column(name = "create_time") private Date createTime; @Column(name = "update_time") private Date updateTime; @Column(name = "delete_time") private Date deleteTime; @Column(name = "task_type") private String taskType; @Column(name = "mq_exchange") private String mqExchange; @Column(name = "mq_routingkey") private String mqRoutingkey; @Column(name = "request_body") private String requestBody; @Column(name = "status") private String status; @Column(name = "errormsg") private String errormsg; public Long getId () { return id; } public void setId (Long id) { this .id = id; } public Date getCreateTime () { return createTime; } public void setCreateTime (Date createTime) { this .createTime = createTime; } public Date getUpdateTime () { return updateTime; } public void setUpdateTime (Date updateTime) { this .updateTime = updateTime; } public Date getDeleteTime () { return deleteTime; } public void setDeleteTime (Date deleteTime) { this .deleteTime = deleteTime; } public String getTaskType () { return taskType; } public void setTaskType (String taskType) { this .taskType = taskType; } public String getMqExchange () { return mqExchange; } public void setMqExchange (String mqExchange) { this .mqExchange = mqExchange; } public String getMqRoutingkey () { return mqRoutingkey; } public void setMqRoutingkey (String mqRoutingkey) { this .mqRoutingkey = mqRoutingkey; } public String getRequestBody () { return requestBody; } public void setRequestBody (String requestBody) { this .requestBody = requestBody; } public String getStatus () { return status; } public void setStatus (String status) { this .status = status; } public String getErrormsg () { return errormsg; } public void setErrormsg (String errormsg) { this .errormsg = errormsg; } }
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 package com.ydles.order.pojo;import javax.persistence.Column;import javax.persistence.Id;import javax.persistence.Table;import java.util.Date;@Table(name = "tb_task_his") public class TaskHis { @Id private Long id; @Column(name = "create_time") private Date createTime; @Column(name = "update_time") private Date updateTime; @Column(name = "delete_time") private Date deleteTime; @Column(name = "task_type") private String taskType; @Column(name = "mq_exchange") private String mqExchange; @Column(name = "mq_routingkey") private String mqRoutingkey; @Column(name = "request_body") private String requestBody; @Column(name = "status") private String status; @Column(name = "errormsg") private String errormsg; public Long getId () { return id; } public void setId (Long id) { this .id = id; } public Date getCreateTime () { return createTime; } public void setCreateTime (Date createTime) { this .createTime = createTime; } public Date getUpdateTime () { return updateTime; } public void setUpdateTime (Date updateTime) { this .updateTime = updateTime; } public Date getDeleteTime () { return deleteTime; } public void setDeleteTime (Date deleteTime) { this .deleteTime = deleteTime; } public String getTaskType () { return taskType; } public void setTaskType (String taskType) { this .taskType = taskType; } public String getMqExchange () { return mqExchange; } public void setMqExchange (String mqExchange) { this .mqExchange = mqExchange; } public String getMqRoutingkey () { return mqRoutingkey; } public void setMqRoutingkey (String mqRoutingkey) { this .mqRoutingkey = mqRoutingkey; } public String getRequestBody () { return requestBody; } public void setRequestBody (String requestBody) { this .requestBody = requestBody; } public String getStatus () { return status; } public void setStatus (String status) { this .status = status; } public String getErrormsg () { return errormsg; } public void setErrormsg (String errormsg) { this .errormsg = errormsg; } }
3 ydles_user新增积分日志表 image-20211209012706482 image-20211209012742948 4 ydles_service_user_api添加实体类 PointLog
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package com.ydles.user.pojo;import javax.persistence.Table;@Table(name = "tb_point_log") public class PointLog { private String orderId; private String userId; private Integer point; public String getOrderId () { return orderId; } public void setOrderId (String orderId) { this .orderId = orderId; } public String getUserId () { return userId; } public void setUserId (String userId) { this .userId = userId; } public Integer getPoint () { return point; } public void setPoint (Integer point) { this .point = point; } }
5rabbitMQ 1 order 服务 导包
From: 元动力 1 2 3 4 <dependency> <groupId>org.springframework.amqp</groupId> <artifactId>spring-rabbit</artifactId> </dependency>
2config 下 rabbitMQ配置类
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 package com.ydles.order.config;import org.springframework.amqp.core.*;import org.springframework.beans.factory.annotation.Qualifier;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;@Configuration public class RabbitMQConfig { public static final String EX_BUYING_ADDPOINTUSER = "ex_buying_addpointuser" ; public static final String CG_BUYING_ADDPOINT = "cg_buying_addpoint" ; public static final String CG_BUYING_FINISHADDPOINT = "cg_buying_finishaddpoint" ; public static final String CG_BUYING_ADDPOINT_KEY = "addpoint" ; public static final String CG_BUYING_FINISHADDPOINT_KEY = "finishaddpoint" ; @Bean(EX_BUYING_ADDPOINTUSER) public Exchange EX_BUYING_ADDPOINTUSER () { return ExchangeBuilder.directExchange(EX_BUYING_ADDPOINTUSER).durable(true ).build(); } @Bean(CG_BUYING_ADDPOINT) public Queue CG_BUYING_ADDPOINT () { Queue queue = new Queue (CG_BUYING_ADDPOINT); return queue; } @Bean(CG_BUYING_FINISHADDPOINT) public Queue CG_BUYING_FINISHADDPOINT () { Queue queue = new Queue (CG_BUYING_FINISHADDPOINT); return queue; } @Bean public Binding BINDING_CG_BUYING_ADDPOINT (@Qualifier(CG_BUYING_ADDPOINT) Queue queue, @Qualifier(EX_BUYING_ADDPOINTUSER) Exchange exchange) { return BindingBuilder.bind(queue).to(exchange).with(CG_BUYING_ADDPOINT_KEY).noargs(); } @Bean public Binding BINDING_CG_BUYING_FINISHADDPOINT (@Qualifier(CG_BUYING_FINISHADDPOINT) Queue queue,@Qualifier(EX_BUYING_ADDPOINTUSER) Exchange exchange) { return BindingBuilder.bind(queue).to(exchange).with(CG_BUYING_FINISHADDPOINT_KEY).noargs(); } }
3配置文件 rabbitmq
From: 元动力 1 2 rabbitmq: host: 192.168.200.128
2.2 订单服务逻辑 需求:
image-20211209014056551 1dao层 增加mapper
From: 元动力 1 2 3 4 5 6 7 package com.ydles.order.dao; import com.ydles.order.pojo.Task; import tk.mybatis.mapper.common.Mapper; public interface TaskMapper extends Mapper<Task> { }
From: 元动力 1 2 3 4 5 6 7 package com.ydles.order.dao; import com.ydles.order.pojo.TaskHis; import tk.mybatis.mapper.common.Mapper; public interface TaskHisMapper extends Mapper<TaskHis> { }
2下单逻辑中增加任务
From: 元动力 1 2 @Autowired TaskMapper taskMapper;
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 System.out.println("向订单数据库中的任务表去添加任务数据" ); Task task = new Task (); task.setCreateTime(new Date ()); task.setUpdateTime(new Date ()); task.setMqExchange(RabbitMQConfig.EX_BUYING_ADDPOINTUSER); task.setMqRoutingkey(RabbitMQConfig.CG_BUYING_ADDPOINT_KEY); Map map = new HashMap (); map.put("username" ,order.getUsername()); map.put("orderId" ,orderId); map.put("point" ,order.getPayMoney()); task.setRequestBody(JSON.toJSONString(map)); taskMapper.insertSelective(task);
3定时任务,定时发送任务表信息到mq 启动类增加
From: 元动力 1 @EnableScheduling //开启定时任务
新建task包,加入定时任务类
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Component public class QueryPointTask { @Autowired private TaskMapper taskMapper; @Autowired private RabbitTemplate rabbitTemplate; @Scheduled(cron = "0/2 * * * * ?") public void queryTask () { List<Task> taskList = taskMapper.findTaskLessThanCurrentTime(new Date ()); if (taskList != null && taskList.size()>0 ){ for (Task task : taskList) { rabbitTemplate.convertAndSend(RabbitMQConfig.EX_BUYING_ADDPOINTUSER, RabbitMQConfig.CG_BUYING_ADDPOINT_KEY, JSON.toJSONString(task)); System.out.println("订单服务向添加积分队列发送了一条消息" ); } } } }
taskmapper中自定义查询小于当前时间的方法
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 @Select("select * from tb_task where update_time<#{currentTime}") @Results({@Result(column = "create_time",property = "createTime"), @Result(column = "update_time",property = "updateTime"), @Result(column = "delete_time",property = "deleteTime"), @Result(column = "task_type",property = "taskType"), @Result(column = "mq_exchange",property = "mqExchange"), @Result(column = "mq_routingkey",property = "mqRoutingkey"), @Result(column = "request_body",property = "requestBody"), @Result(column = "status",property = "status"), @Result(column = "errormsg",property = "errormsg")}) List<Task> findTaskLessThanCurrentTime(Date currentTime);
2.3 用户服务逻辑 需求:
image-20211209021057873 1配置文件增加redis rabbitMQ配置
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 spring: application: name: user datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://192.168.200.128:3306/ydles_user?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC username: root password: root main: allow-bean-definition-overriding: true redis: host: 192.168 .200 .128 rabbitmq: host: 192.168 .200 .128
2导入mq依赖
From: 元动力 1 2 3 4 <dependency> <groupId>org.springframework.amqp</groupId> <artifactId>spring-rabbit</artifactId> </dependency>
3mq配置类 RabbitMQConfig ,从order服务copy
4添加依赖
From: 元动力 1 2 3 4 5 <dependency> <groupId>com.ydles</groupId> <artifactId>ydles_service_order_api</artifactId> <version>1.0-SNAPSHOT</version> </dependency>
4监听mq队列 com.ydles.user.listener创建AddPointListener
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 package com.ydles.user.listener;import com.alibaba.fastjson.JSON;import com.ydles.order.pojo.Task;import com.ydles.user.config.RabbitMQConfig;import com.ydles.user.service.UserService;import org.apache.commons.lang.StringUtils;import org.springframework.amqp.rabbit.annotation.RabbitListener;import org.springframework.amqp.rabbit.core.RabbitTemplate;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.stereotype.Component;@Component public class AddPointListener { @Autowired private RedisTemplate redisTemplate; @Autowired private UserService userService; @Autowired private RabbitTemplate rabbitTemplate; @RabbitListener(queues = RabbitMQConfig.CG_BUYING_ADDPOINT) public void receiveAddPointMessage (String message) { System.out.println("用户服务接收到了任务消息" ); Task task = JSON.parseObject(message, Task.class); if (task == null || StringUtils.isEmpty(task.getRequestBody())){ return ; } Object value = redisTemplate.boundValueOps(task.getId()).get(); if (value != null ){ return ; } }
5更新积分方法实现 1userService 定义接口
From: 元动力 1 int updateUserPoint (Task task) ;
实现类
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 @Override @Transactional public int updateUserPoint (Task task) { System.out.println("用户服务现在开始对任务进行处理" ); Map map = JSON.parseObject(task.getRequestBody(), Map.class); String username = map.get("username" ).toString(); String orderId = map.get("orderId" ).toString(); int point = (int ) map.get("point" ); PointLog pointLog = pointLogMapper.findPointLogByOrderId(orderId); if (pointLog != null ){ return 0 ; } redisTemplate.boundValueOps(task.getId()).set("exist" ,30 , TimeUnit.SECONDS); int result = userMapper.updateUserPoint(username,point); if (result<=0 ){ return 0 ; } pointLog = new PointLog (); pointLog.setUserId(username); pointLog.setOrderId(orderId); pointLog.setPoint(point); result = pointLogMapper.insertSelective(pointLog); if (result <= 0 ){ return 0 ; } redisTemplate.delete(task.getId()); System.out.println("用户服务完成了更改用户积分的操作" ); return 1 ; }
2dao层新建mapper,查询当前任务是否操作过
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 package com.ydles.user.dao;import com.ydles.user.pojo.PointLog;import org.apache.ibatis.annotations.Param;import org.apache.ibatis.annotations.Select;import tk.mybatis.mapper.common.Mapper;public interface PointLogMapper extends Mapper <PointLog> { @Select("select * from tb_point_log where order_id =#{orderId}") PointLog findPointLogByOrderId (@Param("orderId") String orderId) ; }
3userMapper新增一个修改用户积分方法
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 package com.ydles.user.dao;import com.ydles.user.pojo.User;import org.apache.ibatis.annotations.Param;import org.apache.ibatis.annotations.Update;import tk.mybatis.mapper.common.Mapper;public interface UserMapper extends Mapper <User> { @Update("update tb_user set points=points+#{point} where username=#{username}") int updateUserPoint (@Param("username") String username, @Param("point") int point) ; }
4AddPointListener完善
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 package com.ydles.user.listener;import com.alibaba.fastjson.JSON;import com.ydles.order.pojo.Task;import com.ydles.user.config.RabbitMQConfig;import com.ydles.user.service.UserService;import org.apache.commons.lang.StringUtils;import org.springframework.amqp.rabbit.annotation.RabbitListener;import org.springframework.amqp.rabbit.core.RabbitTemplate;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.stereotype.Component;@Component public class AddPointListener { @Autowired private RedisTemplate redisTemplate; @Autowired private UserService userService; @Autowired private RabbitTemplate rabbitTemplate; @RabbitListener(queues = RabbitMQConfig.CG_BUYING_ADDPOINT) public void receiveAddPointMessage (String message) { System.out.println("用户服务接收到了任务消息" ); Task task = JSON.parseObject(message, Task.class); if (task == null || StringUtils.isEmpty(task.getRequestBody())){ return ; } Object value = redisTemplate.boundValueOps(task.getId()).get(); if (value != null ){ return ; } int result = userService.updateUserPoint(task); if (result == 0 ){ return ; } rabbitTemplate.convertAndSend(RabbitMQConfig.EX_BUYING_ADDPOINTUSER,RabbitMQConfig.CG_BUYING_FINISHADDPOINT_KEY,JSON.toJSONString(task)); System.out.println("用户服务向完成添加积分队列发送了一条消息" ); } }
2.4订单服务收尾 需求:
image-20211209025157447 建立监听类DelTaskListener
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package com.ydles.order.listener;import com.alibaba.fastjson.JSON;import com.ydles.order.config.RabbitMQConfig;import com.ydles.order.pojo.Task;import com.ydles.order.service.TaskService;import org.springframework.amqp.rabbit.annotation.RabbitListener;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;@Component public class DelTaskListener { @Autowired private TaskService taskService; @RabbitListener(queues = RabbitMQConfig.CG_BUYING_FINISHADDPOINT) public void receiveDelTaskMessage (String message) { System.out.println("订单服务接收到了删除任务操作的消息" ); Task task = JSON.parseObject(message, Task.class); taskService.delTask(task); } }
创建taskService写出删除任务的方法
From: 元动力 1 2 3 public interface TaskService { void delTask (Task task) ; }
实现taskService
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 package com.ydles.order.service.impl;import com.ydles.order.dao.TaskHisMapper;import com.ydles.order.dao.TaskMapper;import com.ydles.order.pojo.Task;import com.ydles.order.pojo.TaskHis;import com.ydles.order.service.TaskService;import org.springframework.beans.BeanUtils;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;import java.util.Date;@Service public class TaskServiceImpl implements TaskService { @Autowired private TaskHisMapper taskHisMapper; @Autowired private TaskMapper taskMapper; @Override @Transactional public void delTask (Task task) { task.setDeleteTime(new Date ()); Long taskId = task.getId(); task.setId(null ); TaskHis taskHis = new TaskHis (); BeanUtils.copyProperties(task,taskHis); taskHisMapper.insertSelective(taskHis); task.setId(id); taskMapper.delete(task); System.out.println("订单服务完成了添加历史任务并删除原有任务的操作" ); } }
2.5效果测试 order user服务以dubug打开。
从头到最后一步一步测试。关键点:
1order下单任务表中数据
image-20211209031308844 2order 定时任务扫描表,发信息
image-20211209031315436 3user收到信息,检查redis,修改积分
image-20211209031335319 image-20211209031344811 4order收到成功消息
image-20211209031259362 总结:
1分布式事务解决方案
事务:ACID 面试必问,隔离级别,传播行为
本地事务:@transactional
分布式事务
2解决方案
2.1xa 2pc mysql
image-20211208174923396 2.2 tcc
image-20211208180127771 2.3 消息队列
image-20211208221414338 3Seata
4下单 使用消息队列实现分布式事务
image-20211209011831937 第13章 微信扫码支付 角色 : 支付相关模块的开发工程师,难。
学习目标: 能够根据微信支付的开发文档调用微信支付的api 完成统一下单生成微信支付二维码功能 完成支付回调的逻辑处理 完成推送支付通知功能 1. 微信支付快速入门 native 支付文档:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_1open in new window
1.1 微信支付申请(了解) 第一步:注册公众号(类型须为:服务号)
请根据营业执照类型选择以下主体注册:个体工商户open in new window | 企业/公司open in new window | 政府open in new window | 媒体open in new window | 其他类型open in new window 。
第二步:认证公众号
公众号认证后才可申请微信支付,认证费:300元/次 。
第三步:提交资料申请微信支付
登录公众平台,点击左侧菜单【微信支付】,开始填写资料等待审核,审核时间为1-5个工作日内。
第四步:开户成功,登录商户平台进行验证
资料审核通过后,请登录联系人邮箱查收商户号和密码,并登录商户平台填写财付通备付金打的小额资金数额,完成账户验证。
第五步:在线签署协议
本协议为线上电子协议,签署后方可进行交易及资金结算,签署完立即生效。
本课程已经提供好“元动力教育”的微信支付账号,学员无需申请。
完成上述步骤,你可以得到调用API用到的账号和密钥
!!!!真实地测试,所以,我们写的接口,钱都会给我们元动力。钱一定设置成1分钱。
appid :微信公众账号或开放平台APP的唯一标识 wxababcd122d1618eb
mch_id :商户号 1611671554
key :商户密钥 ydlclass66666688888YDLCLASS66688
1.2 微信支付开发文档与SDK 在线微信支付开发文档:
https://pay.weixin.qq.com/wiki/doc/api/index.htmlopen in new window
https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_1open in new window
微信支付接口调用的整体思路:
按API要求组装参数,以XML方式发送(POST)给微信支付接口(URL),微信支付接口也是以XML方式给予响应。程序根据返回的结果(其中包括支付URL)生成二维码或判断订单状态。
我们解压从官网下载的sdk ,安装到本地仓库
com.github.wxpay.sdk.WXPay类下提供了对应的方法:
方法名 说明 microPay 刷卡支付 unifiedOrder 统一下单 orderQuery 查询订单 reverse 撤销订单 closeOrder 关闭订单 refund 申请退款 refundQuery 查询退款 downloadBill 下载对账单 report 交易保障 shortUrl 转换短链接 authCodeToOpenid 授权码查询openid
测试工程
1创建test_pay ,导入依赖。将我的仓库com.github.wxpay相关包发给学生。
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 < dependencies> < dependency> < groupId> com.github.wxpay< /groupId> < artifactId> wxpay-sdk< /artifactId> < version> 3.0.9< /version> < /dependency> < dependency> < groupId> commons-logging< /groupId> < artifactId> commons-logging< /artifactId> < version> 1.2< /version> < /dependency> < /dependencies>
2创建配置类 注意 :包名必须为com.github.wxpay.sdk
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 package com.github.wxpay.sdk;import java.io.InputStream;public class MyConfig extends WXPayConfig { public String getAppID () { return "wxababcd122d1618eb" ; } public String getMchID () { return "1611671554" ; } public String getKey () { return "ydlclass66666688888YDLCLASS66688" ; } public InputStream getCertStream () { return null ; } public IWXPayDomain getWXPayDomain () { return new IWXPayDomain () { public void report (String s, long l, Exception e) { } public DomainInfo getDomain (WXPayConfig wxPayConfig) { return new DomainInfo ("api.mch.weixin.qq.com" , true ); } }; } }
3测试类 包名随意
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package ydles.test;import com.github.wxpay.sdk.MyConfig;import com.github.wxpay.sdk.WXPay;import java.util.HashMap;import java.util.Map;public class PayTest { public static void main (String[] args) throws Exception { MyConfig myConfig=new MyConfig (); WXPay wxPay=new WXPay (myConfig); Map<String,String> map = new HashMap <>(); map.put("body" , "元动力二奢" ); map.put("out_trade_no" , "123654" ); map.put("total_fee" , "1" ); map.put("spbill_create_ip" , "127.0.0.1" ); map.put("notify_url" , "http://www.baidu.com" ); map.put("trade_type" , "NATIVE" ); Map<String, String> resultMap = wxPay.unifiedOrder(map); System.out.println(resultMap); } }
4得到
From: 元动力 1 {nonce_str=l1pTBT12JvPIN0lO, code_url=weixin://wxpay/bizpayurl?pr=PFNjrIXzz, appid=wxababcd122d1618eb, sign=FF218956CA8C00145F4BACBC0AA843012E2DE2498FAAD4809769EB367582340A, trade_type=NATIVE, return_msg=OK, result_code=SUCCESS, mch_id=1611671554, return_code=SUCCESS, prepay_id=wx15174918803690023343d15a9737140000}
QRcode
1打开资源文件夹 weixinpay.html
image-20211215175333830 支付图片为qrcode.js生成。修改源代码为本人生成的,即可微信扫描。
From: 元动力 1 qrcode.makeCode("weixin://wxpay/bizpayurl?pr=PFNjrIXzz");
image-20211215175246842 2. 微信支付二维码 2.1 需求分析 用户在提交订单后,如果是选择支付方式为微信支付,那应该跳转到微信支付二维码页面,用户扫描二维码可以进行支付,金额与订单金额相同。
流程:商品详情页-----》购物车-----》订单----》支付方式pay.html------》微信支付页weixinpay.html--------》支付成功
image-20211221110506739 2.2 实现思路 前端页面向后端传递订单号,后端根据订单号查询订单,检查是否为当前用户的未支付订单,如果是则根据订单号和金额生成支付url返给前端,前端得到支付url生成支付二维码。
2.3 代码实现 2.3.1 提交订单跳转支付页 订单----》支付方式pay.html
1更新ydles_service_order
OrderServiceImpl中add() ,设置返回值为订单Id
From: 元动力 1 public String add (Order order)
OrderService
From: 元动力 1 String add (Order order) ;
OrderController
From: 元动力 1 2 String orderId = orderService.add(order);return new Result (true ,StatusCode.OK,"添加成功" ,orderId);
2修改web-order中提交订单页面,下单成功跳转至选择支付页面。
From: 元动力 1 2 3 4 5 6 7 8 if (response.data.flag){ alert("添加订单成功" ); var orderId = response.data.data; location.href="/api/worder/toPayPage?orderId=" +orderId; } else { alert("添加订单失败" ); }
3在OrderController中新增方法,用于跳转支付页
From: 元动力 1 2 3 4 5 6 7 8 @GetMapping("/toPayPage") public String toPayPage (String orderId,Model model) { Order order = orderFeign.findById(orderId).getData(); model.addAttribute("orderId" ,orderId); model.addAttribute("payMoney" ,order.getPayMoney()); return "pay" ; }
4order服务中已经有方法:orderController中 根据订单id查询订单信息 findById
From: 元动力 1 2 3 4 5 6 7 8 9 10 @GetMapping("/{id}") public Result findById (@PathVariable String id) { Order order = orderService.findById(id); return new Result (true ,StatusCode.OK,"查询成功" ,order); }
把它暴露出去 OrderFeign
From: 元动力 1 2 3 4 5 6 7 8 9 10 @FeignClient(name = "order") public interface OrderFeign { @PostMapping("/order") public Result add (@RequestBody Order order) ; @GetMapping("/order/{id}") public Result<Order> findById(@PathVariable String id); }
5相关页面放至resources
6测试
登陆 http://localhost:8001/api/oauth/toLoginopen in new window
添加商品: http://localhost:8001/api/wcart/add?skuId=1450862568724758528&num=5open in new window
查看购物车: http://localhost:8001/api/wcart/listopen in new window
image-20211221114601627 点击结算
image-20211221114628720 点击提交订单,跳转至支付页面
image-20211221114637493 2.3.2 支付微服务权限集成 image-20211221144256113 1service二级目录下,新建微服务ydles_service_pay
2依赖
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 < dependencies> < dependency> < groupId> com.ydles< /groupId> < artifactId> ydles_common< /artifactId> < version> 1.0-SNAPSHOT< /version> < /dependency> < dependency> < groupId> com.github.wxpay< /groupId> < artifactId> wxpay-sdk< /artifactId> < version> 3.0.9< /version> < /dependency> < dependency> < groupId> org.springframework.boot< /groupId> < artifactId> spring-boot-starter< /artifactId> < exclusions> < exclusion> < groupId> org.springframework.boot< /groupId> < artifactId> spring-boot-starter-logging< /artifactId> < /exclusion> < /exclusions> < /dependency> < dependency> < groupId> org.springframework.boot< /groupId> < artifactId> spring-boot-starter-amqp< /artifactId> < /dependency> < /dependencies>
3配置文件 application.yml
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 server: port: 9010 spring: application: name: pay rabbitmq: host: 192.168 .200 .128 main: allow-bean-definition-overriding: true eureka: client: service-url: defaultZone: http://127.0.0.1:6868/eureka instance: prefer-ip-address: true wxpay: notify_url: http://itlils.cross.echosite.cn/wxpay/notify
4微信支付配置类,copy。注意包名
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 package com.github.wxpay.sdk;import java.io.InputStream;public class MyConfig extends WXPayConfig { @Override String getAppID () { return "wxababcd122d1618eb" ; } @Override String getMchID () { return "1611671554" ; } @Override String getKey () { return "ydlclass66666688888YDLCLASS66688" ; } @Override InputStream getCertStream () { return null ; } @Override IWXPayDomain getWXPayDomain () { return new IWXPayDomain () { @Override public void report (String s, long l, Exception e) { } @Override public DomainInfo getDomain (WXPayConfig wxPayConfig) { return new DomainInfo ("api.mch.weixin.qq.com" , true ); } }; } }
5启动类 com.ydles.pay下
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package com.ydles.pay;import com.github.wxpay.sdk.MyConfig;import com.github.wxpay.sdk.WXPay;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.netflix.eureka.EnableEurekaClient;import org.springframework.context.annotation.Bean;@SpringBootApplication @EnableEurekaClient public class PayApplication { public static void main (String[] args) { SpringApplication.run(PayApplication.class,args); } @Bean public WXPay wxPay () { try { return new WXPay (new MyConfig ()); } catch (Exception e) { e.printStackTrace(); return null ; } } }
2.3.3 支付微服务-下单 ydles_service_pay服务
(1)创建com.ydles.pay.service包,包下创建接口WxPayService
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 package com.ydles.pay.service;import java.util.Map;public interface WxPayService { public Map nativePay (String orderId,Integer money) ; }
(2)创建com.ydles.pay.service.impl 包 ,新增服务类WxPayServiceImpl 拓展:涉及钱的计算,注意用BigDecimal https://www.cnblogs.com/zhangyinhua/p/11545305.htmlopen in new window
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 @Service public class WxPayServiceImpl implements WxPayService { @Autowired WXPay wxPay; @Override public Map nativePay (String orderId, Integer money) { try { Map<String, String> reqData=new HashMap <>(); reqData.put("body" ,"动力二奢下单支付" ); reqData.put("out_trade_no" , orderId); BigDecimal yuan=new BigDecimal ("0.01" ); BigDecimal beishu=new BigDecimal (100 ); BigDecimal fen = yuan.multiply(beishu); fen=fen.setScale(0 ,BigDecimal.ROUND_UP); reqData.put("total_fee" , String.valueOf(fen)); reqData.put("spbill_create_ip" , "192.168.1.1" ); reqData.put("notify_url" , "http://www.baidu.com" ); reqData.put("trade_type" , "NATIVE" ); Map<String, String> resultMap = wxPay.unifiedOrder(reqData); System.out.println(resultMap); return resultMap; }catch (Exception e){ e.printStackTrace(); return null ; } } }
(3)创建com.ydles.pay.controller包 ,新增WxPayController
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @RestController @RequestMapping("/wxpay") public class WxPayController { @Autowired WxPayService wxPayService; @GetMapping("/nativePay") public Result<Map> nativePay(@RequestParam("orderId") String orderId,@RequestParam("money") Integer money){ Map map = wxPayService.nativePay(orderId, money); return new Result (true , StatusCode.OK,"微信下单成功" ,map); } }
测试:http://localhost:9010/wxpay/nativePay?orderId=123321&money=1open in new window 订单号可能重复,大家换一下就好了。
image-20211221150107575 2.3.3 支付渲染页面微服务 支付方式pay.html------》微信支付页weixinpay.html
页面需要调用我们的微服务,所以:
image-20211221151905932 (1)新增ydles_service_pay_api模块 ,pom.xml中加入依赖
From: 元动力 1 2 3 4 5 6 7 < dependencies> < dependency> < groupId> com.ydles< /groupId> < artifactId> ydles_common< /artifactId> < version> 1.0-SNAPSHOT< /version> < /dependency> < /dependencies>
新增com.ydles.pay.feign包,包下创建接口
From: 元动力 1 2 3 4 5 6 @FeignClient(name = "pay") public interface PayFeign { @GetMapping("/wxpay/nativePay") public Result nativePay (@RequestParam("orderId") String orderId, @RequestParam("money") Integer money) ; }
(2)ydles_web_order的pom.xml加入依赖
From: 元动力 1 2 3 4 5 < dependency> < groupId> com.ydles< /groupId> < artifactId> ydles_service_pay_api< /artifactId> < version> 1.0-SNAPSHOT< /version> < /dependency>
启动类增加扫包
From: 元动力 1 @EnableFeignClients(basePackages = {"com.ydles.pay.feign","com.ydles.order.feign","com.ydles.user.feign"})
image-20211221152256881 ydles_web_order新增PayController
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 @Controller @RequestMapping("/wxpay") public class PayController { @Autowired private OrderFeign orderFeign; @Autowired private PayFeign payFeign; @GetMapping public String wxPay (String orderId , Model model) { Result<Order> orderResult = orderFeign.findById(orderId); if (orderResult.getData() == null ){ return "fail" ; } Order order = orderResult.getData(); if (!"0" .equals(order.getPayStatus())){ return "fail" ; } Result payResult = payFeign.nativePay(orderId, order.getPayMoney()); if (payResult.getData() == null ){ return "fail" ; } Map payMap = (Map) payResult.getData(); payMap.put("orderId" ,orderId); payMap.put("payMoney" ,order.getPayMoney()); model.addAllAttributes(payMap); return "wxpay" ; }
(4)将静态原型中wxpay.html拷贝到templates文件夹下作为模板,修改模板,部分代码如下:
二维码地址渲染
From: 元动力 1 qrcode.makeCode([[${code_url}]]);
查看显示订单号与金额
From: 元动力 1 2 < h4 class="fl tit-txt"> < span class="success-icon"> < /span> < span class="success-info" th:text="|订单提交成功,请您及时付款!订单号:${orderId}|"> < /span> < /h4> < span class="fr"> < em class="sui-lead"> 应付金额:< /em> < em class="orange money" th:text="${#numbers.formatDecimal(payMoney/100.0,1,2)}"> < /em> 元< /span>
pay.html
From: 元动力 1 < li> < a th:href="|/api/wxpay?orderId=${orderId}|"> < img src="/./image/_/pay3.jpg"> < /a> < /li>
image-20211221154450109 ydles_gateway_web项目的application.yml文件加入
From: 元动力 1 2 3 4 5 6 7 - id: ydles_order_web_route uri: lb://order-web predicates: - Path=/api/wcart/**,/api/worder/**,/api/wxpay/** filters: - StripPrefix=1
查看ydles_gateway_web项目的UrlFilter.java中
From: 元动力 1 public static String orderFilterPath = "/api/wpay,/api/wpay/**,/api/worder/**,/api/user/**,
测试
1重启gatewaty-web service-pay web-order
2添加商品: http://localhost:8001/api/wcart/add?skuId=1450862568724758528&num=5open in new window
image-20211221162207178 3查看购物车: http://localhost:8001/api/wcart/listopen in new window
image-20211221162214876 4点击:结算---》提交订单--》选择微信支付--》跳到二维码页面--》扫描二维码,你将损失1分钱。
image-20211221200858087 3. 支付回调逻辑处理 image-20211221203416360 3.1 需求分析 在完成支付后,修改订单状态为已支付,并记录订单日志。
3.2 实现思路 (1)接受微信支付平台的回调信息(xml)
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 < xml> < appid> < ![CDATA[wx8397f8696b538317]]> < /appid> < bank_type> < ![CDATA[CFT]]> < /bank_type> < cash_fee> < ![CDATA[1]]> < /cash_fee> < fee_type> < ![CDATA[CNY]]> < /fee_type> < is_subscribe> < ![CDATA[N]]> < /is_subscribe> < mch_id> < ![CDATA[1473426802]]> < /mch_id> < nonce_str> < ![CDATA[c6bea293399a40e0a873df51e667f45a]]> < /nonce_str> < openid> < ![CDATA[oNpSGwbtNBQROpN_dL8WUZG3wRkM]]> < /openid> < out_trade_no> < ![CDATA[1553063775279]]> < /out_trade_no> < result_code> < ![CDATA[SUCCESS]]> < /result_code> < return_code> < ![CDATA[SUCCESS]]> < /return_code> < sign> < ![CDATA[DD4E5DF5AF8D8D8061B0B8BF210127DE]]> < /sign> < time_end> < ![CDATA[20190320143646]]> < /time_end> < total_fee> 1< /total_fee> < trade_type> < ![CDATA[NATIVE]]> < /trade_type> < transaction_id> < ![CDATA[4200000248201903206581106357]]> < /transaction_id> < /xml>
(2)收到通知后,调用查询接口查询订单。
(3)如果支付结果为成功,则调用修改订单状态和记录订单日志的方法。
3.3 代码实现 3.3.1 内网穿透工具 natapp https://natapp.cn/open in new window
image-20211221204125489 3.3.2下载配置文件和客户端 如何使用:https://natapp.cn/article/natapp_newbieopen in new window
解压资料,将配置文件与程序平行放置。修改配置文件。
image-20211221205259145 image-20211221205320127 3.3.3 启动 双击natapp.exe
image-20211221205424476 外网地址默认映射本地的80端口 可以修改
image-20211221205450080 3.3.4 测试 1ydles_service_pay 微服务 WXPayController 增加一个测试方法
From: 元动力 1 2 3 4 @RequestMapping("/notify") public void notifyLogic () { System.out.println("支付成功回调" ); }
2重启ydles_service_pay
3访问 http://localhost:9010/wxpay/notifyopen in new window 程序打印输出
image-20211221205220246 4访问 http://lizihao.cross.echosite.cn/wxpay/notifyopen in new window 程序打印输出
image-20211221205221905 3.3.5 接收回调信息 1 修改支付微服务配置文件
From: 元动力 1 2 wxpay: notify_url: http://mp93g5.natappfree.cc/wxpay/notify
2修改WxPayServiceImpl ,引入
From: 元动力 1 2 @Value("${wxpay.notify_url}") private String notifyUrl;
3修改WxPayServiceImpl 的nativePay方法
From: 元动力 1 map.put("notify_url" ,notifyUrl);
4测试:
4.1重启ydles_service_pay
4.2添加商品: http://localhost:8001/api/wcart/add?skuId=1450862568724758528&num=5open in new window
4.3查看购物车: http://localhost:8001/api/wcart/listopen in new window
image-20211221232224662 4.4点击:结算---》提交订单--》选择微信支付--》跳到二维码页面--》扫描二维码 查看可以回调多次
image-20211221232328432 为什么多次回调? https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_7&index=8open in new window
From: 元动力 1 2 3 注意: 1、同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。 2、后台通知交互时,如果微信收到商户的应答不符合规范或超时,微信会判定本次通知失败,重新发送通知,直到成功为止(在通知一直不成功的情况下,微信总共会发起多次通知,通知频率为15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h - 总计 24h4m),但微信不保证通知最终一定能成功。
3.3.6 处理回调通知-重点 1资源文件夹 ConvertUtils 放到 common工程的utils包下
2微信支付平台发送给回调地址的是二进制流,我们需要提取二进制流转换为字符串,这个字符串就是xml格式。
修改notify方法
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @RequestMapping("/notify") public String wxPayNotify (HttpServletRequest request, HttpServletResponse response) throws IOException { System.out.println("支付成功回调。。。。" ); try { String xml = ConvertUtils.convertToString(request.getInputStream()); System.out.println(xml); response.setContentType("text/xml" ); String data = "<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>" ; response.getWriter().write(data); } catch (Exception e) { e.printStackTrace(); } }
**注意:**1、同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。2、后台通知交互时,如果微信收到商户的应答不符合规范或超时,微信会判定本次通知失败,重新发送通知,直到成功为止(在通知一直不成功的情况下,微信总共会发起10次通知,通知频率为15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h - 总计 24h4m),但微信不保证通知最终一定能成功。
测试后,在控制台看到输出的消息
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 访问到 notify接口了!< xml> < appid> < ![CDATA[wxababcd122d1618eb]]> < /appid> < bank_type> < ![CDATA[OTHERS]]> < /bank_type> < cash_fee> < ![CDATA[1]]> < /cash_fee> < fee_type> < ![CDATA[CNY]]> < /fee_type> < is_subscribe> < ![CDATA[Y]]> < /is_subscribe> < mch_id> < ![CDATA[1611671554]]> < /mch_id> < nonce_str> < ![CDATA[WNXFCMMCQSYiM09qrr4nwDH41iJnwuSs]]> < /nonce_str> < openid> < ![CDATA[o9tV755anFQYm27bYNC1ALQ5Ovfs]]> < /openid> < out_trade_no> < ![CDATA[1473316925973991424]]> < /out_trade_no> < result_code> < ![CDATA[SUCCESS]]> < /result_code> < return_code> < ![CDATA[SUCCESS]]> < /return_code> < sign> < ![CDATA[E6A69A88C986B64411ACB2DA7E945EB48BF6391C06B0972DA6AEC80C00FC74F9]]> < /sign> < time_end> < ![CDATA[20211221233851]]> < /time_end> < total_fee> 1< /total_fee> < trade_type> < ![CDATA[NATIVE]]> < /trade_type> < transaction_id> < ![CDATA[4200001414202112216382710751]]> < /transaction_id> < /xml>
我们可以将此xml字符串,转换为map,提取其中的out_trade_no(订单号),根据订单号修改订单状态。
3.3.7 收到微信通知后的逻辑 支付服务:查询订单验证通知
image-20211221235105274 微信方面查询订单如何做 https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_2open in new window
image-20211221235203111 (1)WxPayService新增方法定义
From: 元动力 1 2 3 4 5 6 Map queryOrder (String orderId) ;
(2)WxPayServiceImpl实现方法
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 @Override public Map queryOrder (String orderId) { try { Map<String ,String> map = new HashMap (); map.put("out_trade_no" ,orderId); Map<String, String> resultMap = wxPay.orderQuery(map); return resultMap; }catch (Exception e){ e.printStackTrace(); return null ; } }
(3)修改notify方法
下单:
image-20211222001359665 下单通知 成功与否在哪儿? https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_7&index=8open in new window
image-20200305014740028 我们的订单号 在哪儿? 在下单后微信回调的请求里。
image-20211221235940049 image-20200305012941294 =================================================================================================
查询:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_2open in new window
查询结果 如何?
image-20200305015926052 查询订单结果 结果如何?
image-20200305015403233 此次查询下单我们商品的订单号是什么?
image-20200305020047787 此次支付动作的id在哪儿?
image-20200305015219298
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 @RequestMapping("/wxpay") @RestController public class WXPayController { @Autowired private WXPayService wxPayService; @Autowired private RabbitTemplate rabbitTemplate; @GetMapping("/nativePay") public Result nativePay (@RequestParam("orderId") String orderId, @RequestParam("money") Integer money) { Map resultMap = wxPayService.nativePay(orderId, money); return new Result (true , StatusCode.OK, "" , resultMap); } @RequestMapping("/notify") public void notifyLogic (HttpServletRequest request, HttpServletResponse response) { System.out.println("支付成功回调" ); try { String xml = ConvertUtils.convertToString(request.getInputStream()); System.out.println(xml); Map<String, String> map = WXPayUtil.xmlToMap(xml); if ("SUCCESS" .equals(map.get("result_code" ))) { Map result = wxPayService.queryOrder(map.get("out_trade_no" )); System.out.println("查询订单结果:" + result); if ("SUCCESS" .equals(result.get("result_code" ))) { Map message = new HashMap (); message.put("orderId" , result.get("out_trade_no" )); message.put("transactionId" , result.get("transaction_id" )); rabbitTemplate.convertAndSend("" , RabbitMQConfig.ORDER_PAY, JSON.toJSONString(message)); } else { System.out.println(map.get("err_code_des" )); } } else { System.out.println(map.get("err_code_des" )); } response.setContentType("text/xml" ); String data = "<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>" ; response.getWriter().write(data); } catch (Exception e) { e.printStackTrace(); } } }
config包下增加rebbitMQ配置类。并检查依赖和配置文件。
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package com.ydles.pay.config;import org.springframework.amqp.core.Queue;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;@Configuration public class RabbitMQConfig { public static final String ORDER_PAY="order_pay" ; @Bean public Queue queue () { return new Queue (ORDER_PAY); } }
3.3.8 订单服务修改订单状态 需求:
image-20211222001437864 1rabbitMQ配置类,把支付服务定义的队列定义
From: 元动力 1 2 3 4 5 6 public static final String ORDER_PAY="order_pay" ;@Bean public Queue queue () { return new Queue (ORDER_PAY); }
2 com.ydles.order.listener包下创建OrderPayListener
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 @Component public class OrderPayListener { @Autowired private OrderService orderService; @RabbitListener(queues = RabbitMQConfig.ORDER_PAY) public void receivePayMessage (String message) { System.out.println("接收到了订单支付的消息:" + message); Map map = JSON.parseObject(message, Map.class); orderService.updatePayStatus((String) map.get("orderId" ), (String) map.get("transactionId" )); } }
3OrderService接口新增方法定义
From: 元动力 1 2 3 4 5 6 void updatePayStatus (String orderId,String transactionId) ;
4OrderServiceImpl新增方法实现
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Autowired private OrderLogMapper orderLogMapper;@Override public void updatePayStatus (String orderId, String transactionId) { Order order = orderMapper.selectByPrimaryKey(orderId); if (order!=null && "0" .equals(order.getPayStatus())){ order.setPayStatus("1" ); order.setOrderStatus("1" ); order.setUpdateTime(new Date ()); order.setPayTime(new Date ()); order.setTransactionId(transactionId); orderMapper.updateByPrimaryKeySelective(order); OrderLog orderLog=new OrderLog (); orderLog.setId( idWorker.nextId()+"" ); orderLog.setOperater("system" ); orderLog.setOperateTime(new Date ()); orderLog.setOrderStatus("1" ); orderLog.setPayStatus("1" ); orderLog.setRemarks("支付流水号" +transactionId); orderLog.setOrderId(order.getId()); orderLogMapper.insertSelective(orderLog); } }
3.3.9 整个流程测试 1重启ydles_service_pay ydles_service_order
2添加商品: http://localhost:8001/api/cart/addCart?skuId=100000006163&num=10open in new window
3查看购物车: http://localhost:8001/api/wcart/listopen in new window
4点击:结算---》提交订单--》选择微信支付--》跳到二维码页面--》扫描二维码
4.1扫码支付之前,查看订单状态
4.1扫码支付之后,查看订单状态
image-20211222003719250 订单日志表,也有一条数据了
image-20211222003728955 4. 推送支付通知 4.1 需求分析 当用户完成扫码支付后,跳转到支付成功页面
4.2 服务端推送方案 需求:我们需要将支付的结果通知前端页面,其实就是我们通过所说的服务器端推送,主要有三种实现方案
(1)Ajax 短轮询 setInterval Ajax 轮询主要通过页面端的 JS 定时异步刷新任务来实现数据的加载
如果我们使用ajax短轮询方式,需要后端提供方法,通过调用微信支付接口实现根据订单号查询支付状态的方法(参见查询订单API) 。 前端每间隔三秒查询一次,如果后端返回支付成功则执行页面跳转。
缺点:这种方式实时效果较差,而且对服务端的压力也较大。
(2)长轮询
长轮询主要也是通过 Ajax 机制,但区别于传统的 Ajax 应用,长轮询的服务器端会在没有数据时阻塞请求直到有新的数据产生或者请求超时才返回,之后客户端再重新建立连接获取数据。
如果使用长轮询,也同样需要后端提供方法,通过调用微信支付接口实现根据订单号查询支付状态的方法,只不过循环是写在后端的。
缺点:长轮询服务端会长时间地占用资源,如果消息频繁发送的话会给服务端带来较大的压力。
(3)WebSocket 双向通信 WebSocket 是 HTML5 中一种新的通信协议,能够实现浏览器与服务器之间全双工通信。如果浏览器和服务端都支持 WebSocket 协议的话,该方式实现的消息推送无疑是最高效、简洁的。并且最新版本的 IE、Firefox、Chrome 等浏览器都已经支持 WebSocket 协议,Apache Tomcat 7.0.27 以后的版本也开始支持 WebSocket。
4.3 RabbitMQ Web STOMP 插件 借助于 RabbitMQ 的 Web STOMP 插件,实现浏览器与服务端的全双工通信。从本质上说,RabbitMQ 的 Web STOMP 插件也是利用 WebSocket 对 STOMP 协议进行了一次桥接,从而实现浏览器与服务端的双向通信。
4.3.1 STOMP协议 STOMP即Simple (or Streaming) Text Orientated Messaging Protocol ,简单(流)文本定向消息协议 。前身是TTMP协议(一个简单的基于文本的协议),专为消息中间件设计。它提供了一个可互操作的连接格式,允许STOMP客户端与任意STOMP消息代理(Broker)进行交互。STOMP协议由于设计简单,易于开发客户端,因此在多种语言和多种平台上得到广泛地应用。
4.3.2 插件安装 1查询所有docker 容器
image-20200305023436779 2我们进入rabbitmq容器
From: 元动力 1 docker exec -it 410a37e15588 /bin/bash
image-20200305023655720 执行下面的命令开启stomp插件
From: 元动力 1 rabbitmq-plugins enable rabbitmq_web_stomp rabbitmq_web_stomp_examples
image-20200305023810038 退出容器
image-20200305023840612 将当前的容器提交为新的镜像
From: 元动力 1 docker commit 410a37e15588 rabbitmq:stomp
image-20200305024558360 停止当前的容器
From: 元动力 1 docker stop 410a37e15588
image-20200305024622094 删除当前的容器
image-20200305024639584 根据新的镜像重新创建容器
From: 元动力 1 docker run -di --name=ydles_rabbitmq1 -p 5671:5617 -p 5672:5672 -p 4369:4369 -p 15671:15671 -p 15672:15672 -p 25672:25672 -p 15670:15670 -p 15674:15674 rabbitmq:stomp
设置容器开机自动启动
From: 元动力 1 docker update --restart=always 54b0eea9edbb
image-20200305024852480 插件已经安转好了http://192.168.200.128:15670/open in new window
image-20211222005732519 4.3.3 消息推送测试 1观察wxpay.html
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 < script src="/js/plugins/qrcode.min.js"> < /script> < script src="/js/plugins/stomp.min.js"> < /script> < script type="text/javascript" th:inline="javascript"> let qrcode = new QRCode(document.getElementById("qrcode"), { width : 240, height : 240 }); qrcode.makeCode("weixin://wxpay/bizpayurl?pr=XzofHwG"); let client = Stomp.client('ws://192.168.200.128:15674/ws'); let on_connect = function(x) { id = client.subscribe("/exchange/paynotify", function(d) { }); }; let on_error = function() { console.log('error'); }; client.connect('guest', 'guest', on_connect, on_error, '/');< /script>
所以我们需要创建一个交换机paynotify
destination 在 RabbitMQ Web STOM 中进行了相关的定义,根据使用场景的不同,主要有以下 4 种:
1./exchange/<exchangeName> 对于 SUBCRIBE frame,destination 一般为/exchange/ <exchangeName>/[/pattern] 的形式。该 destination 会创建一个唯一的、自动删除的、名为<exchangeName>的 queue,并根据 pattern 将该 queue 绑定到所给的 exchange,实现对该队列的消息订阅。 对于 SEND frame,destination 一般为/exchange/<exchangeName>/[/routingKey] 的形式。这种情况下消息就会被发送到定义的 exchange 中,并且指定了 routingKey。
2./queue/ <queueName> 对于 SUBCRIBE frame,destination 会定义 <queueName>的共享 queue,并且实现对该队列的消息订阅。 对于 SEND frame,destination 只会在第一次发送消息的时候会定义 <queueName>的共享 queue。该消息会被发送到默认的 exchange 中,routingKey 即为 <queueName>。
3./amq/queue/<queueName> 这种情况下无论是 SUBCRIBE frame 还是 SEND frame 都不会产生 queue。但如果该 queue 不存在,SUBCRIBE frame 会报错。 对于 SUBCRIBE frame,destination 会实现对队列<queueName>的消息订阅。 对于 SEND frame,消息会通过默认的 exhcange 直接被发送到队列<queueName>中。
4./topic/<topicName> 对于 SUBCRIBE frame,destination 创建出自动删除的、非持久的 queue 并根据 routingkey 为<topicName>绑定到 amq.topic exchange 上,同时实现对该 queue 的订阅。 对于 SEND frame,消息会被发送到 amq.topic exchange 中,routingKey 为<topicName>。
2 http://192.168.200.128:15672/#/exchangesopen in new window 新建交换机。名字:paynotify 。类型 :fanout。
image-20200305025600949 3修改wxpay.html
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 < script src="/js/plugins/qrcode.min.js"> < /script> < script src="/js/plugins/stomp.min.js"> < /script> < script type="text/javascript" th:inline="javascript"> let qrcode = new QRCode(document.getElementById("qrcode"), { width : 240, height : 240 }); qrcode.makeCode("weixin://wxpay/bizpayurl?pr=XzofHwG"); //自己的微信图片 let client = Stomp.client('ws://192.168.200.128:15674/ws'); let on_connect = function(x) { id = client.subscribe("/exchange/paynotify", function(d) { alert(d.body); }); }; let on_error = function() { console.log('error'); }; client.connect('guest', 'guest', on_connect, on_error, '/');< /script>
4将wxpay.html放到static中。因为对比静态页面文件夹,页面获取的是平级的js。打开页面,观察console。如果有错,将static打包,发给大家。
页面效果
image-20211222012152665 5尝试发送一条消息
点击rabbitMQ队列交换机页面的paynotify
image-20211222012132026 点击发送消息
image-20211222012123305 效果
image-20211222012108075 6新建用户
为了安全,我们在页面上不能用我们的rabbitmq的超级管理员用户guest,所以我们需要在rabbitmq中新建一个普通用户webguest(普通用户无法登录管理后台)
image-20200306075452263 设置虚拟目录权限,打开这个用户权限页:
image-20200306075534030 点击设置权限
页面用户名和密码修改
From: 元动力 1 client.connect('webguest', 'webguest', on_connect, on_error, '/');
刷新页面,还可以看到连接信息即可。
image-20200306075942810 4.4 代码实现 实现思路:后端在收到回调通知后发送订单号给mq(paynotify交换器),前端通过stomp连接到mq订阅
image-20211222013147811 1 修改notifyLogic方法,在"SUCCESS".equals(result.get("result_code"))
后添加
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 if ("SUCCESS" .equals(result.get("result_code" ))) { Map message = new HashMap (); message.put("orderId" , result.get("out_trade_no" )); message.put("transactionId" , result.get("transaction_id" )); rabbitTemplate.convertAndSend("" , RabbitMQConfig.ORDER_PAY, JSON.toJSONString(message)); rabbitTemplate.convertAndSend("paynotify" ,"" , result.get("out_trade_no" )); }
2 ydles_web_order的PayController中新增跳转支付成功接口
From: 元动力 1 2 3 4 5 6 @GetMapping("/topaysuccess") public String topaysuccess (String payMoney, Model model) { model.addAttribute("payMoney" , payMoney); return "paysuccess" ; }
image-20200423120048804 3 修改ydles_web_order项目的wxpay.html ,渲染js代码订单号和支付金额部分
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <script src="/js/plugins/qrcode.min.js" ></script> <script src="/js/plugins/stomp.min.js" ></script> <script type="text/javascript" th :inline="javascript" > let qrcode = new QRCode (document .getElementById ("qrcode" ), { width : 240 , height : 240 }); qrcode.makeCode ([[${code_url}]]); let client = Stomp .client ('ws://192.168.200.128:15674/ws' ); let on_connect = function (x ) { id = client.subscribe ("/exchange/paynotify" , function (d ) { alert (d.body ); let orderId = [[${orderId}]] if (d.body == orderId) { location.href = "/api/wxpay/toPaySuccess?payMoney=" + [[${payMoney}]] } }); }; let on_error = function ( ) { console .log ('error' ); }; client.connect ('webguest' , 'webguest' , on_connect, on_error, '/' ); </script>
(3)将paysuccess.html拷贝到templates文件夹 。
5. 测试访问路径 1重启ydles_service_pay ydles_service_order
2添加商品: http://localhost:8001/api/cart/addCart?skuId=100000006163&num=10open in new window
3查看购物车: http://localhost:8001/api/wcart/listopen in new window
4点击:结算---》提交订单--》选择微信支付--》跳到二维码页面--》扫描二维码
5 支付完成 回传订单数据
6确定后,跳转至支付成功页面
image-20211222014108729 总结:
1了解微信支付怎么做
第三方接口平台对接:第三方官网文档一步一步做
2申请支付的二维码
image-20211221144256113 3支付回推
image-20211221203416360 natapp 内网穿透
image-20211221235105274 4用户回推 支付成功消息
image-20211222013147811 image-20211222014807522 第14章 订单处理 角色 :还是订单组,资深架构师。
课程内容: 通过rabbitmq的延迟消息完成超时订单处理 完成批量发货功能,了解第三方物流系统 完成自动收货功能 1. 超时未支付订单处理-重点 1.1 需求分析 超过限定时间并未支付的订单,我们需要进行超时订单的处理:先调用微信支付api,查询该订单的支付状态。如果未支付调用关闭订单的api,并修改订单状态为已关闭,并回滚库存数。如果该订单已经支付,则做补偿操作(修改订单状态和记录)。
1.2 实现思路 如何获取超过限定时间的订单?我们可以使用延迟消息 队列(死信队列)来实现。
所谓延迟消息队列,就是消息的生产者发送的消息并不会立刻被消费,而是在设定的时间之后才可以消费。
我们可以在订单创建时发送一个延迟消息,消息为订单号,系统会在限定时间之后取出这个消息,然后查询订单的支付状态,根据结果做出相应的处理。
1.3 rabbitmq延迟消息 使用RabbitMQ来实现延迟消息必须先了解RabbitMQ的两个概念:消息的TTL 和死信Exchange ,通过这两者的组合来实现上述需求。
1.3.1 消息的TTL(Time To Live) 消息的TTL就是消息的存活时间。RabbitMQ可以对队列和消息分别设置TTL。对队列设置就是队列没有消费者连着的保留时间,也可以对每一个单独的消息做单独的设置。超过了这个时间,我们认为这个消息就死了,称之为死信。
我们创建一个队列queue.temp,在Arguments 中添加x-message-ttl 为5000 (单位是毫秒),那每一个进入这个队列的消息在5秒后会消失。
image-20211227105923204 1.3.2 死信交换器 Dead Letter Exchanges 面试:什么时候死信?
一个消息在满足如下条件下,会进死信交换机,记住这里是交换机而不是队列,一个交换机可以对应很多队列。
(1) 一个消息被Consumer拒收了,并且reject方法的参数里requeue是false。也就是说不会被再次放在队列里,被其他消费者使用。
**(2)**上面的消息的TTL到了,消息过期了 。
**(3)**队列的长度限制满了。排在前面的消息会被丢弃或者扔到死信交换机上。
Dead Letter Exchange其实就是一种普通的exchange,和创建其他exchange没有两样。只是在某一个设置Dead Letter Exchange的队列中有消息过期了,会自动触发消息的转发,发送到Dead Letter Exchange中去。
image-20211227111258327 我们现在可以测试一下延迟队列。
(1)创建死信交换器 exchange.ordertimeout (fanout )
(2)创建队列queue.ordertimeout
(3)建立死信交换器 exchange.ordertimeout 与队列queue.ordertimeout 之间的绑定
(4)创建队列queue.ordercreate,Arguments添加
x-message-ttl=10000
x-dead-letter-exchange: exchange.ordertimeout
(5)测试:向queue.ordercreate队列添加消息,等待10秒后消息从queue.ordercreate队列消失,
image-20211227111214185 image-20211227111247704 1.4 代码实现 image-20211227113439128 1.4.1 微信支付-关闭订单 image-20211227114511089 (1)WxPayController新增方法
From: 元动力 1 2 3 4 5 6 7 8 9 10 @PutMapping("/close/{orderId}") public Result closeOrder (@PathVariable String orderId) { Map map = wxPayService.closeOrder( orderId ); return new Result ( true ,StatusCode.OK,"" ,map ); }
(2)ydles_service_pay的WxPayService新增方法定义
From: 元动力 1 2 3 4 5 6 Map closeOrder (String orderId) ;
(3)ydles_service_pay的 WxPayServiceImpl实现该方法
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 @Override public Map closeOrder (String orderId) { Map map=new HashMap ( ); map.put( "out_trade_no" ,orderId ); try { return wxPay.closeOrder( map ); } catch (Exception e) { e.printStackTrace(); return null ; } }
(4)ydles_service_pay_api的WxPayFeign新增方法
From: 元动力 1 2 3 4 5 6 7 @PutMapping("/wxpay/close/{orderId}") public Result closeOrder (@PathVariable("orderId") String orderId) ;
1.4.2 微信支付-查询订单 (1)WxPayController新增方法
From: 元动力 1 2 3 4 5 6 7 8 9 10 @GetMapping("/query/{orderId}") public Result queryOrder (@PathVariable String orderId) { Map map = wxPayService.queryOrder( orderId ); return new Result ( true ,StatusCode.OK,"" ,map ); }
(2)WxPayFeign新增方法
From: 元动力 1 2 3 4 5 6 7 @GetMapping("/wxpay/query/{orderId}") public Result queryOrder (@PathVariable("orderId") String orderId) ;
商品服务回滚库存
image-20211227115532726 (1)dao
From: 元动力 1 2 3 @Update("update tb_sku set num=num+#{num},sale_num=sale_num-#{num} where id=#{skuId}") void resumeStockNum (@Param("skuId") String skuId, @Param("num") Integer num) ;
(2)service
From: 元动力 1 2 void resumeStockNum (String skuId,Integer num) ;
(3)serivceImpl
From: 元动力 1 2 3 4 @Override public void resumeStockNum (String skuId, Integer num) { skuMapper.resumeStockNum(skuId,num); }
(4)controller
From: 元动力 1 2 3 4 5 6 @PutMapping(value = "/resumeStockNum") public Result resumeStockNum (@RequestParam("skuId") String skuId, @RequestParam("num") Integer num) { skuService.resumeStockNum(skuId,num); return new Result (true ,StatusCode.OK,"回滚库存成功!" ); }
(4)skuFeign
From: 元动力 1 2 @PutMapping(value = "/sku/resumeStockNum") public Result resumeStockNum (@RequestParam("skuId") String skuId, @RequestParam("num") Integer num) ;
1.4.3 订单关闭逻辑 image-20211227121318859 如果为未支付,查询微信订单
如果确认为未支付,调用关闭本地订单( 修改订单表的订单状态、记录订单日志、恢复商品表库存)和微信订单的逻辑。
如果为已支付进行状态补偿。
(1)ydles_service_order新增依赖
From: 元动力 1 2 3 4 5 < dependency> < groupId> com.ydles< /groupId> < artifactId> ydles_service_pay_api< /artifactId> < version> 1.0-SNAPSHOT< /version> < /dependency>
(2)ydles_service_order的OrderService新增方法定义
From: 元动力 1 2 3 4 5 void closeOrder (String orderId) ;
(3)OrderServiceImpl实现该方法
实现逻辑:
1)根据id查询订单信息,判断订单是否存在,订单支付状态是否为未支付
2)基于微信查询订单支付状态
2.1)如果为success,则修改订单状态
2.2)如果为未支付,则修改订单,新增日志,恢复库存,关闭订单
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 @Autowired PayFeign payFeign;@Override @Transactional public void closeOrder (String orderId) { System.out.println("关闭订单开启了:" +orderId); Order order = orderMapper.selectByPrimaryKey(orderId); if (order==null ){ throw new RuntimeException ("这笔订单不存在!" ); } if (!order.getOrderStatus().equals("0" )){ System.out.println("这笔订单不用关闭" ); return ; } System.out.println("关闭订单逻辑通过校验:" +orderId); Map<String, String> wxQueryMap = payFeign.queryOrder(orderId).getData(); if (wxQueryMap.get("trade_state" ).equals("SUCCESS" )){ updatePayStatus(orderId,wxQueryMap.get("transaction_id" )); System.out.println("已支付" +orderId); } if (wxQueryMap.get("trade_state" ).equals("NOTPAY" )){ payFeign.closeOrder(orderId); System.out.println("本项目关闭订单了" ); order.setOrderStatus("9" ); order.setCloseTime(new Date ()); orderMapper.updateByPrimaryKeySelective(order); OrderLog orderLog = new OrderLog (); orderLog.setId(idWorker.nextId()+"" ); orderLog.setOperater("system" ); orderLog.setOperateTime(new Date ()); orderLog.setOrderId(orderId); orderLog.setOrderStatus("9" ); orderLog.setPayStatus("0" ); orderLog.setConsignStatus("0" ); orderLog.setRemarks("超时未支付!" ); orderLogMapper.insertSelective(orderLog); OrderItem orderItem=new OrderItem (); orderItem.setOrderId(orderId); List<OrderItem> orderItemList = orderItemMapper.select(orderItem); for (OrderItem orderItem1 : orderItemList) { skuFeign.resumeStock(orderItem1.getSkuId(),orderItem1.getNum()); } } }
1.4.4 延迟消息处理 image-20211227170736337 从消息队列queue.ordertimeout 中提取消息
(1)修改OrderServiceImpl的add方法,追加代码,实现mq发送
From: 元动力 1 rabbitTemplate.convertAndSend( "" ,"queue.ordercreate" , orderId);
(2)ydles_service_order新建监听类
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Component public class OrderTimeoutListener { @Autowired private OrderService orderService; @RabbitListener(queues = "queue.ordertimeout") public void closeOrder (String orderId) { System.out.println("接收到关闭订单消息:" +orderId); try { orderService.closeOrder( orderId ); } catch (Exception e) { e.printStackTrace(); } } }
测试关键点 :
1 10秒后,能不能order服务接受消息
2 微信支付了,本地order没支付 ---》下单,回调接口写错。
3 没支付---------》二维码页面,等10秒。
image-20211227172604859 2. 订单批量发货 角色:店主
2.1 批量发货业务逻辑 2.1.1 需求分析 实现批量发货的业务逻辑
image-20211227173827318 2.1.2 代码实现 (1)OrderController新增方法
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 [ { orderId: 8345 , shipping_name: 顺丰, shipping_code: 123 , ....... } , { } , { } ]
From: 元动力 1 2 3 4 5 6 7 8 9 @PostMapping("/batchSend") public Result batchSend ( @RequestBody List<Order> orders) { orderService.batchSend( orders ); return new Result ( true ,StatusCode.OK,"发货成功" ); }
(2)OrderService新增方法定义
From: 元动力 1 2 3 4 5 void batchSend (List<Order> orders) ;
(3)OrderServiceImpl实现该方法
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 @Override @Transactional public void batchSend (List<Order> orderList) { for (Order order : orderList) { if (order.getId()==null ){ throw new RuntimeException ("订单号为空!" ); } if (order.getShippingName()==null ||order.getShippingCode()==null ){ throw new RuntimeException ("物流公司或单号为空!" ); } } for (Order order : orderList) { Order queryOrder = orderMapper.selectByPrimaryKey(order.getId()); if (!queryOrder.getOrderStatus().equals("1" )||!queryOrder.getConsignStatus().equals("0" )){ throw new RuntimeException ("订单状态不对,不能发货!" ); } } for (Order order : orderList) { order.setOrderStatus("2" ); order.setConsignStatus("1" ); order.setUpdateTime(new Date ()); order.setConsignTime(new Date ()); orderMapper.updateByPrimaryKeySelective(order); OrderLog orderLog = new OrderLog (); orderLog.setId(idWorker.nextId()+"" ); orderLog.setOperater("店小二" ); orderLog.setOperateTime(new Date ()); orderLog.setOrderId(order.getId()); orderLog.setOrderStatus("2" ); orderLog.setPayStatus("1" ); orderLog.setConsignStatus("1" ); orderLog.setRemarks("批量发货" ); orderLogMapper.insertSelective(orderLog); } }
2.2 对接第三方物流(了解) 当我们在电商平台购买了商品后,一般会非常关心商品的物流轨迹。那这些信息是如何获取的呢?我们需要对接第三方的物流系统。比较常用的有菜鸟物流、快递鸟等。
我们这里推荐使用快递鸟 http://www.kdniao.comopen in new window
我们可以使用快递鸟提供的以下接口:
(1)预约取件API
预约取件API为用户提供了在线下单,预约快递员上门揽件的功能,为用户解决在线发货需求。
我们可以在实现批量发货功能后调用预约取件API
(2)即时查询API
物流查询API提供实时查询物流轨迹的服务,用户提供运单号和快递公司,即可查询当前时刻的最新物流轨迹。
用户可以在用户中心调用此API完成物流信息的查询,电商平台也可以调用此API完成运单的跟踪。
image-20211227180923988 3. 确认收货与自动收货 3.1 确认收货 3.1.1 需求分析与实现思路 当物流公司将货物送到了用户收货地址之后,需要用户点击确认收货,当用户点击了确认收货之后,会修改订单状态为已完成
3.1.2 代码实现 (1)OrderController新增方法
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 @PutMapping("/take/{orderId}/operator/{operator}") public Result take (@PathVariable String orderId, @PathVariable String operator) { orderService.take( orderId,operator ); return new Result ( true ,StatusCode.OK,"" ); }
(2)OrderService新增方法定义
From: 元动力 1 2 3 4 5 6 void confirmTask (String orderId,String operator) ;
(3)OrderServiceImpl实现该方法
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Override public void confirmTask (String orderId, String operator) { Order order = orderMapper.selectByPrimaryKey( orderId ); if (order==null ){ throw new RuntimeException ( "订单不存在" ); } if ( !"1" .equals( order.getConsignStatus() )){ throw new RuntimeException ( "订单未发货" ); } order.setConsignStatus("2" ); order.setOrderStatus( "3" ); order.setUpdateTime( new Date () ); order.setEndTime( new Date () ); orderMapper.updateByPrimaryKeySelective( order ); OrderLog orderLog=new OrderLog (); orderLog.setId( idWorker.nextId()+"" ); orderLog.setOperateTime(new Date ()); orderLog.setOperater( operator ); orderLog.setOrderStatus("3" ); orderLog.setOrderId(order.getId()); orderLogMapper.insertSelective(orderLog); }
3.2 自动收货处理 3.2.1 需求分析 如果用户在15天(可以在订单配置表中配置)没有确认收货,系统将自动收货。如何实现?我们这里采用定时任务springTask来实现。
image-20211227184231554 逻辑 :每天凌晨2点,查询order,发货15天,自动收货。
每天凌晨1点,删除生产环境,删除log。
技术:
spring--->quartz
spring task 定时任务
xxl-job
3.2.2 Cron表达式 Cron表达式是一个字符串,字符串分为七个部分,每一个域代表一个含义。
Cron表达式7个域格式为: 秒 分 小时 日 月 星期几 年
Cron表达式6个域格式为: 秒 分 小时 日 月 周
序号 说明 是否必填 允许填写的值 允许的通配符 1 秒 是 0-59 , - * / 2 分 是 0-59 , - * / 3 小时 是 0-23 , - * / 4 日 是 1-31 , - * ? / L W 5 月 是 1-12或JAN-DEC , - * / 6 星期几 是 1-7或SUN-SAT , - * ? / L W 7 年 否 empty 或1970-2099 , - * /
使用说明:
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 通配符说明: * 表示所有值. 例如:在分的字段上设置 "*",表示每一分钟都会触发。 ? 表示不指定值。使用的场景为不需要关心当前设置这个字段的值。 例如:要在每月的10号触发一个操作,但不关心是周几,所以需要周位置的那个字段设置为"?" 具体设置为 0 0 0 10 * ? - 表示区间。例如 在小时上设置 "10-12",表示 10,11,12点都会触发。 , 表示指定多个值,例如在周字段上设置 "MON,WED,FRI" 表示周一,周三和周五触发 / 用于递增触发。如在秒上面设置"5/15" 表示从5秒开始,每增15秒触发(5,20,35,50)。 在月字段上设置'1/3'所示每月1号开始,每隔三天触发一次。 L 表示最后的意思。在日字段设置上,表示当月的最后一天(依据当前月份,如果是二月还会依据是否是润年[leap]), 在周字段上表示星期六,相当于"7"或"SAT"。如果在"L"前加上数字,则表示该数据的最后一个。例如在周字段上设置"6L"这样的格式,则表示“本月最后一个星期五" W 表示离指定日期的最近那个工作日(周一至周五). 例如在日字段上设置"15W",表示离每月15号最近的那个工作日触发。如果15号正好是周六,则找最近的周五(14号)触发, 如果15号是周未,则找最近的下周一(16号)触发.如果15号正好在工作日(周一至周五),则就在该天触发。如果指定格式为 "1W",它则表示每月1号往后最近的工作日触发。如果1号正是周六,则将在3号下周一触发。(注,"W"前只能设置具体的数字,不允许区间"-"). # 序号(表示每月的第几个周几),例如在周字段上设置"6#3"表示在每月的第三个周六.注意如果指定"#5",正好第五周没有周六,则不会触发该配置(用在母亲节和父亲节再合适不过了) ;
常用表达式
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 0 0 10,14,16 * * ? 每天上午10点,下午2点,4点 0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时 0 0 12 ? * WED 表示每个星期三中午12点 "0 0 12 * * ?" 每天中午12点触发 "0 15 10 ? * *" 每天上午10:15触发 "0 15 10 * * ?" 每天上午10:15触发 "0 15 10 * * ? *" 每天上午10:15触发 "0 15 10 * * ? 2005" 2005年的每天上午10:15触发 "0 * 14 * * ?" 在每天下午2点到下午2:59期间的每1分钟触发 "0 0/5 14 * * ?" 在每天下午2点到下午2:55期间的每5分钟触发 "0 0/5 14,18 * * ?" 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发 "0 0-5 14 * * ?" 在每天下午2点到下午2:05期间的每1分钟触发 "0 10,44 14 ? 3 WED" 每年三月的星期三的下午2:10和2:44触发 "0 15 10 ? * MON-FRI" 周一至周五的上午10:15触发 "0 15 10 15 * ?" 每月15日上午10:15触发 "0 15 10 L * ?" 每月最后一日的上午10:15触发 "0 15 10 ? * 6L" 每月的最后一个星期五上午10:15触发 "0 15 10 ? * 6L 2002-2005" 2002年至2005年的每月的最后一个星期五上午10:15触发 "0 15 10 ? * 6#3" 每月的第三个星期五上午10:15触发
3.2.3 代码实现 image-20211227195336675 3.2.3.1 发送消息 (1)创建order_tack队列 。
(2)创建工程ydles_task,引入依赖
From: 元动力 1 2 3 4 5 6 7 8 9 10 < dependencies> < dependency> < groupId> org.springframework.boot< /groupId> < artifactId> spring-boot-starter< /artifactId> < /dependency> < dependency> < groupId> org.springframework.amqp< /groupId> < artifactId> spring-rabbit< /artifactId> < /dependency> < /dependencies>
(3)创建配置文件 application.yml
From: 元动力 1 2 3 4 5 6 7 server: port: 9202 spring: application: name: task rabbitmq: host: 192.168 .200 .128
(4)创建启动类 com.ydles.task
From: 元动力 1 2 3 4 5 6 7 8 @SpringBootApplication @EnableScheduling public class TaskApplication { public static void main (String[] args) { SpringApplication.run( TaskApplication.class,args ); } }
@EnableScheduling
注解用于开启任务调度
(5)创建com.ydles.task包,包下创建类OrderTask
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Component public class OrderTask { @Autowired private RabbitTemplate rabbitTemplate; @Scheduled(cron = "0 0 0 * * ?") public void autoTake () { System.out.println(new Date ( ) ); rabbitTemplate.convertAndSend( "" ,"order_tack" ,"-" ); } }
3.2.3.2 接收消息 image-20211227200800397 (1)ydles_service_order工程,编写消息监听类
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 @Component public class OrderTackListener { @Autowired private OrderService orderService; @RabbitListener(queues = "order_tack") public void autoTack (String message) { System.out.println("收到自动确认收货消息" ); orderService.autoTack(); } }
(2)OrderService新增方法定义
(3)OrderServiceImpl实现此方法
实现思路:
1)从订单配置表中获取订单自动确认期限
2)得到当前日期向前数(订单自动确认期限)天。作为过期时间节点
3)从订单表中获取过期订单(发货时间小于过期时间,且为未确认收货状态)
4)循环批量处理,执行确认收货
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @Autowired OrderConfigMapper orderConfigMapper;@Override public void autoTack () { OrderConfig orderConfig = orderConfigMapper.selectByPrimaryKey("1" ); Integer takeTimeout = orderConfig.getTakeTimeout(); LocalDate now=LocalDate.now(); LocalDate date = now.plusDays(-takeTimeout); System.out.println(date); Example example=new Example (Order.class); Example.Criteria criteria = example.createCriteria(); criteria.andEqualTo("orderStatus" ,"2" ); criteria.andLessThan("consignTime" , date); List<Order> orderList = orderMapper.selectByExample(example); for (Order order : orderList) { take(order.getId(),"system" ); } }
总结:
1超时未支付订单处理
image-20211227113439128 2订单批量发货(店主)
image-20211227173827318 3确认收货与自动收货
手动:/take/{orderId}/operator/
自动:
image-20211227184231554 第15章-秒杀前端 角色 :独立模块 秒杀模块。架构师 。
课程内容: 1 秒杀业务分析 1.1 需求分析 所谓“秒杀”,就是网络卖家发布一些超低价格的商品,所有买家在同一时间网上抢购的一种销售方式。通俗一点讲就是网络商家为促销等目的组织的网上限时抢购活动。由于商品价格低廉,往往一上架就被抢购一空,有时只用一秒钟。
秒杀商品通常有两种限制 :库存限制、时间限制。
需求:
From: 元动力 1 2 3 4 (1)秒杀频道首页列出秒杀商品 (4)点击立即抢购实现秒杀下单,下单时扣减库存。当库存为0或不在活动期范围内时无法秒杀。 (5)秒杀下单成功,直接跳转到支付页面(微信扫码),支付成功,跳转到成功页,填写收货地址、电话、收件人等信息,完成订单。 (6)当用户秒杀下单5分钟内未支付,取消预订单,调用微信支付的关闭订单接口,恢复库存。
image-20211227221818494 1.2 表结构说明 秒杀商品信息表
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 CREATE TABLE `tb_seckill_goods` ( `id` bigint (20 ) NOT NULL AUTO_INCREMENT, `goods_id` bigint (20 ) DEFAULT NULL COMMENT 'spu ID' , `item_id` bigint (20 ) DEFAULT NULL COMMENT 'sku ID' , `title` varchar (100 ) DEFAULT NULL COMMENT '标题' , `small_pic` varchar (150 ) DEFAULT NULL COMMENT '商品图片' , `price` decimal (10 ,2 ) DEFAULT NULL COMMENT '原价格' , `cost_price` decimal (10 ,2 ) DEFAULT NULL COMMENT '秒杀价格' , `seller_id` varchar (100 ) DEFAULT NULL COMMENT '商家ID' , `create_time` datetime DEFAULT NULL COMMENT '添加日期' , `check_time` datetime DEFAULT NULL COMMENT '审核日期' , `status` char (1 ) DEFAULT NULL COMMENT '审核状态,0未审核,1审核通过,2审核不通过' , `start_time` datetime DEFAULT NULL COMMENT '开始时间' , `end_time` datetime DEFAULT NULL COMMENT '结束时间' , `num` int (11 ) DEFAULT NULL COMMENT '秒杀商品数' , `stock_count` int (11 ) DEFAULT NULL COMMENT '剩余库存数' , `introduction` varchar (2000 ) DEFAULT NULL COMMENT '描述' , PRIMARY KEY (`id`) ) ENGINE= InnoDB AUTO_INCREMENT= 4 DEFAULT CHARSET= utf8;
image-20211227221948208 秒杀订单表
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 CREATE TABLE `tb_seckill_order` ( `id` bigint (20 ) NOT NULL COMMENT '主键' , `seckill_id` bigint (20 ) DEFAULT NULL COMMENT '秒杀商品ID' , `money` decimal (10 ,2 ) DEFAULT NULL COMMENT '支付金额' , `user_id` varchar (50 ) DEFAULT NULL COMMENT '用户' , `seller_id` varchar (50 ) DEFAULT NULL COMMENT '商家' , `create_time` datetime DEFAULT NULL COMMENT '创建时间' , `pay_time` datetime DEFAULT NULL COMMENT '支付时间' , `status` char (1 ) DEFAULT NULL COMMENT '状态,0未支付,1已支付' , `receiver_address` varchar (200 ) DEFAULT NULL COMMENT '收货人地址' , `receiver_mobile` varchar (20 ) DEFAULT NULL COMMENT '收货人电话' , `receiver` varchar (20 ) DEFAULT NULL COMMENT '收货人' , `transaction_id` varchar (30 ) DEFAULT NULL COMMENT '交易流水' , PRIMARY KEY (`id`) ) ENGINE= InnoDB DEFAULT CHARSET= utf8;
image-20211227222002467 1.3 秒杀需求分析==拓展内容 秒杀技术实现核心思想是运用缓存减少数据库瞬间的访问压力!读取商品详细信息时运用缓存,当用户点击抢购时减少缓存中的库存数量,当库存数为0时或活动期结束时,同步到数据库。 产生的秒杀预订单也不会立刻写到数据库中,而是先写到缓存,当用户付款成功后再写入数据库。
当然,上面实现的思路只是一种最简单的方式,并未考虑其中一些问题,例如并发状况容易产生的问题。我们看看下面这张思路更严谨的图:
1578267434606 2 秒杀商品存入缓存-重点 image-20211228160425046 秒杀商品由B端存入Mysql,设置定时任务,每隔一段时间就从Mysql中将符合条件的数据从Mysql中查询出来并存入缓存中,redis以Hash类型进行数据存储。
KEY的设计:
From: 元动力 1 2 3 4 5 6 7 8 9 10 seckill_goods_2021122812 map id SecKillGoods 1 Prada包包 3 guccy包包 seckill_goods_2021122814 map id SecKillGoods 2 lv包包 seckill_goods_2021122816 map seckill_goods_2021122818 map seckill_goods_2021122820 map
我们这里秒杀商品列表和秒杀商品详情都是从Redis中取出来的,所以我们首先要将符合参与秒杀的商品定时查询出来,并将数据存入到Redis缓存中。
数据存储类型我们可以选择Hash类型。
秒杀分页列表这里可以通过获取redisTemplate.boundHashOps(key).values()获取结果数据。
秒杀商品详情,可以通过redisTemplate.boundHashOps(key).get(key)获取详情。
2.1 秒杀服务搭建 我们将商品数据压入到Reids缓存,可以在秒杀工程的服务工程中完成,可以按照如下步骤实现:
From: 元动力 1 2 3 4 5 6 7 1.查询活动没结束的所有秒杀商品 1)状态必须为审核通过 status=1 2)商品库存个数>0 3)活动没有结束 endTime>=now() 4)在Redis中没有该商品的缓存 5)执行查询获取对应的结果集 2.将活动没有结束的秒杀商品入库
我们首先搭建一个秒杀服务工程,然后按照上面步骤实现。
搭建ydles-service-seckill,作为秒杀工程的服务提供工程。
1)新建服务ydles_service_seckill
2)添加依赖信息,详情如下:
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 < dependencies> < dependency> < groupId> com.ydles< /groupId> < artifactId> ydles_common_db< /artifactId> < version> 1.0-SNAPSHOT< /version> < /dependency> < dependency> < groupId> org.springframework.cloud< /groupId> < artifactId> spring-cloud-starter-netflix-eureka-client< /artifactId> < /dependency> < dependency> < groupId> com.ydles< /groupId> < artifactId> ydles_service_order_api< /artifactId> < version> 1.0-SNAPSHOT< /version> < /dependency> < dependency> < groupId> com.ydles< /groupId> < artifactId> ydles_service_seckill_api< /artifactId> < version> 1.0-SNAPSHOT< /version> < /dependency> < dependency> < groupId> com.ydles< /groupId> < artifactId> ydles_service_goods_api< /artifactId> < version> 1.0-SNAPSHOT< /version> < /dependency> < dependency> < groupId> org.springframework.amqp< /groupId> < artifactId> spring-rabbit< /artifactId> < /dependency> < !--oauth依赖--> < dependency> < groupId> org.springframework.cloud< /groupId> < artifactId> spring-cloud-starter-oauth2< /artifactId> < /dependency> < /dependencies>
ydles_service_seckill_api创建 依赖
From: 元动力 1 2 3 4 5 6 7 < dependencies> < dependency> < groupId> com.ydles< /groupId> < artifactId> ydles_common< /artifactId> < version> 1.0-SNAPSHOT< /version> < /dependency> < /dependencies>
添加包 com.ydles.seckill.feign com.ydles.seckill.pojo
pojo中将资料中的两个实体类放入,对应数据库的两张表
image-20211228161100576 回到ydles_service_seckill工程。添加启动类 com.ydles.seckill
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @SpringBootApplication @EnableEurekaClient @EnableFeignClients @MapperScan(basePackages = {"com.ydles.seckill.dao"}) @EnableScheduling public class SeckillApplication { public static void main (String[] args) { SpringApplication.run(SeckillApplication.class,args); } @Bean public IdWorker idWorker () { return new IdWorker (1 ,1 ); } @Bean public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<Object, Object> template = new RedisTemplate <>(); template.setConnectionFactory(redisConnectionFactory); GenericToStringSerializer genericToStringSerializer = new GenericToStringSerializer (Object.class); template.setValueSerializer(genericToStringSerializer); template.setKeySerializer(new StringRedisSerializer ()); template.afterPropertiesSet(); return template; } }
添加dao层的两个mapper
From: 元动力 1 2 public interface SeckillGoodsMapper extends Mapper <SeckillGoods> { }
From: 元动力 1 2 public interface SeckillOrderMapper extends Mapper <SeckillOrder> { }
添加application.yml
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 server: port: 9016 spring: jackson: time-zone: GMT+8 application: name: seckill datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://192.168.200.128:3306/ydles_seckill?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true&serverTimezone=GMT%2b8 username: root password: root main: allow-bean-definition-overriding: true redis: host: 192.168 .200 .128 rabbitmq: host: 192.168 .200 .128 eureka: client: service-url: defaultZone: http://127.0.0.1:6868/eureka instance: prefer-ip-address: true feign: hystrix: enabled: true client: config: default: connectTimeout: 60000 readTimeout: 20000 hystrix: command: default: execution: timeout: enabled: true isolation: strategy: SEMAPHORE thread: timeoutInMilliseconds: 20000
添加公钥 copy认证服务的公钥
添加Oauth配置类 config包
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 @Configuration @EnableResourceServer @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) public class ResourceServerConfig extends ResourceServerConfigurerAdapter { private static final String PUBLIC_KEY = "public.key" ; @Bean public TokenStore tokenStore (JwtAccessTokenConverter jwtAccessTokenConverter) { return new JwtTokenStore (jwtAccessTokenConverter); } @Bean public JwtAccessTokenConverter jwtAccessTokenConverter () { JwtAccessTokenConverter converter = new JwtAccessTokenConverter (); converter.setVerifierKey(getPubKey()); return converter; } private String getPubKey () { Resource resource = new ClassPathResource (PUBLIC_KEY); try { InputStreamReader inputStreamReader = new InputStreamReader (resource.getInputStream()); BufferedReader br = new BufferedReader (inputStreamReader); return br.lines().collect(Collectors.joining("\n" )); } catch (IOException ioe) { return null ; } } @Override public void configure (HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest(). authenticated(); } }
更改网关路径过滤类,添加秒杀工程过滤信息 image-20211228162810677 更改网关配置文件,添加请求路由转发
From: 元动力 1 2 3 4 5 6 7 - id: ydles_seckill_route uri: lb://seckill predicates: - Path=/api/seckill/** filters: - StripPrefix=1
2.2 时间操作 2.2.1 秒杀商品时间段分析 image-20211228171058257 根据产品原型图结合秒杀商品表设计可以得知,秒杀商品是存在开始时间与结束时间的,当前秒杀商品是按照秒杀时间段进行显示,如果当前时间在符合条件的时间段范围之内,则用户可以秒杀购买当前时间段之内的秒杀商品。
缓存数据加载思路:定义定时任务,每天凌晨会进行当天所有时间段秒杀商品预加载。并且在B端进行限制,添加秒杀商品的话,只能添加当前日期+1的时间限制,比如说:当前日期为8月5日,则添加秒杀商品时,开始时间必须为6日的某一个时间段,否则不能添加。
2.2.2 秒杀商品时间段计算 将资源/DateUtil.java
添加到公共服务中。基于当前工具类可以进行时间段的计算。
在该工具类中,进行时间计算测试:
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public static void main (String[] args) { List<Date> dateList = new ArrayList <>(); Date currentData = toDayStartHour(new Date ()); for (int i=0 ;i<12 ;i++){ dateList.add(addDateHour(currentData,i*2 )); } for (Date date : dateList) { SimpleDateFormat simpleDateFormat = new SimpleDateFormat ("yyyy-MM-dd HH:mm:ss" ); String format = simpleDateFormat.format(date); System.out.println(format); } }
测试结果:
image-20211228171813908 2.2.3 当前业务整体流程分析
From: 元动力 1 2 3 4 5 6 7 8 9 10 1.定时任务:查询所有符合条件的秒杀商品 1) 获取时间段集合并循环遍历出每一个时间段 2) 获取每一个时间段名称,用于后续redis中key的设置 3) 状态必须为审核通过 status=1 4) 商品库存个数>0 5) 秒杀商品开始时间>=当前时间段 6) 秒杀商品结束<当前时间段+2小时 7) 排除之前已经加载到Redis缓存中的商品数据 8) 执行查询获取对应的结果集 2.将秒杀商品存入缓存
2.3 代码实现 2.3.1 更改启动类,添加开启定时任务注解 2.3.2 时间菜单分析 image-20211227224705978 我们将商品数据从数据库中查询出来,并存入Redis缓存,但页面每次显示的时候,只显示当前正在秒杀以及往后延时2个小时、4个小时、6个小时、8个小时的秒杀商品数据。我们要做的第一个事是计算出秒杀时间菜单,这个菜单是从后台获取的。
这个时间菜单的计算我们来分析下,可以先求出当前时间的凌晨,然后每2个小时后作为下一个抢购的开始时间,这样可以分出12个抢购时间段,如下:
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 00 :00-02:00 02 :00-04:00 04 :00-06:00 06 :00-08:00 08 :00-10:00 10 :00-12:00 12 :00-14:00 14 :00-16:00 16 :00-18:00 18 :00-20:00 20 :00-22:00 22 :00-00:00
而现实的菜单只需要计算出当前时间在哪个时间段范围,该时间段范围就属于正在秒杀的时间段,而后面即将开始的秒杀时间段的计算也就出来了,可以在当前时间段基础之上+2小时、+4小时、+6小时、+8小时。
关于时间菜单的运算,在给出的DateUtil包里已经实现,代码如下:
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public static List<Date> getDateMenus(){ List<Date> dates = getDates(12 ); Date now = new Date (); for (Date cdate : dates) { if (cdate.getTime()<=now.getTime() && now.getTime()<addDateHour(cdate,2 ).getTime()){ now = cdate; break ; } } List<Date> dateMenus = new ArrayList <Date>(); for (int i = 0 ; i <5 ; i++) { dateMenus.add(addDateHour(now,i*2 )); } return dateMenus; }public static List<Date> getDates(int hours) { List<Date> dates = new ArrayList <Date>(); Date date = toDayStartHour(new Date ()); for (int i = 0 ; i <hours ; i++) { dates.add(addDateHour(date,i*2 )); } return dates; }
2.3.2 定义定时任务类 秒杀工程新建task包,并新建任务类SeckillGoodsPushTask
业务逻辑:
注意 :用到很多工具类,了解功能即可
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 @Component public class SeckillGoodsPushTask { @Autowired private SeckillGoodsMapper seckillGoodsMapper; @Autowired private RedisTemplate redisTemplate; public static final String SECKILL_GOODS_KEY = "seckill_goods_" ; @Scheduled(cron = "0/30 * * * * ?") public void loadSecKillGoodsToRedis () { List<Date> dateMenus = DateUtil.getDateMenus(); for (Date dateMenu : dateMenus) { SimpleDateFormat simpleDateFormat = new SimpleDateFormat ("yyyy-MM-dd HH:mm:ss" ); String redisExtName = DateUtil.date2Str(dateMenu); Example example = new Example (SeckillGoods.class); Example.Criteria criteria = example.createCriteria(); criteria.andEqualTo("status" , "1" ); criteria.andGreaterThan("stockCount" , 0 ); criteria.andGreaterThanOrEqualTo("startTime" , simpleDateFormat.format(dateMenu)); criteria.andLessThan("endTime" , simpleDateFormat.format(DateUtil.addDateHour(dateMenu, 2 ))); Set keys = redisTemplate.boundHashOps(SECKILL_GOODS_KEY + redisExtName).keys(); if (keys != null && keys.size() > 0 ) { criteria.andNotIn("id" , keys); } List<SeckillGoods> seckillGoodsList = seckillGoodsMapper.selectByExample(example); for (SeckillGoods seckillGoods : seckillGoodsList) { redisTemplate.opsForHash().put(SECKILL_GOODS_KEY + redisExtName, seckillGoods.getId(), seckillGoods); } } } }
测试 :
1修改一条数据,满足搜索条件
image-20211228180524421 2结果查看redis
image-20211228180537463 3 秒杀商品-首页-了解 image-20211228181401546 秒杀商品首页会显示处于秒杀中以及未开始秒杀的商品。
3.1 秒杀首页实现分析 image-20211227224737382 秒杀首页需要显示不同时间段的秒杀商品信息,然后当用户选择不同的时间段,查询该时间段下的秒杀商品,实现过程分为两大过程:
From: 元动力 1 2 1) 加载时间菜单 2)加载时间菜单下秒杀商品信息
3.1.1 加载时间菜单分析 image-20211228181433725 每2个小时就会切换一次抢购活动,所以商品发布的时候,我们将时间定格在2小时内抢购,每次发布商品的时候,商品抢购开始时间和结束时间是这2小时的边界。
每2小时会有一批商品参与抢购,所以我们可以将24小时切分为12个菜单,每个菜单都是个2小时的时间段,当前选中的时间菜单需要根据当前时间判断,判断当前时间属于哪个秒杀时间段,然后将该时间段作为选中的第1个时间菜单。
3.1.2 加载对应秒杀商品分析 image-20211228181441279 进入首页时,到后台查询时间菜单信息,然后将第1个菜单的时间段作为key,在Redis中查询秒杀商品集合,并显示到页面,页面每次点击切换不同时间段菜单的时候,都将时间段传入到后台,后台根据时间段获取对应的秒杀商品集合。
3.2 秒杀渲染服务 - 渲染秒杀首页 3.2.1 新建秒杀渲染服务 1)创建工程ydles_web_seckill,用于秒杀页面渲染
添加依赖
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 < dependencies> < dependency> < groupId> com.ydles< /groupId> < artifactId> ydles_service_seckill_api< /artifactId> < version> 1.0-SNAPSHOT< /version> < /dependency> < dependency> < groupId> org.springframework.boot< /groupId> < artifactId> spring-boot-starter-thymeleaf< /artifactId> < /dependency> < /dependencies>
添加启动类 com.ydles.seckill.webopen in new window
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 @SpringBootApplication @EnableDiscoveryClient @EnableFeignClients(basePackages = "com.ydles.seckill.feign") public class WebSecKillApplication { public static void main (String[] args) { SpringApplication.run(WebSecKillApplication.class,args); } @Bean public FeignInterceptor feignInterceptor () { return new FeignInterceptor (); } @Bean public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<Object, Object> template = new RedisTemplate <>(); template.setConnectionFactory(redisConnectionFactory); GenericToStringSerializer genericToStringSerializer = new GenericToStringSerializer (Object.class); template.setValueSerializer(genericToStringSerializer); template.setKeySerializer(new StringRedisSerializer ()); template.afterPropertiesSet(); return template; } }
添加application.yml
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 server: port: 9104 eureka: client: service-url: defaultZone: http://127.0.0.1:6868/eureka instance: prefer-ip-address: true feign: hystrix: enabled: true spring: jackson: time-zone: GMT+8 thymeleaf: cache: false application: name: seckill-web main: allow-bean-definition-overriding: true redis: host: 192.168 .200 .128 hystrix: command: default: execution: timeout: enabled: true isolation: strategy: SEMAPHORE thread: timeoutInMilliseconds: 60000 ribbon: ReadTimeout: 4000 ConnectTimeout: 3000
添加静态化资源 image-20211228181821971 6)对接网关
From: 元动力 1 2 3 4 5 6 7 - id: ydles_seckill_web_route uri: lb://seckill-web predicates: - Path=/api/wseckillgoods/** filters: - StripPrefix=1
3.3 时间菜单实现 时间菜单显示,先运算出每2小时一个抢购,就需要实现12个菜单,可以先计算出每个时间的临界值,然后根据当前时间判断需要显示12个时间段菜单中的哪个菜单,再在该时间菜单的基础之上往后挪4个菜单,一直显示5个时间菜单。
3.3.1 时间菜单获取 ydles_web_seckill com.ydles.seckill.web.controller 新增控制类SecKillGoodsController
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 @Controller @RequestMapping("/wseckillgoods") public class SecKillGoodsController { @Autowired private SecKillGoodsFeign secKillGoodsFeign; @RequestMapping("/toIndex") public String toIndex () { return "seckill-index" ; } @RequestMapping("/timeMenus") @ResponseBody public List<String> dateMenus(){ List<Date> dateMenus = DateUtil.getDateMenus(); List<String> result = new ArrayList <>(); SimpleDateFormat simpleDateFormat = new SimpleDateFormat ("yyyy-MM-dd HH:mm:ss" ); for (Date dateMenu : dateMenus) { String format = simpleDateFormat.format(dateMenu); result.add(format); } return result; } }
3.3.2 页面加载时间菜单 修改seckill-index.html
image-20211228183316377
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 var app = new Vue ({ el : '#app' , data ( ) { return { goodslist : [], dateMenus :[] } }, methods :{ loadMenus :function ( ) { axios.get ("/api/wseckillgoods/timeMenus" ).then (function (response ) { app.dateMenus =response.data ; }) } }, created :function ( ) { this .loadMenus (); } })
1查看以下代码
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 < div class="item-time active" v-for="(item,index) in dateMenus"> < div class="time-clock"> {{item}}< /div> < div class="time-state-on"> < span class="on-text" v-if="index==0"> 快抢中< /span> < span class="on-over" v-if="index==0"> 距离结束:01:02:03< /span> < span class="on-text" v-if="index> 0"> 即将开始< /span> < span class="on-over" v-if="index> 0"> 距离开始:03:02:01< /span> < /div> < /div>
2通过网关访问,需要将 moment.min.js 放入网关的静态资源中
image-20211228183457892 3启动web-seckill 重启,访问 http://localhost:8001/api/wseckillgoods/toIndexopen in new window
效果如下:
image-20211228184523710 3.3.3 时间格式化 上面菜单循环输出后,会出现如上图效果,时间格式全部不对,我们需要引入一个moment.min.js来格式化时间。
1)引入moment.min.js
2)添加过滤器
From: 元动力 1 2 3 4 Vue .filter ("dateFilter" , function (date, formatPattern ){ return moment (date).format (formatPattern || "YYYY-MM-DD HH:mm:ss" ); });
取值格式化
From: 元动力 1 < div class="time-clock"> {{item | dateFilter('HH:mm')}}< /div>
重启webseckill,重新访问:http://localhost:8001/api/wseckillgoods/toIndexopen in new window 。时间菜单效果如下
image-20211228220700312 3.3.4 选中实现 -了解 3.3.4.1 思路分析 image-20211227225123244 根据原型图,是让当前第一个时间菜单为选中状态,并且加载第一个菜单对应的数据。
我们可以先定义一个ctime=0,用来记录当前选中的菜单下标,因为默认第一个选中,第一个下标为0,所以初始值为0,每次点击对应菜单的时候,将被点击的菜单的下标值赋值给ctime,然后在每个菜单上判断,下标=ctime则让该菜单选中。
3.3.4.2 代码实现 1)定义ctime=0
From: 元动力 1 2 3 4 5 6 7 8 9 10 var app = new Vue ({ el : '#app' , data ( ) { return { goodslist : [], dateMenus :[], ctime :0 } } })
2)页面样式控制:
From: 元动力 1 2 3 4 5 6 7 8 9 10 < div class="item-time " v-for="(item,index) in dateMenus" :class="['item-time',index==ctime?'active':'']" @click="ctime=index;"> < div class="time-clock"> {{item | dateFilter('HH:mm')}}< /div> < div class="time-state-on"> < span class="on-text" v-if="index==0"> 快抢中< /span> < span class="on-over" v-if="index==0"> 距离结束:01:02:34< /span> < span class="on-text" v-if="index> 0"> 即将开始< /span> < span class="on-over" v-if="index> 0"> 距离开始:01:02:34< /span> < /div> < /div>
重启webseckill,重新访问:http://localhost:8001/api/wseckillgoods/toIndexopen in new window 。时间菜单效果如下
image-20211228221742713 3.3.5 倒计时实现 3.3.5.1 倒计时实现 3.3.5.1.1 基础数据显示 定义一个集合,用于存放五个时间段的倒计时时间差,集合中每一个角标都对应一个倒计时时间差,比如:集合角标为0,对应第一个倒计时时间差。集合角标为1,对应第二个倒计时时间差,依次类推。
image-20211228233511074
From: 元动力 1 alltimes:[5555555,66666666,77777777,888888888,9999999999]
2从该集合中获取内容,并更新倒计时时间
image-20211228233617374
From: 元动力 1 2 <span class="on-over" v-if="index==0">距离结束:{{alltimes[index]}}</span> <span class="on-over" v-if="index>0">距离开始:{{alltimes[index]}}</span>
重启webseckill,重新访问:http://localhost:8001/api/wseckillgoods/toIndexopen in new window 。时间菜单效果如下
image-20211228222758434 3.3.5.1.2 每个时间差倒计时实现 周期执行函数用法如下:
From: 元动力 1 window.setInterval(function(){//要做的事},1000);
结束执行周期函数用法如下:
From: 元动力 1 window.clearInterval(timers);
具体代码如下:
image-20211228233649926
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 let timers = window .setInterval (function ( ) { for (var i=0 ;i<app.alltimes .length ;i++){ app.$set(app.alltimes ,i,app.alltimes [i]-1000 ); if (app.alltimes [i]<=0 ){ window .clearInterval (timers); app.loadMenus (); } } },1000 );
重启webseckill,重新访问:http://localhost:8001/api/wseckillgoods/toIndexopen in new window 。时间菜单效果如下可以发现每一个时间段的时间都在每秒递减。
image-20211228223420084 3.3.5.1.3 倒计时时间格式化 将此工具引入页面js方法中,用于时间计算。注意和loadMenus方法平行
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 timedown :function (num ) { var oneSecond = 1000 ; var oneMinute=oneSecond*60 ; var oneHour=oneMinute*60 var hours =Math .floor (num/oneHour); var minutes=Math .floor ((num%oneHour)/oneMinute); var seconds=Math .floor ((num%oneMinute)/oneSecond); var str = hours+':' +minutes+':' +seconds; return str; }
修改时间差显示设置
image-20211228233710801
From: 元动力 1 2 3 4 5 6 7 < div class="time-state-on"> < span class="on-text" v-if="index==0"> 快抢中< /span> < span class="on-over" v-if="index==0"> 距离结束:{{timedown(alltimes[index])}}< /span> < span class="on-text" v-if="index> 0"> 即将开始< /span> < span class="on-over" v-if="index> 0"> 距离开始:{{timedown(alltimes[index])}}< /span> < /div>
重新访问进行测试。效果如下:
image-20211228223724484 3.3.5.1.4 正确倒计时时间显示 现在页面中,对于倒计时时间集合内的数据,暂时写的为假数据,现在需要让集合内容的数据是经过计算得出的。第一个是距离结束时间倒计时,后面的4个都是距离开始倒计时,每个倒计时其实就是2个时差,计算方式如下:
From: 元动力 1 2 3 4 5 第1个时差:第2个抢购开始时间-当前时间,距离结束时间 第2个时差:第2个抢购开始时间-当前时间,距离开始时间 第3个时差:第3个抢购开始时间-当前时间,距离开始时间 第4个时差:第4个抢购开始时间-当前时间,距离开始时间 第5个时差:第5个抢购开始时间-当前时间,距离开始时间
image-20211228233749415
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 loadMenus :function ( ) { axios.get ("/wseckill/timeMenus" ).then (function (response ) { app.dateMenus =response.data ; 353 -362 for (var i=0 ;i<app.dateMenus .length ;i++){ if (i==0 ){ var x =i+1 ; app.$set(app.alltimes ,i,new Date (app.dateMenus [x]).getTime ()-new Date ().getTime ()); } else { app.$set(app.alltimes ,i,new Date (app.dateMenus [i]).getTime ()-new Date ().getTime ()); } } let timers = window .setInterval (function ( ) { for (var i=0 ;i<app.alltimes .length ;i++){ app.$set(app.alltimes ,i,app.alltimes [i]-1000 ); } },1000 ); }) }
清空alltimes数据
重启webseckill,重新访问:http://localhost:8001/api/wseckillgoods/toIndexopen in new window 。
image-20211228224140244 3.4 加载秒杀商品实现 image-20211228224817607 当前已经完成了秒杀时间段菜单的显示,那么当用户在切换不同的时间段的时候,需要按照用户所选择的时间去显示相对应时间段下的秒杀商品。
3.4.1 秒杀服务-查询秒杀商品列表 image-20211228224907652 3.4.1.1 秒杀服务- service
From: 元动力 1 2 3 4 public interface SecKillGoodsService { List<SeckillGoods> list(String time); }
实现类
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Service public class SecKillGoodsServiceImpl implements SecKillGoodsService { @Autowired private RedisTemplate redisTemplate; private static final String SECKILL_KEY = "seckill_goods_" ; @Override public List<SeckillGoods> list(String time) { return redisTemplate.boundHashOps(SECKILL_KEY+time).values(); } }
3.4.1.2 秒杀服务-controller
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @RestController @RequestMapping("/seckillgoods") public class SecKillController { @Autowired private SecKillGoodsService secKillGoodsService; @RequestMapping("/list") public Result<List<SeckillGoods>> list(@RequestParam("time") String time){ List<SeckillGoods> seckillGoodsList = secKillGoodsService.list(time); return new Result <List<SeckillGoods>>(true , StatusCode.OK,"查询秒杀商品成功" ,seckillGoodsList); } }
3.4.1.3 杀服务Api- feign接口定义
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 @FeignClient(name="seckill") public interface SecKillFeign { @RequestMapping("/seckillgoods/list") public Result<List<SeckillGoods>> list(@RequestParam("time") String time); }
3.4.1.4 查询秒杀商品放行-想想为什么 更改秒杀微服务的ResourceServerConfig类,对查询方法放行
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 @Override public void configure (HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers( "/seckillgoods/list/**" ). permitAll() .anyRequest(). authenticated(); }
3.4.2 秒杀渲染服务-查询秒杀商品列表 image-20211228230009990 oauth2 :1 导包 2公钥 3配置类
调用feign:1导包 api 2EnableFeignClients 3feign拦截器
3.4.2.1 更新ydles_web_seckill的启动类 添加feign接口扫描
From: 元动力 1 @EnableFeignClients(basePackages = "com.ydles.seckill.feign")
3.4.2.2 更新ydles_web_seckill的SecKillGoodsController 注入secKillFeign,并添加获取秒杀商品列表方法实现
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 @RequestMapping("/list") @ResponseBody public Result<List<SeckillGoods>> list(String time){ String timeStr = DateUtil.formatStr(time); System.out.println(timeStr); return secKillGoodsFeign.list(timeStr); }
3.4.2.3 更新secKill-index.html。添加按照时间查询方法 image-20200310181130832
From: 元动力 1 2 3 4 5 6 7 8 searchList:function (time) { axios.get('/api/wseckillgoods/list?time=' +time).then(function (response) { if (response.data.flag){ app.goodslist = response.data.data; } }) }
3.4.2.4 更新secKill-index.html。 加载页面时,默认当前时间查询 image-20211228233810290
From: 元动力 1 2 app.searchList(app.dateMenus[0 ]);
3.4.2.5 更新secKill-index.html。切换时间菜单,查询秒杀商品 image-20211228233831720
From: 元动力 1 2 3 4 5 < div class="item-time " v-for="(item,index) in dateMenus" :class="['item-time',index==ctime?'active':'']" @click="ctime=index;searchList(item)"> < /div>
重启秒杀和秒杀页面服务, http://localhost:8001/api/wseckillgoods/toIndexopen in new window
image-20211228232748991 3.5 抢购按钮 因为当前业务设定为用户秒杀商品为sku,所以当用户点击立即抢购按钮的时候,则直接进行下单操作。
3.5.1 js定义 在秒杀首页添加下单方法
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 add:function(id){ app.msg ='正在下单' ; axios.get("/api/wseckillorder/add?time=" +moment(app.dateMenus[0 ]).format("YYYYMMDDHH" )+"&id=" +id).then(function (response) { if (response.data.flag){ app.msg='抢单成功,即将进入支付!' ; }else { app.msg='抢单失败' ; } }) }
3.5.2 调用下单方法 修改抢购按钮,添加事件
From: 元动力 1 < a class='sui-btn btn-block btn-buy' href='javascript:void(0)' @click="add(item.id)"> 立即抢购< /a>
3.5.3 秒杀web页面 新增Controller SecKillOrderController
From: 元动力 1 2 3 4 5 6 7 8 9 10 @RestController @RequestMapping("/wseckillorder") public class SecKillOrderController { @RequestMapping("/add") public Result add (@RequestParam("time") String time, @RequestParam("id") Long id) { System.out.println("进入秒杀订单逻辑了!" ); return null ; } }
网关配置:
From: 元动力 1 2 3 4 5 6 7 #秒杀渲染微服务 - id: ydles_seckill_web_route uri: lb://seckill-web predicates: - Path=/api/wseckillgoods/**,/api/wseckillorder/** filters: - StripPrefix=1
测试:重启秒杀页面,网关服务, 访问http://localhost:8001/api/wseckillgoods/toIndexopen in new window ,点击立即抢购
image-20211228235033816 后台打印:
image-20211228235038565 总结:
1秒杀业务
三高:
控制:时间段 数量
单独:数据库 微服务 页面
2秒杀商品 存入缓存
image-20211228160425046 3首页
image-20211228235345701 第16章-秒杀后端 角色:架构师
测试:功能测试 压力测试 1万/s 20万/s
1实现秒杀异步 下单,掌握如何保证生产者&消费者消息不丢失
2实现防止恶意刷单
3实现防止相同商品重复秒杀
4实现秒杀下单接口隐藏
5实现下单接口限流
1 秒杀异步下单-重点-难点 用户在下单的时候,需要基于JWT令牌信息进行登陆人信息认证,确定当前订单是属于谁的。
为什么要异步下单 :针对秒杀的特殊业务场景,仅仅依靠对象缓存或者页面静态化等技术去解决服务端压力还是远远不够。对于数据库压力还是很大,所以需要异步下单,异步是最好的解决办法,但会带来一些额外的程序上的复杂性 。
流程:异步 service_seckill接收下单消息---》MQ ------》service_consume 完成剩余操作
image-20220102104824795 1.1 秒杀服务-下单实现 image-20220102115218841 1)将tokenDecode工具类从order工程放入秒杀服务并声明Bean, ydles_service_seckill服务
image-20220102115503524 启动类添加
From: 元动力 1 2 3 4 @Bean public TokenDecode tokenDecode () { return new TokenDecode (); }
2)新建下单controller并声明方法
ydles_service_seckill服务
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @RestController @CrossOrigin @RequestMapping("/seckillorder") public class SecKillOrderController { @Autowired private TokenDecode tokenDecode; @Autowired private SecKillOrderService secKillOrderService; @RequestMapping("/add") public Result add (@RequestParam("time") String time, @RequestParam("id") Long id) { String username = tokenDecode.getUserInfo().get("username" ); boolean result = secKillOrderService.add(id,time,username); if (result){ return new Result (true , StatusCode.OK,"下单成功" ); }else { return new Result (false ,StatusCode.ERROR,"下单失败" ); } } }
3) 新建service接口
ydles_service_seckill服务
From: 元动力 1 2 3 4 5 6 7 8 9 10 public interface SecKillOrderService { boolean add (Long id, String time, String username) ; }
image-20220102121935545 4)更改预加载秒杀商品
当预加载秒杀商品的时候,提前加载每一个商品的库存信息,后续减库存操作也会先预扣减缓存中的库存再异步扣减mysql数据 。
预扣减库存会基于redis原子性操作实现
image-20220102122324659
From: 元动力 1 2 3 4 5 6 public static final String SECKILL_GOODS_STOCK_COUNT_KEY="seckill_goods_stock_count_" ; redisTemplate.opsForValue().set(SECKILL_GOODS_STOCK_COUNT_KEY + seckillGoods.getId(), seckillGoods.getStockCount());
5)秒杀下单业务层实现
业务逻辑:
获取秒杀商品数据与库存量数据,如果没有库存则抛出异常
预扣减库存,如果扣完库存量<0,删除商品数据与库存数据
如果库存量>=0,创建秒杀订单,并存入redis
基于mq异步方式完成与mysql数据同步(最终一致性)
注意:库存数据从redis中取出,转换成String
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 @Service public class SecKillOrderServiceImpl implements SecKillOrderService { @Autowired RedisTemplate redisTemplate; @Autowired IdWorker idWorker; public static final String SECKILL_GOODS_KEY = "seckill_goods_" ; public static final String SECKILL_GOODS_STOCK_COUNT_KEY = "seckill_goods_stock_count_" ; @Override public boolean add (Long id, String time, String username) { SeckillGoods seckillGoods = (SeckillGoods) redisTemplate.boundHashOps(SECKILL_GOODS_KEY + time).get(id); if (seckillGoods == null ) { return false ; } String redisStock = (String) redisTemplate.boundValueOps(SECKILL_GOODS_STOCK_COUNT_KEY + id).get(); if (StringUtils.isEmpty(redisStock)) { return false ; } int stock = Integer.parseInt(redisStock); if (stock <= 0 ) { return false ; } Long decrement = redisTemplate.opsForValue().decrement(SECKILL_GOODS_STOCK_COUNT_KEY + id); if (decrement<=0 ){ redisTemplate.boundHashOps(SECKILL_GOODS_KEY + time).delete(id); redisTemplate.delete(SECKILL_GOODS_STOCK_COUNT_KEY + id); } SeckillOrder seckillOrder=new SeckillOrder (); seckillOrder.setId(idWorker.nextId()); seckillOrder.setSeckillId(id); seckillOrder.setMoney(seckillGoods.getCostPrice()); seckillOrder.setUserId(username); seckillOrder.setSeckillId(Long.parseLong(seckillGoods.getSellerId())); seckillOrder.setCreateTime(new Date ()); seckillOrder.setStatus("0" ); return false ; } }
1.2 生产者保证消息不丢失--面试常问问题-重点 image-20220102172807666 1rabbitMQ 工作流程。
生产者:tcp --->channel--> exchang
消费者:tcp ------>channal------>queue 一个消费者监听一个 队列
绑定关系:交换机类型----绑定到队列----routingkey
2持久化
按照现有rabbitMQ的相关知识,生产者会发送消息到达消息服务器。但是在实际生产环境下,消息生产者发送的消息很有可能当到达了消息服务器之后,由于消息服务器的问题导致消息丢失,如宕机。因为消息服务器默认会将消息存储在内存中。一旦消息服务器宕机,则消息会产生丢失。因此要保证生产者的消息不丢失,要开始持久化策略 。
From: 元动力 1 2 3 4 rabbitMQ持久化: 1. 交换机持久化 2. 队列持久化 3. 消息持久化
3rabbitmq数据保护机制
但是如果仅仅只是开启这两部分的持久化,也很有可能造成消息丢失。因为消息服务器很有可能在持久化的过程中出现宕机。因此需要通过数据保护机制来保证消息一定会成功进行持久化,否则将一直进行消息发送。
From: 元动力 1 2 3 4 5 6 7 8 RabbitMQ数据保护机制 1 事务机制 事务机制采用类数据库的事务机制进行数据保护,当消息到达消息服务器,首先会开启一个事务,接着进行数据磁盘持久化,只有持久化成功才会进行事务提交,向消息生产者返回成功通知,消息生产者一旦接收成功通知则不会再发送此条消息。当出现异常,则返回失败通知.消息生产者一旦接收失败通知,则继续发送该条消息。 事务机制虽然能够保证数据安全,但是此机制采用的是同步机制,会产生系统间消息阻塞,影响整个系统的消息吞吐量。从而导致整个系统的性能下降,因此不建议使用。 2 confirm机制 confirm模式需要基于channel进行设置, 一旦某条消息被投递到队列之后,消息队列就会发送一个确认信息给生产者,如果队列与消息是可持久化的, 那么确认消息会等到消息成功写入到磁盘之后发出. confirm的性能高,主要得益于它是异步的.生产者在将第一条消息发出之后等待确认消息的同时也可以继续发送后续的消息.当确认消息到达之后,就可以通过回调方法处理这条确认消息. 如果MQ服务宕机了,则会返回nack消息. 生产者同样在回调方法中进行后续处理。
image-20220102174038302 拓展资料: https://www.cnblogs.com/vipstone/p/9350075.htmlopen in new window
4即使我们设计得这么完美,也不能保证数据100%全部发送、接受。
99.9999999% 机器运行成功率。 aws--->paypal youtube
1.2.1 开启confirm机制 ydles_service_seckill服务
1)更改秒杀服务配置文件
From: 元动力 1 2 3 rabbitmq: host: 192.168 .200 .128 publisher-confirms: true
2)开启队列持久化 rabbit配置文件
rabbitMq使用:1导包 2配置文件 3配置类
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Configuration public class RabbitMQConfig { public static final String SECKILL_ORDER_QUEUE="seckill_order" ; @Bean public Queue queue () { return new Queue (SECKILL_ORDER_QUEUE,true ); } }
3)消息持久化源码查看
image-20220102175146418 image-20220102175205923 image-20220102180304161 image-20220102180334328 image-20220102180355716 的确,在消息convert转换的时候,设置属性是持久化的。
**4)增强rabbitTemplate ** config包下新建。重要:此类相当于我们手动完成confirm逻辑!
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 @Component public class ConfirmMessageSender implements RabbitTemplate .ConfirmCallback{ @Autowired RabbitTemplate rabbitTemplate; @Autowired RedisTemplate redisTemplate; public static final String MESSAGE_CONFIRM_KEY="message_confirm_" ; public ConfirmMessageSender (RabbitTemplate rabbitTemplate) { this .rabbitTemplate=rabbitTemplate; rabbitTemplate.setConfirmCallback(this ); } @Override public void confirm (CorrelationData correlationData, boolean ack, String cause) { if (ack){ redisTemplate.delete(correlationData.getId()); redisTemplate.delete(MESSAGE_CONFIRM_KEY+correlationData.getId()); }else { Map<String, String> map = redisTemplate.boundHashOps(MESSAGE_CONFIRM_KEY + correlationData.getId()).entries(); String exchange = map.get("exchange" ); String routingKey = map.get("routingKey" ); String message = map.get("message" ); rabbitTemplate.convertAndSend(exchange,routingKey,message); } } public void send (String exchange,String routingKey,String message) { CorrelationData correlationData=new CorrelationData (UUID.randomUUID().toString()); redisTemplate.boundValueOps(correlationData.getId()).set(message); Map<String, String> map=new HashMap <>(); map.put("exchange" ,exchange); map.put("routingKey" ,routingKey); map.put("message" ,message); redisTemplate.boundHashOps(MESSAGE_CONFIRM_KEY+correlationData.getId()).putAll(map); rabbitTemplate.convertAndSend(exchange,routingKey,message,correlationData); } }
1.2.2发送消息 更改下单业务层实现
ydles_service_seckill服务SecKillOrderServiceImpl类
From: 元动力 1 2 @Autowired private ConfirmMessageSender confirmMessageSender;
From: 元动力 1 2 confirmMessageSender.sendMessage("" , RabbitMQConfig.SECKILL_ORDER_KEY, JSON.toJSONString(seckillOrder));
测试:三个关键位置打断点:
1add方法
image-20220102183240326 2confirmSender 的send
image-20220102183257423 3confirmSender 回调方法
image-20220102183310057 4基于网关访问 修改网关路由
From: 元动力 1 2 3 4 5 6 7 #秒杀微服务 - id: ydles_seckill_route uri: lb://seckill predicates: - Path=/api/seckill/**,/api/seckillorder/** filters: - StripPrefix=1
5重启网关、秒杀服务。
登陆: http://localhost:8001/api/oauth/toLoginopen in new window
根据时间 模拟秒杀: http://localhost:8001/api/seckillorder/add?time=2022-01-02 18:00:00&id=1open in new window
1.3 秒杀下单服务-更新库存库 1.3.1 异步下单服务ydles_service_consume 1)添加依赖
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 < dependencies> < dependency> < groupId> com.ydles< /groupId> < artifactId> ydles_common_db< /artifactId> < version> 1.0-SNAPSHOT< /version> < /dependency> < dependency> < groupId> org.springframework.cloud< /groupId> < artifactId> spring-cloud-starter-netflix-eureka-client< /artifactId> < /dependency> < dependency> < groupId> com.ydles< /groupId> < artifactId> ydles_service_order_api< /artifactId> < version> 1.0-SNAPSHOT< /version> < /dependency> < dependency> < groupId> com.ydles< /groupId> < artifactId> ydles_service_seckill_api< /artifactId> < version> 1.0-SNAPSHOT< /version> < /dependency> < dependency> < groupId> com.ydles< /groupId> < artifactId> ydles_service_goods_api< /artifactId> < version> 1.0-SNAPSHOT< /version> < /dependency> < dependency> < groupId> org.springframework.amqp< /groupId> < artifactId> spring-rabbit< /artifactId> < /dependency> < /dependencies>
2)新建application.yml
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 server: port: 9022 spring: jackson: time-zone: GMT+8 application: name: sec-consume datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://192.168.200.128:3306/ydles_seckill?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true&serverTimezone=GMT%2b8 username: root password: root main: allow-bean-definition-overriding: true redis: host: 192.168 .200 .128 rabbitmq: host: 192.168 .200 .128 eureka: client: service-url: defaultZone: http://127.0.0.1:6868/eureka instance: prefer-ip-address: true feign: hystrix: enabled: true client: config: default: connectTimeout: 60000 readTimeout: 20000 hystrix: command: default: execution: timeout: enabled: true isolation: strategy: SEMAPHORE thread: timeoutInMilliseconds: 20000
3)新建启动类 com.ydles.consume
From: 元动力 1 2 3 4 5 6 7 8 9 @SpringBootApplication @EnableDiscoveryClient @MapperScan(basePackages = {"com.ydles.consume.dao"}) public class OrderConsumerApplication { public static void main (String[] args) { SpringApplication.run(OrderConsumerApplication.class,args); } }
1.3.2 消费者手动ACK下单实现--面试重点-认真听 image-20220102191752824 按照现有RabbitMQ知识,可以得知当消息消费者成功接收到消息后,会进行消费并自动通知消息服务器将该条消息删除。此种方式的实现使用的是消费者自动应答机制。但是此种方式非常的不安全。
在生产环境下,当消息消费者接收到消息,很有可能在处理消息的过程中出现意外情况从而导致消息丢失,因为如果使用自动应答机制是非常不安全。
我们需要确保消费者当把消息成功处理完成之后,消息服务器才会将该条消息删除。此时要实现这种效果的话,就需要将自动应答转换为手动应答 ,只有在消息消费者将消息处理完,才会通知消息服务器将该条消息删除。
1)更改配置文件
From: 元动力 1 2 3 4 5 rabbitmq: host: 192.168 .200 .128 listener: simple: acknowledge-mode: manual
mq配置类
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Configuration public class RabbitMQConfig { public static final String SECKILL_ORDER_QUEUE="seckill_order" ; @Bean public Queue queue () { return new Queue (SECKILL_ORDER_QUEUE,true ); } }
2)定义监听类 回顾 rabbitMQ与springboot整合时,形参Channel channel, Message message意义。
com.ydles.consumer.listener
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 @Component public class SecKillOrderListener { @Autowired SeckillOrderService seckillOrderService; @RabbitListener(queues = RabbitMQConfig.SECKILL_ORDER_QUEUE) public void receiveMsg (Message message, Channel channel) { String msgStr = new String (message.getBody()); System.out.println("接受到了秒杀订单" +msgStr); SeckillOrder seckillOrder = JSON.parseObject(message.getBody(), SeckillOrder.class); int result = seckillOrderService.createOrder(seckillOrder); if (result>0 ){ try { channel.basicAck(message.getMessageProperties().getDeliveryTag(),false ); } catch (IOException e) { e.printStackTrace(); } }else { try { channel.basicNack(message.getMessageProperties().getDeliveryTag(),false ,true ); } catch (IOException e) { e.printStackTrace(); } } } }
3)定义业务层接口与实现类
image-20220103094738284
From: 元动力 1 2 3 public interface SecKillOrderService { int createOrder (SeckillOrder seckillOrder) ; }
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 @Service public class SecKillOrderServiceImpl implements SecKillOrderService { @Autowired private SeckillGoodsMapper seckillGoodsMapper; @Autowired private SeckillOrderMapper seckillOrderMapper; @Override @Transactional public int createOrder (SeckillOrder seckillOrder) { int result = seckillGoodsMapper.updateStockCountById(seckillOrder.getId()); if (result<=0 ){ return result; } result = seckillOrderMapper.insertSelective(seckillOrder); if (result<=0 ){ return result; } return result; } }
4)dao层两个mapper从秒杀服务copy
5)启动类扫包
From: 元动力 1 @MapperScan(basePackages = {"com.ydles.consumer.dao"})
6)完善 SeckillGoodsMapper
From: 元动力 1 2 3 4 public interface SeckillGoodsMapper extends Mapper <SeckillGoods> { @Update("update tb_seckill_goods set stock_count=stock_count-1 where id=#{id} and stock_count>=1") int updateStockCount (@Param("id") Long id) ; }
From: 元动力 1 2 3 4 数据库字段unsigned介绍 unsigned-----无符号,修饰int 、char ALTER TABLE tb_seckill_goods MODIFY COLUMN stock_count int(11) UNSIGNED DEFAULT NULL COMMENT '剩余库存数';
1.5 流量削峰 在秒杀这种高并发的场景下,每秒都有可能产生几万甚至十几万条消息,如果没有对消息处理量进行任何限制的话,很有可能因为过多的消息堆积从而导致消费者宕机的情况。因此官网建议对每一个消息消费者都设置处理消息总数(消息抓取总数 )。
image-20220103100123839 消息抓取总数的值,设置过大或者过小都不好,过小的话,会导致整个系统消息吞吐能力下降,造成性能浪费。过大的话,则很有可能导致消息过多,导致整个系统OOM(out of memory)内存溢出。因此官网建议每一个消费者将该值设置在100-300 之间。
1)更新消费者。
ydles_service_consume服务SecKillOrderListener监听器
From: 元动力 1 2 3 4 5 6 7 8 try { channel.basicQos(300 ); } catch (IOException e) { e.printStackTrace(); }
1.6 秒杀渲染服务-下单实现 image-20220103100219129 1)ydles_service_seckill_api服务定义feign接口
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 @FeignClient(name="seckill") public interface SecKillOrderFeign { @RequestMapping("/seckillorder/add") public Result add (@RequestParam("time") String time, @RequestParam("id") Long id) ; }
2)ydles_web_seckill服务定义controller
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 @RestController @RequestMapping("/wseckillorder") public class SecKillOrderController { @Autowired private SecKillOrderFeign secKillOrderFeign; @RequestMapping("/add") public Result add (@RequestParam("time") String time, @RequestParam("id") Long id) { Result result = secKillOrderFeign.add(time, id); return result; } }
测试: 重启相关服务,登陆,秒杀 http://localhost:8001/api/wseckillgoods/toIndexopen in new window
查看秒杀商品表和秒杀订单表数据变化。redis变化。
image-20220103101942966 image-20220103101928814 1.7秒杀商品真实数量获取 需求:
image-20220103102444911 1从秒杀任务类中拿出 redis秒杀商品库存头
From: 元动力 1 2 public static final String SECKILL_GOODS_STOCK_COUNT_KEY="seckill_goods_stock_count_" ;
2修改获取数据的业务方法 SecKillGoodsServiceImpl
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @Service public class SecKillGoodsServiceImpl implements SecKillGoodsService { @Autowired private RedisTemplate redisTemplate; private static final String SECKILL_KEY = "seckill_goods_" ; public static final String SECKILL_GOODS_STOCK_COUNT_KEY="seckill_goods_stock_count_" ; @Override public List<SeckillGoods> list(String time) { List<SeckillGoods> list = redisTemplate.boundHashOps(SECKILL_KEY + time).values(); for (SeckillGoods seckillGoods : list) { String value = (String) redisTemplate.opsForValue().get(SECKILL_GOODS_STOCK_COUNT_KEY+seckillGoods.getId()); seckillGoods.setStockCount(Integer.parseInt(value)); } return list; } }
测试: 重启相关服务,登陆,秒杀 http://localhost:8001/api/wseckillgoods/toIndexopen in new window
image-20220103102806192 2 防止恶意刷单解决 在生产场景下,很有可能会存在某些用户恶意刷单的情况出现。这样的操作对于系统而言,会导致业务出错、脏数据、后端访问压力大等问题的出现。
一般要解决这个问题的话,需要前端进行控制,同时后端也需要进行控制。后端实现可以通过Redis incrde 原子性递增来进行解决。
2.1 更新秒杀服务下单 image-20220103111001121 ydles_service_seckill服务的SecKillOrderServiceImpl类
From: 元动力 1 2 3 4 5 6 7 8 @Override public boolean add (Long id, String time, String username) { String result = this .preventRepeatCommit(username, id); if ("fail" .equals(result)){ return false ; } }
2.2 防重方法实现
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 private String preventRepeatCommit (String username,Long id) { String redis_key = "seckill_user_" +username+"_id_" +id; long count = redisTemplate.opsForValue().increment(redis_key, 1 ); if (count == 1 ){ redisTemplate.expire(redis_key,5 , TimeUnit.MINUTES); return "success" ; } if (count>1 ){ return "fail" ; } return "fail" ; } }
3 防止相同商品重复秒杀 需求:一个userID,只能买一个秒杀商品。
解决:秒杀订单表根据userName和秒杀商品Id控制。
image-20220103112122408 3.1 修改下单业务层实现 ydles_service_seckill服务的SecKillOrderServiceImpl类
From: 元动力 1 2 3 4 5 SeckillOrder querySeckillOrder=seckillOrderMapper.getOrderInfoByUserNameAndGoodsId(username,id);if (querySeckillOrder!=null ){ return false ; }
3.2 dao层新增查询方法
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 public interface SeckillOrderMapper extends Mapper <SeckillOrder> { @Select("select * from tb_seckill_order where user_id=#{username} and seckill_id=#{id}") SeckillOrder getSecKillOrderByUserNameAndGoodsId (String username, Long id) ; }
4 秒杀下单接口隐藏 背景:当前虽然可以确保用户只有在登录的情况下才可以进行秒杀下单,但是无法方法有一些恶意的用户在登录了之后,猜测秒杀下单的接口地址进行恶意刷单。所以需要对秒杀接口地址进行隐藏。
需求:在用户每一次点击抢购的时候,都首先去生成一个随机数并存入redis,接着用户携带着这个随机数去访问秒杀下单,下单接口首先会从redis中获取该随机数进行匹配,如果匹配成功,则进行后续下单操作,如果匹配不成功,则认定为非法访问。
这样可有防止黑客恶意大量调取后端的秒杀下单接口。
image-20220103114529621 4.1 将随机数工具类放入common工程中 image-20220103114547263 RandomUtil.java放入util包下
image-20220103114754530
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class RandomUtil { public static String getRandomString () { int length = 15 ; String base = "abcdefghijklmnopqrstuvwxyz0123456789" ; Random random = new Random (); StringBuffer sb = new StringBuffer (); for (int i = 0 ; i < length; i++) { int number = random.nextInt(base.length()); sb.append(base.charAt(number)); } return sb.toString(); } public static void main (String[] args) { String randomString = RandomUtil.getRandomString(); System.out.println(randomString); } }
4.2 秒杀渲染服务定义随机数接口 1ydles_web_seckill服务 util包下放入资料中的CookieUtil.java工具类
2ydles_web_seckill服务WSecKillOrderController类
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @Autowired RedisTemplate redisTemplate;@GetMapping("/getToken") @ResponseBody public String getToken () { String randomString = RandomUtil.getRandomString(); String cookieValue = this .readCookie(); redisTemplate.opsForValue().set("randomcode_" + cookieValue, randomString, 10 , TimeUnit.SECONDS); return randomString; }private String readCookie () { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); String jti = CookieUtil.readCookie(request, "uid" ).get("uid" ); return jti; }
4.3 js修改 修改js下单方法
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 add :function (id ) { axios.get ("/api/wseckillorder/getToken" ).then (function (response ) { var random =response.data ; axios.get ("/api/wseckillorder/add?time=" +moment (app.dateMenus [0 ]).format ("YYYYMMDDHH" )+"&id=" +id+"&random=" +random).then (function (response ) { if (response.data .flag ){ alert ("抢单成功,即将进入支付" ); } else { alert ("抢单失败" ); } }) }) }
4.4 秒杀渲染服务更改 image-20220103153812440 修改秒杀渲染服务下单接口
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 @RestController @RequestMapping("/wseckillorder") public class WSecKillOrderController { @Autowired private SecKillOrderFeign secKillOrderFeign; @Autowired private RedisTemplate redisTemplate; @RequestMapping("/add") public Result add (@RequestParam("time") String time, @RequestParam("id") Long id, String random) { String cookieValue = this .readCookie(); String redisRandomCode = (String) redisTemplate.opsForValue().get("randomcode_" + cookieValue); if (StringUtils.isEmpty(redisRandomCode)) { return new Result (false , StatusCode.ERROR, "下单失败" ); } if (!random.equals(redisRandomCode)) { return new Result (false , StatusCode.ERROR, "下单失败" ); } Result result = secKillOrderFeign.add(time, id); return result; } }
5秒杀下单接口限流-难点-不是重点 因为秒杀的特殊业务场景,生产场景下,还有可能要对秒杀下单接口进行访问流量控制,防止过多的请求进入到后端服务器。对于限流的实现方式,我们之前已经接触过通过nginx限流 ,网关限流 。但是他们都是对一个大的服务进行访问限流,如果现在只是要对某一个服务中的接口方法进行限流 呢?这里推荐使用google提供的guava工具包 中的RateLimiter 进行实现,其内部是基于令牌桶算法进行限流计算。
1限流 技术:guava
2自定义注解:@interface 基础的元注解信息
3aop 注解方式面向切面编程:前置增强、后置增强、环绕增强、最终增强
4springmvc 自带 HttpServletResponse response
5流 首先定义出来,最后关流
ydles_web_seckill服务 :
1添加依赖
From: 元动力 1 2 3 4 5 < dependency> < groupId> com.google.guava< /groupId> < artifactId> guava< /artifactId> < version> 28.0-jre< /version> < /dependency>
2自定义限流注解 com.ydles.seckill.web.aspect
From: 元动力 1 2 3 4 5 6 @Inherited @Documented @Target({ElementType.FIELD,ElementType.METHOD,ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface AccessLimit { }
3自定义切面类 com.ydles.seckill.web.aspect包下
From: 元动力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 @Component @Scope @Aspect public class AccessLimitAop { @Autowired private HttpServletResponse response; private RateLimiter rateLimiter = RateLimiter.create(2.0 ); @Pointcut("@annotation(com.ydles.seckill.web.aspect.AccessLimit)") public void limit () { } @Around("limit()") public Object around (ProceedingJoinPoint proceedingJoinPoint) { boolean result = rateLimiter.tryAcquire(); Object object = null ; if (result){ try { object = proceedingJoinPoint.proceed(); } catch (Throwable throwable) { throwable.printStackTrace(); } }else { Result<Object> objectResult = new Result <>(false , StatusCode.ACCESSLIMIT, "限流了,请售后再试" ); String msg = JSON.toJSONString(objectResult); writeMsg(response,msg); } return object; } public void writeMsg (HttpServletResponse response,String msg) { ServletOutputStream outputStream=null ; try { response.setContentType("application/json;charset=utf-8" ); outputStream = response.getOutputStream(); outputStream.write(msg.getBytes("utf-8" )); } catch (IOException e) { e.printStackTrace(); }finally { try { outputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } }
4使用自定义限流注解 @AccessLimit放在下单方法上即可
image-20220103165242789 总结:
1秒杀后台异步下单
image-20220103165527365 mq 保证消息不丢
1交换机 队列 消息 持久化
2生产者:comfirm机制
image-20220103165641227 3消费者:手动ack
2防止恶意刷单
redis:key username_id 5min
3防止相同商品秒杀
数据库order查询 拒绝
4秒杀接口隐藏
image-20220103170003264 5秒杀 接口限流
自定义注解 aop