微服务项目的编写
这里是续写97章博客(上一章博客)的,所以若没有看的话,最好看完再来:
接着续写:再创建子项目支付微服务edu-pay-boot(8006):
最终成果:
对应的依赖:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>com.lagougroupId>
<artifactId>edu-lagouartifactId>
<version>1.0-SNAPSHOTversion>
parent>
<groupId>com.lagougroupId>
<artifactId>edu-pay-bootartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>edu-pay-bootname>
<description>edu-pay-bootdescription>
<properties>
<java.version>11java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>com.github.wxpaygroupId>
<artifactId>wxpay-sdkartifactId>
<version>0.0.3version>
dependency>
<dependency>
<groupId>org.apache.httpcomponentsgroupId>
<artifactId>httpclientartifactId>
<version>4.5.12version>
dependency>
<dependency>
<groupId>com.jfinalgroupId>
<artifactId>jfinalartifactId>
<version>3.5version>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
将配置文件后缀修改成yml,并加上如下内容:
server:
port: 8006
spring:
application:
name: edu-pay-boot
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
register-with-eureka: true
fetch-registry: true
instance:
prefer-ip-address: true
instance-id: ${spring.cloud.client.ip-address}:${server.port}
启动类如下:
package com.lagou;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableEurekaClient
public class EduPayBootApplication {
public static void main(String[] args) {
SpringApplication.run(EduPayBootApplication.class, args);
}
}
现在我们在启动类所在的包下,创建commons.PayConfig类:
package com.lagou.commons;
public class PayConfig {
public static String appid = "wx8397f8696b538317";
public static String partner = "1473426802";
public static String partnerKey = "8A627A4578ACE384017C997F12D68B23";
public static String notifyurl="http://localhost:8006/pay/wxCallback";
}
然后直接创建controller.WxPayController类:
package com.lagou.controller;
import com.github.wxpay.sdk.WXPayUtil;
import com.jfinal.kit.HttpKit;
import com.lagou.commons.PayConfig;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("pay")
@CrossOrigin
public class WxPayController {
@GetMapping("createCode")
public Object createCode(String courseid,String coursename,String price) throws Exception {
coursename = new String(coursename.getBytes("ISO-8859-1"),"UTF-8");
Map<String,String> map = new HashMap<>();
map.put("appid", PayConfig.appid);
map.put("mch_id",PayConfig.partner);
map.put("nonce_str", WXPayUtil.generateNonceStr());
map.put("body",coursename);
map.put("out_trade_no",WXPayUtil.generateNonceStr());
map.put("total_fee",price);
map.put("spbill_create_ip","127.0.0.1");
map.put("notify_url",PayConfig.notifyurl);
map.put("trade_type","NATIVE");
System.out.println("商户信息:" +map);
String xml = WXPayUtil.generateSignedXml(map, PayConfig.partnerKey);
System.out.println("商户的xml信息:" +xml);
String url = "https://api.mch.weixin.qq.com/pay/unifiedorder";
String post = HttpKit.post(url, xml);
System.out.println(post);
Map<String, String> map1 = WXPayUtil.xmlToMap(post);
map1.put("orderId",map.get("out_trade_no"));
System.out.println(map1);
return map1;
}
@GetMapping("checkOrderStatus")
public Object createCode(String orderId) throws Exception {
Map<String,String> map = new HashMap<>();
map.put("appid", PayConfig.appid);
map.put("mch_id",PayConfig.partner);
map.put("out_trade_no", orderId);
map.put("nonce_str",WXPayUtil.generateNonceStr());
System.out.println(orderId);
String xml = WXPayUtil.generateSignedXml(map, PayConfig.partnerKey);
String url = "https://api.mch.weixin.qq.com/pay/orderquery";
long l = System.currentTimeMillis();
while(true) {
String post = HttpKit.post(url, xml);
Map<String, String> map1 = WXPayUtil.xmlToMap(post);
if(map1.get("trade_state").equalsIgnoreCase("SUCCESS")) {
return map1;
}
if(System.currentTimeMillis() - l>30*1000){
return map1;
}
Thread.sleep(3000);
}
}
@RequestMapping("wxCallback")
public String wxCallBack(HttpServletRequest request, HttpServletResponse response) throws IOException {
InputStream inStream = null;
ByteArrayOutputStream outSteam = null;
String resultxml = null;
try {
inStream = request.getInputStream();
outSteam = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = 0;
while ((len = inStream.read(buffer)) != -1) {
outSteam.write(buffer, 0, len);
}
resultxml = new String(outSteam.toByteArray(), "utf-8");
} catch (Exception e) {
System.out.println("回调处理失败");
}finally {
if(null != outSteam) {
outSteam.close();
}
if(null != inStream) {
inStream.close();
}
}
System.out.println("wxCallback - 回调请求参数:"+ resultxml);
return resultxml;
}
}
当然,需要支付,自然首先需要订单,所以这里先写到这里(即先不启动这个微服务)
然后创建子项目订单微服务edu-order-boot(8007):
最终成果:
对应的依赖:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>com.lagougroupId>
<artifactId>edu-lagouartifactId>
<version>1.0-SNAPSHOTversion>
parent>
<groupId>com.lagougroupId>
<artifactId>edu-order-bootartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>edu-order-bootname>
<description>edu-order-bootdescription>
<properties>
<java.version>11java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.3.2version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>javax.persistencegroupId>
<artifactId>javax.persistence-apiartifactId>
<version>2.2version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.12version>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
将配置文件后缀修改成yml,内容如下:
server:
port: 8007
spring:
application:
name: edu-order-boot
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.164.128:3306/edu_order?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
username: root
password: QiDian@666
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
register-with-eureka: true
fetch-registry: true
instance:
prefer-ip-address: true
instance-id: ${spring.cloud.client.ip-address}:${server.port}
对应的启动类:
package com.lagou;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableEurekaClient
@MapperScan("com.lagou.mapper")
public class EduOrderBootApplication {
public static void main(String[] args) {
SpringApplication.run(EduOrderBootApplication.class, args);
}
}
然后在启动类所在的包下,创建entity包,该包下,创建如下实体类:
package com.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import java.io.Serializable;
import java.util.Date;
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class UserCourseOrder {
private Long id;
private String orderNo;
private Object userId;
private Object courseId;
private Integer activityCourseId;
private Object sourceType;
private Object status;
private Date createTime;
private Date updateTime;
private Object isDel;
}
创建mapper.OrderDao接口:
package com.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.entity.UserCourseOrder;
public interface OrderDao extends BaseMapper<UserCourseOrder> {
}
创建service.OrderService接口及其实现类:
package com.service;
import com.entity.UserCourseOrder;
import java.util.List;
public interface OrderService {
public void saveOrder(String orderNo,String user_id, String course_id, String activity_course_id, String source_type);
Integer updateOrder(String orderNo,Integer status);
Integer deleteOrder(String orderNo);
List<UserCourseOrder> getOrdersByUserId(String userId);
}
package com.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.entity.UserCourseOrder;
import com.mapper.OrderDao;
import com.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.List;
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderDao orderDao;
@Override
public void saveOrder(String orderNo, String user_id, String course_id, String activity_course_id, String source_type) {
UserCourseOrder order = new UserCourseOrder();
order.setOrderNo(orderNo);
order.setUserId(user_id);
order.setCourseId(course_id);
order.setActivityCourseId(Integer.parseInt(activity_course_id));
order.setSourceType(source_type);
order.setStatus(0);
order.setIsDel(0);
order.setCreateTime(new Date() );
order.setUpdateTime(new Date() );
orderDao.insert(order);
}
@Override
public Integer updateOrder(String orderNo, Integer status) {
UserCourseOrder order = new UserCourseOrder();
order.setStatus(status);
QueryWrapper q = new QueryWrapper();
q.eq("order_no", orderNo);
return orderDao.update(order,q);
}
@Override
public Integer deleteOrder(String orderNo) {
QueryWrapper q = new QueryWrapper();
q.eq("order_no", orderNo);
return orderDao.delete(q);
}
@Override
public List<UserCourseOrder> getOrdersByUserId(String userId) {
QueryWrapper q = new QueryWrapper();
q.eq("user_id", userId);
return orderDao.selectList(q);
}
}
创建controller.OrderController类:
package com.entity.controller;
import com.entity.UserCourseOrder;
import com.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/order")
@CrossOrigin
public class OrderController {
@Autowired
private OrderService orderService;
@GetMapping("saveOrder/{userid}/{courseid}/{acid}/{stype}")
public String saveOrder(String orderNo, @PathVariable("userid") String userid, @PathVariable("courseid") String courseid, @PathVariable("acid") String acid, @PathVariable("stype") String stype) {
orderService.saveOrder(orderNo, userid, courseid, acid, stype);
return orderNo;
}
@GetMapping("updateOrder/{orderno}//{status}")
public Integer updateOrder(@PathVariable("orderno") String orderno, @PathVariable("status") Integer status) {
Integer integer = orderService.updateOrder(orderno, status);
return integer;
}
@GetMapping("deleteOrder/{orderno}")
public Integer deleteOrder(@PathVariable("orderno") String orderno) {
Integer integer = orderService.deleteOrder(orderno);
return integer;
}
@GetMapping("getOrdersByUserId/{userId}")
public List<UserCourseOrder> getOrdersByUserId(@PathVariable("userId") String userId) {
List<UserCourseOrder> ordersByUserId = orderService.getOrdersByUserId(userId);
return ordersByUserId;
}
}
至此,项目初步完成
现在我们继续在entity包下,创建如下的类:
package com.entity;
import lombok.Data;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;
import java.util.Date;
@Data
@Table(name = "pay_order")
public class PayOrder implements Serializable {
private static final long serialVersionUID = 777308790778683330L;
@Id
private Long id;
private String order_no;
private String user_id;
private String product_id;
private String product_name;
private Double amount;
private Integer count;
private String currency;
private String channel;
private Integer status;
private Integer channel_status;
private Integer order_type;
private Integer source;
private String client_ip;
private String buy_id;
private String out_trade_no;
private Date created_time;
private Date updated_time;
private Date pay_time;
private String extra;
private String goods_order_no;
private Integer platform;
private Integer wx_type;
}
package com.entity;
import lombok.Data;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;
import java.util.Date;
@Data
@Table(name = "pay_order_record")
public class PayOrderRecord implements Serializable {
private static final long serialVersionUID = 777308790778683330L;
@Id
private Long id;
private String order_no;
private String type;
private String from_status;
private String to_status;
private Double paid_amount;
private String remark;
private String created_by;
private Date created_at;
}
然后到mapper包下创建如下接口:
package com.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.entity.PayOrder;
public interface PayOrderDao extends BaseMapper<PayOrder> {
}
package com.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.entity.PayOrderRecord;
public interface PayOrderRecordDao extends BaseMapper<PayOrderRecord> {
}
然后在OrderService接口里添加如下方法:
void saveOrderInfo(PayOrder payOrder);
void saveOrderRecord(PayOrderRecord payOrderRecord);
然后对应的实现类里也添加如下:
@Autowired
private PayOrderDao payOrderDao;
@Autowired
private PayOrderRecordDao payOrderRecordDao;
@Override
public void saveOrderInfo(PayOrder payOrder) {
payOrderDao.insert(payOrder);
}
@Override
public void saveOrderRecord(PayOrderRecord payOrderRecord) {
payOrderRecordDao.insert(payOrderRecord);
}
然后在OrderController类里修改如下:
@GetMapping("saveOrder")
public String saveOrder(String orderNo,String user_id , String course_id,String activity_course_id,String source_type,Double price) {
orderService.saveOrder(orderNo, user_id, course_id, activity_course_id, source_type);
PayOrderRecord record = new PayOrderRecord();
record.setOrder_no(orderNo);
record.setType("CREATE");
record.setFrom_status("0");
record.setTo_status("1");
record.setPaid_amount(price);
record.setCreated_by(user_id);
record.setCreated_at(new Date());
System.out.println("创建订单记录 = " + orderNo);
orderService.saveOrderRecord(record);
return orderNo;
}
@GetMapping("updateOrder")
public Integer updateOrder(HttpServletRequest request,String orderNo , Integer status,String user_id,String course_id,String course_name,Double price,String phone) {
System.out.println("订单编号 = " + orderNo);
System.out.println("状态编码 = " + status);
Integer integer = orderService.updateOrder(orderNo, status);
System.out.println("订单更新 = " + integer);
if(integer == 1){
PayOrder po = new PayOrder();
po.setOrder_no(orderNo);
po.setUser_id(user_id);
po.setProduct_id(course_id);
po.setProduct_name(course_name);
po.setAmount(price);
po.setCount(1);
po.setCurrency("cny");
po.setChannel("weChat");
po.setStatus(2);
po.setOrder_type(1);
po.setSource(3);
String ip = request.getRemoteAddr();
if("0:0:0:0:0:0:0:1".equals(request.getRemoteAddr())){
ip = "127.0.0.1";
}
po.setClient_ip(ip);
po.setCreated_time(new Date());
po.setUpdated_time(new Date());
orderService.saveOrderInfo(po);
PayOrderRecord record = new PayOrderRecord();
record.setOrder_no(orderNo);
record.setType("PAY");
record.setFrom_status("1");
record.setTo_status("2");
record.setPaid_amount(price);
record.setCreated_by(user_id);
record.setCreated_at(new Date());
System.out.println("创建订单记录 = " + orderNo);
orderService.saveOrderRecord(record);
}
return integer;
}
启动该项目,访问localhost:8007/order/saveOrder?orderNo=1&user_id=2&course_id=3&activity_course_id=4&source_type=5&price=6
但是会发现,报错了,没有对应的表,我们可以看看数据库,发现是水平分表的,所以单纯的指定的表是不存在的
看错误就知道了,即出现了Table ‘edu_order.user_course_order’ doesn’t exist的错误,所以的确是表不存在
所以我们需要分库分表,但是为了测试,我们先创建一个user_course_order表,代码如下:
CREATE TABLE `user_course_order` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`order_no` VARCHAR(64) DEFAULT NULL COMMENT '订单号',
`user_id` INT(11) UNSIGNED NOT NULL DEFAULT '0' COMMENT '用户id',
`course_id` INT(11) UNSIGNED NOT NULL DEFAULT '0' COMMENT '课程id,根据订单中的课程类型来选择',
`activity_course_id` INT(11) DEFAULT '0' COMMENT '活动课程id',
`source_type` TINYINT(5) UNSIGNED NOT NULL DEFAULT '0' COMMENT '订单来源类型: 1 用户下单购买 2 后台添加专栏',
`status` TINYINT(2) UNSIGNED NOT NULL DEFAULT '0' COMMENT '当前状态: 0已创建 10未支付 20已支付 30已取消 40已过期 ',
`create_time` DATETIME NOT NULL DEFAULT '1971-01-01 00:00:00' COMMENT '创建时间',
`update_time` DATETIME NOT NULL DEFAULT '1971-01-01 00:00:00' COMMENT '更新时间',
`is_del` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0' COMMENT '是否删除',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uniq_userId_sourceType_refDataId_courseId` (`user_id`,`source_type`,`course_id`) USING BTREE
) ENGINE=INNODB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='用户课程订单表'
然后继续访问localhost:8007/order/saveOrder?orderNo=1&user_id=2&course_id=3&activity_course_id=4&source_type=5&price=6
查看数据表,若出现了数据,代表操作成功
现在我们操作分库分表,在95章博客里就操作过了,所以我们利用95章博客的知识来操作分库分表,首先,先导入具体依赖:
<dependency>
<groupId>org.apache.shardingspheregroupId>
<artifactId>sharding-jdbc-spring-boot-starterartifactId>
<version>4.1.0version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.2.1version>
dependency>
然后修改配置文件,内容如下:
server:
port: 8007
spring:
application:
name: edu-order-boot
shardingsphere:
datasource:
names: ds0
ds0:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.164.128:3306/edu_order?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
username: root
password: QiDian@666
sharding:
tables:
user_course_order:
actualDataNodes: ds0.user_course_order_$->{0..2}
tableStrategy:
inline:
shardingColumn: id
algorithmExpression: user_course_order_$->{id % 3}
keyGenerator:
type: SNOWFLAKE
column: id
props:
sql:
show: true
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
register-with-eureka: true
fetch-registry: true
instance:
prefer-ip-address: true
instance-id: ${spring.cloud.client.ip-address}:${server.port}
重新启动项目,访问localhost:8007/order/saveOrder?orderNo=1&user_id=2&course_id=3&activity_course_id=4&source_type=5&price=6
查看数据库表内容,若对应的表有数据,代表操作成功(没有保存在创建的那个表里面了)
接下来,我们回到前端的Course.vue组件中,找到如下:
<el-dialog :visible.sync="dialogFormVisible" :before-close="cancelOrder" :modal="true" :close-on-click-modal="false" style="width:800px;margin:0px auto;" >
<h1 style="font-size:30px;color:#00B38A" >微信扫一扫支付h1>
<div id="qrcode" style="width:210px;margin:20px auto;">div>
<h2 id="statusText">h2>
<p id="closeText">p>
el-dialog>
我们找到如下:
cancelOrder(){
this.dialogFormVisible= false;
return this.axios
.get("http://localhost:8007/order/deleteOrder",{
params:{
orderno:this.orderNo,
}
})
.then((result) => {
console.log("取消订单");
})
.catch( (error)=>{
this.$message.error("取消订单失败!");
});
},
那么我们修改项目的OrderController类的如下方法(像这样说明的修改,自然是将对应的修改成如下写的):
@GetMapping("deleteOrder")
public Integer deleteOrder(String orderno) {
Integer integer = orderService.deleteOrder(orderno);
return integer;
}
然后重启项目,再找到如下:
createCode(){
document.getElementById("qrcode").innerHTML = "";
this.axios
.get("http://localhost:8006/pay/createCode",{
params:{
courseid: this.course.id,
coursename: this.course.courseName,
price:1,
}
})
.then((result) => {
console.log(result);
let qrcode = new QRCode('qrcode',{
width:200,
height:200,
text:"1"
});
this.orderNo = result.data.orderId;
this.saveOrder();
this.axios
.get("http://localhost:8006/pay/checkOrderStatus",{
params:{
orderId: result.data.orderId
}
})
.then((result) => {
if(result.data.trade_state=="SUCCESS"){
document.getElementById("statusText").innerHTML = " 支付成功!";
this.updateOrder(20);
let s = 3;
this.closeQRForm(s);
}
})
.catch( (error)=>{
this.$message.error("查询订单失败!");
});
})
.catch( (error)=>{
this.$message.error("生成二维码失败!");
});
},
然后启动支付微服务edu-pay-boot(8006,前面说的先不启动的项目)
再找到如下:
saveOrder(){
return this.axios
.get("http://localhost:8007/order/saveOrder",{
params:{
orderNo:this.orderNo,
user_id: this.user.userid,
course_id:this.course.id,
activity_course_id:this.course.id,
source_type:1,
price:1
}
})
.then((result) => {
console.log("保存订单");
})
.catch( (error)=>{
this.$message.error("保存订单失败!");
});
},
updateOrder(statusCode){
return this.axios
.get("http://localhost:8007/order/updateOrder",{
params:{
orderNo:this.orderNo,
status:statusCode,
user_id:this.user.userid,
course_id:this.course.id,
course_name:this.course.name,
price:1,
}
})
.then((result) => {
console.log("更新订单【"+this.orderNo+"】状态:" + statusCode);
}).catch( (error)=>{
this.$message.error("更新订单失败!");
});
},
如果对应的代码没有问题,那么我们可以进行测试,首先删除所有相关的表,即edu_order数据库里面的表信息都删除
这里给出一个方便的操作,如图所示:
其中,可以点击字段左边,使得打上勾勾(点击只有一个记录的左边的勾勾也会使得他打上勾勾,因为只有一个相当于是全部了)
然后点击删除图标,那么就是删除当前表的所有数据了
如果只点击一个,或者点击其中的那条记录(默认是点击第一条的,除非你再次的点击,那么就是你点击的地方)某个地方
都可以点击删除图标进行删除该一条数据,但也只是一条
这里要注意:如果有勾勾的情况下(无论点击全部,还是单个记录的勾勾)
点击某个地方(没有勾勾的)是不会使得他删除的(而只删除勾勾的)
没有勾勾(完全没有,一个都没有)的情况下,会删除,即勾勾优先
接着上面要说明的测试,我们测试代码时,会发现,测试不了,问题如下:
第一,保存订单的问题,因为我们是测试,所以可以多次的使得用户购买一个课程,但是由于表的设置是符合实际情况的
即由于对应的是使用的唯一索引(总体,即只要他们都相同,那么添加不了,自然会保存失败)
那么添加(操作)到同一个表时,自然添加不了,即保存失败
具体解决方式:我们只需要多次的删除即可,所以这个问题并不是很严重,因为再实际情况下,基本是不可能多次的购买的
所以通常不会考虑,也基本只有在测试的时候才会出现
第二:对应的支付状态的问题,对应的出现问题的代码如下(在支付微服务的WxPayController类的createCode方法):
if(map1.get("trade_state").equalsIgnoreCase("SUCCESS")) {
return map1;
}
所以为了可以使得认为成功,我们改变他的代码,使得他不报错,从而不会执行前端的catch里面的内容(方法)
修改如下:
while(true) {
String post = HttpKit.post(url, xml);
Map<String, String> map1 = WXPayUtil.xmlToMap(post);
return null;
}
返回空数据即可,然后修改前端如下部分:
.then((result) => {
document.getElementById("statusText").innerHTML = " 支付成功!";
this.updateOrder(20);
let s = 3;
this.closeQRForm(s);
})
认为他支付成功,然后我们再次的点击购买,查看数据库,若有数据了,代表操作完成
当然,如果你是正常的数据,那么如果你支付成功了,自然对应的值会得到(循环的)
然后给前端自然也会成功,如果没有支付,自然他会一直循环的,直到你支付完成,然后then里面的内容(方法)才会执行
之后为了全部优化,最好修改如下:
<button
@click="buy(course.id)"
type="button"
class="weui-btn purchase-button weui-btn_mini weui-btn_primary"
style="width:155px;height:45px;font-size:17px;"
>
立即购买
button>
closeQRForm( s ){
let that = this;
var a = window.setInterval(function(){
document.getElementById("closeText").innerHTML = "( "+ s-- +" ) 秒后关闭本窗口";
console.log(s)
console.log(this)
if(s <= 0){
document.getElementById("closeText").innerHTML = ""
clearInterval(a);
console.log(990)
that.dialogFormVisible = false;
that.isBuy = true;
}
}, 1000);
},
buy(courseid) {
if(this.user != null){
this.dialogFormVisible = true;
document.getElementById("closeText").innerHTML = ""
至此微信支付操作完成,但是这里有个问题,购买后,对应的按钮还是立即购买,这个问题在后面解决
因为我们先解决完对应的已购操作
在这之前,我们都认为对应的表是分开的,实际上也是如此,所以可以分析如下
已购操作需要分析,可以有如下方式:
第一种:根据用户id去课程微服务里找到对应的所有已购课程的信息(关联的)
但是我们不操作关联且对应的表是分开的,不在一个数据库,所以这种方式可以去除
第二种:根据用户id去订单微服务里面找到所有的已购课程id,然后根据这些id
去课程微服务里面找到对应的课程信息,但是很明显,至少需要访问两次,所以基本不考虑
第三种:根据用户id去订单微服务里面找到所有的已购课程id,然后订单微服务帮我们访问对应的课程微服务得到其对应的信息
这个可以只需要访问两次就可以了,因为可以操作sql的in操作,所以我们通常考虑这个
那么你可能会有疑惑,第三种不是有耦合了吗,那么这里有个问题,第二种是否有耦合,通常我们可能认为他并没有
但实际上与第三种是一样的有耦合,只是这种耦合在前端进行连接了,所以实际上耦合的程度中,第二种和第三种是差不多的
但是第二种还是要少点,可是在时间上降低了很多效率
且第三种他不需要取出对应的值来访问了,且可以一次访问都获取(因为sql的in),且可以操作Spring Cloud的一些组件
所以各有利弊,但通常我们使用第三种(利大于弊),因为无论在数据量大的情况下
还是数据量少的情况下,第三种都非常好,即最好不要多次的访问(因为循环取出值,或者多次访问需要大量时间)
所以我们最好使用第三种(时间上效率高)
那么微服务之间如何访问呢,我们可以使用89章博客知识中的Feign远程调用组件来操作:
在这之前,我们先回到前端的Index.vue组件中,找到如下(部分代码):
.then( (result)=>{
console.log( result );
if(result.data.state == 4){
this.isLogin = true;
this.setCookie("user",token,600);
this.user = jwtDecode(token);
this.getMyCourseList();
}
})
回到课程微服务,找到CourseController类里的getCourseByUserId方法修改成如下:
@GetMapping("getCoursesByUserId")
public List<CourseDTO> getCoursesByUserId( Integer userid) {
System.out.println("userid = " + userid);
return courseService.getCoursesByUserId(userid);
}
添加依赖:
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
然后在启动类上添加如下:
package com.lagou;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableEurekaClient
@MapperScan("com.lagou.mapper")
@EnableFeignClients
public class EduCourseBootApplication {
public static void main(String[] args) {
SpringApplication.run(EduCourseBootApplication.class, args);
}
}
然后创建remote.OrderRemoteService接口:
package com.lagou.remote;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
@FeignClient(name = "edu-order-boot",path = "order")
public interface OrderRemoteService {
@GetMapping("getOKOrderCourseIds")
List<Object> getOKOrderCourseIds(@RequestParam(value = "userid") Integer userid );
}
修改CourseService接口及其实现类的对应getCourseByUserId方法:
List<CourseDTO> getCoursesByUserId(Integer userid);
List<Course> initCourse = getInitCourse(null);
private List<Course> getInitCourse(List<Object> ids){
QueryWrapper q = new QueryWrapper();
q.eq("status", 1);
q.eq("is_del", Boolean.FALSE);
if(null != ids ){
if(0 != ids.size()) {
q.in("id", ids);
}else{
q.eq("id",0);
}
}
q.orderByDesc(" sort_num ");
return this.courseMapper.selectList(q);
}
@Autowired
private OrderRemoteService orderRemoteService;
@Override
public List<CourseDTO> getCoursesByUserId(Integer userid) {
List<Object> ids = orderRemoteService.getOKOrderCourseIds(userid);
System.out.println("ids = " + ids);
return this.getMyCourses(ids);
}
private List<CourseDTO> getMyCourses(List<Object> ids) {
RedisSerializer rs = new StringRedisSerializer();
redisTemplate.setKeySerializer(rs);
System.out.println("***查询redis***");
List<CourseDTO> courseDTOS = ( List<CourseDTO>)redisTemplate.opsForValue().get("myCourses");
if(null == courseDTOS){
synchronized (this) {
courseDTOS = ( List<CourseDTO>)redisTemplate.opsForValue().get("myCourses");
if(null == courseDTOS){
System.out.println("===查询mysql===");
List<Course> courses = getInitCourse(ids);
courseDTOS = new ArrayList<>();
for(Course course : courses){
CourseDTO dto = new CourseDTO();
BeanUtils.copyProperties(course, dto);
courseDTOS.add(dto);
setTeacher(dto);
setTop2Lesson(dto);
}
redisTemplate.opsForValue().set("myCourses", courseDTOS, 10, TimeUnit.MINUTES);
}
}
}
return courseDTOS;
}
至此,获取已购课程的操作完毕,接下来,我们来补全getOKOrderCourseIds方法:
回到订单微服务,在OrderController类里加上如下:
@GetMapping("getOKOrderCourseIds")
public List<Object> getOKOrderCourseIds(Integer userid){
List<UserCourseOrder> list = orderService.getOKOrderCourseIds(userid);
List<Object> ids = new ArrayList<>();
for(UserCourseOrder order : list){
ids.add( order.getCourseId() );
}
return ids;
}
然后在OrderService接口及其实现类里加上如下:
List<UserCourseOrder> getOKOrderCourseIds(Integer userId);
@Override
public List<UserCourseOrder> getOKOrderCourseIds(Integer userId) {
QueryWrapper q = new QueryWrapper();
q.eq("status", 20);
q.eq("is_del", Boolean.FALSE);
q.eq("user_id", userId);
return orderDao.selectList(q);
}
现在我们将课程微服务和订单微服务都重启,然后访问localhost:8007/order/getOKOrderCourseIds?userid=100030011
你可以适当的添加订单,如果添加再次访问后,得到了数据,代表操作成功
那么这样就说明订单微服务的改动没有问题,现在,我们来测试课程微服务的改动是否有问题
我们直接访问localhost:8004/course/getCoursesByUserId?userid=100030011
只要返回了数据(空数据也算数据,只是没有关联而已,之前没有说明,这里说明一下),就代表操作成功
至此课程微服务的改动也没有问题
现在我们回到前端,找到Index.vue组件,找到如下:
.then( (result)=>{
console.log( result );
if(result.data.state == 4){
this.isLogin = true;
this.setCookie("user",token,600);
this.user = jwtDecode(token);
this.getMyCourseList();
}
})
对应的方法如下:
getMyCourseList(){
return this.axios
.get("http://localhost:8004/course/getCoursesByUserId",{
params:{
userid:this.user.userid
}
}).then((result) => {
console.log(result);
this.myCourseList = result.data;
}).catch( (error)=>{
this.$message.error("获取已购买的课程信息失败!");
});
},
很明显,他的确是查询我们用户已购买的信息,前端的对应的已购,自然使用的是this.myCourseList
而普通的全部是使用的this.courseList,这是不同的,我们可以添加一条记录,来测试
若发现,出现了对应的已购课程,那么操作成功
接下来,我们操作前端中,立即购买的地方,首先再data里加上如下:
import QRCode from 'qrcodejs2';
data() {
return {
activeName: "allLesson",
courseList:[],
myCourseList:[],
isLogin:false,
user:null,
adList:null,
course:null,
dialogFormVisible:false,
};
},
对应的前端中,有如下:
<el-dialog :visible.sync="dialogFormVisible" :before-close="cancelOrder" :modal="true" :close-on-click-modal="false" style="width:800px;margin:0px auto;" >
<h1 style="font-size:30px;color:#00B38A" >微信扫一扫支付h1>
<div id="qrcode" style="width:210px;margin:20px auto;">div>
<h2 id="statusText">h2>
<p id="closeText">p>
el-dialog>
添加如下方法:
buy(course) {
this.course = course
if(this.user != null){
this.dialogFormVisible = true;
this.$nextTick(function(){
this.createCode();
});
}else{
this.$message.error("请先登录,再来购买课程!");
}
},
createCode(){
document.getElementById("qrcode").innerHTML = "";
this.axios
.get("http://localhost:8006/pay/createCode",{
params:{
courseid: this.course.id,
coursename: this.course.courseName,
price:1,
}
})
.then((result) => {
console.log(8787)
console.log(result);
let qrcode = new QRCode('qrcode',{
width:200,
height:200,
text:"1"
});
this.orderNo = result.data.orderId;
this.saveOrder();
this.axios
.get("http://localhost:8006/pay/checkOrderStatus",{
params:{
orderId: result.data.orderId
}
})
.then((result) => {
document.getElementById("statusText").innerHTML = " 支付成功!";
this.updateOrder(20);
let s = 3;
this.closeQRForm(s);
})
.catch( (error)=>{
this.$message.error("查询订单失败!");
});
})
.catch( (error)=>{
this.$message.error("生成二维码失败!");
});
},
closeQRForm( s ){
let that = this;
var b = "1";
console.log(77)
console.log(this.b)
var a = setInterval(function(){
document.getElementById("closeText").innerHTML = "( "+ s-- +" ) 秒后关闭本窗口";
console.log(s)
console.log(this)
if(s <= 0){
document.getElementById("closeText").innerHTML = ""
clearInterval(a);
console.log(990)
that.dialogFormVisible = false;
that.isBuy = true;
}
}, 1000);
},
saveOrder(){
return this.axios
.get("http://localhost:8007/order/saveOrder",{
params:{
orderNo:this.orderNo,
user_id: this.user.userid,
course_id:this.course.id,
activity_course_id:this.course.id,
source_type:1,
price:1
}
})
.then((result) => {
console.log("保存订单");
})
.catch( (error)=>{
this.$message.error("保存订单失败!");
});
},
updateOrder(statusCode){
return this.axios
.get("http://localhost:8007/order/updateOrder",{
params:{
orderNo:this.orderNo,
status:statusCode,
user_id:this.user.userid,
course_id:this.course.id,
course_name:this.course.name,
price:1,
}
})
.then((result) => {
console.log("更新订单【"+this.orderNo+"】状态:" + statusCode);
}).catch( (error)=>{
this.$message.error("更新订单失败!");
});
},
cancelOrder(){
this.dialogFormVisible= false;
return this.axios
.get("http://localhost:8007/order/deleteOrder",{
params:{
orderno:this.orderNo,
}
})
.then((result) => {
console.log("取消订单");
})
.catch( (error)=>{
this.$message.error("取消订单失败!");
});
},
然后点击对应的立即购买,如果出现二维码,并自动的操作了,说明我们操作成功,但是现在有个问题
立即购买这个字需要改变吗,或者说,如果要改变,我们怎么改变,这里,我们最好进行改变,主要是为了更好的观察
修改前端代码:
<div class="btn btn-green btn-offset" @click="buy(item)">立即购买div>
在操作后面之前,我们首先需要修改订单微服务的保存订单的OrderController类里面的updateOrder方法(他使得认为已支付)
为什么这里要修改呢:
很明显,保存订单中,只要我们已经支付了,肯定会使得获取的已购课程进行改变,可是,对应的已购是操作缓存的
在前面我们说过,写的操作最好需要删除对应的缓存,所以我们需要删除redis的缓存(他是内容,通常我们认为是程序的缓存)
修改如下:
在订单微服务里添加如下依赖:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
@Autowired
private RedisTemplate<Object,Object> redisTemplate;
@GetMapping("updateOrder")
public Integer updateOrder(HttpServletRequest request,String orderNo , Integer status,String user_id,String course_id,String course_name,Double price,String phone) {
System.out.println("订单编号 = " + orderNo);
System.out.println("状态编码 = " + status);
Integer integer = orderService.updateOrder(orderNo, status);
System.out.println("订单更新 = " + integer);
RedisSerializer rs = new StringRedisSerializer();
redisTemplate.setKeySerializer(rs);
redisTemplate.delete("myCourses");
if(integer == 1){
对应的配置记得加上(按照自己的地址来操作):
spring:
redis:
host: 192.168.164.128
port: 6379
重新启动订单微服务项目,然后在前端(Index.vue组件)修改如下方法:
closeQRForm( s ){
let that = this;
var b = "1";
console.log(77)
console.log(this.b)
var a = setInterval(function(){
document.getElementById("closeText").innerHTML = "( "+ s-- +" ) 秒后关闭本窗口";
console.log(s)
console.log(this)
if(s <= 0){
document.getElementById("closeText").innerHTML = ""
clearInterval(a);
console.log(990)
that.dialogFormVisible = false;
that.$router.go(0);
}
}, 1000);
},
buy(course) {
console.log(course)
console.log(this.myCourseList.length)
console.log(this.myCourseList)
var b = true;
if(this.myCourseList.length!=0){
for(var a =0;a<this.myCourseList.length;a++){
if(course.id == this.myCourseList[a].id){
b = false
}
}
}
if(b == true){
this.course = course
if(this.user != null){
this.dialogFormVisible = true;
this.$nextTick(function(){
this.createCode();
});
}else{
this.$message.error("请先登录,再来购买课程!");
}
}else{
this.$message.error("课程已经购买了!");
}
},
然后看前端显示,若的确是符合要求的,那么操作成功,接下来我们来改变具体的显示内容
首先,我们来分析一下,我们在改变时,肯定是改变某个部分,而不是全部,那么自然需要有具体的课程的唯一
所以在正常情况下(没有随意的添加标签),可以这样修改:
<div :id="item.id" class="btn btn-green btn-offset" @click="buy(item)">立即购买div>
那么就可以这样修改:
getMyCourseList(){
return this.axios
.get("http://localhost:8004/course/getCoursesByUserId",{
params:{
userid:this.user.userid
}
}).then((result) => {
console.log(5555)
console.log(result);
this.myCourseList = result.data;
var that = this
var b = null;
setTimeout(function (){
for(var a =0;a<that.myCourseList.length;a++){
b = document.getElementById(that.myCourseList[a].id)
b.setAttribute("class","btn btn-yellow btn-offset");
b.innerHTML = '好好学习'
}
}, 1000);
}).catch( (error)=>{
this.$message.error("获取已购买的课程信息失败!");
});
},
至此,我们修改了显示内容,我们继续分析,实际上更新操作,可能也会出现问题,为什么,我们看代码:
this.saveOrder();
this.axios
.get("http://localhost:8006/pay/checkOrderStatus",{
params:{
orderId: result.data.orderId
}
})
.then((result) => {
document.getElementById("statusText").innerHTML = " 支付成功!";
this.updateOrder(20);
console.log(90)
let s = 3;
this.closeQRForm(s);
})
我们发现,由于异步的关系且我们没有判断,所以通常更新操作是可能会先操作的(对保存来说),虽然这种情况很小
而后面的关闭二维码窗口因为有时间,所以更新操作基本会先操作完毕(对更新来说),即我们需要这样的修改:
.then((result) => {
document.getElementById("statusText").innerHTML = " 支付成功!";
let that = this
setTimeout(function (){
that.updateOrder(20);
console.log(90)
let s = 3;
that.closeQRForm(s);
}, 500);
})
500毫秒通常够了,至此,我们也基本解决了这个问题,但是,在实际情况中,我们并不建议这样,因为500是固定的
当然,实际情况,也基本是有判断的(因为后端循环,所以也通常不会这样操作)
我们再次的分析,由于我们点击取消,即框框的叉叉(x),可以取消订单
那么这里就有个问题,如果支付完成后,点击取消订单,那么订单删除,又因为查询已购是查询订单的
那么会认为我们没有购买,所以相当于我们使用了小钱钱,但是,我们却没有购买,所以这是很严重的现象,那么如何解决呢
第一种:没有取消订单的操作,但是这样,并不友好,因为,如果没有取消订单
对于这里的数据库来说,他是不能出现相同的订单的,所以必须取消,否则可能添加不了订单了
第二种:在适当的时间,使得取消订单,不会操作,很明显,我们需要这一种,而适当的时间
自然是当我们会更新或者认为我们已经支付了的时候,所以代码修改如下:
data() {
return {
activeName: "allLesson",
courseList:[],
myCourseList:[],
isLogin:false,
user:null,
adList:null,
course:null,
dialogFormVisible:false,
isBuy:false,
};
},
document.getElementById("statusText").innerHTML = " 支付成功!";
this.isBuy = true
let that = this
setTimeout(function (){
that.updateOrder(20);
console.log(90)
cancelOrder(){
if(this.isBuy == false){
this.dialogFormVisible= false;
return this.axios
.get("http://localhost:8007/order/deleteOrder",{
params:{
orderno:this.orderNo,
}
})
.then((result) => {
console.log("取消订单");
})
.catch( (error)=>{
this.$message.error("取消订单失败!");
});
}else{
this.$message.error("已经购买完毕,订单不能取消了,因为需要操作已购");
}
},
现在我们再次的进行测试,发现,问题解决,而如果我们取消了,那么自然在后端,返回的结果是不受影响的
所以也就没有后续的操作,虽然会打印信息
至此,分析完毕,但是这是Index.vue组件里面的,我们到Course.vue组件进行改变
因为他也有立即购买的显示以及这里的某些问题,所以我们也需要改变他的代码:
if(result.data.state == 4){
this.isLogin = true;
this.setCookie("user",token,600);
this.user = jwtDecode(token);
this.getMyCourseList();
this.userid = this.user.userid;
}
我们可以找到他这个方法:
getMyCourseList(){
return this.axios
.get("http://localhost:8004/course/getCoursesByUserId",{
params:{
userid:this.user.userid
}
})
.then((result) => {
console.log(result);
this.myCourseList = result.data;
for(let i = 0; i<this.myCourseList.length ; i++){
if( this.myCourseList[i].id == this.course.id ){
this.isBuy = true;
break;
}
}
}).catch( (error)=>{
this.$message.error("获取课程信息失败!");
} );
},
很明显,他使用this.isBuy来保证是否购买,所以我们到对于的如下代码进行修改:
<div v-if="isBuy == false">立即购买div>
<div v-if="isBuy == true">已购买div>
buy(courseid) {
if(this.isBuy == false){
if(this.user != null){
this.dialogFormVisible = true;
this.$nextTick(function(){
this.createCode();
});
}else{
this.$message.error("请先登录,再来购买课程!");
}
}else{
this.$message.error("已经购买过了!");
}
},
至此,我们可以看下前端页面,若改变了,代表操作成功,接下来,我们来解决取消订单问题:
data() {
return {
activeName: "intro",
course:null,
totalLessons:0,
commentList:null,
isLogin:false,
isBuy:false,
user:null,
userid:666666,
myCourseList:[],
comment:null,
dialogFormVisible:false,
time:null,
orderNo:"",
isOrder:false
};
.then((result) => {
document.getElementById("statusText").innerHTML = " 支付成功!";
this.isOrder = true
let that = this
setTimeout(function (){
that.updateOrder(20);
console.log(90)
let s = 3;
that.closeQRForm(s);
}, 500);
})
cancelOrder(){
if(this.isOrder == false){
this.dialogFormVisible= false;
return this.axios
.get("http://localhost:8007/order/deleteOrder",{
params:{
orderno:this.orderNo,
}
})
.then((result) => {
console.log("取消订单");
})
.catch( (error)=>{
this.$message.error("取消订单失败!");
});
}else{
this.$message.error("已经购买完毕,订单不能取消了,因为需要操作已购");
}
},
至此,取消订单问题解决了,顺便也解决了部分异步问题(500毫秒即可)
到这里,基本上没有什么问题了,现在,我们来添加一个功能:
当我们购买后,需要一个短信的通知,即订单支付成功后,进行短信通知
那么如果你的通知有很多个,那么最好使用消息队列,看如图:
所以如果按照传统的方式,必须都需要执行完,才可返回,而使用消息队列,那么我们只需要写入消息即可
虽然他并不是在返回数据的时候,才发送,但是通知,是可以延迟的或者不通知的
这对用户的体验影响并不大,所以这里就使用消息队列
接下来我们需要mq的知识,如果你没有学习过,那么可以看81章博客进行学习
这里就使用该博客说明的RabbitMQ了(使用了他里面操作的用户)
然后我们启动RabbitMQ,创建虚拟主机lagou,再创建他的消息队列order_queue
然后添加对应的依赖(在订单微服务里面):
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
然后在配置文件里添加如下:
spring:
rabbitmq:
host: 192.168.164.128
port: 5672
username: laosun
password: 123123
virtual-host: lagou
queue: order_queue
为了进行测试,我们到OrderController类里加上如下内容:
@Autowired
private RabbitTemplate rabbitTemplate;
@Value("${spring.rabbitmq.queue}")
private String queue;
@GetMapping("sendMQ")
public void sendMQ(){
String msg = "你好啊,mq!";
rabbitTemplate.convertAndSend(queue,msg);
}
然后重启项目,访问http://localhost:8007/order/sendMQ,查看消息队列,若出现了消息,代表操作成功
现在,我们创建rabbit.OrderRever类:
package com.lagou.rabbit;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class OrderRever {
@RabbitListener( queues = "${spring.rabbitmq.queue}")
public void process(String msg){
System.out.println("得到通知,开始发送 = " + msg);
}
}
再次的重新启动,访问http://localhost:8007/order/sendMQ,看看打印信息,若出现了打印信息,则代表操作成功
然后加上这个依赖:
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.47version>
dependency>
<dependency>
<groupId>com.aliyungroupId>
<artifactId>aliyun-java-sdk-coreartifactId>
<version>4.5.3version>
dependency>
然后再配置文件里加上:
spring:
application:
name: edu-order-boot
ali:
sms:
signName: 大佬孙
templateCode: SMS_177536068
assessKeyId: LTAI4FwKDkeZ6StZvRxg5RDf
assessKeySecret: 09IMDRUia2uIC7HMXpSmM5CiXuUgvf
现在,我们创建sms.SmsService类来完成我们的短信通知:
package com.lagou.sms;
import com.aliyuncs.CommonRequest;
import com.aliyuncs.CommonResponse;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.http.MethodType;
import com.aliyuncs.profile.DefaultProfile;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class SmsService {
@Value("${ali.sms.signName}")
private String signName;
@Value("${ali.sms.templateCode}")
private String templateCode;
@Value("${ali.sms.assessKeyId}")
private String accessKeyId;
@Value("${ali.sms.assessKeySecret}")
private String assessKeySecret;
public Object sendSms(String phoneNumber,String courseName) {
DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", accessKeyId, assessKeySecret);
IAcsClient client = new DefaultAcsClient(profile);
CommonRequest request = new CommonRequest();
request.setSysMethod(MethodType.POST);
request.setSysDomain("dysmsapi.aliyuncs.com");
request.setSysVersion("2017-05-25");
request.setSysAction("SendSms");
request.putQueryParameter("RegionId", "cn-hangzhou");
request.putQueryParameter("PhoneNumbers", phoneNumber);
request.putQueryParameter("SignName", signName);
request.putQueryParameter("TemplateCode", templateCode);
request.putQueryParameter("TemplateParam", "{\"phone\":\"" + phoneNumber + "\",\"courseName\":\""+courseName+"\"}");
try {
CommonResponse response = client.getCommonResponse(request);
String jsonStr = response.getData();
System.out.println("jsonStr = " + jsonStr);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
再在entity包下创建SmsVo类:
package com.lagou.entity;
import lombok.Data;
@Data
public class SmsVo implements Serializable{
private String phone;
private String courseName;
}
然后我们修改OrderController类的updateOrder方法(部分添加):
System.out.println("创建订单记录 = " + orderNo);
orderService.saveOrderRecord(record);
SmsVo smsVo = new SmsVo();
smsVo.setPhone(phone);
smsVo.setCourseName(course_name);
rabbitTemplate.convertAndSend(queue,smsVo);
然后改变OrderRever类:
package com.lagou.rabbit;
import com.lagou.entity.SmsVo;
import com.lagou.sms.SmsService;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class OrderRever {
@Autowired
private SmsService smsService;
@RabbitListener( queues = "${spring.rabbitmq.queue}")
public void process(SmsVo smsVo){
System.out.println("得到通知,开始发送 = " + smsVo.getCourseName());
smsService.sendSms(smsVo.getPhone(), smsVo.getCourseName());
}
}
然后修改这里(Index.vue和Course.vue都进行修改):
.get("http://localhost:8007/order/updateOrder",{
params:{
orderNo:this.orderNo,
status:statusCode,
user_id:this.user.userid,
course_id:this.course.id,
course_name:this.course.courseName,
price:1,
phone:"17600870878"
}
})
至此,操作完成,重启项目进行测试,看打印信息即可(这里如果显示格式不对或者没有匹配,前面说明过的错误)
则代表操作完成,因为的确是发送了
改造播放组件:
前端中,我们使用的是html的
只能播放而已,无法保证视频资源的安全性,容易被爬取,总不能我是收费的,但是你投机取巧使得得到免费的了吧
所以我们采用阿里云视频点播方案
阿里官网:https://www.alibabacloud.com/help/zh/doc-detail/51236.htm?spm=a2c63.p38356.b99.2.28213799QTbeE3
高端的额外服务是按照流量收取费用的,且需要某些参数(相当于微信支付需要某些参数,比如需要公司注册等等)
所以这里只是给出部分的演示,具体到工作中,可以去操作
现在改造前端项目,加入阿里播放组件
在vue项目的public目录中的index.html文件中引入css和js
<link rel="stylesheet" href="https://g.alicdn.com/de/prismplayer/2.8.2/skins/default/aliplayer-min.css" />
<script charset="utf-8" type="text/javascript" src="https://g.alicdn.com/de/prismplayer/2.8.2/aliplayer-min.js">script>
在videoDetail.vue组件中引入如下:
<div class="prism-player" id="J_prismPlayer" >div>
<div
class="video" id="player-box">
<div class="prism-player" id="J_prismPlayer" style="height:850px;min-width:720px; max-height:940px;overflow-x: hidden;overflow-y: hidden;">div>
div>
然后添加如下:
var player = new Aliplayer({
id: 'J_prismPlayer',
width: '100%',
height: '900px',
autoplay: true,
source : 'https://video.pearvideo.com/mp4/short/20200914/cont-1697119-15382138-hd.mp4',
});
mounted() {
this.myvideo = new Aliplayer({
id: 'J_prismPlayer',
width: '100%',
height: '880px',
autoplay: true,
source : 'https://video.pearvideo.com/mp4/short/20200914/cont-1697119-15382138-hd.mp4',
});
},
为了解决登录的问题(videoDetail.vue组件得不到登录是否成功的信息),修改如下:
created() {
let token = this.getValueByUrlParams('token');
if(token == null || token == ""){
token = this.getCookie("user");
}
this.token = token;
console.log("刷新页面token=>"+token);
if(token != null || token != ""){
this.axios
.get("http://localhost:80/user/checkToken",{
params:{
token:token
}
})
.then( (result)=>{
console.log( result );
console.log(666)
if(result.data.state == 4){
this.isLogin = true;
this.setCookie("user",token,600);
this.user = jwtDecode(token);
}
})
.catch( (error)=>{
});
}
this.course = this.$route.params.course;
this.lessonid = this.$route.params.lessonid;
this.isBuy = this.$route.params.isBuy;
},
然后添加如下方法(vue添加方法时,基本都是在methods里面添加方法):
getValueByUrlParams(paramKey) {
var url = location.href;
var paraString = url.substring(url.indexOf("?") + 1, url.length).split("&");
var paraObj = {}
var i, j
for (i = 0; j = paraString[i]; i++) {
paraObj[j.substring(0, j.indexOf("=")).toLowerCase()] = j.substring(j.indexOf("=") + 1, j.length);
}
var returnValue = paraObj[paramKey.toLowerCase()];
if (typeof(returnValue) == "undefined") {
return "";
} else {
return returnValue;
}
},
getCookie(key){
var name = key + "=";
if(document.cookie.indexOf(';') > 0){
var ca = document.cookie.split(';');
for(var i=0; i<ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name)==0) {
return c.substring(name.length,c.length);
}
}
}else{
var ca = document.cookie
if (ca.indexOf(name)==0) {
console.log(9)
console.log(ca.substring(name.length,ca.length))
return ca.substring(name.length,ca.length);
}
}
},
setCookie(key,value,expires){
var exp = new Date();
exp.setTime(exp.getTime() + expires*1000);
document.cookie = key + "=" + escape (value) + ";expires=" + exp.toGMTString();
},
然后导入如下:
<script>
import jwtDecode from 'jwt-decode';
export default {
name: "videoDetail",
至此登录问题解决,但是他总是一开始给出固定的地址
即https://video.pearvideo.com/mp4/short/20200914/cont-1697119-15382138-hd.mp4
然后查看如下:
playNow(lesson){
this.lessonid = lesson.id;
document.getElementById("J_prismPlayer").remove();
var pdiv = document.createElement("div");
pdiv.setAttribute("class","prism-player");
pdiv.setAttribute("id","J_prismPlayer");
document.getElementById("player-box").appendChild(pdiv);
this.myvideo = new Aliplayer({
id: 'J_prismPlayer',
width: '100%',
height: '900px',
autoplay: true,
source : lesson.courseMedia.fileEdk,
});
},
initplay(){
for( let i = 0 ; i< this.course.courseSections.length;i++ ){
let section = this.course.courseSections[i];
for(let j = 0; j<section.courseLessons.length ; j++){
let lesson = section.courseLessons[j];
if(lesson.courseMedia!=null){
if(this.lessonid == lesson.courseMedia.lessonId){
console.log("视频地址:" + lesson.courseMedia.fileEdk);
this.lessonName = lesson.theme;
playNow(lesson);
return;
}
}
}
}
}
this.course = this.$route.params.course;
this.lessonid = this.$route.params.lessonid;
this.isBuy = this.$route.params.isBuy;
this.initplay();
接下来有个问题,那么视频怎么存放呢,在小视频的情况下可以使用fastdfs
虽然他没有合并的操作(即并不是很适合操作大的视频),但这里,我们使用fastdfs也并不是很友好
因为他传递过来的链接容易被盗,即安全性不是很高
使得其他人可以直接的访问我们的服务器的视频(知道地址的情况下,甚至该链接包含了地址),而不用付费了
说到安全,这里就需要提一下,一般第三方公开的框架,也最好不要使用,因为如果发现漏洞
自然会对公司造成大的影响,所以一般大公司都是自研框架的,中小型公司使用的多点第三方公开的框架
第三方框架:简称其他不是自己的框架
大多数时候将第三方认为是其他人,也通常是这样认为,所以也基本没有什么第一方和第二方
虽然我们可以解决fastdfs的安全性问题,但是耗时耗力,难度通常比较大,具体可以百度
那么除了fastdfs,我们还能使用什么来解决文件的存储且能够保证安全呢:
阿里云OSS:
我们要做文件服务,阿里云oss是一个很好的分布式文件服务系统,所以我们只需要集成阿里云oss即可
但这里也要提一下,基本上,没有绝对的安全,比如说,如果我们不能直接的盗取视频
但是确可以录视频,就算有防止录的操作,比如不能执行某些录频软件,或者过一段时间在视频上滑过学员的信息
但还是有方式可以盗取,比如直接用手机录取,然后使用剪辑或者暂停来使得取消掉滑过的学员信息或者等待他滑过等等
所以没有绝对的安全,当然,盗取视频虽然并不是好的操作,但只要传播了,就算违法(看你是否被发现吧)
所以只要不传播,只留给自己基本不会有影响
回归正题,现在我们进入该地址:https://www.aliyun.com/,然后找到如下:
点击对象存储OSS,或者你可以搜索他,然后进入如下:
往下面滑,可以看到收费的项目,通常开通不会收取费用,而使用容量,或者特殊容量(如同城冗余存储)都需要收费
通常也会提示的(信息提示,或者打印提示,或者控制台提示的,通常是打印提示)
现在我们开通"对象存储OSS"服务(通常默认是小容量,自然只要根据默认的操作,使用时是用收费的,即我们上传文件需要收取保存的费用,到后面你就知道了):
点击上面的立即开通:
申请阿里云账号并实名认证(前面已经操作过了),从这里我们也可也看出,一个账号在多个网站使用
实际上类似于操作相同的token,或者使用类似于redis保存登录信息的操作
然后开通"对象存储OSS"服务(只需要一路点击即可,很容易知道的)
然后到这里:
点击并进入管理控制台即可,也就是https://oss.console.aliyun.com/overview这个网站
可以不加overview(因为自动会跳转到加上这个路径的地址)
然后到如下:
然后往下滑,找到如下:
点击"创建Bucket",到如下:
内容我们填写如下:
可以看到oss-cn-beijing.aliyuncs.com地址,也就是帮我们的快速找到存放文件的服务器地址
相当于下载地址的镜像(类似于Linux和maven的镜像等等)
低频访问存储,通常是操作身份证图片这些信息,如实名认证等等
他的对应存储费用通常比较低,他也一般存放不怎么频繁访问的图片,这里我们操作标准存储即可
还有一个同城冗余存储,这里没有给出了,实际上他可以认为是备份的作用,可是需要对应的更多存储费用
即不同的存储费用是不同的收取方式的,但是总体来说,还是量的问题,因为同城冗余存储
需要更多的容量,所以实际上相同的量费用相同,主要看使用了多少容量了
其余的不用改变,当然,这里并没有看到什么费用,但是有些需要收费
如使用容量需要收费,特殊操作需要收费(如同城冗余存储)等等
自然我们这些测试的,基本都只是小容量,如果要收费时,会提示的(不操作程序,一般给出短信通知)
然后点击确定,到如下:
往下滑可以到这里:
很明显,实际上地域节点,是帮我们去访问Bucket域名的
因为我们自己访问可能会很慢(因为路由选择需要更多的时间,即转发表索引很慢出来,虽然通常也可人工干预)
然后点击这里:
点击上传文件,到如下:
之后,自己操作上传文件,即可,很简单的,这里就不多说,比如我这里操作好后(我上传的是图片),出现如下:
因为我们上传文件了,那么就使用了容量,所以会出现费用,一般没有余额会有通知,通常是短信(没有操作程序的打印的)
我们可以点击他后面的详情(上面图没有显示出来),到如下:
我们来访问https://hasa.oss-cn-beijing.aliyuncs.com/1.png,就可以得到图片信息了
他是直接下载的(好像,下载的太快了,没有时间取消,或者可能取消不了,但大文件通常可以取消的,会提示,比如视频,大图片可能没有提示,但可能一般也有提示,因为够大,但这些都只是浏览器的原因,所以并不需要理会)
而正是因为直接下载,所以他基本不会改变原来的url,自己测试就知道了
那么这里为什么没有使用对应的地域节点的地址(oss-cn-beijing.aliyuncs.com)呢
实际上可以使用,但是再浏览器上,访问没有上面作用,需要在程序上操作,也规定了这样,使得更快操作(如保存和读取)
因为浏览器基本不会操作镜像操作,而通常只操作域名,所以这里没有使用地域节点的地址
至此,我们操作完毕,现在我们来操作程序使得上传文件:
通常来说,我们看官方文档比较好,比如到如下:
往下滑,找到如下(随着时间的推移,界面的显示可能与这里不同,但通常可以找到,到那时,自己去找吧)
点击"对象存储OSS学习路径"进入,并找到如下:
点击"Java SDK"进入:
现在就看自己的咯,好吧,这里还是给出具体的代码:
我们创建子项目,通用的公共子模块edu-api:
最终成果:
依赖如下:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>com.lagougroupId>
<artifactId>edu-lagouartifactId>
<version>1.0-SNAPSHOTversion>
<relativePath/>
parent>
<groupId>com.lagougroupId>
<artifactId>edu-apiartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>edu-apiname>
<description>edu-apidescription>
<properties>
<java.version>11java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>joda-timegroupId>
<artifactId>joda-timeartifactId>
<version>2.9.4version>
dependency>
<dependency>
<groupId>com.aliyun.ossgroupId>
<artifactId>aliyun-sdk-ossartifactId>
<version>3.15.1version>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
修改对应配置文件后缀为yml,内容如下:
server:
port: 8081
aliyun:
oss:
endpoint: oss-cn-beijing.aliyuncs.com
accessKeyId: LTGI5t98MKFDrsyegTYozxaV
secret: 74ZcCcxGWVqGZMgDM5MQYIRbZ3ZOsB
bucket: hasa
注意:因为后面需要他web依赖里面的内容,比如MultipartFile类(导入的依赖包含了需要的依赖),所以导入了web依赖
虽然操作测试是不会操作端口的(即不会占用端口),但这里还是操作访问,即web
在启动类所在的包下,创建service.FileService接口及其实现类:
package com.lagou.eduapi.service;
import org.springframework.web.multipart.MultipartFile;
public interface FileService {
public String upload(MultipartFile file);
}
package com.lagou.eduapi.service.impl;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.lagou.eduapi.service.FileService;
import org.joda.time.DateTime;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.util.UUID;
@Service
public class FileServiceImpl implements FileService {
@Value("${aliyun.oss.endpoint}")
private String endpoint;
@Value("${aliyun.oss.accessKeyId}")
private String accessKeyId;
@Value("${aliyun.oss.secret}")
private String accessKeySecret;
@Value("${aliyun.oss.bucket}")
private String bucket;
@Override
public String upload(MultipartFile file) {
try {
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
InputStream inputStream = file.getInputStream();
String fileName = file.getOriginalFilename();
String uuid = UUID.randomUUID().toString().replaceAll("-","");
fileName = uuid+fileName;
String timeUrl = new DateTime().toString("yyyy/MM/dd");
fileName = timeUrl+"/"+fileName;
ossClient.putObject(bucket, fileName, inputStream);
ossClient.shutdown();
String url = "https://"+bucket+"."+endpoint+"/"+fileName;
return url;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
}
然后创建controller.FileController类:
package com.lagou.eduapi.controller;
import com.lagou.eduapi.service.FileService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
@RestController
@RequestMapping("file")
public class FileController {
@Autowired
private FileService fileService;
@PostMapping("fileUpload")
public String fileUpload(@RequestParam("file") MultipartFile file) {
String url = fileService.upload(file);
return url;
}
}
启动项目,在访问之前,首先检查一下你用户的访问权限(前面操作过的权限)
比如添加这个权限(不要是只读那个,他不能保存)
还有,解除权限和添加权限都有延迟的,所以通常需要等待一会作用,且可能你的访问会使得来回跳动,因为可能没有被拦截
自己测试来回切换解除和添加权限就知道了,可以出现,当访问成功后
下次的访问竟然是失败的,拦截的问题,比如没有被拦截,相当于抢占cpu资源
只是权重小点而已(而不是与多线程一样是平等的)
使得后续的可能会放行,但是只要延迟结束,就不会出现这样的问题了
即上面的已添加的这个部分权限,需要你去给用户添加
否则你是操作不了的,即一般在代码中的ossClient.putObject(bucket, fileName, inputStream);这里报错
然后操作如下访问:
执行后,查看我们的文件列表,如果有数据代表操作成功
你也可也访问返回的地址,若也操作了对应的文件(如下载),也代表操作成功
至此,我们的文件操作完毕,但是你会发现,他是下载的,那么请问一下,前端的标签可以获取吗
答:可以,可以放在img标签的src里面进行访问,那么为什么没有下载呢,实际上这是浏览器的问题
浏览器访问的话,那么他会下载到我们的本地,无论是否是小文件还是大文件都会(这里就与fastdfs是不同的,fastdfs使用浏览器基本只能下载大文件,虽然他们都有提示,小文件是直接的显示,而OSS确会下载小文件)
注意:虽然我说的是大文件,但是,某些后缀也会看出大文件的,无论是否是大的空间
比如将小文件的后缀修改成mp4,那么基本就有提示你是否下载了,即看成大文件了
而标签,即代码,默认为给浏览器来显示,即可以认为是只读
虽然没有下载到我们的本地,但是他用来显示了,退出自然也就没有(也就是一个窗口占用系统资源的,退出则释放)
所以OSS的确也操作了fastdfs的功能,虽然容量(上传后文件的保存,根据大小来收费,即容量需要收费)需要收费
但他还是有其他功能的(比如加密),虽然可能某些加密要收费,但也有免费的
具体看创建bucket时的介绍即可
Config 分布式配置中心:
分布式配置中心应用场景
往往,我们使用配置文件管理一些配置信息,比如application.yml
单体应用架构:配置信息的管理、维护并不会显得特别麻烦,手动操作就可以,因为就一个工程
微服务架构:因为我们的分布式集群环境中可能有很多个微服务,我们不可能一个一个去修改配置然后重启生效
在一定场景下我们还需要在运行期间动态调整配置信息
比如:根据各个微服务的负载情况,动态调整数据源连接池大小,我们希望配置内容发生变化的时候,微服务可以自动更新
场景总结如下:
1:集中配置管理,一个微服务架构中可能有成百上千个微服务,所以集中配置管理是很重要的(一次修改、到处生效)
2:不同环境不同配置,比如数据源配置在不同环境(开发dev,测试test,生产prod)中是不同的
3:运行期间可动态调整,例如,可根据各个微服务的负载情况,动态调整数据源连接池大小等配置修改后可自动更新
4:如配置内容发生变化,微服务可以自动更新配置(一般都是有延迟的,这是肯定的,就如访问也需要延迟)
那么,我们就需要对配置文件进行集中式管理,这也是分布式配置中心的作用
Spring Cloud Config 是一个分布式配置管理方案,包含了 Server端和 Client端两个部分
一句话理解:将所有的配置文件统一保存到云端(比如github),通过服务端应用去获取,再分发给对应的每一个微服务
云端包括:
1:github (如果访问不了或者访问缓慢,可以使用代理网址:https://hub.fastgit.org 等价于 https://github.com)
现在好像已经改变成了hub.fastgit.xyz(http://hub.fastgit.xyz/),所以这里需要注意,当然代理网址(http://hub.fastgit.xyz/)也并不是一定可以访问(甚至比https://github.com还要差,即可能出现https://github.com可以访问,而http://hub.fastgit.xyz/不能访问)
所以有时候 https://github.com也行,如果需要好的解决,可以看这个文章https://zhuanlan.zhihu.com/p/358183268
2:gitee 码云(国产github,很好用,用法和github一样)
服务端配置:
在这之前,如果你不会使用或者只是大致了解github,可以访问这个网站来学习
https://docs.github.com/cn/get-started/quickstart/hello-world
在GitHub上新建一个库,名称是yaml,然后将项目中,所有的yml文件都推送该库里面去(他们都修改名称为,“项目名-dev.yml”)
具体实操,可以查看上面的这个网站,或者可以看看73章博客中,操作的具体流程
具体推送文件如下:
当然,可能有些不会操作,但上传也并没有坏处,用到了就知道吧
创建服务端工程:
创建子项目,edu-config-boot配置中心(8008):
最终成果:
对应的依赖:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>com.lagougroupId>
<artifactId>edu-lagouartifactId>
<version>1.0-SNAPSHOTversion>
<relativePath/>
parent>
<groupId>com.lagougroupId>
<artifactId>edu-config-bootartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>edu-config-bootname>
<description>edu-config-bootdescription>
<properties>
<java.version>11java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-config-serverartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
将配置文件后缀修改成yml,内容如下:
server:
port: 8008
spring:
application:
name: edu-config-boot
cloud:
config:
server:
git:
uri: https://github.com/wobushigoudao/yaml.git
username: [email protected]
password: Sunguoan123
search-paths:
- yaml
default-label: master
clone-on-start: true
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
register-with-eureka: true
fetch-registry: true
instance:
prefer-ip-address: true
instance-id: ${spring.cloud.client.ip-address}:${server.port}
对应的启动类:
package com.lagou.educonfigboot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableEurekaClient
@EnableConfigServer
public class EduConfigBootApplication {
public static void main(String[] args) {
SpringApplication.run(EduConfigBootApplication.class, args);
}
}
启动该项目
访问localhost:8008/master/edu-ad-boot-dev.yml(master可以不加,因为默认是到该分支的,且这里也是设置为该分支,所以这里可以不行,否则如果设置的是其他分支,那么通常需要指定,因为这时如果不指定的话,可能会获取不到文件信息)
查看结果,如果出现了结果,且基本对应,代表操作成功
注意:如果出现了启动不了,无缘无故停止,但实际上并没有什么问题(有一定可能是因为超时原因,但通常也不是)
通常可以关闭项目重新打开来解决
一般这样的情况端口自动的没有清除干净,idea默认为占用的端口,但基本很少出现这种情况(电脑好的情况下)
或者因为太卡的原因以及内存的原因造成这样的
如果有问题的,那么自然是某些配置问题啦,通常也会出现日志,那么自己看日志错误提示吧
现在,我们再创建客户端工程配置(以广告微服务edu-ad-boot为例):
因为application.yml配置都推送到git(github或者gitee,gitee即码云,这里是github)上
所以以往的application.yml文件里面的内容都可以不写了
但是要将application.yml文件更名为bootstrap.yml
因为bootstrap.yml文件启动优先级更高
这里提一下:一般来说,该文件是属于cloud操作的而不是boot操作的,他也有后缀,也符合boot的那三个后缀优先级
他在启动时就加载,加载就去edu-config-boot中找属于自己的配置信息,edu-config-boot得到请求后,会去git中找
bootstrap.yml的内容(也就是说,我们只需要下面的配置即可,我们将所有的配置内容删除,然后加上下面的配置即可):
spring:
cloud:
config:
name: edu-ad-boot
profile: dev
label: master
uri: http://localhost:8008
然后加上依赖:
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-config-clientartifactId>
dependency>
因为springboot本身是不支持bootstrap文件的,需要结合springcloud的组件一起使用
通常需要spring-cloud-context依赖,使得支持该bootstrap文件
大多数的spring-cloud的主要依赖都有他spring-cloud-context依赖
比如spring-cloud-starter-netflix-eureka-client依赖里面就有spring-cloud-context依赖
且从Spring Boot 2.4版本开始,配置文件加载方式进行了重构
这个重构,导致我们还需要加上如下依赖,使得加载bootstrap文件,否则的话
一般不会进行加载该bootstrap文件,那么可能会出现没有什么属性的问题,比如数据源没有加上(如没有sql的url):
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-bootstrapartifactId>
dependency>
在Spring Boot 2.4版本之前,通常可以不加
现在我们重启广告微服务,然后访问http://localhost:8001/ad/getAdsBySpaceId/1
如果出现了数据,代表他的确读取了文件,即操作成功
这里因为读取缓存,导致我们修改git的内容,项目的配置内容并不会改变,所以需要刷新,比如手动刷新和自动刷新
具体的刷新机制可以到89章博客查看,这里就不多说了
IDEA集成Docker部署微服务:
回顾docker,引入大多数网上的内容(如果需要具体的学习,可以看看90章博客):
我想要盖房子,于是我搬砖、砍木头、画图纸、和水泥,一顿操作猛如虎,终于把这个房子盖好了
住了一段时间后,心血来潮想搬回东北老家,这时候按之前的办法
我只能回到东北后,再次搬砖、砍木头、画图纸、和水泥、盖房子
突然,降临一位神仙姐姐教了我一种法术,这个法术可以把我的房子复制一份,做成镜像,并可以放在百宝箱里
抱着百宝箱就回了东北,就用这个镜像,复制一套房子,完美复刻,拎包入住
是不是很神奇,对应到我们的项目中来,房子就是项目本身,镜像就是项目的复制,百宝箱就是镜像仓库
如果要动态扩容,从仓库中取出项目镜像,随便复制就可以了
不用再关注版本、兼容、部署等问题,彻底解决了开发完美,上线就崩,无终止排查环境的尴尬
安装docker:
yum -y install docker
systemctl start docker
systemctl status docker
开启远程访问:
Docker默认是不允许远程访问的,基本只能是docker所在的自己的服务器访问(所以在不允许之前,通常操作端口映射的)
vim /lib/systemd/system/docker.service
修改成这样:
即修改在这个地方(可能版本不同,修改的地方或者操作不同,到那时,还是去百度吧,这里我是直接执行yum -y install docker的,当然,可能随着时间的推移,得到的版本可能就不同了,那时候就去百度查看解决吧):
ExecStart=/usr/bin/dockerd-current -H tcp://0.0.0.0:2375 -H unix://var/run/docker.sock \
systemctl daemon-reload
service docker restart
netstat -nlpt
curl http://192.168.164.128:2375/info
IDEA集成插件:
在Plugins中搜索Docker,并安装
也就是第一个,其实一般大多数的idea会自带docker插件的,但看一下也没有关系
然后点击并输入如下:
只要出现如下,就代表连接成功:
即出现了Connection successful这个,就代表操作成功
注意:记得关闭服务器的防火墙,比如输入如下:
systemctl stop firewalld.service
否则可能是连接失败的
然后到如下(这个与docker打包并没有什么关系,即可以不配置,他只是代表自己仓库的镜像,当然,如果推送镜像到自己的仓库的操作以及从自己的仓库里拉取镜像的操作,可以百度查看操作,自己的仓库也就是在https://hub.docker.com/这个地址里注册并创建的仓库):
记得输入自己docker的用户名和密码,docker的地址如下:https://hub.docker.com/,在这里注册即可
若出现了出现了Connection successful这个,代表操作成功
然后点击ok退出窗口(也可也点击应用,但是反正要点击ok,ok包含应用)
然后可以在这里看到docker相关的选项了:
但是这里可能会有一个问题,在idea中,他可能并不能获取docker的一些信息,但是实际上还是操作了docker
可能与docker的文件开放有关,或者与idea本身,以及docker插件有关,也有可能是某些特殊原因,具体可以百度
Docker的Maven插件:
传统的过程中,要经历打包,部署,上传到linux
然后编写Dockerfile(从这里开始的步骤,具体实现,可以百度查看),构建镜像,创建容器等步骤
Dockerfile是一个用来构建镜像的文本文件,文本内容包含了一条条构建镜像所需的指令和说明
如果需要学习操作Dockerfile,可以百度找学习资源,这里就不多说了
docker-maven-plugin就是帮助我们在开发构成中,自动生成镜像并推送到仓库中
docker打包项目的插件有两种:docker-maven-plugin,dockerfile
我们通常需要都进行打包,这里以edu-eureka-boot服务中心(7001)为例,使得进行打包
在打包之前,安装相关的项目(前面说明过了),因为他是单独(因为这里的单独的打包会报错的)
所以需要先安装(主要是安装父项目),才可以打包
在该项目中,加上或者修改如下依赖:
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
<plugin>
<dependency>
<groupId>javax.activationgroupId>
<artifactId>activationartifactId>
<version>1.1.1version>
dependency>
<groupId>com.spotifygroupId>
<artifactId>docker-maven-pluginartifactId>
<version>1.0.0version>
<configuration>
<imageName>laosun/${project.artifactId}imageName>
<imageTags>
<imageTag>latestimageTag>
imageTags>
<baseImage>javabaseImage>
<maintainer>laosun [email protected]maintainer>
<entryPoint>["java", "-jar", "/${project.build.finalName}.jar"]entryPoint>
<dockerHost>http://192.168.164.128:2375dockerHost>
<resources>
<resource>
<targetPath>/targetPath>
<directory>${project.build.directory}directory>
<include>${project.build.finalName}.jarinclude>
resource>
resources>
configuration>
plugin>
plugins>
build>
执行命令:
对项目进行打包,并构建镜像到docker上
第一次执行多等一会,因为要拉取java的环境(初始化,以及拉取镜像的操作)等等操作
注意:构建镜像,要将项目中用到的localhost改为docker所在服务器的ip,然后再进行构建镜像
当然,如果都是同一个机器(这里代表容器,而不是服务器)里面,可以不用改变
但是这里的操作,通常自然不会到一个容器里面,所以也最好改变
那么有个问题,没有端口映射,是怎么互相访问的,实际上需要我们自己来操作端口映射,即需要我们自己对应
他虽然是docker里面的,但是无论是他访问别人,还是别人访问他,都需要宿主机的地址来操作的
相当于别人访问宿主机,或者宿主机访问别人的作用,这时端口映射的功劳
记得改变上面的localhost(是eureka,不用注意图片,这是我中途修改的,图片没有换了)变成对应服务器的地址,比如我就需要变成192.168.164.128即可
现在我们点击如下:
不给你看我的用户(嘿嘿(●ˇ∀ˇ●)),然后执行即可,命令是:mvn clean package docker:build(不用手写啦)
代表清除,打包,然后操作docker的打包,当然,可以先打包,然后直接执行mvn docker:build也可以
这里需要注意:如果你在docker上启动不了,那么这里自然也不会操作打包
因为我们需要操作内容,自然要启动的,所以这里也会出现对应类似的报错
至此命令执行完成,会自动将jar包镜像推送到docker
在idea的docker界面,根据镜像创建容器即可,如果没有显示,可以到Linux服务器上进行创建
这里给出显示的操作图片示例:
当然了,一个镜像可以创建N个容器
相当于多个服务器,只是宿主机端口不同而已,当然,一个容器也可以对应多个宿主机端口
现在,我们启动后,查看启动的容器:
docker ps
然后我们访问http://192.168.164.128:7001/,如果出现了页面,代表操作成功
至此,你可以自己进行测试其他微服务了,这里就不多说了
现在,我们继续分析之前的事务
前面已经说明了("因为不同数据库之间的事务基本是不会共享的,在后面会说明该问题"就是这句话)
所以我们需要分布式的事务,那么我们该如何操作呢:
分布式事务解决方案-Seata:
为什么选择seata:
1:我们的项目是微服务项目,多数据源,因此很多业务操作避免不了跨数据源(库)操作,传统的事务是不能解决跨数据源的
因此我们需要寻求分布式事务的解决方案
2:Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务
3:Seata 是 Simple Extensible Autonomous Transaction Architecture 的简写,Fescar 品牌升级,更名为 Seata
4:在 Seata 开源之前,Seata 对应的内部版本在阿里经济体内部一直扮演着分布式一致性中间件的角色
并能帮助经济体平稳的度过历年的双11,可以说性能十分强大
5:在2019年1月份,为了打造更加完善的技术生态和普惠技术成果,Seata 正式宣布对外开源
6:使用seata需要服务端(官网提供下载)和客户端(微服务)配合来完成
通用案例:
订单保存成功 & 累计账户积分+10
账户微服务和订单微服务,都各自有独立的数据库
因为数据库不同,所以不同数据库事务之间不会操作,这就是主要解决的问题
为了进行测试,我们操作如下,这三个微服务与前面的微服务不同,如果出现名称相同的,并不需要理会
比如下面的订单微服务,他与前面的订单微服务是不同的,注意即可
在操作之前,记得启动服务中心edu-eureka-boot(7001),如果没有启动,启动即可
如果已经启动了,那么就不需要启动了(不用变)
现在开始操作:
创建父项目test-seata
最终成果:
依赖如下:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.1.6.RELEASEversion>
<relativePath/>
parent>
<groupId>com.lagougroupId>
<artifactId>test-seataartifactId>
<version>1.0-SNAPSHOTversion>
<packaging>pompackaging>
<properties>
<maven.compiler.source>11maven.compiler.source>
<maven.compiler.target>11maven.compiler.target>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
exclusion>
exclusions>
dependency>
dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>Greenwich.RELEASEversion>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-dependenciesartifactId>
<version>2.1.0.RELEASEversion>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
然后创建子项目(路径是子的)
创建账户微服务test-account(8100):
最终成果:
数据库(记得是后面的数据源哦,即自己的数据库,那么后面的数据源自然也是对应的数据库地址,后面的就不提示了):
CREATE DATABASE `test-account` ;
USE `test-account`;
DROP TABLE IF EXISTS `taccount`;
CREATE TABLE `taccount` (
`id` int(11) unsigned zerofill NOT NULL AUTO_INCREMENT COMMENT '账户编号',
`name` varchar(64) NOT NULL COMMENT '昵称',
`score` int(11) NOT NULL DEFAULT '0' COMMENT '积分',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
insert into `taccount`(`id`,`name`,`score`) values
(00000000001,'吕布',0),
(00000000002,'赵云',0),
(00000000003,'典韦',0);
对应依赖如下:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<artifactId>test-seataartifactId>
<groupId>com.lagougroupId>
<version>1.0-SNAPSHOTversion>
parent>
<groupId>com.lagougroupId>
<artifactId>test-accountartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>test-accountname>
<description>test-accountdescription>
<properties>
<java.version>11java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.12version>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.3.2version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
将配置文件后缀修改成yml,内容如下:
server:
port: 8100
spring:
application:
name: test-account
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.164.128:3306/test-account?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
username: root
password: QiDian@666
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
register-with-eureka: true
fetch-registry: true
instance:
prefer-ip-address: true
instance-id: ${spring.cloud.client.ip-address}:${server.port}
在启动类所在的包下,创建entity.Taccount类:
package com.lagou.entity;
import lombok.Data;
@Data
public class Taccount {
private long id;
private String name;
private long score;
}
然后创建mapper.AccountDao接口:
package com.lagou.mapper;
public interface AccountDao extends BaseMapper<Taccount> {
}
再创建service.AccountService接口及其实现类:
package com.lagou.service;
public interface AccountService {
public int updateAccountScore(int userid, int score);
}
package com.lagou.service.impl;
import com.lagou.entity.Taccount;
import com.lagou.mapper.AccountDao;
import com.lagou.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
@Override
public int updateAccountScore(int userid, int score) {
Taccount account = accountDao.selectById(userid);
account.setScore(account.getScore()+score);
return accountDao.updateById(account);
}
}
创建controller.AccountController类:
package com.lagou.controller;
import com.lagou.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("account")
public class AccountController {
@Autowired
private AccountService accountService;
@GetMapping("update")
public int updateAccountScore(){
return accountService.updateAccountScore(1, 10);
}
}
对应的启动类:
package com.lagou;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableEurekaClient
@MapperScan("com.lagou.mapper")
public class TestUserApplication {
public static void main(String[] args) {
SpringApplication.run(TestUserApplication.class, args);
}
}
然后启动项目,访问localhost:8100/account/update,查看数据库数据是否变化,若发生了变化,则操作成功
创建订单微服务test-order(8101):
最终成果:
数据库:
CREATE DATABASE `test-order` ;
USE `test-order`;
DROP TABLE IF EXISTS `torder`;
CREATE TABLE `torder` (
`id` varchar(128) NOT NULL COMMENT '订单编号',
`uid` int(11) NOT NULL DEFAULT '0' COMMENT '账户编号',
`pid` int(11) NOT NULL DEFAULT '0' COMMENT '商品编号',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
对应的依赖:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<artifactId>test-seataartifactId>
<groupId>com.lagougroupId>
<version>1.0-SNAPSHOTversion>
parent>
<groupId>com.lagougroupId>
<artifactId>test-orderartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>test-ordername>
<description>test-orderdescription>
<properties>
<java.version>11java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.12version>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.3.2version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
将配置文件后缀修改成yml,内容如下:
server:
port: 8101
spring:
application:
name: test-order
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.164.128:3306/test-order?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
username: root
password: QiDian@666
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
register-with-eureka: true
fetch-registry: true
instance:
prefer-ip-address: true
instance-id: ${spring.cloud.client.ip-address}:${server.port}
在启动类所在的包下,创建entity.Torder类:
package com.lagou.entity;
import lombok.Data;
@Data
public class Torder {
private String id;
private long uid;
private long pid;
}
然后创建mapper.OrderDao接口(mapper和dao可以互相变化,具体看你自己,比如dao包或者OrderMapper类):
package com.lagou.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lagou.entity.Torder;
public interface OrderDao extends BaseMapper<Torder> {
}
创建service.OrderService接口及其实现类:
package com.lagou.service;
import com.lagou.entity.Torder;
public interface OrderService {
public int saveOrder(Torder order);
}
package com.lagou.service.impl;
import com.lagou.entity.Torder;
import com.lagou.mapper.OrderDao;
import com.lagou.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderDao orderDao;
@Override
public int saveOrder(Torder order) {
return orderDao.insert(order);
}
}
创建controller.OrderController类:
package com.lagou.controller;
import com.lagou.entity.Torder;
import com.lagou.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.UUID;
@RestController
@RequestMapping("order")
public class OrderController {
@Autowired
private OrderService orderService;
@GetMapping("save")
public int save(){
Torder order = new Torder();
order.setId(UUID.randomUUID().toString().replaceAll("-", ""));
order.setUid(1);
order.setPid(11);
return orderService.saveOrder(order);
}
}
对应的启动类:
package com.lagou;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableEurekaClient
@MapperScan("com.lagou.mapper")
public class TestOrderApplication {
public static void main(String[] args) {
SpringApplication.run(TestOrderApplication.class, args);
}
}
然后启动项目,访问localhost:8101/order/save,查看数据库,若有数据添加了,代表操作成功
创建业务入口微服务:test-front(8102):
最终成果:
对应的依赖:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<artifactId>test-seataartifactId>
<groupId>com.lagougroupId>
<version>1.0-SNAPSHOTversion>
parent>
<groupId>com.lagougroupId>
<artifactId>test-frontartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>test-frontname>
<description>test-frontdescription>
<properties>
<java.version>11java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
将配置文件后缀修改成yml,内容如下:
server:
port: 8102
spring:
application:
name: test-front
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
register-with-eureka: true
fetch-registry: true
instance:
prefer-ip-address: true
instance-id: ${spring.cloud.client.ip-address}:${server.port}
在启动类所在的包下,创建remote.AccountRemoteService接口:
package com.lagou.remote;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
@FeignClient(name = "test-account",path = "account")
public interface AccountRemoteService {
@GetMapping("update")
int update();
}
再在该remote包下创建OrderRemoteService接口:
@FeignClient(name = "test-order",path = "order")
public interface OrderRemoteService {
@GetMapping("save")
int save();
}
创建service.BusinessService接口及其实现类:
package com.lagou.service;
public interface BusinessService {
public boolean business();
}
package com.lagou.service.impl;
import com.lagou.remote.AccountRemoteService;
import com.lagou.remote.OrderRemoteService;
import com.lagou.service.BusinessService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class BusinessServiceImpl implements BusinessService {
@Autowired
private AccountRemoteService accountRemoteService;
@Autowired
private OrderRemoteService orderRemoteService;
@Override
public boolean business() {
accountRemoteService.update();
orderRemoteService.save();
return true;
}
}
创建controller.frontController类:
package com.lagou.controller;
import com.lagou.service.BusinessService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("front")
public class frontController {
@Autowired
private BusinessService businessService;
@GetMapping("business")
public boolean business(){
return businessService.business();
}
}
对应的启动类:
package com.lagou;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class TestFrontApplication {
public static void main(String[] args) {
SpringApplication.run(TestFrontApplication.class, args);
}
}
然后我们启动项目,访问localhost:8102/front/business,查看两个数据库
若对应数据库数据发生改变(变化)了,且操作添加了,代表操作成功
至此,基本项目部署基本完毕,接下来我们需要操作分布式事务了
首先,我们修改业务入口微服务的BusinessServiceImpl类的如下:
@Override
public boolean business() {
accountRemoteService.update();
int i = 10/0;
orderRemoteService.save();
return true;
}
重启项目,访问localhost:8102/front/business,查看对应两个数据库,会发现,我们操作了变化数据,但是没有添加数据
接下来,我们来验证,数据库事务是否传递(事务传递:也就是不同数据库之间的事务基本是不会共享的),操作如下:
加上如下依赖:
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-jdbcartifactId>
<version>5.1.5.RELEASEversion>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
修改配置文件:
spring:
application:
name: test-front
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.164.128:3306/test-account?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
username: root
password: QiDian@666
然后修改BusinessServiceImpl类的如下:
@Override
@Transactional()
public boolean business() {
accountRemoteService.update();
orderRemoteService.save();
int i = 10/0;
return true;
}
重启项目,继续访问localhost:8102/front/business,查看是否操作事务传递
我们可以发现,数据变化了,且添加了,即并没有操作回滚,为什么呢
这是因为对应操作的是其他微服务的连接(相当于http访问一样)
虽然,数据源相同,但是连接不同(连接不同,也可以认为是窗口不同)
所以事务也并没有操作,所以我们需要在他们的微服务里面操作事务,先注释掉上面的int i = 10/0;代码和@Transactional()代码
然后操作如下,以账户微服务为例:
修改AccountServiceImpl类的如下:
@Override
@Transactional()
public int updateAccountScore(int userid, int score) {
Taccount account = accountDao.selectById(userid);
account.setScore(account.getScore()+score);
int i = 10/0;
return accountDao.updateById(account);
}
重新启动账户微服务,和业务入口微服务,继续访问localhost:8102/front/business
发现,在业务入口微服务的accountRemoteService.update();报错
也就是说,虽然类似于http访问,但是对方报错了,我们也会受影响
且我们查看数据库,发现,都没有发生改变或者添加,我们将业务入口微服务的BusinessServiceImpl类,修改如下:
@Override
public boolean business() {
int save = orderRemoteService.save();
System.out.println(save);
accountRemoteService.update();
return true;
}
我们换一下代码位置(测试完后,可以选择不换回来,因为我们只是测试事务而已,不需要具体业务顺序)
重启业务入口微服务,继续访问localhost:8102/front/business
查看数据库,我们可以很明显的看到,他只是添加了数据,但是没有修改数据
也就是说,对应的注解@Transactional() ,并没有操作其他微服务的事务。只是操作当前的
所以通过上面的多个测试,所以的确,不同数据库(不同连接一般也算,一般是针对不同项目,因为相同的,可能通常都是一个连接,虽然前面也并没有说明过,这里说明一下,因为sql连接的默认隔离,基本不会出现什么隔离问题,这里就不考虑了)
事务的确不会共享,所以我们的确需要一种分布式的事务来使得他们可以认为都在一个事务中
而不是具体的一个微服务的事务(数据库或者连接)
至此测试完毕,将AccountServiceImpl类的updateAccountScore方法的对应的int i = 10/0;以及@Transactional()注解注释掉吧
然后重启账户微服务吧
现在我们来完成分布式事务的具体操作:
下载与安装:
官网:http://seata.io/zh-cn/index.html
点击上面的下载,找到如下:
尽量不要选择最新的版本因为通常"不太稳定",我们选择(点击)下载 1.2.0 (2020-04-20)的binary
这样就可以下载对应的zip文件了,当然,你也可以到如下地址下载:
链接:https://pan.baidu.com/s/1fTDRwt6PyVApNRRDZ9m7tw
提取码:alsk
我们将得到的seata-server-1.2.0.zip(可能随时间,该名称会改变)上传到linux,并解压
配置服务端:
操作解压:
unzip seata-server-1.2.0.zip
cd seata/conf
主要配置两个文件:
file.conf:配置数据库
registry.conf:配置注册中心
file.conf:
在mysql里创建一个seata库,需要执行下面的脚本代码,因为seata需要在操作mysql事务时,需要用到这些表
否则可能不会操作事务,或者事务不起作用,也有可能项目启动时会报错,这些等等原因
一般情况下,下面的脚本代码需要在seata的1.0之前(通常不包括1.0)的版本中,从db_store.sql(一般是这个名称)中获取
seata的1.0之后(通常包括1.0)取消了这个脚本文件,所以我们复制下面代码
CREATE DATABASE `seata` ;
USE `seata`;
DROP TABLE IF EXISTS `branch_table`;
CREATE TABLE `branch_table` (
`branch_id` bigint(20) NOT NULL,
`xid` varchar(128) NOT NULL,
`transaction_id` bigint(20) DEFAULT NULL,
`resource_group_id` varchar(32) DEFAULT NULL,
`resource_id` varchar(256) DEFAULT NULL,
`lock_key` varchar(128) DEFAULT NULL,
`branch_type` varchar(8) DEFAULT NULL,
`status` tinyint(4) DEFAULT NULL,
`client_id` varchar(64) DEFAULT NULL,
`application_data` varchar(2000) DEFAULT NULL,
`gmt_create` datetime DEFAULT NULL,
`gmt_modified` datetime DEFAULT NULL,
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `global_table`;
CREATE TABLE `global_table` (
`xid` varchar(128) NOT NULL,
`transaction_id` bigint(20) DEFAULT NULL,
`status` tinyint(4) NOT NULL,
`application_id` varchar(32) DEFAULT NULL,
`transaction_service_group` varchar(32) DEFAULT NULL,
`transaction_name` varchar(128) DEFAULT NULL,
`timeout` int(11) DEFAULT NULL,
`begin_time` bigint(20) DEFAULT NULL,
`application_data` varchar(2000) DEFAULT NULL,
`gmt_create` datetime DEFAULT NULL,
`gmt_modified` datetime DEFAULT NULL,
PRIMARY KEY (`xid`),
KEY `idx_gmt_modified_status` (`gmt_modified`,`status`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `lock_table`;
CREATE TABLE `lock_table` (
`row_key` varchar(128) NOT NULL,
`xid` varchar(96) DEFAULT NULL,
`transaction_id` mediumtext,
`branch_id` mediumtext,
`resource_id` varchar(256) DEFAULT NULL,
`table_name` varchar(32) DEFAULT NULL,
`pk` varchar(36) DEFAULT NULL,
`gmt_create` datetime DEFAULT NULL,
`gmt_modified` datetime DEFAULT NULL,
PRIMARY KEY (`row_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
然后在mysql中执行即可
现在打开file.conf文件,修改两处:
记得改成自己的数据源即可,注意改变url这个地方
很明显,他是操作我们创建的seata数据库的
再打开registry.conf:
修改如下:
注意:不要写localhost,因为他是在虚拟机的
所以需要我们本机的具体地址(在cmd命令提示符那里,输入ipconfig查看即可,比如我的就是192.168.164.1)
因为localhost在虚拟机那里就代表虚拟机了,而不是我们的本机了
很明显他也是操作了服务中心edu-eureka-boot(7001),而之所以这样
是为了确定并操作微服务的事务管理,在最后我会解释为什么要这样做,现在你先认为这样
启动:
先启动eureka
再启动seata的服务端
cd /opt/seata/bin/
./seata-server.sh -p 9099 -m db
然后我们访问http://localhost:7001/,查看他是否注册(显示),如果出现了,则代表操作成功(即服务端启动成功)
配置客户端:
在账户微服务里加上如下依赖:
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-seataartifactId>
dependency>
<dependency>
<groupId>io.seatagroupId>
<artifactId>seata-allartifactId>
<version>1.2.0version>
dependency>
然后在资源文件夹下,加上三个配置文件:
分别是file.conf,registry.conf,seata.conf(通常需要自己写)
其中file.conf和registry.conf可以去github示例项目中获取:
https://github.com/seata/seata-samples/tree/master/springboot-dubbo-seata/samples-business/src/main/resources
这里我直接给出具体内容,当然,可能随着时间的推移,内容会改变,所以最好看这地址里面的内容:
file.conf:
transport {
type = "TCP"
server = "NIO"
heartbeat = true
enableClientBatchSendRequest = true
threadFactory {
bossThreadPrefix = "NettyBoss"
workerThreadPrefix = "NettyServerNIOWorker"
serverExecutorThread-prefix = "NettyServerBizHandler"
shareBossWorker = false
clientSelectorThreadPrefix = "NettyClientSelector"
clientSelectorThreadSize = 1
clientWorkerThreadPrefix = "NettyClientWorkerThread"
bossThreadSize = 1
workerThreadSize = "default"
}
shutdown {
wait = 3
}
serialization = "seata"
compressor = "none"
}
service {
vgroupMapping.my_tx_group = "seata-server"
seata-server.grouplist = "192.168.164.128:9099"
enableDegrade = false
disableGlobalTransaction = false
}
client {
rm {
asyncCommitBufferLimit = 10000
lock {
retryInterval = 10
retryTimes = 30
retryPolicyBranchRollbackOnConflict = true
}
reportRetryCount = 5
tableMetaCheckEnable = false
reportSuccessEnable = false
}
tm {
commitRetryCount = 5
rollbackRetryCount = 5
}
undo {
dataValidation = true
logSerialization = "jackson"
logTable = "undo_log"
}
log {
exceptionRate = 100
}
}
registry.conf:
registry {
type = "eureka"
nacos {
application = "seata-server"
serverAddr = "localhost"
namespace = ""
username = ""
password = ""
}
eureka {
serviceUrl = "http://localhost:7001/eureka"
application = "seata-server"
weight = "1"
}
redis {
serverAddr = "localhost:6379"
db = "0"
password = ""
timeout = "0"
}
zk {
serverAddr = "127.0.0.1:2181"
sessionTimeout = 6000
connectTimeout = 2000
username = ""
password = ""
}
consul {
serverAddr = "127.0.0.1:8500"
}
etcd3 {
serverAddr = "http://localhost:2379"
}
sofa {
serverAddr = "127.0.0.1:9603"
region = "DEFAULT_ZONE"
datacenter = "DefaultDataCenter"
group = "SEATA_GROUP"
addressWaitTime = "3000"
}
file {
name = "file.conf"
}
}
config {
type = "file"
nacos {
serverAddr = "localhost"
namespace = ""
group = "SEATA_GROUP"
username = ""
password = ""
}
consul {
serverAddr = "127.0.0.1:8500"
}
apollo {
appId = "seata-server"
apolloMeta = "http://192.168.1.204:8801"
namespace = "application"
}
zk {
serverAddr = "127.0.0.1:2181"
sessionTimeout = 6000
connectTimeout = 2000
username = ""
password = ""
}
etcd3 {
serverAddr = "http://localhost:2379"
}
file {
name = "file.conf"
}
}
seata.conf:
client {
application.id = test-account
transaction.service.group = my_tx_group
}
我们发现,定义了注册中心地址(为什么已经注册了,还需要定义呢,实际上他是若在seata服务端,那么认为是注册加上具体地址,但是若在客户端,则只认为是具体地址,为什么这样说,可以认为,我们id所在的事务组中,需要指定一个服务端在注册中心的名称,也就是说的具体地址当成操作区域),且定义了一个seata客户端项目名称(用作事务id)
然后定义了对应的事务组由seata管理(即seata-server,指定了其在服务器的地址)
且我们的事务id由该事务组管理,那么自然由seata管理,所以简单来说就是:
file.conf文件指定一个seata服务端地址,然后创建一个事务组给他管理,也就是提供了唯一操作者(事务组)
registry.conf文件,指定seata服务端在注册中心的名称,因为都在同一个注册中心
所以可以操作事务共享(需要对应的代理数据源)
也就是说,我们操作的具体地址都需要是一样的,这样,事务才可操作,也就是提供了唯一区域(没有区域怎么操作呢)
seata.conf文件,定义了对应微服务在指定事务组里面的唯一标识
因为同一个事务组里面是事务共享的,且在一个区域操作,导致我们可以操作事务共享(需要对应的代理数据源)
至此,我们完成了一个微服务,现在,我们在订单微服务和业务入口微服务里也加上对应的依赖
然后在资源文件夹下,都加上上面的三个文件吧(业务入口加上,是为了确定组名称以及区域,在后面的@GlobalTransactional注解就是这样的作用),即seata依赖会读取这三个文件,其中名称不要改变咯,否则可能操作不了了
唯一要改变的就是seata.conf文件的application.id属性,自己操作吧,
可能要修改启动类:
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
配置类(记得订单微服务和账户微服务都加上该类,业务入口微服务不需要,因为他并不操作数据库交互):
创建config.SeataConfig类:
package com.lagou.config;
import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import javax.sql.DataSource;
@Configuration
public class SeataConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDataSource(){
DruidDataSource ds = new DruidDataSource();
return ds;
}
@Primary
@Bean("dataSource")
public DataSourceProxy dataSource(DataSource druidDataSource){
return new DataSourceProxy(druidDataSource);
}
}
然后在订单微服务和账户微服务以及业务入口微服务的配置文件上,添加如下:
spring:
cloud:
alibaba:
seata:
tx-service-group: my_tx_group
事务id之间的事务共享是需要数据库的,也就是代理数据源DataSourceProxy需要一个数据库或者表来进行事务之间的共享
相当于我们事务操作的日志
我们在订单微服务和账户微服务对应的数据源的数据库里,执行如下sql语句:
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
然后我们在业务入口微服务中,将BusinessServiceImpl类的该方法修改如下:
@Override
@GlobalTransactional
public boolean business() {
accountRemoteService.update();
int i = 1/0;
int save = orderRemoteService.save();
return true;
}
然后我们重启订单微服务和账户微服务以及业务入口微服务,然后访问localhost:8102/front/business
查看数据库,如果对应的数据没有修改,且报错信息是(/ by zero),那么也就是说操作了回滚,即分布式事务操作成功
接下来,我们来看看有没有多余的代码,首先删除对应三个微服务的如下配置:
cloud:
alibaba:
seata:
tx-service-group: my_tx_group
然后将他们都重启,那么我经过测试,他们都需要加上,其中,业务入口的数据源和不操作自动配置都可以不加,因为并不操作
最后注意:可能访问时会有超时的错误,我们再次的执行即可,大概与电脑卡顿有关,因为他并不是错误,只是访问慢而已
最后,还要验证一下,seata是否有类似于心跳的机制,那么我们关闭seata服务端(在服务器上的)
然后再次的启动,再访问,如果回滚了,代表,他们之间的确有心跳的机制(在项目之间的心跳机制一般也代表循环获取,虽然大多数框架内的心跳机制是每隔一段时间发送一个包,在一定时间后,会断开,但项目之间是基本是没有断开的,cloud那里也有类似的这个,不如eureka的客户端和服务端)
经过测试,他们的确有心跳机制(基本是不断开的,可以自己测试)
这里也要提一下,最好不要在对应微服务里面加上@Transactional()操作事务,会冲突的
使得访问时会报错,即可能会导致找不到对应客户端,即找不到事务id的错误,直接报错,而不会执行sql语句
即执行到那里就报错了,这里还需要提一下,业务入口微服务实际本身也是一个事务id
即对应的三个配置不只是给@GlobalTransactional注解,也给了他自己的seata,其他两个也都给了自己的seata
如果他也操作数据源,那么对应的@GlobalTransactional操作的方法里面如果有自己对数据源的操作,那么也会认为在事务组里面
而不会发生冲突,所以真正的事务入口,实际上就是@GlobalTransactional注解,而不是该项目本身
且自己也可以操作数据源,当成其他两个微服务一样的,即不会冲突自己(也操作事务组回滚)
你可以试着将账户微服务的相关数据源操作放在业务入口微服务里面
然后再@GlobalTransactional注解的对应方法里面加上操作数据库的代码,然后重启,并访问
也会发现,进行了回滚,所以不会冲突(与是否同一个数据源无关,因为是看事务id的,虽然连接也基本不会相同)
但并不建议这样做,因为这样耦合度比较高,我们需要要具体分工,才好进行维护
所以一般有@GlobalTransactional注解的微服务只提供访问(用户访问他,然后他访问其他微服务,而不会让自己去操作数据源)
如果事务id相同怎么办,答:继续操作,相同并没有什么关系,可以认为他一个事务id指定多个数据源
虽然之前我们也认为一个事务id对应一个mysql,但是这是平常的
而实际上我们可以认为事务id只是操作mysql事务的日志产生者,所以实际上只是来记录日志的
而事务组就结合他们的日志(有时间顺序的)来进行回滚,所以事务id相同并没有什么关系,只是他一个人干了两个日志产生而已
至此,分布式事务的seata大致介绍完毕,具体的坑也大致说明完毕
实际上一个项目基本不可能将所有的技术进行覆盖,所以具体问题或者业务,只要有能够解决他们的技术,那么就使用即可
即具体问题,具体分析,然后具体使用技术