虚拟机环境配置

这次使用Virtual Box + Vagrant完成虚拟机安装,比传统VMware配置镜像更方便,Vagrant可下载打包好的镜像文件进行安装,无需详细配置。

https://blog.csdn.net/qq_15990969/article/details/113096469

https://www.cnblogs.com/lxwphp/p/11121283.html

快速反向工程辅助crud

使用renren脚手架辅助开发,对应前后端分离,renren-fast,renren-fast-vue,renren-generator使用前需删除.git文件,这是官方的git信息

renren的service层无需注入dao,它继承了一个父类,父类注入了baseMapper,而这个baseMapper的类型是对应继承时类的泛型M的,人人继承这个父类就会用自己的dao实现泛型,所以此时父类注入的baseMapper即我们需要的dao,封装nb。

商品服务

https://blog.csdn.net/unique_perfect/article/details/113824202

可以拿sql数据,1000+商品词条。

菜单分级展示

先查一级菜单,然后递归查询二级菜单,给product商品加上子菜单,方便设置,数据库没有该字段需注明exist。

网关(跨域)

通过gateway路由请求,涉及跨域问题(同源策略)。

非简单请求先发送预检请求option,服务器不允许则不发送请求。

我们在gateway的微服务中进行config配置,使用前置filter过滤请求即可,放行所有的请求都跨域。CorsWebFilter是官方提供跨域过滤器。

端口号的动态刷新,好像能取改变后的值,但访问的url没变?

CRUD

我们删除使用逻辑删除,即属性有个字段为是否展示,我们提供改变这个字段状态将属性隐藏显示,而不是将数据真正的删除。

mybatis-plus自带逻辑删除组件配置,yml配置

JSR303

品牌管理进行后端校验,仅仅前端校验是不够的,以防越过前端直接访问接口。

@NotBlank等注解标明字段,@Valid标明业务实体类参数,随后进行校验,@NotBlank可自定义message,校验失败返回对应信息。

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

可写一个统一的异常处理类,使用@ControllerAdvice

JSR303还可进行分组校验。使用象征性接口进行标识。

属性分组

SPU相当于一个抽象概念,如米8手机。

SKU是具体概念,也就是米8手机的具体配置,配置不同价值也不同。

数据库基于这个概念进行分类,如米8 128g,只有黑色、白色内存大小8g,米8 256g,有黑白蓝绿等颜色,内存大小16g,不同的SKU具体实现的内容不同。

关联分类

品牌管理可将品牌和分类的关系存到一张表记录,我们修改品牌与分类名时,要联动修改这个表的数据。

VO

view object,我们实体类一般用于数据库操作,有时引入其他字段还需要注解标明字段不属于数据库,所以我们可以多进行一层封装,继承实体类,添加用于视图返回的字段。

我们可以根据应用场景的不同,对原实体类继承后二次封装,防止实体类注解冗余。

生成商品Vo

先创建json数据,然后使用工具反向生成vo,导入vo然后用这个实体去进行保存。为了保证精度,小数操作值使用BigDecimal,id使用long。

@Transactional

对商品进行添加时,会影响多个表,所以我们使用事务进行控制

feign服务调用

spu需要记录优惠信息,调用其他服务的接口,使用TO传输,跨服务传输,可在常用服务组件中封装TO。

服务调用时,feign接口调用参数需要和服务一致???参数需要@RequestBody注解,转为json传输(如果传的是对象实例),可以和服务端实例不同,只要两个转换的json能相互转换即可。但是为了方便我们还是使用同一个传输对象作为保准比较好对应。

但这里我们调用远端服务使用的是逆向工程直接创建的方法,参数不好改,也可以直接使用实体类,因为json可以互转。

商品详情

采用CompletableFuture异步编排进行优化,商品信息是逐个查询的,采用异步编排,自定义线程池,对商品查询进行多线程任务,加快信息的查询速度。

阿里云第三方服务

第三方服务云存储OSS

由于微服务负载均衡,图片等本地存储资源不能存放于服务器,否则其他服务器找不到服务。我们跨域统一存放这类资源到云服务器上,进行统一的调用。阿里云OSS

文件上传,使用服务端签名后直传,借助阿里云封装的sdk完成上传

https://help.aliyun.com/document_detail/32009.html

https://github.com/alibaba/aliyun-spring-boot/tree/master/aliyun-spring-boot-samples/aliyun-oss-spring-boot-sample

官方依赖有坑,yml也坑,这里建议比对网络博客

1
2
3
4
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alicloud-oss</artifactId>
</dependency>
1
2
3
4
5
6
7
8
spring:
cloud:
# 阿里云服务配置
alicloud:
access-key:
secret-key:
oss:
endpoint: oss-cn-shanghai.aliyuncs.com

短信验证码

去阿里云随便找一家短信服务提供商免费测试即可,0元20次套餐够用了,看官方的接口文档进行参数配置。

调用官方封装的java类,自定义传参即可。还可抽取配置文件,将路径、api等信息再配置文件中进行配置,使用@ConfigurationProperties注解自定义配置文件属性。

  • 接口防刷
  • 验证码多次校验

Elasticsearch(7.6.2)

索引、文档、倒排索引、分词匹配。

elasticsearch + kibana,分词器ik-analyzer。

我们项目使用es使用9200端口,9300相关连接方法已舍弃,只支持6.x。可通过Elasticsearch-Rest-Client,跟进新版本迭代,支持7.x。

官方:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.6/java-rest-high.html

商品上架

ES只检索上架的商品,上架商品改变状态,并存放到ES,只选择存储关键信息,如商品标题字段就是关键,还要配置分词器,其他的一些展示字段如图片等则不参与聚合操作,只是为了显示.

检索功能

检索有多个方式,如关键字检索、分类检索、排序、过滤等。将检索页面需要用到的条件,全部封装成一个对象。

先封装dsl请求检索语句,将语句通过es对象search后,封装其返回数据交予前端。

总结

ES有更新和检索两个功能,更新在商品上架时,由product远程调用,检索则是页面搜索框调用,其url拼接条件进行检索,根据不同情况写全拼接的状态,如关键词搜索,会对检索关键字自行前端标签的拼接,使其在其前端展示时,取出高亮的前端标签字段。

思考

ES更新操作怎么处理?我们先将商品下架,删除ES,随后上架再更新?

Nginx

动静分离,nginx分离静态资源和动态资源。修改hosts,配置虚拟机ip映射。使用插件SwitchHosts。

nginx代理网关时,会丢失host等信息。请求发送到nginx,nginx转发到网关进一步分配。

修改host将商城域名映射到linux的ip

1
192.168.56.11 mall.com
1
2
3
4
5
6
7
8
9
10
11
12
13
14
server {
listen 80;
listen [::]:80;
server_name mall.com *.mall.com;

location /static/ {
root /usr/share/nginx/html;
}

location / {
proxy_set_header Host $host;
proxy_pass http://mall;
}
}
1
2
3
upstream mall{
server 192.168.158.1:3000;
}

分析以上配置,访问mall.com以及其子域名,会走nginx,若是静态资源,其后链接会跟static,然后走conf的配置,请求去查看nginx的静态资源。其他的正常连接,则走网关代理,网关的ip我们会在nginx.conf中配置,也就是我们主机的网关ip和端口。而且要设置host值,nginx代理会丢失。

然后我们在网关中进行host映射配置。nginx转发到网关进行代理,会有一开始输入的域名host,这个host则对应网关配置的映射,路由到对应的微服务。

1
2
3
4
5
6
7
8
9
- id: mall_host_route
uri: lb://mall-product
predicates:
- Host=mall.com,item.mall.com

- id: mall_search_route
uri: lb://mall-search
predicates:
- Host=search.mall.com

动静分离

静态资源放置于nginx,而不是项目。

1
2
3
4
5
6
7
8
location /static/ {
root /usr/share/nginx/html;
}

location / {
proxy_set_header Host $host;
proxy_pass http://mall;
}

配置请求路径的分发策略,页面所有静态资源+static前缀,对应nginx来匹配其static路径下的资源,其他的接口url走项目。静态资源由nginx处理,和我们服务器无关。

前端页面有static前缀资源就走nginx内部请求,其他请求走页面请求。

测试与调优

使用Jmeter压力测试,Jvisualvm操作JVM(jdk自带,直接命令行启动)。通过测试来判断程序性能,进而对数据库和代码进行调优。

代码逻辑应尽少于数据库交互,以此来优化代码。所以需要使用缓存来提高性能。

Redis

redis缓存

设计缓存是为了减少数据库的压力,将部分数据存放到缓存中,比如:

  • 高访问量,且更新频率小的数据,多读少写(高频热点数据)
  • 对即时性、数据一致性要求不高的数据

我们注入redis的start依赖,使用StringRedisTemplate操纵redis

对即时性、一致性要求高的数据就应该去查数据库,不要求的数据则放入缓存,设置过期时间保证隔段时间获得最新数据。

分布式锁(Redisson)

保证多服务调用的数据一致性。

官方配置了不同语言的分布式锁实现,Java 对应 Redisson。

Redisson和JUC锁的使用相对应。

SpringCache

主启动类 + @EnableCaching

方法 + @Cacheable等注解,不同注解效果不同,默认是对注解方法的结果进行缓存操作,若结果存在则不调用方法。

@CacheEvict缓存失效模式

@Caching多个缓存操作进行组合

使用SpringCache是再进行复杂查询操作时使用,将结果进行缓存,若在方法内部想使用缓存可直接操作redisTemplate对象,这个注解只是用于缓存返回值。

场景

  • 登录验证码
  • 多次查询的数据,如多级菜单
  • 全局统一存储session
  • 购物车信息
  • 热点数据

系统登录

页面跳转

以前都是使用controller,进行mapping路径映射返回string走对应页面,现在由于请求路径过多,配置一个config实现WebMvcConfigurer接口,调用addViewControllers添加路径映射。以防controller有太多空请求方法,导致代码冗余。

注册时发送验证码

验证码有很多种情况,我们一个账户当然可以要很多个验证码,一般过期时间时5min,但间隔请求时间是60s。

由于前端规定验证码获取是60s一次,但可以通过直接接口调用等手段避开前端判断,所以我们需要后端进行逻辑验证。

在生成验证码时,我们是随机数UUID生成,然后可在其后拼接一个系统时间。

当前端请求验证码时,先在缓存中获取当前手机的验证码信息,若验证码信息不为空则继续判断,若其生成时间和当前时间间隔不到60s,则需要等待,返回报错信息,这是后端进行接口层上的逻辑完善。

当然验证码我们拼接了系统时间,可以通过截取字符串来获取前几位的校验字段发给用户,后几位截取记为标记时间。

注册信息校验

密码

又是md5 + 盐值。。。

不过有改进,可以使用spring的BCryptPasswordEncoder,不用另行存储盐值,其生成密文通过算法可自行解析盐值,省去了在数据库中进行盐值存储,进一步加强了安全。

同一个密码明文 + 不同盐值生成的不同随机数,经过算法解密后可得到同一个明文,所以同一个密码无影响,算法自身会解析盐值。

而登录进行验证时,调用BCryptPasswordEncodermatches方法

1
boolean matches(CharSequence rawPassword, String encodedPassword)

参数一是页面输入原密码,参数二是数据库存储加密密码,由于这个加密是该方法自行生成的,所以可自行验证页面原密码的正确性。返回boolean。

1
public String encode(CharSequence rawPassword)

通过调用encode方法对原密码加密。

社交登录 OAuth2.0协议

系统登录向用户申请第三方请求认证,用户前往第三方服务器进行登录操作,通过验证后,由第三方向系统发送登录令牌,我们系统可通过登录令牌访问第三方服务器获取头像、昵称等公开资源。

使用QQ作为社交登录,自行验证身份并创建应用。

https://op.open.qq.com/appregv2/

https://connect.qq.com/manage.html#/

可以根据官方api获取一些数据。

。。。。。审核太恶心,不做了。注意社交登录也应将信息注册到系统的数据库。

Session的状态记录

以前的单体项目可以使用Session,但Session不能跨域名共享。

但现在是微服务的项目,服务是分模块的,不同服务间的Session也是不共享的,集群也是。

  • 同一个服务,不同机器,也就是同一个服务的集群session共享解决:

    • session复制,进行集群间机器的session同步,但是同步数据需要数据传输,占用网络资源,且为了保证同步,所有的服务器都要保存,每台服务都要开辟这个session的空间,对于分布式集群很不友好。
    • 客户端存储,用户自行保存session到cookie,不安全,而且cookie不能存储大量数据。
    • hash一致性,根据服务ip,将同一个ip的请求都分配到同一个服务器上,也就是负载均衡是分配不同ip的,同一个ip一直用同一个服务器,但是机器水平扩展会导致hash算法出现问题。
    • 统一存储,将session不存到服务器,而是redis或其他数据库,没有安全隐患,水平扩展集群不会影响,但缺点是需要多进行一次网络连接数据库,且查询数据库一定比直接的内存查询慢。

    推荐使用hash一致性 或 数据库统一存储。

SpringSession

https://docs.spring.io/spring-session/docs/2.4.6/reference/html5/#samples

1
2
3
4
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>

yml需要指定session的存储数据库,主启动类加上注解@EnableRedisHttpSession,需要修改session的作用域,作用于父域则所有子域都会被辐射到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration
public class MallSessionConfig {
@Bean
public CookieSerializer cookieSerializer(){
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
// 扩大作用域
cookieSerializer.setDomainName("mall.com");
cookieSerializer.setCookieName("MALL_SESSION");
return cookieSerializer;
}

@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
}

我们每个服务需要用到session时,需要加载pom、config配置类、主启动类注解、yml的配置(redis、session-type)

  • spring-session,yml配置其缓存使用的数据库,我们使用redis
  • session需要存储的实体类实现序列化Serializable接口,然后在config中自行配置放大作用域,以及redis的json序列化存储。因为我们是用户登录服务进行登录的,是二级域名,需要放大作用域,以便辐射其他服务。
  • session首次被set是按kv设置的,我们后续从redis取,也是用这个key去拿。

单点登录

多系统间登录互通,比如登录百度账号,所有百度的应用都会自行登录。

我们前面使用session是对子域名来说的,即mall.com是顶级域名,search.mall.com和auth.mall.com都是其子域名。而单点登录是作用于完全不同的几个域名之间的不同1.com、2.com、3.com。

https://gitee.com/xuxueli0323/xxl-sso?_from=gitee_search

单点登录一般有一个认证服务器,通过该服务器进行跳转。

总结

在子域名进行注册操作,导致要使用统一存储session,并自定义sesssion扩域的config配置,redis缓存session,让每个域名都来使用session。这个项目用不到单点登录,因为只有一个域名,其余都是其子域名。

购物车(redis hash存储)

存储结构

购物车缓存使用hash存储,key是固定封装前缀 + 用户id,从session取用户id,value是商品id和商品信息的kv组合,商品信息是远程商品服务调用查询的。

分析概述

购物车首先分两种情况,登录用户和游客。

  • 登录用户:选择redis,由于购物车需要多读写不应使用mysql造成压力,可使用nosql频繁读写。
  • 游客:可以存储到本地的cookie或localstorage,但这些数据我们服务器后端是没有数据的,就无法对用户的喜好进行分析进而推送数据,综合大数据时代的现状,游客数据也会存储到redis。

当然大数据是使用redis,减轻mysql的压力,小数据量使用mysql也可以。但是基于现在的商城业务,离线购物车这个应该舍弃,因为连账号都没有的用户,更容易利用这一点存储垃圾数据。

redis存储使用hash结构。

细节

redis存储购物车,先查session拿id,添加购物车时,若已经拥有则直接修改,没有则新增。

使用ThreadLocal,让service层可以使用session信息,配置到自定义拦截器中,然后将自定义拦截器注入。

添加购物车数据采用异步任务,提高效率。

关于价格,我们封装类就重写了get、set方法,在get、set时会计算值。

ThreadLocal

把ThreadLocal配置到拦截器中,session存在则存储到threadlocal中,保证当前线程的session共享,后续订单服务也要使用这个。

订单服务

Service层获取Session

我们设置mvc拦截器,除了拦截未登录用户外,还可以定义一个ThreadLocal变量,该变量在同一个线程中不会改变,我们可以在拦截器中,通过request获取session,然后将session的值存储到ThreadLocal中,在Service层就可以直接使用ThreadLocal的存储数据即可,解决了service层获取session的问题。

或者注入HttpSession,在Service获取session信息,但拦截器本来就要获取session,可以直接在拦截器里获取。这也为了实战ThreadLocal。

远程调用与session冲突

场景:

1
2
我们确认订单信息,远程调用服务,首先使用用户服务获取用户地址,我们这个远程服务传入了该线程中的session相关数据,成功调用。
然后我们要获取所有购物车的内容,调用购物车远程服务,但是购物车这个远程服务无需参数,当时写功能时,就是让它通过当前拦截器ThreadLocal拿Session数据进行处理的。结果这里远程调用出现的了问题,ThreadLocal拿不到数据。

原因是feign远程调用会丢失请求头信息,它会创建一个新请求去访问远端服务,但原请求的请求头信息就没有了。这时远程购物车服务会判断我们没有登录,所以拿不到session,ThreadLocal肯定拿不到值。

解决办法:添加一个feign调用的请求拦截器,将原请求Cookie信息存储到新的远程调用请求。配置RequestInterceptor这个拦截器。

因为feign处理远程服务使用的是一个初始化的拦截器,我们对这个拦截器进行配置,可将原请求的cookie封装到新请求。防止Cookie信息丢失。

异步任务与feign调用冲突

ThreadLocal只能同一个线程中变量共享。

我们使用CompletableFuture异步任务加快服务操作的效率,但信息不能共享,我们可以在主线程获取上下文信息,然后在子线程执行操作时先将上下文信息保存。

1
RequestContextHolder.setRequestAttributes(requestAttributes);

接口幂等性问题

对于提交订单这个操作,用户市不能对一个订单进行多次提交操作的,也就是幂等性问题。

幂等性即多次请求结果一致,多次接口请求只提交一次订单。一些场景如下:

  • 用户多次操作
  • 页面回退后返回再次提交
  • 微服务间的调用,如库存克扣问题。

解决方法有:

  • 数据库设计唯一,标记唯一标识,如数据库唯一索引
  • 携带token,令牌机制,其令牌操作要保证原子性,redis存储token可使用lua进行辅助
  • 悲观锁、乐观锁
  • 分布式锁
  • 携带全局的唯一id

Token令牌防重

redis存储令牌,使用string结构,k是用户id,v是uuid生成的token,并设置30min过期时间。后续提交订单要通过token进行防重操作。

在购物车确认并生成订单,封装订单信息时,会生成一个防重令牌并缓存到redis(token是string结构,反复确认订单会把上次的token覆盖),然后controller会将封装信息设置到model中并跳转到订单提交页,提交订单去结算时,会使用lua脚本比对缓存与订单信息的token,比对通过就删除token,并创建订单、锁库存,然后通过mq进行解锁判定处理。订单创建完后将购物车session信息删除。

使用lua脚本是为了保证令牌的获取、比较、删除三个操作的原子性,防止多次请求同时完成操作,导致订单重复创建。

提交订单

直接去购物车获取信息,而不是提交页面,因为用户可能提前打开提交页面,然后再去购物车操作数据,所以直接去购物车拿数据即可。

提交订单后,可以分成以下操作。

  • 下单

  • 创建订单

  • 验证令牌并删除

    1
    2
    3
    4
    5
    if 
    redis.call('get', KEYS[1]) == ARGV[1]
    then return redis.call('del', KEYS[1]) else
    return 0
    end;

    使用lua脚本保证令牌验证与删除操作的原子性。

    令牌即用即删,防止其他订单使用。

    1
    2
    3
    4
    5
    6
    String orderToken = vo.getOrderToken();
    Long result = stringRedisTemplate.execute(
    new DefaultRedisScript<Long>(script, Long.class),
    Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberResponseVo.getId()),
    orderToken
    );

    这里script即上述脚本,我们在购物车确认订单界面,会在redis存储token,kv示例上面说过,然后在提交订单到支付界面进行token二次确认。获取当前订单id,从redis取token并与当前订单自身携带的token进行对比,二者相同说明是同一个订单未被消费,然后去付款。校验后马上删除redis的token,确保订单不会被重复创建。

  • 验证价格

    将页面价格与购物车价格计算后校验。

  • 锁库存

    先锁在卖,订单远程调用库存服务的方法,使用事务。

    由于存在远程服务调用,对于锁库存涉及到分布式事务。

首先整个订单提交服务是一个本地事务,有多次校验,令牌校验、价格校验(订单总额与购物车总额对比,防止用户在订单页去修改购物车)、库存校验,一次校验失败就会返回对应报错信息,并执行回滚操作;而其远程调用库存涉及分布式事务,采用MQ实现最终一致性来解决

分布式事务

由于我们服务会远程调用服务,如a、b两次远程调用,a成功,b失败,但b失败只会导致b服务与当前服务回滚,a服务因为远程调用并通过,后续回滚它却不会接收到信息。

在分布式项目中,我们可能服务嵌套调用,以往的本地事务就不在适用,所以我们需要使用分布式事务。

库存回滚使用分布式事务。CAP性质保证AP,即可用性,一致性不用特别强制。

可以使用微服务组件seata解决分布式事务。

seata

首先数据库中每个微服务需要创建undo_log回滚日志记录表,这是官方规定的。然后启动seata服务器。

在需要的类上添加@GlobalTransactional开启全局事务。

而对于需要使用分布式事务的微服务,需要由seata代理其数据源。

分布式事务主入口标明全局事务,其他的远端调用仍使用本地事务注解即可。

消息队列最终一致性

定时任务,订单超时关闭订单,而下单的时候会锁定库存,库存设置定时任务,若一定时间后订单不存在,则相应的库存自行解锁。但定时任务资源开销大,不建议使用。

延迟队列,订单创建后进行锁库存,锁库存存入延迟队列,过期后交予解锁库存服务,此时解库存再去看订单是否存在进而判断是否解库存。采用主题模式,消息进来按照路由键走死信,过期后设置路由键走解库存队列。

延迟队列相比于定时任务,优点在于其处理一定在当前订单过期时间之后,定时任务则不稳定。

库存解锁!!!

柔性事务,最终一致性:https://www.jianshu.com/p/d70df89665b9

mq使用时选择手动应答ack。只有死信队列转发后的订单表信息,其订单不存在或订单取消支付,此时将会解库存。手动ack模式,我们可以确保业务执行完毕在应答队列。

由死信转发到解库存的消息,若当前机器服务出现异常或宕机,则将消息重新放入队列,等待下一次的消费。直到消息全部消费。

取消订单后主动发消息给解库存MQ。

一开始设置订单支付等待1min,库存解锁等待2min,解库存mq收到消息时,订单状态肯定从新建变为了支付或取消,这是正常状态,但如果我们遇到网络问题,导致订单支付在解库存后才响应,解库存判断状态是新建则不会有动作,所以为了保底,我们取消订单时要去通知解库存mq,以防上述情况发生,当然支付成功就不用通知。

消息的手动接收和业务处理都是写在try-catch中,捕获异常就拒收消息并将其重入队。

消息确认、重复

队列和交换机一定是开启持久化,保存到磁盘上。

保证手动ack,业务执行无误再进行消息确认,关闭自动ack。

由于服务器宕机等因素,业务完成后没有及时应答,导致消息重回队列进行消费,我们可以再业务相关处理中设置状态,比方说上述订单解库存,只处理被取消的订单和不存在的订单,且解库存时只处理被锁定的库存,已解锁过的当然不用二次处理了。保证业务的幂等性。

支付宝沙箱模拟支付

调用官方sdk,引用方法即可支付选择支付宝,成功后跳转回我们的结算页面。还可以配置支付宝提供的自动收单,因为订单会在规定时间内失效,如果等到订单失效后再支付,我们库存都回滚了,但支付成功订单仍变为了已付款,这样肯定是逻辑问题,所以我们配置自动收单,保证付款窗口的有效期不超过订单过期区间。保证订单过期后不能付款。

秒杀

  • 定时任务,库存预热

    秒杀商品上架使用定时任务,执行异步编排。采用cron表达式 + 注解完成定时任务。

    @EnableScheduling:定时调度,@EnableAsync:异步任务

    或者直接执行异步任务。

    定时上架,选择流量较小的深夜错峰上架。提前3天扫描秒杀活动场次,提前将数据缓存到redis

  • 时间处理

    使用java8新增的时间处理类,LocalTime,LocalDate,获取当天准确日期、时间,完成秒杀获取3天商品的时间需求。

  • 秒杀加密

    携带随机码,获取秒杀商品数据时,会进行时间校验,只有到时了才会给当前商品添加随机码,也就是秒杀生效,其余时间还是正常价。

  • 动静分离

    只让后端承受动态请求,降低静态请求带来的压力。

  • 信号量限流

    信号量即redis库存,靠这个保证库存

    设置Redisson信号量

    https://blog.csdn.net/weixin_43931625/article/details/103232670

  • 流量错峰

    验证码过滤,不是即点即用。加入购物车

  • 限流、熔断、降级 sentinel

    页面降级,点击频率过高就挂掉

  • 队列消峰

    信号量再redis,扣减是原子性操作

库存

库存在上架前提前扣减了,活动结束后再根据失效订单进行解锁,上架库存就以信号量保存到redis中,通过redisson分布式锁完成原子性扣减,保证不会超卖,信号量用完即止。

秒杀设计

设置秒杀限制数,限制商品的抢购件数。

上架时会设置商品随机码,在主页进入商品页时,会先判断当前商品是否秒杀,如果秒杀,在判断当前是否处于秒杀时间段(封装了起始时间),不存在时间段就将随机码至null,然后传到前端,商品的封装包括了秒杀信息。在秒杀商品页面进行秒杀click时,会传入当前商品的秒杀信息,包括秒杀商品id、秒杀商品的随机码、秒杀数量,然后进行请求,秒杀时进行校验,通过秒杀id获取缓存中的对应商品,校验页面随机码和缓存是否相等,若当前商品不是在秒杀时间内进入则随机码为null,无法通过校验。

熔断降级

熔断:多服务相互调用,某个服务的调用失败比例超过设定值,或调用失败频率超过设定值。则将异常服务停止,但后续会根据策略对该服务测试,若该服务调用比例恢复到正常值,则重新启用服务。

降级:对某个方法、接口采用兜底方法,即某个方法、接口请求频率达到一定次数就返回兜底方法进行处理,防止大量请求进行访问。

面试mark

  • 如何保证缓存与数据库的双写一致性?

    双写(mysql修改后修改redis)

    失效(mysql修改后删除redis缓存)

    对于购物车缓存情况如何处理

  • Java设计缓存(Map本地缓存)

    和单例模式的双重锁定类似。

    对于分布式微服务,不能使用,因为本地缓存时相互隔离的,服务每次负载均衡到其他服务器还要重新加载缓存。且多个服务器间还有数据一致性的问题。

    https://blog.csdn.net/u010452388/article/details/82725299

  • 点赞功能设计

    https://www.cnblogs.com/liuyupen/p/14015407.html

    https://www.zhangshengrong.com/p/281oD6Yawz/

  • 账号重复登录问题

    建立在线活跃用户表

    https://blog.csdn.net/weixin_38295272/article/details/117964982

    登录存储客户端ip,若多次登录ip不同,说明是不同ip登录,提醒上次登录用户。可以kv存redis,k是用户id,v是客户端ip。还要设置过期时间,如果正常下线,会发请求删除,非正常下线自动过期。一直在线会自动续期。也就是活跃操作续期。

    文章中用token,这个可能会受浏览器影响,我们可使用用户的ip。

  • 幂等性

  • rabbitmq怎么保证消息可靠:开启队列、交换机的持久化,开启手动ack模式。

    发生时消息丢失、发送后消息丢失:保证双端确认,消费者手动ack,出现异常拒收消息将消息重入队。

    消息重复:业务逻辑避免,如我们解库存业务,消息来了,要解库存,会判断商品库存,只处理锁定状态的商品。