02、订单业务:准备订单相关元素
1)Pojo实体类
提供订单对象
package com.leyou.order.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@Data
@TableName("tb_order")
public class Order {
/**
* 订单编号
*/
@TableId(type = IdType.NONE)
private Long orderId;
/**
* 商品金额
*/
private Long totalFee;
/**
* 邮费
*/
private Long postFee;
/**
* 实付金额
*/
private Long actualFee;
/**
* 付款方式:1:在线支付, 2:货到付款
*/
private Integer paymentType;
/**
* 优惠促销的活动id,
*/
private String promotionIds;
/**
* 用户id
*/
private Long userId;
/**
* 订单状态
*/
private Integer status;
/**
* 创建时间
*/
private Date createTime;
/**
* 付款时间
*/
private Date payTime;
/**
* 发货时间
*/
private Date consignTime;
/**
* 确认收货时间
*/
private Date endTime;
/**
* 交易关闭时间
*/
private Date closeTime;
/**
* 评价时间
*/
private Date commentTime;
/**
* 更新时间
*/
private Date updateTime;
/**
* 发票类型,0无发票,1普通发票,2电子发票,3增值税发票
*/
private Integer invoiceType;
/**
* 订单来源 1:app端,2:pc端,3:微信端
*/
private Integer sourceType;
}
提供订单状态枚举对象
此处我们为订单状态定义了枚举,方便订单状态的记录:
package com.leyou.order.pojo;
/**
* @author 黑马程序员
*/
public enum OrderStatusEnum {
INIT(1, "初始化,未付款"),
PAY_UP(2, "已付款,未发货"),
DELIVERED(3, "已发货,未确认"),
CONFIRMED(4, "已确认,未评价"),
CLOSED(5, "已关闭"),
RATED(6, "已评价,交易结束")
;
private Integer value;
private String msg;
OrderStatusEnum(Integer value, String msg) {
this.value = value;
this.msg = msg;
}
public Integer value(){
return this.value;
}
public String msg(){
return msg;
}
}
提供订单详情对象
package com.leyou.order.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Date;
@Data
@TableName("tb_order_detail")
public class OrderDetail {
@TableId(type = IdType.NONE)
private Long id;
/**
* 订单编号
*/
private Long orderId;
/**
* 商品id
*/
private Long skuId;
/**
* 商品购买数量
*/
private Integer num;
/**
* 商品标题
*/
private String title;
/**
* 商品单价
*/
private Long price;
/**
* 商品规格数据
*/
private String ownSpec;
/**
* 图片
*/
private String image;
private Date createTime;
private Date updateTime;
}
提供物流对象
package com.leyou.order.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Date;
/**
* @author yy
*/
@Data
@TableName("tb_order_logistics")
public class OrderLogistics {
/**
* 订单id,与订单表一对一
*/
@TableId(type = IdType.NONE)
private Long orderId;
/**
* 物流单号
*/
private String logisticsNumber;
/**
* 物流名称
*/
private String logisticsCompany;
/**
* 收件人
*/
private String addressee;
/**
* 手机号
*/
private String phone;
/**
* 省
*/
private String province;
/**
* 市
*/
private String city;
/**
* 区
*/
private String district;
/**
* 街道
*/
private String street;
/**
* 邮编
*/
private String postcode;
private Date createTime;
private Date updateTime;
}
2)DTO相关类
提供购物车的DTO
package com.leyou.order.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CartDTO {
private Long skuId;// 商品skuId
private Integer num;// 购买数量
}
提供接收订单参数DTO
package com.leyou.order.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotNull;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class OrderDTO {
@NotNull
private Long addressId; // 收获人地址id
@NotNull
private Integer paymentType;// 付款类型
@NotNull
private List<CartDTO> carts;// 订单详情
}
提供返回订单用于展示的VO
package com.leyou.order.dto;
import com.leyou.order.pojo.OrderDetail;
import com.leyou.order.pojo.OrderLogistics;
import lombok.Data;
import java.util.Date;
import java.util.List;
/**
* @author yy
*/
@Data
public class OrderVO {
/**
* 订单编号
*/
private Long orderId;
/**
* 商品金额
*/
private Long totalFee;
/**
* 邮费
*/
private Long postFee = 0L;
/**
* 实付金额
*/
private Long actualFee;
/**
* 付款方式:1:在线支付, 2:货到付款
*/
private Integer paymentType;
/**
* 优惠促销的活动id,
*/
private String promotionIds;
/**
* 用户id
*/
private Long userId;
/**
* 订单状态
*/
private Integer status;
/**
* 创建时间
*/
private Date createTime;
/**
* 付款时间
*/
private Date payTime;
/**
* 发货时间
*/
private Date consignTime;
/**
* 确认收货时间
*/
private Date endTime;
/**
* 交易关闭时间
*/
private Date closeTime;
/**
* 评价时间
*/
private Date commentTime;
/**
* 发票类型,0无发票,1普通发票,2电子发票,3增值税发票
*/
private Integer invoiceType = 0;
/**
* 订单来源 1:app端,2:pc端,3:微信端
*/
private Integer sourceType = 1;
/**
* 订单物流信息
*/
private OrderLogistics logistics;
/**
* 订单详情信息
*/
private List<OrderDetail> detailList;
}
3)Mapper相关接口
提供订单的Mapper
package com.leyou.order.mapper;
import com.leyou.common.mapper.BaseMapper;
import com.leyou.order.entity.Order;
public interface OrderMapper extends BaseMapper<Order> {
}
提供订单详情的Mapper
package com.leyou.order.mapper;
import com.leyou.common.mapper.BaseMapper;
import com.leyou.order.entity.OrderDetail;
public interface OrderDetailMapper extends BaseMapper<OrderDetail> {
}
提供物流的Mapper
package com.leyou.order.mapper;
import com.leyou.common.mapper.BaseMapper;
import com.leyou.order.entity.OrderLogistics;
public interface OrderLogisticsMapper extends BaseMapper<OrderLogistics> {
}
4)订单的Service
package com.leyou.order.service;
import com.leyou.order.mapper.OrderDetailMapper;
import com.leyou.order.mapper.OrderLogisticsMapper;
import com.leyou.order.mapper.OrderMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private OrderDetailMapper orderDetailMapper;
@Autowired
private OrderLogisticsMapper orderLogisticsMapper;
}
5)订单的Controller
package com.leyou.order.web;
import com.leyou.order.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class OrderController {
@Autowired
private OrderService orderService;
}
03、订单业务:分布式ID
订单id的特殊性
订单数据非常庞大,将来一定会做分库分表。那么这种情况下, 要保证id的唯一,就不能靠数据库自增,而是自己来实现算法,生成唯一id。
雪花算法
这里的订单id是通过一个工具类生成的:
而工具类所采用的生成id算法,是由Twitter公司开源的snowflake(雪花)算法。
简单原理
雪花算法会生成一个64位的二进制数据,为一个Long型。(转换成字符串后长度最多19) ,其基本结构:
第一部分:备用
第二部分:41位为毫秒级时间(41位的长度可以使用69年)
第三部分:5位datacenterId和5位workerId(10位的长度最多支持部署1024个节点)
第四部分:最后12位是毫秒内的计数(12位的计数顺序号支持每个节点每毫秒产生4096个ID序号)
snowflake生成的ID整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由datacenter和workerId作区分),并且效率较高。经测试snowflake每秒能够产生26万个ID。
配置
为了保证不重复,我们在application.yaml中给每个部署的节点都配置机器id:
ly:
worker:
workerId: 1
dataCenterId: 1
加载属性:
package com.leyou.order.config;
import com.leyou.common.utils.IdWorker;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
/**
*
*/
@Data
@Component
@ConfigurationProperties(prefix = "ly.worker")
public class IdWorkProperties {
private long workerId;
private long dataCenterId;
}
编写配置类:
package com.leyou.order.config;
import com.leyou.common.utils.IdWorker;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 初始化分布式ID对象
*/
@Configuration
public class IdWorkerConfig {
@Bean
public IdWorker idWorker(IdWorkerProperties idWorkerProps){
return new IdWorker(
idWorkerProps.getWorkerId(),
idWorkerProps.getDataCenterId());
}
}
04、订单业务:商品微服务查询sku集合接口
ly-item商品微服务中提供根据skuId集合 查询Sku对象集合 的接口:
1)GoodsController
/**
* 根据skuId集合查询Sku对象集合
*/
@GetMapping("/sku/list")
public ResponseEntity<List<Sku>> findSkusByIds(@RequestParam("ids") List<Long> ids){
List<Sku> skus = goodsService.findSkusByIds(ids);
return ResponseEntity.ok(skus);
}
2)GoodsService
public List<Sku> findSkusByIds(List<Long> ids) {
List<Sku> skus = skuMapper.selectBatchIds(ids);
if(CollectionUtils.isEmpty(skus)){
throw new LyException(ExceptionEnum.GOODS_NOT_FOUND);
}
return skus;
}
3)ItemClient
/**
* 根据skuId集合查询Sku对象集合
*/
@GetMapping("/sku/list")
public List<Sku> findSkusByIds(@RequestParam("ids") List<Long> ids);
05、订单业务:用户微服务物流假数据
我们前端页面传来的是addressId,我们需要根据id查询物流信息,但是因为还没做物流地址管理。所以我们准备一些假数据。
首先是实体类:
我们在ly-pojo-user的dto包中添加物流实体类:
package com.leyou.user.dto;
import lombok.Data;
@Data
public class AddressDTO {
private Long id;
private Long userId;
private String addressee;// 收件人姓名
private String phone;// 电话
private String province;// 省份
private String city;// 城市
private String district;// 区
private String street;// 街道地址
private String postcode;// 邮编
private Boolean isDefault;
}
然后在ly-user中编写controller:
/**
* 查询指定收货地址信息
*/
@GetMapping("/address")
public ResponseEntity<AddressDTO> findAddressById(
@RequestParam("userId") Long userId,
@RequestParam("id") Long id
){
AddressDTO addressDTO = userService.findAddressById(userId,id);
return ResponseEntity.ok(addressDTO);
}
ly-user中编写Service
public AddressDTO findAddressById(Long userId, Long id) {
AddressDTO address = new AddressDTO();
address.setId(1L);
address.setStreet("测试街一号");
address.setCity("广州");
address.setDistrict("天河区");
address.setAddressee("小飞飞");
address.setPhone("15800000000");
address.setProvince("广东");
address.setPostcode("510000");
address.setIsDefault(true);
return address;
}
然后在ly-client-user的UserClient中添加新功能:
package com.leyou.user.client;
import com.leyou.user.pojo.AddressDTO;
import com.leyou.user.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
/**
* 用户远程接口
*/
@FeignClient("user-service")
public interface UserClient {
/**
* 判断用户名和密码是否正确
*/
@GetMapping("/query")
public User findUserByNameAndPwd(@RequestParam("username") String username, @RequestParam("password") String password);
/**
* 查询指定用户的指定收货地址
*/
@GetMapping("/address")
public AddressDTO findAddressById(@RequestParam("userId") Long userId, @RequestParam("id") Long id);
}
06、订单业务:商品微服务减库存准备工作
创建订单,肯定需要减库存,我们还要在商品微服务提供减库存接口
1) 在ly-item模块中提供减库存的处理器
/**
* 扣减商品库存量
*/
@PutMapping("/stock/minis")
public ResponseEntity<Void> minusStock(@RequestBody Map<Long,Integer> paramMap){
goodsService.minusStock(paramMap);
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}
2) 在ly-item模块中提供减库存的service
public void minusStock(Map<Long, Integer> paramMap) {
paramMap.entrySet().forEach(entry->{
Long skuId = entry.getKey();
Integer num = entry.getValue();
//查询Sku对象
Sku sku = skuMapper.selectById(skuId);
//判断库存量是否足够
if(sku.getStock()>=num){
//修改库存量
sku.setStock(sku.getStock()-num);
skuMapper.updateById(sku);
}
});
}
3) 在ly-client-item中提供feign接口
package com.leyou.item.client;
import com.leyou.common.pojo.PageResult;
import com.leyou.item.dto.SpecGroupDTO;
import com.leyou.item.dto.SpuDTO;
import com.leyou.item.entity.*;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@FeignClient("item-service")
public interface ItemClient {
……
/**
* 扣减商品库存量
*/
@PutMapping("/stock/minis")
public Void minusStock(@RequestBody Map<Long,Integer> paramMap);
}
07、订单业务:获取登录用户信息
在创建订单的过程中,需要用到登录用户的ID,这时需要编写过滤器校验并获取登录用户信息。
1)编写拦截器(写过)
拷贝ly-cart的以上两个拦截器到ly-order项目中
2)配置拦截器(写过)
在ly-order项目中配置拦截器
package com.leyou.order.config;
import com.leyou.order.interceptor.AppTokenInterceptor;
import com.leyou.order.interceptor.UserTokenInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 配置拦截器
*/
@Configuration
public class MvcConfig implements WebMvcConfigurer{
@Autowired
private AppTokenInterceptor appTokenInterceptor;
@Autowired
private UserTokenInterceptor userTokenInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(appTokenInterceptor).addPathPatterns("/**");
registry.addInterceptor(userTokenInterceptor).addPathPatterns("/**");
}
}
08、订单业务:下单的业务准备代码
订单信息共有3张表,内容很多,但是前台提交的数据却只很少,也就是说我们需要自己填充很多的数据。
接下来我们查看接口文档,编写处理器:
1)编写OrderController
package com.leyou.order.controller;
import com.leyou.order.dto.OrderDTO;
import com.leyou.order.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
/**
* 订单
*/
@RestController
public class OrderController {
@Autowired
private OrderService orderService;
/**
* 创建订单
*/
@PostMapping("/order")
public ResponseEntity<Long> buildOrder(@RequestBody OrderDTO orderDTO){
Long orderId = orderService.buildOrder(orderDTO);
return ResponseEntity.ok(orderId);
}
}
2)编写OrderService
package com.leyou.order.service;
import com.leyou.item.client.ItemClient;
import com.leyou.order.dto.OrderDTO;
import com.leyou.order.mapper.OrderDetailMapper;
import com.leyou.order.mapper.OrderLogisticsMapper;
import com.leyou.order.mapper.OrderMapper;
import com.leyou.order.pojo.Order;
import com.leyou.order.pojo.OrderDetail;
import com.leyou.order.pojo.OrderLogistics;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* 订单业务
*/
@Service
@Transactional
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private OrderDetailMapper orderDetailMapper;
@Autowired
private OrderLogisticsMapper orderLogisticsMapper;
@Autowired
private ItemClient itemClient;
public Long buildOrder(OrderDTO orderDTO) {
//1.插入订单表
Order order = new Order();
orderMapper.insert(order);
//2.插入订单明细表
OrderDetail orderDetail = new OrderDetail();
orderDetailMapper.insert(orderDetail);
//3.插入订单物流表
OrderLogistics orderLogistics = new OrderLogistics();
orderLogisticsMapper.insert(orderLogistics);
//4.扣减对应商品的库存量
itemClient.minusStock(null);
return order.getOrderId();
}
}
09、订单业务:下单的业务实现逻辑(***)
1)OrderService的下单代码
package com.leyou.order.service;
import com.leyou.common.auth.utils.UserInfo;
import com.leyou.common.utils.BeanHelper;
import com.leyou.common.utils.IdWorker;
import com.leyou.common.utils.UserHolder;
import com.leyou.item.client.ItemClient;
import com.leyou.item.pojo.Sku;
import com.leyou.order.dto.CartDTO;
import com.leyou.order.dto.OrderDTO;
import com.leyou.order.mapper.OrderDetailMapper;
import com.leyou.order.mapper.OrderLogisticsMapper;
import com.leyou.order.mapper.OrderMapper;
import com.leyou.order.pojo.Order;
import com.leyou.order.pojo.OrderDetail;
import com.leyou.order.pojo.OrderLogistics;
import com.leyou.order.pojo.OrderStatusEnum;
import com.leyou.user.client.UserClient;
import com.leyou.user.pojo.AddressDTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 订单业务
*/
@Service
@Transactional
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private OrderDetailMapper orderDetailMapper;
@Autowired
private OrderLogisticsMapper orderLogisticsMapper;
@Autowired
private ItemClient itemClient;
@Autowired
private IdWorker idWorker;
@Autowired
private UserClient userClient;
public Long buildOrder(OrderDTO orderDTO) {
//1.插入订单表
Order order = new Order();
//订单ID
order.setOrderId(idWorker.nextId());
//订单总金额
//1)取出当前购买的所有skuId集合
List<Long> skuIds = orderDTO.getCarts().stream().map(CartDTO::getSkuId).collect(Collectors.toList());
//2)根据skuId集合查询Sku对象集合
List<Sku> skuList = itemClient.findSkusByIds(skuIds);
//3)设计Map集合,key:skuId,value:num
Map<Long,Integer> skuMap = orderDTO.getCarts().stream().collect(Collectors.toMap( CartDTO::getSkuId , CartDTO::getNum ));
//4)计算商品总金额
Long totalFee = skuList.stream().mapToLong(sku -> sku.getPrice()*skuMap.get(sku.getId())).sum();
order.setTotalFee(totalFee);
//实付金额 = 总金额-优惠金额+运费
order.setActualFee(1L);
//优惠活动
order.setPromotionIds("1");
//支付类型
order.setPaymentType(orderDTO.getPaymentType());
//邮费
order.setPostFee(50L);
//用户ID
UserInfo userInfo = UserHolder.getUser();
order.setUserId(userInfo.getId());
//发票类型
order.setInvoiceType(0);
//订单来源
order.setSourceType(2);
//订单状态
order.setStatus(OrderStatusEnum.INIT.value());
orderMapper.insert(order);
//2.插入订单明细表
skuList.forEach(sku -> {
OrderDetail orderDetail = new OrderDetail();
orderDetail.setId(idWorker.nextId());
orderDetail.setOrderId(order.getOrderId());//订单ID
orderDetail.setSkuId(sku.getId());
orderDetail.setTitle(sku.getTitle());
orderDetail.setImage(sku.getImages());
orderDetail.setNum(skuMap.get(sku.getId()));
orderDetail.setPrice(sku.getPrice());
orderDetail.setOwnSpec(sku.getOwnSpec());
orderDetailMapper.insert(orderDetail);
});
//3.插入订单物流表
//根据地址ID查询收货人地址信息
AddressDTO address = userClient.findAddressById(userInfo.getId(), orderDTO.getAddressId());
OrderLogistics orderLogistics = BeanHelper.copyProperties(address,OrderLogistics.class);
//订单ID
orderLogistics.setOrderId(order.getOrderId());
//物流单号
orderLogistics.setLogisticsNumber("2122222211");
//物流名称
orderLogistics.setLogisticsCompany("菜鸟物流");
orderLogisticsMapper.insert(orderLogistics);
//4.扣减对应商品的库存量
itemClient.minusStock(skuMap);
return order.getOrderId();
}
}
10、订单业务:下单测试
启动项目,在页面再次点击提交订单,发现提交成功,跳转到了支付页面:
查看数据库,发现订单已经生成:
订单
订单详情:
订单状态:
库存数量:
如果在OrderService方法的最后加上异常
你会发现,订单的三张表数据会随着业务异常而回滚,但是远程调用扣库存的方法还是执行成功啦!这个结果不对的! 因为四个业务逻辑不在一个事务整体里面控制。
11、订单业务:查询订单信息
1)接口分析
支付页面需要展示订单信息,页面加载时,就会发起请求,查询订单信息:
因此我们应该提供查询订单接口:
- 请求方式:Get
- 请求路径:/order/{id}
- 请求参数:路径占位符的id
- 返回结果:订单对象的json结构,包含订单状态,订单详情,需要定义对应的VO对象
对应的api文档如下:
2)编写OrderController
/**
* 根据ID查询订单
*/
@GetMapping("/order/{id}")
public ResponseEntity<OrderVO> findOrderById(@PathVariable("id") Long id){
OrderVO orderVO = orderService.findOrderById(id);
return ResponseEntity.ok(orderVO);
}
3)编写OrderService
public OrderVO findOrderById(Long id) {
//1.查询订单表信息
Order order = orderMapper.selectById(id);
//拷贝数据
OrderVO orderVO = BeanHelper.copyProperties(order, OrderVO.class);
//查询订单明细
OrderDetail orderDetail = new OrderDetail();
orderDetail.setOrderId(order.getOrderId());
QueryWrapper<OrderDetail> queryWrapper = Wrappers.query(orderDetail);
List<OrderDetail> orderDetails = orderDetailMapper.selectList(queryWrapper);
//封装订单明细
orderVO.setDetailList(orderDetails);
//查询订单物流
OrderLogistics orderLogistics = orderLogisticsMapper.selectById(id);
//封装订单物流信息
orderVO.setLogistics(orderLogistics);
return orderVO;
}
4)测试
重启服务测试,可以看到成功了:
12、分布式事务:本地事务与分布式事务
刚才我们编写的扣减库存与保存订单是在两个服务中存在的,如果扣减库存后订单保存失败了是不会回滚的,这样就会造成数据不一致的情况,这其实就是我们所说的分布式事务的问题,接下来我们来学习分布式事务的解决方案。
1)事务
数据库事务(简称:事务,Transaction)是指数据库执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。
事务拥有以下四个特性,习惯上被称为ACID特性:
原子性(Atomicity):事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。
一致性(Consistency):事务应确保数据库的状态从一个一致状态转变为另一个一致状态。一致状态是指数据库中的数据应满足完整性约束。除此之外,一致性还有另外一层语义,就是事务的中间状态不能被观察到(这层语义也有说应该属于原子性)。
隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行,如同只有这一个操作在被数据库所执行一样。
持久性(Durability):已被提交的事务对数据库的修改应该永久保存在数据库中。在事务结束时,此操作将不可逆转。
2)本地事务
起初,事务仅限于对单一数据库资源的访问控制,架构服务化以后,事务的概念延伸到了服务中。倘若将一个单一的服务操作作为一个事务,那么整个服务操作只能涉及一个单一的数据库资源,这类基于单个服务单一数据库资源访问的事务,被称为本地事务(Local Transaction)。
在一个微服务里面所有业务方法操作的且是同一个数据库资源,这就是受到本地事务控制!
Spring的声明事务事务就是很好地本地事务解决方案! 加上 @Transactional注解
3)分布式事务
分布式事务指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上,且属于不同的应用,分布式事务需要保证这些操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。
最早的分布式事务应用架构很简单,不涉及服务间的访问调用,仅仅是服务内操作涉及到对多个数据库资源的访问。
当一个服务操作访问不同的数据库资源,又希望对它们的访问具有事务特性时,就需要采用分布式事务来协调所有的事务参与者。
对于上面介绍的分布式事务应用架构,尽管一个服务操作会访问多个数据库资源,但是毕竟整个事务还是控制在单一服务的内部。如果一个服务操作需要调用另外一个服务,这时的事务就需要跨越多个服务了。在这种情况下,起始于某个服务的事务在调用另外一个服务的时候,需要以某种机制流转到另外一个服务,从而使被调用的服务访问的资源也自动加入到该事务当中来。下图反映了这样一个跨越多个服务的分布式事务:
如果将上面这两种场景(一个服务可以调用多个数据库资源,也可以调用其他服务)结合在一起,对此进行延伸,整个分布式事务的参与者将会组成如下图所示的树形拓扑结构。在一个跨服务的分布式事务中,事务的发起者和提交均系同一个,它可以是整个调用的客户端,也可以是客户端最先调用的那个服务。
较之基于单一数据库资源访问的本地事务,分布式事务的应用架构更为复杂。在不同的分布式应用架构下,实现一个分布式事务要考虑的问题并不完全一样,比如对多资源的协调、事务的跨服务传播等,实现机制也是复杂多变。
只要跨多个微服务,或者跨多个数据库(用了多个数据源),都属于分布式事务的场景!分布式事务的方法不受到@Transactional注解的控制!
13、分布式事务:CAP定理和BASE理论
1) CAP定理
CAP定理是在 1998年加州大学的计算机科学家 Eric Brewer (埃里克.布鲁尔)提出,分布式系统有三个指标
- Consistency 一致性
- Availability 可用性
- Partition tolerance 分区容错(容错性)
它们的第一个字母分别是 C、A、P。Eric Brewer 说,这三个指标不可能同时做到。这个结论就叫做 CAP 定理。
一般要求P必须要成立,A和C只能顾及一个,也就是说,我们只能满足AP或者CP。
分区容错 Partition tolerance
大多数分布式系统都分布在多个子网络。每个子网络就叫做一个区(partition)。分区容错的意思是,区间通信可能失败。比如,一台服务器放在中国,另一台服务器放在美国,这就是两个区,它们之间可能无法通信。
上图中,G1 和 G2 是两台跨区的服务器。G1 向 G2 发送一条消息,G2 可能无法收到。系统设计的时候,必须考虑到这种情况。
一般来说,分区容错无法避免,因此可以认为 CAP 的 P 总是成立。CAP 定理告诉我们,剩下的 C 和 A 无法同时做到。
可用性 Availability
Availability 中文叫做"可用性",意思是只要收到用户的请求,服务器就必须给出回应。
用户可以选择向 G1 或 G2 发起读操作。不管是哪台服务器,只要收到请求,就必须告诉用户,到底是 v0 还是 v1,否则就不满足可用性。
一致性 Consistency
Consistency 中文叫做"一致性"。意思是,写操作之后的读操作,必须返回该值。
举例来说,某条记录是 v0,用户向 G1 发起一个写操作,将其改为 v1。
![在这里插入图片描述](https://img-blog.csdnimg.cn/22982ca9d49e40c0972b08a032560542.png)
问题是,用户有可能向 G2 发起读操作,由于 G2 的值没有发生变化,因此返回的是 v0。G1 和 G2 读操作的结果不一致,这就不满足一致性了。
为了让 G2 也能变为 v1,就要在 G1 写操作的时候,让 G1 向 G2 发送一条消息,要求 G2 也改成 v1。
一致性和可用性的矛盾
一致性和可用性,为什么不可能同时成立?答案很简单,因为可能通信失败(即出现分区容错)。
如果保证 G2 的一致性,那么 G1 必须在写操作时,锁定 G2 的读操作和写操作。只有数据同步后,才能重新开放读写。锁定期间,G2 不能读写,没有可用性。
如果保证 G2 的可用性,那么势必不能锁定 G2,所以一致性不成立。
综上所述,G2 无法同时做到一致性和可用性。系统设计时只能选择一个目标。如果追求一致性,那么无法保证所有节点的可用性;如果追求所有节点的可用性,那就没法做到一致性。
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(最终一致性)
系统能够保证在没有其他新的更新操作的情况下,数据最终一定能够达到一致的状态,因此所有客户端对系统的数据访问最终都能够获取到最新的值。
14、分布式事务:分布式解决方案
1)基于XA协议的两阶段提交
首先我们来简要看下分布式事务处理的XA规范 :Spring整合JTA
可知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%保证强一致)
缺点: 实现复杂,牺牲了可用性,对性能影响较大,不适合高并发高性能场景。
2)TCC补偿机制
TCC 其实就是采用的补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。它分为三个阶段:
- Try 阶段主要是对业务系统做检测及资源预留
- Confirm 阶段主要是对业务系统做确认提交,Try阶段执行成功并开始执行 Confirm阶段时,默认 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。
- Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。
例如:A客户使用携程APP到东方航空和北京航空去订票,思路大概是:
票:广州->北京 东方航空: 广州->上海 北京航空: 上海->北京
每家航空公司的订票流程
1)Try: 预留机票座位(返回预留成功或失败的结果)
2) Confirm:如果两家公司Try返回的结果都是预留成功,继续调用两家公司的Confirm(下订单)
3)Cancel:只要其中一家公司Try返回的结果预留失败,则统一两家公司的Cancel(把之前预留成功的位置回退)
优点: 相比两阶段提交,可用性比较强
缺点: 数据的一致性要差一些。TCC属于应用层的一种补偿方式,所以需要程序员在实现的时候多写很多补偿的代码,在一些场景中,一些业务流程可能用TCC不太好定义及处理。
3)消息最终一致性(*)
消息最终一致性应该是业界使用最多的,其核心思想是将分布式事务拆分成本地事务进行处理,这种思路是来源于ebay。我们可以从下面的流程图中看出其中的一些细节:
基本思路就是:
消息生产方,需要额外建一个消息表,并记录消息发送状态。消息表和业务数据要在一个事务里提交,也就是说他们要在一个数据库里面。然后消息会经过MQ发送到消息的消费方。如果消息发送失败,会进行重试发送。
消息消费方,需要处理这个消息,并完成自己的业务逻辑。此时如果本地事务处理成功,表明已经处理成功了,如果处理失败,那么就会重试执行。如果是业务上面的失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚等操作。
生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。如果有靠谱的自动对账补账逻辑,这种方案还是非常实用的。
这种方案遵循BASE理论,采用的是最终一致性,比较适合实际业务场景,即不会出现像2PC那样复杂的实现(当调用链很长的时候,2PC的可用性是非常低的),也不会像TCC那样可能出现确认或者回滚不了的情况。
优点: 一种非常经典的实现,避免了分布式事务,实现了最终一致性,性能比较好。
缺点: 消息表会耦合到业务系统中(成本高)。
4)MQ事务性消息(了解)
MQ的解决方案需要该类型的MQ能够有事务消息的情况下进行;当下只有RocketMQ能做到,所以基于MQ的分布式事务解决方案中的MQ都是说的是RocketMQ。
RocketMQ通过事务性消息来实现本地事务与MQ消息的原子性、一致性和可靠性;而远程事务的处理是尽可能保证(上述案例中,订单生成的操作尽可能100%成功),如果处理失败还是需要人工介入的。
15、分布式事务:Seata简介
分布式事务框架很多:tcc-transaction、Hmily、ByteTCC、myth、EasyTransaction、tx-lcn、seata等等框架,这里有一篇关于这些框架压测的测试报告【不包括seata】:http://springcloud.cn/view/374 ,可以了解下 。
这里我们采用seata来实现分布式事务。
2019 年 1 月,阿里巴巴中间件团队发起了开源项目 Fescar(Fast & EaSy Commit And Rollback),和社区一起共建开源分布式事务解决方案。Fescar 的愿景是让分布式事务的使用像本地事务的使用一样,简单和高效,并逐步解决开发者们遇到的分布式事务方面的所有难题。
Fescar 开源后,蚂蚁金服加入 Fescar 社区参与共建,并在 Fescar 0.4.0 版本中贡献了 TCC 模式。
为了打造更中立、更开放、生态更加丰富的分布式事务开源社区,经过社区核心成员的投票,大家决定对 Fescar 进行品牌升级,并更名为 Seata,意为:Simple Extensible Autonomous TransactionArchitecture,是一套一站式分布式事务解决方案。
Seata 融合了阿里巴巴和蚂蚁金服在分布式事务技术上的积累,并沉淀了新零售、云计算和新金融等场景下丰富的实践经验。
Seata简介
Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。
https://seata.io/zh-cn/
解决分布式事务问题,有两个设计初衷
**对业务无侵入:**即减少技术架构上的微服务化所带来的分布式事务问题对业务的侵入 高性能:减少分布式事务解决方案所带来的性能消耗
Seata目前有三种分布式事务实现方案:AT、TCC及SAGA
- AT模式主要关注多 DB 访问的数据一致性,当然也包括多服务下的多 DB 数据访问一致性问题2PC-改进
- TCC 模式主要关注业务拆分,在按照业务横向扩展资源时,解决微服务间调用的一致性问题
- Saga模式是SEATA提供的长事务解决方案,在Saga模式中,业务流程中每个参与者都提交本地事
务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都
由业务开发实现。
16、分布式事务:Seata模式说明
1)AT模式
Seata AT模式是基于XA事务演进而来的一个分布式事务中间件,XA是一个基于数据库实现的分布式事务协议,本质上和两阶段提交一样,需要数据库支持,Mysql5.6以上版本支持XA协议,其他数据库如Oracle,DB2也实现了XA接口。
事务协调器Transaction Coordinator (TC): 事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。
事务管理器Transaction Manager(TM): 控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。
**资源管理器Resource Manager (RM):**控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。
Branch就是指的分布式事务中每个独立的本地局部事务。
一阶段
在一阶段,Seata 会拦截“业务 SQL”,首先解析 SQL 语义,找到“业务 SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”,然后执行“业务 SQL”更新业务数据,在业务数据更新之后,再将其保存成“after image”,最后生成行锁。以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。任何提交的业务数据的更新一定有相应的回滚日志存在
基于这样的机制,分支的本地事务便可以在全局事务的第一阶段提交,并马上释放本地事务锁定的资源;这也是Seata和XA事务的不同之处,两阶段提交往往对资源的锁定需要持续到第二阶段实际的提交或者回滚操作,而有了回滚日志之后,可以在第一阶段释放对资源的锁定,降低了锁范围,提高效率,即使第二阶段发生异常需要回滚,只需找对undolog中对应数据并反解析成sql来达到回滚目的。
同时Seata通过代理数据源将业务sql的执行解析成undolog来与业务数据的更新同时入库,达到了对业务无侵入的效果。
二阶段提交
二阶段如果是提交的话,因为“业务 SQL”在一阶段已经提交至数据库, 所以 Seata 框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。
二阶段“回滚”
二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的“业务 SQL”,还原业务数据。回滚方式便是用“before image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和“after image”,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。
2)TCC模式(了解)
2019 年 3 月,蚂蚁金服加入分布式事务 Seata 的社区共建中,并贡献其 TCC 模式。TCC 模式通常用于非关系型数据库的分布式事务的实现,作为AT模式的补充。可以与AT模式混合使用。
Seata也针对TCC做了适配兼容,支持TCC事务方案,原理前面已经介绍过,基本思路就是使用侵入业务上的补偿及事务管理器的协调来达到全局事务的一起提交及回滚。
详细可参考 https://seata.io/zh-cn/docs/dev/mode/tcc-mode.html