Thanks to visit codestin.com
Credit goes to github.com

Skip to content

基于 SpringBoot(Spring + Springboot + MyBatis)+ES+RabbitMQ 前后端分离电商项目

hmProgramer/hm_mall

Repository files navigation

hm_mall项目介绍

翰墨商城是一个基于互联网电商平台的后台项目 主要包括 商品分类,品牌管理,文件上传下载,商品搜索服务,商品页面服务,结合RSA的授权中心

使用到的技术

该项目是基于springboot,sping,mybatis的通用mapper框架进行开发; 通过Elasticsearch 实现商品搜索服务;基于rabbitmq消息队列实现商品服务与搜索服务和静态页面的同步管理; 通过fastDFS实现文件的上传下载;使用redis实现存储用户的临时验证码; `基于JWT + RSA非对称加密 实现用户的认证授权;

1. 商品的品牌管理和查询

商品分类表设计注意点:商品分类是分层级的,如何区分儿子和爹的关系,就是在表中添加一个parentID字段 通用mapper会把对象中的非空属性作为查询条件 问题整体解决思路: 通过前端请求,分析请求方式(post,get..),请求路径(http://api.leyou.com/api/item/category/list?pid=0)请求参数(pid),返回类型(是个json数组) 进而来设计和构造controller和业务层 利用cors解决跨域问题 具体使用:在网关中加入GlobalCorsConfig,添加跨域信息,定义请求方式,头信息等

2. 商品分类表设计注意事项

数据库设计的时候注意,tb_category_brand用于商品分类和品牌之间的关联,同时这表里没有外键,为什么不加外键呢?为了提高电商系统的性能,因为外键关联会影响到商品的删除 也会影响数据库的读写性能 根据返回数据的特点:有个用brand对象包装的list还有个总条数,所以选择用对象来包装返回结果 具体逻辑处理时:包括1 分页,2.过滤 3.排序 4.查询 5.解析分页结果 商品新增功能

3. 商品的文件上传

注意一点:由于文件上传在经过zuul网关时,再高并发时可能会导致网络阻塞,zuul网关不可用,所以我们在做文件上传时最好绕过请求的缓存(也会经过zuul网关,只是不会在缓存请求) 解决方法:通过nginx的指令对地址进行重写,修改到以/zuul为前缀

4. 文件上传下载的升级

通过fastDFS进行小文件的上传下载 将代码中的字符串,及变量配置到配置文件中去 具体操作步骤: 1 在application.yml文件中配置 hm.upload.baseUrl 和 hm.upload.allowTypes属性 2 定义 UploadProperties属性类 3 在UploadService中注入 UploadProperties属性类 并使用属性替代字符串

5. spu与sku的关系

spu:标准产品单位 sku:库存量单位 spu是一个抽象的商品集概念,作用:为了方便后台管理, sku因具体特征不同而区分出的商品,sku是具体要销售的商品,用户购买的是sku 每一个分类都有统一的规格参数模板,但不同商品其参数值可能不同 商品分类与规格模板是一对一关系 商品分类与商品spu是一对多关系 规模模板与商品spu是一对多关系 商品spu保存有规格参数的具体值 因为sku的特有属性是商品规格参数的一部分, 所以可以将规格参数中的属性划分为 SKU 通用规格与SKU特有规格 所以 spu与sku是一对多关系 spu保存着所有sku共享的规格属性 sku中保存每个sku特有的规格属性

6. 将库存表和sku表分开处理的目的?

因为库存字段写频率较高,而sku的其他字段以读为主,因此我们将两张表分离,读写不会干扰 sku表中的indexes字段是一个特有规格属性字段,它表示spu属性模板中的对应下标组合字段 因为在spu表中其实已经对特有规格参数及可选项作了保存,我们在sku表中将不同的角标串联起来作为spu下不同 sku的标识,这就是index字段;;;、

当前规格参数的显示应该是以能够查询到的所有商品为准,而不是以数据库中的所有查询区间 解决思路是查询数据库中的商品尺寸规格,然后处理成段,再覆盖原来的值

进行搜索时,存的其实是spu,以spu为单位,主要包括,图片,价格,标题,副标题;暗藏的数据:spu的id,sku的id 首先要编写分类和品牌查询的相关服务 当实现将数据库中的数据同步到索引库时 需要用到Feign(使用feign之后,我们调用eureka 注册的其他服务,在代码中就像各个service之间相互调用那么简单。) 同时要想再serach-service中获取品牌信息和分类等信息,如果直接在serach-service中定义接口这种做法不好,因为调用方无法知道被调用方的接口信息和参数, 这样也将两者绑定死了 如何来操作? 在被调用方的item-interface中定义接口信息,在serach-service中引入hm-interface的包,然后继承interface中的接口 @FeignClient(value = "item-service") public interface GoodsClient extends GoodsApi { }

7. 解决搜索服务与商品页面服务同步的问题

1. 两种传统路线,

  1. 每一次对商品做增删改查时,同时修改索引库数据及页面数据
  2. 搜索服务与商品页面服务对外提供接口,后台在商品增删改后调用接口 但是这两种路线有缺陷,代码耦合度较高,违背了微服务的独立原则

2. 新的解决路线

通过消息队列 消息队列的优势:消息队列分为生产者和消费者,生产者负责消息的发送,消费者负责消息的接收,两者是异步的,而且也只关注消息的接收与发送, 没有业务逻辑的侵入,对代码的耦合度较低

3. 消息队列中的问题:

  1. 消息丢失如何处理?(如果消费者领取消息后,还没执行操作就挂掉了呢?或者抛出了异常?消息消费失败,这时消息就丢失了) 采用rabbitmq的ack确认机制 而ack机制分为两种。自动ack与手动ack, 两者区别:自动ack是消息一旦被接收,那么消费者就会自动发送ack; 而手动ack是消息接收后不会自动发送ack,需要手动来调用 而如何来去选择 就要看消息的重要性
  • 如果消息不太重要,丢失也没有影响,那么自动ACK会比较方便
  • 如果消息非常重要,不容丢失。那么最好在消费完成后手动ACK,否则接收消息后就自动ACK,RabbitMQ就会把消息从队列中删除。 如果此时消费者宕机,那么消息就丢失了。 如何实现 channel.basicConsume(QUEUE_NAME, false, consumer); 当boolean类型值为true为自动ack, 反之为false同时当为手动ack时要注意如果设置为手动ack,一定要有手动ack代码 channel.basicAck(envelope.getDeliveryTag(), false); 引申:如果消息在消费者还未来得及消费时就丢失了?该如何处理? 通过将消息持久化,但是消息持久化的前提是:队列、Exchange都持久化,消息的持久化是在发送消息时设置devilerMode为2 对于生产者方出现消息丢失时可以采用生产者确认机制来保证,就是MQ向生产方发送消息 而对于除MQ以外的其他消息工具不带生产者确认时,应该怎么做?先将消息持久化到数据库,并记录消息状态(可靠消息服务)
  1. 如何避免消息堆积? 什么情况会消息堆积?比如第三方的发短信,转账业务,或者大量订单 1)采用workqueue,多个消费者监听同一队列 2)接收到消息后,通过线程池,异步消费

  2. 如何避免消息的重复执行? 要保证消息的幂等性(同一接口被重复执行,其结果一致)

  3. 消息队列的几种类型? simple(基本消息模型),--------》一个生产者,一个消费者,一个队列 worker(work消息模型),----》一个生产者,多个消费者,一个队列 工作队列模型 默认是队列把消息每一轮平均分给多个消费者,这样就有个缺点,无法根据消费者的自身能力来去消费; 解决办法就是配置 channel.basicQos(1); (设置每个消费者同时只能处理一条消息)---》能者多劳 订阅模型 fanout(发布订阅模式--fanout)----------》一个生产者,可以有多个消费者,每个消费者绑定自己的队列,每个队列都要绑定到交换机, 生产者发送的消息只能发送到交换机, 注意 只有队列可以存消息,而消费者是将消息发送到交换机的,但是交换机是不存储消息的,所以如果没有队列绑定 该交换机,那么消息就会丢失 direct(订阅模型--direct) ------》队列与交换机的绑定不能是任意绑定,必须要指定至少一个routingkey(路由key)、

    Topic(订阅模型--Topic)--------》与direct模式类似,但是通过通配符的方式进行配置,所以更加灵活

4. 具体的业务思路:

1 当对商品数据做增删改查后会调用消息队列,发送消息,也不关心消息被谁接收 2 搜索服务和静态页面服务接收到消息后,分别去处理索引库和静态页面

5. 如何实现?

在hm-item服务中当对商品做增删改查时执行mq操作; 同时在hm-search服务中监听商品操作,

8. 实现用户中心服务

具体包括:用户的注册,登录,个人信息管理,用户地址管理,用户收藏管理,订单管理,优惠券 具体操作:新建hm-user

9. 实现发送短信服务

*因为短信发送API调用时长的不确定性,为了提高程序的响应速度 短信发送我们都将采用异步发送方式,即: - 短信服务监听MQ消息,收到消息后发送短信。 - 其它服务要发送短信时,通过MQ通知短信微服务。 短信服务心得总结: 1. 短信服务因为不止在一个服务模块用到,所以独立出来作为一个模块 2. 而具体的发送短信请求通过mq消息通知, 即其他服务(如用户服务): 需要发送短信时--》(1 生成6位数字验证码,2 将验证码存到redis中,并设置有效期,3调用mq消息 通知短信服务) 短信服务:短信服务处理短信时-->(1 短信服务的listener负责监听mq传递的消息; 2 调用发短信接口,3 短信服务发送短信返回响应(验证码)) 3. 短信服务中用到了一些参数,这些参数可以独立出来放到配置文件中去, 然后定义一个bean类(包含这些属性)且加上@ConfigurationProperties(prefix = "ly.sms")注解, 进而在其他类中 加上@EnableConfigurationProperties(SmsProperties.class)就可以读取这些参数 4. 针对手机号码进行限流 1)在短信工具类,发送短信成功后,将手机号作为key,时间戳作为value存到redis中 2)然后在每次方法调用前,从redis中取出当前手机号的key对应的value值 3)判断当前时间与value值的差值是否大于1,如果大于1 调用发送短信,否则直接返回 null

10. 授权中心

1. 有状态与无状态区分

有状态服务:即服务端需要记录每次会话的客户端信息,从而识别客户端身份,根据用户身份进行请求的处理 缺点:1)服务端保存了大量的数据,增加了服务端的压力 2)服务端保存了用户的状态,无法进行水平扩展 3)客户端的请求依赖与服务端 无状态服务:服务端不保存任何客户端请求者信息,客户端的每次请求必须具备自描述信息,通过这些信息识别客户端身份 优点:1)减轻了服务端的压力 2)服务端可以进行任意的迁移和伸缩 3)客户端的请求信息 不依赖与服务端的信息 任何多次请求不需要必须访问到同一台服务 典型代表:rest服务

2. 如何实现无状态服务?

无状态登录的流程: 1)客户端第一次请求访问服务端,服务端会对用户进行信息认证 2)认证通过后,会将用户信息加密形成token,返回给客户端,作为登录凭证 3)以后每次请求,客户端都会携带认证的token 4)客户端会对token进行解密,判断是否有效 登录过程中的关键点:token的安全性 具体实现:我们将采用JWT + RSA非对称加密

3. 结合RSA的鉴权

1)用户在客户端请求 发起认证 2)授权中心会对用户信息进行校验,然后通过私钥生成jwt凭证 3)返回jwt给用户 4)用户会携带jwt进行访问 5)zuul网关会直接通过公钥解析jwt,进行验证,验证通过之后则放行 6)请求到达微服务,微服务直接用公钥解析jwt,获取用户信息,无需访问授权中心

4. 授权中心的职责:

1)用户鉴权 接收用户的请求,通过用户中心的接口进行校验,通过后,使用私钥生成jwt并返回

2)服务鉴权 微服务之间通过鉴权中心进行认证 用户登录校验 问题:因为cokile的有效期是30分钟,所以当用户在活跃时,应该刷新token,重新生成token

11. 授权中心在zuul里面完成鉴权操作

需要在网关中编写拦截逻辑,当用户请求到达后,解析cookie,从cookie中获得token,进而判断token是否有效 具体操作: 在网关中添加AuthFilter的拦截器 包括: 添加过滤器(这里用前置过滤器) 添加过滤器顺序 是否过滤 在run方法中添加拦截逻辑(1 获取request,2 获取token ,3 解析token,4 校验权限) 注意 解析token失败时拦截;解析成功时 在进一步校验可以访问到的权限 常见面试题 如果 微服务地址暴露了怎么办? 首先微服务地址一般是在局域网内通过zuul进行访问,对外暴露的只有zuul 而万一暴露了?可以通过服务间鉴权 定义微服务之间的权限表,将服务与服务之间的访问关系记录下来,而一个服务如果要访问另外一个服务时 必须要通过管理信息界面进行操作 ,同时服务与服务之间的访问要借助于鉴权中心,将服务当做具体的用户来操作,每一个服务都有 自己的用户名与密码

如果cookie 被禁用了怎么办? 首先可以提示用户,网站必须使用cookie,不能禁用 或者把token 放入头中进行返回,js中获取头信息,存入web存储,每次请求都需要手动携带token写入头中

如果cookie被盗用了怎么办? 加入ip地址识别身份 使用Https协议,防止数据泄露 https://blog.csdn.net/qq_38559956/article/details/103826541 https://blog.csdn.net/weixin_45443931/article/details/98869617?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-3.channel_param&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-3.channel_param

12. 购物车

1.购物车实现技术

通过redis(hash结构)来实现 购物车结构是一个双层Map:Map<String,Map<String,String>>

  • 第一层Map,Key是用户id
  • 第二层Map,Key是购物车中商品id,值是购物车数据

2.购物车逻辑

1 通过拦截器来解析cookie中的token,返回user对象,并存入到threadloacal线程域中
2 在service中通过线程域获取当前登录用户
3 判断当前登录用户的(redis)购物车中是否存在该商品
(如何判断,利用redis的结构特点,将商品id作为底层map的key,通过key来判断)
3.1存在 获取原先购物车中该商品的数量,将当前商品数量累加到原有购物车中的该商品中 通过redis的操作对象存入到redis购物车中
3.2不存在 直接通过redis操作对象将该商品存入到redis中

Springmvc拦截器作用及操作: 通过springmvc的拦截器来解析token
具体操作 在前置拦截方法中,解析cookie中的token,返回user对象,并将user对象传递到controller中 而如何传递到controller中呢? 1)可以通过request 域 2)可以通过threadlocal(线程域 threadlocal实质上是一种k-v结构,k是线程本身)
为什么要用拦截器----》 当请求到达购物车后,要通过cookie知道当前登录用户是谁

优惠券的规则制定思路: 1 首先 对用户做限制,哪些用户可以用,哪些用户不能用 2 其次 对商品做限制 哪些商品可以用,哪些商品不能用 3 最后 对价格做限制,满足多少钱可以用优惠券 还要考虑到用户的退款

13. 下单--订单服务

订单模块中有三张表,订单表与订单详情表,订单状态表(里面有订单的创建时间,收货时间等,可以用来做自动确认) 同时在下单时 请求参数只有收货地址编号与购物车对象(具体包括每个商品的skuid与数量),为了安全起见,不会传具体的价格信息,收货信息 订单id没有采用自增长,(因为订单量数据量过大) 当分库分表时如何保证全局唯一的id ?(如何生成订单) 通过Twitter公司的雪花算法
(雪花算法分为四部分,第一部分未使用,第二部分为毫秒级时间,第三部分为5位datacenterId和5位workerId,第四部分是12位的毫秒内计数 所以整体是按照时间自增排序 )

1. 下单逻辑

1.新增订单 1.1订单编号,基本信息 1.2用户信息 1.3收货人地址 1.4金额

2. 创建订单后如何完成减库存操作?

减库存可以通过同步减,或者异步减 而异步减可能会出现 分布式事务不一致的问题,就是说我在订单服务模块通过mq发送消息在商品服务减库存, 而消息发送后,商品服务是否减库存成功是不知道的, 而如何解决分布式事务问题呢? TCC模式 https://www.cnblogs.com/jajian/p/10014145.html https://www.baidu.com/link?url=rNXi-Le5znbe7KEOH920oiGYNYnoW7nYkPZSGiLkONB2sFhIxcVONlXTwLlLLrh0JJBWcuabtZNm_f9cPsY764eWZjrFmA1sAIPktztmWKm&wd=&eqid=b49f94fc000374a2000000025f8a7d8e https://sourcegraph.com/github.com/Wasabi1234/Java-Interview-Tutorial/-/blob/%E6%95%B0%E6%8D%AE%E5%AD%98%E5%82%A8/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/RabbitMQ/RabbitMQ%E9%AB%98%E7%BA%A7%E7%89%B9%E6%80%A7-%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88.md

3. 如何避免超卖现象?

不能通过加锁,因为在分布式集群环境下加锁只能锁住他自己的jvm,根本不能保证线程安全
所以要用分布式锁,而分布式锁如何实现呢? 1)使用zookeeper zookeeper的原理是根据节点的唯一性 当执行decreaseStock操作时,先去通过zookeeper在某个目录下创建节点,创建成功就说明获取了锁,否则没有获取锁,等待 2)使用redis redis的setnx命令,如果返回0,表示当前key已经存在,返回1,不存在 但是有缺陷,redis宕机,可能会出现死锁问题 而zookeeper就不会有这个问题,因为zookeeper创建的节点可以是临时节点,当服务器一旦断开连接,会自动删除临时节点

或者可以使用乐观锁思想 一开始就进行减库存操作,在sql当中加条件 即update tb_stock set stock = stock-1 where id =123 and stock >=1

如何保证库存与订单的事务的一致性? 将创建订单与减库存操作放在一个一个事务里面,我们的逻辑就是先创建订单,然后生成订单的基本信息(包括生成订单编号, 用户信息,计算金额),之后在减库存,当生成订单抛出异常就不会执行到减库存,而如果减库存出现了问题,整个事务也会回滚, 这样也就保证了事务的一致性

About

基于 SpringBoot(Spring + Springboot + MyBatis)+ES+RabbitMQ 前后端分离电商项目

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published