商品、购物车
商品服务:
1.全品类购物平台
SPU:Standard Product Unit 标准化产品单元。是商品信息聚合的最小单位。是一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特性。通俗点讲,属性值、特性相同的商品就可以称为一个SPU。通俗的说法:商品属性
SKU:Stock Keeping Unit,库存进出计量的基本单元,可以是以件,盒,托盘等为单位。通俗的说法:影响商品库存的属性。
SPU包含SKU
一个商品有100个属性,SPU,2个属性影响库存,2个SKU
购物车服务:
加减号的性能:Redis缓存->更新Redis->MQ->更新Mysql
一、需求
实现订单业务:预览、下单、订单超时、取消订单、更新订单状态、我的订单
下单(超卖)、超时(回滚)、取消(回滚)
二、分析
订单状态:
- 待付款:代表买家下单了但是还没有付款。
- 待发货(同待接单):代表买家付款了卖家还没有发货。
- 已发货(同待收货):代表卖家已经发货并寄出商品了。
- 已完成(同待评价):代表买家已经确认收到货了。
- 已关闭(同已取消):代表订单过期了买家也没付款、或者卖家关闭了订单。
- 已超时:超过一定时间,没有付款
订单的核心接口:
下单:购物车选择
下单:商品详情-立即购买
下单接口
订单超时-MQ的延迟队列
订单取消接口
查询我的订单
更改订单状态接口
三、设计
数据库设计:
CREATE TABLE `t_items` (
`id` int(11) primary key auto_increment,
`name` varchar(20) ,
`no` varchar(10)
) comment '10.商品类型表';
CREATE TABLE `t_goods` (
`id` int(11) primary key AUTO_INCREMENT,
`item_id` int(11) comment '类型id',
`title` varchar(256) comment '名称',
`promo_words` varchar(2048) comment '描述',
`small_pic` varchar(100) comment '图片',
`price` decimal(10, 2) comment '价格',
`create_time` datetime,
`status` char(1) comment '状态',
`stock_num` int(11) comment '库存数量'
) comment '11.商品表';
CREATE TABLE `t_cart` (
`id` int(11) primary key auto_increment,
`uid` int,
`gid` int,
`gjprice` decimal(10, 2) comment '加入时价格',
`num` int
) comment '12.购物车表';
CREATE TABLE `t_order` (
`id` int(11) primary key auto_increment,
`uid` int,
`uaid` int comment '用户收货地址',
`total_money` decimal(10, 2) comment '总金额',
`pay_money` decimal(10, 2) comment '支付金额',
`free_money` decimal(10, 2) comment '优惠金额',
`pay_type` int comment '支付类型',
`flag` int comment '订单状态',
`create_time` datetime comment '创建时间',
`update_time` datetime comment '更新时间'
) comment '13.订单表';
CREATE TABLE `t_orderitem` (
`id` int(11) primary key auto_increment,
`oid` int,
`gid` int,
`price` decimal(10, 2) comment '价格',
`num` int
) comment '14.订单详情表';
CREATE TABLE `t_orderlog` (
`id` int(11) primary key auto_increment,
`oid` int,
`type` int comment '对应订单状态类型',
`info` varchar(50) comment '内容',
`create_time` datetime
) comment '15.订单状态变化表';
难点:
1.超卖 -锁
2.数据一致性–事务
3.超时-MQ死信机制
4.性能-MQ削峰填谷
锁:保证多个线程,同时只能有一个访问
事务:保证多个SQL语句,要么都成功要么都失败
四、编码
4.1 商品服务
DDD领域驱动模型
什么时候可以使用DDD领域驱动:大型互联网
业务:快速变化
需求变更快,需求迭代快,业务扩展快的项目。
开发人员:3-5年经验
需要熟练应用过很多设计模式,有了这些经验才能理解为什么这么设计,才能不偷懒的去做这件事,否则很容易对领域造成污染。
时间:更充分
需要有充分的时间去划分业务,设计领域,需要更多的类,编写更多代码量
DDD领域驱动设计的分包:
- gate: 对外暴露的服务
- rest:对外暴露的rest服务
- application:应用服务层
- domain:领域层
- item:商品分类
- goods:商品
- infra:基础设施层
- repo:仓储实现
- mapper: 数据库交互
ORM框架:操作数据库的框架
Mybatis(Mybatis-plus)、Hibernate(曾经的王者)、Spring Data JPA
SSH框架:Struts2+Spring+Hibernate
SSM框架:SpringMVC+Spring+Mybatis
SSS框架:SpringMVC+Spring+Spring Data JPA
SpringBoot框架:Spring+SpringMVC+Mybatis(Spring Data JPA)
Spring Data JPA:Spring开发的一套操作数据库的框架
Spring Data系列框架,都是Spring封装操作数据的各种框架的组合
比如:
操作Mysql数据库:Spring Data JPA
操作Redis数据库:Spring Data Redis
操作ElasticSearch:Spring Data ElasticSearch
Spring Data JPA基于Java 的JPA技术,实现的操作数据库的框架,基于注解实现数据库的操作
特性:自动生成对应表,封装了单表的操作,多种实现方式(JPQL、方法名解析查询、原生SQL、CRUD接口)
使用步骤:
1.依赖jar包
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
2.实现配置
spring:
application:
name: GoodsServer-xph
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
datasource:
url: jdbc:mysql://110.40.192.129:3310/db_cloud
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: zzjava
type: com.alibaba.druid.pool.DruidDataSource
druid:
initial-size: 15
max-active: 100
min-idle: 15
max-wait: 60000
web-stat-filter:
enabled: true
url-pattern: "/*"
exclusions: "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*"
stat-view-servlet:
url-pattern: "/druid/*"
reset-enable: false
login-username: admin
login-password: zzjava
enabled: true
jpa:
database: mysql
hibernate:
ddl-auto: update
3.编写实体类
@Data
@Entity //标识 映射类
@Table(name = "t_goods")//设置表名
public class Goods {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
private Integer itemId;
private String title;
private String promoWords;
private String smallPic;
private Date createTime;
private Integer status;
private Integer stockNum;
}
4.编写持久
创建接口继承JpaRepository
public interface GoodsDao extends JpaRepository<Goods,Integer> {
//查询 JQPL:面向对象查询语言 表->类
@Query(value = "from Goods")
List<Goods> selectAll();
//方法名解析查询:按照约定,编写方法名,就会自动生成SQL语句
Goods getById(int id);
//模糊查询
List<Goods> getByTitleLike(String title);
//原生SQL语句
@Modifying//执行DML类型
@Query(value = "update t_goods set stock_num=stock_num+:num where id=:id",nativeQuery = true)
int updateStock(int num,int id);
}
5.使用
Spring Data JPA的核心:
1.接口:JpaRepository<实体类名,注解的数据类型>
提供:单表的增删改查分页和排序
2.JPQL:面向对象查询语言
Spring Data JPA-封装的面向对象查询语句
表名—>类名
字段—>属性
@Query 注解
@Query(value = "from Goods")
List<Goods> selectAll();
比如:
sql:select * from t_goods
jpql:from Goods
3.方法名解析查询
根据规则,合理的命名方法名,可以自动根据方法名生成SQL语句
4.原生SQL语句
可以在注解@Query上面写SQL语句,但是需要设置属性:nativeQuery = true。否则只能写jpql
如果执行的sql语句是DML类型,那么必须使用一个注解:@Modifying//执行DML类型
@Modifying//执行DML类型
@Query(value = "update t_goods set stock_num=stock_num+:num where id=:id",nativeQuery = true)
int updateStock(int num,int id);
4.2 订单服务
搬砖
下单接口–>考虑实现订单超时
下单接口的第一版实现:
@Service
public class OrderServiceImpl implements OrderService {
@Resource
private OrderDao dao;
@Resource
private OrderLogDao logDao;
@Resource
private OrderItemDao itemDao;
@Resource
private RabbitTemplate rabbitTemplate;
@Resource
private GoodsProvider goodsProvider;
@Resource
private CouponProvider couponProvider;
//难点:1.超卖-锁 2.多服务调用-数据一致性-事务 3.性能-Redis MQ 4.订单超时
@Override
public R save(OrderAddDto dto,int uid) {
//下单业务-流程
//1.查询商品 跨服务远程调用
R rgoods=goodsProvider.single(dto.getGid());
if(RCode.成功.getCode()==rgoods.getCode()){
GoodsDto goodsDto= (GoodsDto) rgoods.getData();
//2.验证库存 校验商品是否存在并查验商品状态
if(goodsDto!=null && SystemConfig.GOODS_STATUS_UP==goodsDto.getStatus()){
//检查库存 普通锁
Lock lock=new ReentrantLock();
lock.lock();
try {
if (goodsDto.getStockNum() >= dto.getNum()) {
//库存够
//3.生成订单
Order order = new Order();
order.setFlag(OrderFlag.待付款.getCode());
order.setTotalMoney(goodsDto.getPrice() * dto.getNum());
//优惠金额=积分抵扣+优惠券减免
order.setFreeMoney(dto.getScore() / 100.0);
//4.查询优惠信息,计算优惠
boolean isCoupon = false;
UserCouponDto couponDto = couponProvider.detail(dto.getUcid());
if (couponDto != null) {
//前端已经校验,为什么后端还需要校验,防止绕过前端
// 验证优惠券是否可用 1.有没有失效 2.满减规则 3.范围
if (couponDto.getCategory() == 51) {
//满减 满一定金额。才可以使用优惠券
if (order.getTotalMoney() >= couponDto.getLimitmoney()) {
order.setFreeMoney(order.getFreeMoney() + couponDto.getDiscount());
isCoupon = true;
}
} else if (couponDto.getCategory() == 52) {
//折扣 满一定金额才可以才可以打折
if (order.getTotalMoney() >= couponDto.getLimitmoney()) {
order.setFreeMoney(order.getFreeMoney() + order.getTotalMoney() * couponDto.getDiscount());
isCoupon = true;
}
} else if (couponDto.getCategory() == 53) {
//立减
order.setFreeMoney(order.getFreeMoney() + couponDto.getDiscount());
isCoupon = true;
}
}
//设置支付金额
order.setPayMoney(order.getTotalMoney() - order.getFreeMoney());
order.setCreateTime(new Date());
order.setUaid(dto.getUaid());
order.setUpdateTime(new Date());
//操作数据库,新增订单
if (dao.insert(order) > 0) {
//4.扣减库存
goodsProvider.update(new GoodsStockDto(goodsDto.getId(), dto.getNum()));
//5.积分抵扣
if (dto.getScore() > 0) {
//MQ
rabbitTemplate.convertAndSend("",
RabbitMQConstConfig.Q_USERSCORE,
new MqMsgBo(SnowFlowUtil.getInstance().nextId(), RabbitMQConstConfig.MQTYPE_ORDERADD,
new UserMqBo(uid, dto.getScore(), SystemConfig.USER_OP_SIGN, "", "")));
}
//6.优惠券使用
if (isCoupon) {
couponProvider.update(couponDto.getId(), SystemConfig.USER_COUPON_USED);
}
//7.新增订单详情
OrderItem item = new OrderItem(order.getId(), dto.getGid(), goodsDto.getPrice(), dto.getNum());
itemDao.insert(item);
//8.记录订单流水
logDao.insert(new OrderLog(order.getId(), OrderFlag.待付款.getCode(), "商品详情下单成功!"));
return RUtil.ok(order);
}
} else {
//库存不够
return RUtil.fail("亲,商品库存不足!");
}
}finally {
lock.unlock();
}
//分布式锁
}else {
return RUtil.fail("亲,商品不存在!");
}
}
return RUtil.fail("亲,商品服务异常!");
}
}
查询我的订单
取消订单
4.3 分布式事务
事务、分布式事务:解决数据操作(DML)的一致性
分布式事务:解决多个服务嵌套调用(涉及到数据库的数据改变),保证要么都成功,要么都失败
比如:订单服务,远程调用商品服务、优惠券服务,保证三个服务要么都成功,要么都失败!
为什么需要使用分布式事务?
因为在服务的嵌套调用的时候,每个服务都有自己的Connection,所以之前的事务,只能保证同一个Connection下操作的一致性。
什么时候使用分布式事务:
服务嵌套调用,且服务(>1)都涉及到了DML语句的执行
为什么说:分布式事务是世界性难题?
原因:CAP
分布式事务解决方案:
1.2PC
1.准备阶段
2.确认阶段:提交或回滚
2.3PC
1.准备阶段
2.预提交阶段
3.确认阶段:提交或回滚
3.TCC
try:尝试执行
confrim:尝试成功,确认
cancel:尝试失败,取消
4.基于MQ
订单业务实现分布式事务:
订单服务:
1.依赖jar包
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<version>2021.1</version>
</dependency>
2.实现配置
3.入口方法上开启全局事务
所有被分布式事务协调的方法都需要本地事务的支持
商品服务:
1.依赖jar包
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<version>2021.1</version>
</dependency>
2.实现配置
3.涉及方法上开启本地事务
优惠券服务:
1.依赖jar包
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<version>2021.1</version>
</dependency>
2.实现配置
3.涉及方法上开启本地事务
默认使用的是Seata中的XA模式的事务提交方案,运行前务必保证nacos的运行和Seata-Server的运行
启动Seata-Server:sh seata-server.sh -p 8091 -m file
4.4 订单超时
代码示例:
// 下单->Redis->MQ->数据同步+延迟队列
@Override
public R save2(OrderAddDto dto, int uid) {
R rgoods=goodsProvider.single(dto.getGid());
if(RCode.成功.getCode()==rgoods.getCode()) {
GoodsDto goodsDto = (GoodsDto) rgoods.getData();
//2.验证库存 校验商品是否存在并查验商品状态
if (goodsDto != null && SystemConfig.GOODS_STATUS_UP == goodsDto.getStatus()) {
//检查库存 普通锁
Lock lock = new ReentrantLock();
lock.lock();
try {
if (goodsDto.getStockNum() >= dto.getNum()) {
//生成订单 存储Redis--MQ--Mysql
Order order = new Order();
order.setNo(SnowFlowUtil.getInstance().nextId());
order.setFlag(OrderFlag.待付款.getCode());
order.setCreateTime(new Date());
order.setUaid(dto.getUaid());
order.setUid(uid);
order.setUpdateTime(new Date());
order.setTotalMoney(goodsDto.getPrice() * dto.getNum());
if(dto.getUcid()>0){
//有优惠券
UserCouponDto couponDto = couponProvider.detail(dto.getUcid());
order.setFreeMoney(couponDto.getCategory()==52?order.getTotalMoney()-(order.getTotalMoney() * couponDto.getDiscount()):couponDto.getDiscount());
}
if(dto.getScore()>0){
//有积分抵扣
order.setFreeMoney(order.getFreeMoney()+dto.getScore()/100.0);
}
order.setPayMoney(order.getTotalMoney()-order.getFreeMoney());
//扣减库存
R r=goodsProvider.update(new GoodsStockDto(goodsDto.getId(), -dto.getNum()));
if(r.getCode()==RCode.成功.getCode()){
//订单存储到Redis中 类型 有效期 数据 同步
RedissonUtils.setHash(RedisKeyConfig.ORDER_ADD,order.getNo()+"",order);
//基于MQ发送消息
rabbitTemplate.convertAndSend(RabbitMQConstConfig.EX_ORDERADD,"",
new MqMsgBo(SnowFlowUtil.getInstance().nextId(), RabbitMQConstConfig.MQTYPE_ORDERSYNC,
new OrderSyncDto(order.getNo()+"",dto.getUcid(),dto.getGid(),dto.getNum(),goodsDto.getPrice())));
//.积分抵扣
if (dto.getScore() > 0) {
//MQ
rabbitTemplate.convertAndSend("",
RabbitMQConstConfig.Q_USERSCORE,
new MqMsgBo(SnowFlowUtil.getInstance().nextId(), RabbitMQConstConfig.MQTYPE_ORDERADD,
new UserMqBo(uid, dto.getScore(), SystemConfig.USER_OP_SIGN, "", "")));
}
return RUtil.ok();
}else {
return RUtil.fail("亲,网络异常,商品服务库存问题!");
}
}
} finally {
lock.unlock();
}
}
}
return RUtil.fail("亲,下单失败!");
}
@Override
public R queryMy(int uid, int flag) {
QueryWrapper<Order> queryWrapper=new QueryWrapper<>();
queryWrapper.eq("uid",uid);
if(flag>0){
queryWrapper.eq("flag",flag);
}
queryWrapper.orderByDesc("id");
return RUtil.ok(dao.selectList(queryWrapper));
}
//后台调用-发货
@Override
public R updateFlag(OrderFlagDto dto) {
if(dao.updateFlag(dto.getOid(),dto.getFlag())>0){
return RUtil.ok();
}else {
return RUtil.fail();
}
}
@Transactional
@GlobalTransactional //分布式事务
@Override
public R cancel(String no,int uid) {
//1.查询订单状态,防止
Order order=dao.selectOne(new QueryWrapper<Order>().eq("no",no).eq("uid",uid));
if(order!=null){
//验证订单状态
if(order.getFlag()==OrderFlag.待付款.getCode()){
//超时了,取消订单
//更改订单状态
if(dao.updateFlag(order.getId(),OrderFlag.已关闭.getCode())>0){
//查询订单详情
List<OrderItem> items=itemDao.selectList(new QueryWrapper<OrderItem>().eq("oid",order.getId()));
items.forEach(oi->{
//释放库存
goodsProvider.update(new GoodsStockDto(oi.getGid(),oi.getNum()));
});
//释放优惠券
OrderLog couponlog=logDao.selectOne(new QueryWrapper<OrderLog>().eq("oid",order.getId()).eq("type",100));
if(couponlog!=null){
//用了优惠券,释放
couponProvider.update(Integer.parseInt(couponlog.getInfo()),SystemConfig.USER_COUPON_NO);
}
//释放积分
OrderLog scorelog=logDao.selectOne(new QueryWrapper<OrderLog>().eq("oid",order.getId()).eq("type",101));
if(scorelog!=null){
//用了积分,释放
userProvider.updateScore(order.getUid(),Integer.parseInt(scorelog.getInfo()),SystemConfig.SCORETYPE_ORDERCANCEL);
}
//记录日志
logDao.insert(new OrderLog(order.getId(), OrderFlag.已超时.getCode(), "订单超时,自动取消!"));
}
}
}
return RUtil.fail("订单取消失败!");
}
如果感觉有用点个关注,一键三连吧!蟹蟹!!!
各位看官》创作不易,点个赞!!!
诸君共勉:看花不是花,看雪不是雪
免责声明:本文章仅用于学习参考