代码拉取完成,页面将自动刷新
翰墨商城 该商城是基于乐优商城进行开发的 主要包括 商品分类,品牌管理,文件上传下载,商品搜索服务,商品页面服务,结合RSA的授权中心
使用到的技术
该项目是基于springboot,sping,mybatis的通用mapper框架进行开发;
通过Elasticsearch 实现商品搜索服务;基于rabbitmq消息队列实现商品服务与搜索服务和静态页面的同步管理;
通过fastDFS实现文件的上传下载;使用redis实现存储用户的临时验证码;
`基于JWT + RSA非对称加密 实现用户的认证授权;
商品的品牌管理和查询
商品分类表设计注意点:商品分类是分层级的,如何区分儿子和爹的关系,就是在表中添加一个parentID字段
通用mapper会把对象中的非空属性作为查询条件
问题整体解决思路:
通过前端请求,分析请求方式(post,get..),请求路径(http://api.leyou.com/api/item/category/list?pid=0)请求参数(pid),返回类型(是个json数组)
进而来设计和构造controller和业务层
利用cors解决跨域问题
具体使用:在网关中加入GlobalCorsConfig,添加跨域信息,定义请求方式,头信息等
商品分类表设计注意事项
数据库设计的时候注意,tb_category_brand用于商品分类和品牌之间的关联,同时这表里没有外键,为什么不加外键呢?为了提高电商系统的性能,因为外键关联会影响到商品的删除
也会影响数据库的读写性能
根据返回数据的特点:有个用brand对象包装的list还有个总条数,所以选择用对象来包装返回结果
具体逻辑处理时:包括1 分页,2.过滤 3.排序 4.查询 5.解析分页结果
商品新增功能
商品的文件上传
注意一点:由于文件上传在经过zuul网关时,再高并发时可能会导致网络阻塞,zuul网关不可用,所以我们在做文件上传时最好绕过请求的缓存(也会经过zuul网关,只是不会在缓存请求)
解决方法:通过nginx的指令对地址进行重写,修改到以/zuul为前缀
文件上传下载的升级
通过fastDFS进行小文件的上传下载
将代码中的字符串,及变量配置到配置文件中去
具体操作步骤:
1 在application.yml文件中配置 hm.upload.baseUrl 和 hm.upload.allowTypes属性
2 定义 UploadProperties属性类
3 在UploadService中注入 UploadProperties属性类 并使用属性替代字符串
spu与sku的关系
spu:标准产品单位
sku:库存量单位
spu是一个抽象的商品集概念,作用:为了方便后台管理,
sku因具体特征不同而区分出的商品,sku是具体要销售的商品,用户购买的是sku
每一个分类都有统一的规格参数模板,但不同商品其参数值可能不同
商品分类与规格模板是一对一关系
商品分类与商品spu是一对多关系
规模模板与商品spu是一对多关系
商品spu保存有规格参数的具体值
因为sku的特有属性是商品规格参数的一部分,
所以可以将规格参数中的属性划分为 SKU 通用规格与SKU特有规格
所以 spu与sku是一对多关系
spu保存着所有sku共享的规格属性
sku中保存每个sku特有的规格属性
将库存表和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 {
}
解决搜索服务与商品页面服务同步的问题
两种传统路线,
1 每一次对商品做增删改查时,同时修改索引库数据及页面数据
2 搜索服务与商品页面服务对外提供接口,后台在商品增删改后调用接口
但是这两种路线有缺陷,代码耦合度较高,违背了微服务的独立原则
新的解决路线
通过消息队列
消息队列的优势:消息队列分为生产者和消费者,生产者负责消息的发送,消费者负责消息的接收,两者是异步的,而且也只关注消息的接收与发送,
没有业务逻辑的侵入,对代码的耦合度较低
消息队列中的问题:
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以外的其他消息工具不带生产者确认时,应该怎么做?先将消息持久化到数据库,并记录消息状态(可靠消息服务)
2 如何避免消息堆积?
什么情况会消息堆积?比如第三方的发短信,转账业务,或者大量订单
1)采用workqueue,多个消费者监听同一队列
2)接收到消息后,通过线程池,异步消费
3 如何避免消息的重复执行?
要保证消息的幂等性(同一接口被重复执行,其结果一致)
4 消息队列的几种类型?
simple(基本消息模型),--------》一个生产者,一个消费者,一个队列
worker(work消息模型),----》一个生产者,多个消费者,一个队列
工作队列模型 默认是队列把消息每一轮平均分给多个消费者,这样就有个缺点,无法根据消费者的自身能力来去消费;
解决办法就是配置 channel.basicQos(1); (设置每个消费者同时只能处理一条消息)---》能者多劳
订阅模型
fanout(发布订阅模式--fanout)----------》一个生产者,可以有多个消费者,每个消费者绑定自己的队列,每个队列都要绑定到交换机,
生产者发送的消息只能发送到交换机,
注意 只有队列可以存消息,而消费者是将消息发送到交换机的,但是交换机是不存储消息的,所以如果没有队列绑定
该交换机,那么消息就会丢失
direct(订阅模型--direct) ------》队列与交换机的绑定不能是任意绑定,必须要指定至少一个routingkey(路由key)、
Topic(订阅模型--Topic)--------》与direct模式类似,但是通过通配符的方式进行配置,所以更加灵活
具体的业务思路:
1 当对商品数据做增删改查后会调用消息队列,发送消息,也不关心消息被谁接收
2 搜索服务和静态页面服务接收到消息后,分别去处理索引库和静态页面
如何实现?
在hm-item服务中当对商品做增删改查时执行mq操作;
同时在hm-search服务中监听商品操作,
实现用户中心服务
具体包括:用户的注册,登录,个人信息管理,用户地址管理,用户收藏管理,订单管理,优惠券
具体操作:新建hm-user
实现发送短信服务
*因为短信发送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
授权中心
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
在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
购物车
购物车实现技术
通过redis(hash结构)来实现
购物车结构是一个双层Map:Map<String,Map<String,String>>
- 第一层Map,Key是用户id
- 第二层Map,Key是购物车中商品id,值是购物车数据
购物车逻辑
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 最后 对价格做限制,满足多少钱可以用优惠券
还要考虑到用户的退款
下单--订单服务
订单id没有采用自增长,
当分库分表时如何保证全局唯一的id ?(如何生成订单)
通过Twitter公司的雪花算法
下单逻辑
1新增订单
1.1订单编号,基本信息
1.2用户信息
1.3收货人地址
1.4金额
2新增订单详情
2.1
3新增订单状态
4减库存
创建订单后如何完成减库存操作?
减库存可以通过同步减,或者异步减
而异步减可能会出现 分布式事务不一致的问题,就是说我在订单服务模块通过mq发送消息在商品服务减库存,
而消息发送后,商品服务是否减库存成功是不知道的,
而如何解决分布式事务问题呢?
TCC模式 https://www.cnblogs.com/jajian/p/10014145.html
如何避免超卖现象?
不能通过加锁,因为在分布式集群环境下加锁只能锁住他自己的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
如何保证库存与订单的事务的一致性?
将创建订单与减库存操作放在一个一个事务里面,我们的逻辑就是先创建订单,然后生成订单的基本信息(包括生成订单编号,
用户信息,计算金额),之后在减库存,当生成订单抛出异常就不会执行到减库存,而如果减库存出现了问题,整个事务也会回滚,
这样也就保证了事务的一致性
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。