12306项目


12306项目

1-4章小结

common

LogAspect用来记录日志,有@Before和@Around

BusinessException中有BusinessExceptionEnum,自定义异常

ControllerExeption用来处理异常,有上面的BusinessException,也有因为valid校验的BindException

ComonResp用来统一返回给前端的参数

SnowItil封装了一下雪花算法

gateway

GateApplication作为一个启动类

application.properties用来做路由转发

logback-spring.xml 用来记录日志,配置一些日志的路径,隐藏的参数等

generator

下面的generator-config-member.xml 等一系列xml,作为代码生成器

http

做一些接口测试

member

MemberApplication作为启动类

Controller写了一个注册接口,一个计数接口。将service层返回的东西再用CommonResp<>封装一下

Member和MemberExample是代码生成器生成的,Member是根据数据库的表生成的。MemberExample是查询条件。

MemberMapper是自动生成的

MemberRegisterReq是用户传进来的东西。包装了一下

MemberService写了一些对应于controller的简单的业务。

其他

hutool的一些包

ObjectUtil.isNotNull(memberDB)
CollUtil.isEmpty(list)

memberExample的查询条件

memberExample.createCriteria().andMobileEqualTo(mobile);

5-6章小结

因为前端自己搭建的过程经常出bug,就直接把所有的前端页面拷贝过来了。

http

###
POST http://localhost:8000/member/member/send-code
Content-Type: application/json

{
  "mobile": "13000000001"
}

这种的,传json.中间要空一行,是headers,下面才是参数。

json的话后端接受参数要加一个@ResponseBody的注解,当然与此同时就不能接受Content-Type: application/x-www-form-urlencoded类型的http了

gateway

修改配置application.properties可以解决跨域问题

# 允许请求来源(老版本叫allowedOrigin)
spring.cloud.gateway.globalcors.cors-configurations.[/**].allowedOriginPatterns=*
# 允许携带的头信息
spring.cloud.gateway.globalcors.cors-configurations.[/**].allowedHeaders=*
# 允许的请求方式
spring.cloud.gateway.globalcors.cors-configurations.[/**].allowedMethods=*
# 是否允许携带cookie
spring.cloud.gateway.globalcors.cors-configurations.[/**].allowCredentials=true
# 跨域检测的有效期,会发起一个OPTION请求
spring.cloud.gateway.globalcors.cors-configurations.[/**].maxAge=3600

req

@NotBlank(message = "【手机号】不能为空")
@Pattern(regexp = "^1\\d{10}$", message = "手机号码格式错误")
private String mobile;

手机号的一个正则表达式

service

在所有的用到log的地方,要先加个这个

private static final Logger LOG = LoggerFactory.getLogger(MemberService.class);

login函数的返回值应该为MemberLoginResp

MemberLoginResp memberLoginResp = BeanUtil.copyProperties(memberDB, MemberLoginResp.class);
        String token = JwtUtil.createToken(memberLoginResp.getId(), memberLoginResp.getMobile());
        memberLoginResp.setToken(token);

这里使用了BeanUtil的复制属性,把用户的所有信息转换为了一个没有敏感信息的对象

然后再加上token

JWT

因为JWTUtil.createToken()的第一个参数是Map.所以要把对象转为Map

Map<String,Object> map=BeanUtil.beanToMap(memberLoginResp);
String key="wzh";
String token=JWTUtil.createToken(map,key.getBytes());

https://loolly_admin.oschina.io/hutool-site/docs/#/jwt/%E6%A6%82%E8%BF%B0

为了这一个类引入整个common不太好。所以gateway里又重新写了一个JwtUtil

pom

热部署的依赖

       <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>

7章小结

完成乘车人(单表)增删改查,为后面的代码生成器做准备

分页

在train的pom.xml和common的xml中都引入,当然common里不需要版本号

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>5.2.0</version>
</dependency>

然后就可以使用PageHelper和PageInfo了

pageHelper怎么起作用,在执行sql语句的上一行加入即可,[注:只会对下一条执行的sql起作用]类似如下:

LOG.info("查询页码:{}", req.getPage());
LOG.info("每页条数:{}", req.getSize());
PageHelper.startPage(req.getPage(), req.getSize());
List<Passenger> passengerList = passengerMapper.selectByExample(passengerExample);

对日期进行格式化处理(※※)

在返回值这个Resp中,要把日期返回好看一点。

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
private Date createTime;

关于Long的类型精度丢失问题

因为Java的long是19位,而JS的long是16位。所以会出现1234567891234567891变成1234567891234567000

对于雪花算法生成的这种long类型的大数

@JsonSerialize(using= ToStringSerializer.class)
private Long id;

可以加个注解,将其传到前端的时候转为String

89章代码生成器

1011章定时任务quartz

对于多节点,是以每一个最细粒度的一次Job为单位进行轮询的。

12章-基本的车票预定功能开发

如果是req的接受的get请求。字符串转时间的参数:

 @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date date;

如果是req的post请求。

 @JsonFormat(pattern = "yyyy-MM-dd",timezone = "GMT+8")
    @NotNull(message = "【日期】不能为空")
    private Date date;

req接受前端传过来的请求。

一旦用到列表,都要对列表进行判断。否则可能有空指针。示例如下:

 public void genDaily(Date date) {
        List<Train> trainList = trainService.selectAll();
        if (CollUtil.isEmpty(trainList)) {
            LOG.info("没有车次基础数据,任务结束");
            return;
        }

        for (Train train : trainList) {
            genDailyTrain(date, train);
        }
    }

得到15天后的日期

import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
LOG.info("生成15天后的车次数据开始");
Date date = new Date();
DateTime dateTime = DateUtil.offsetDay(date, 15);
Date offsetDate = dateTime.toJdkDate();
DateUtil.formatDate(date) 

把date转为规范的yyyy-MM-dd格式

为一个空字符串填充“0”

 String sell = StrUtil.fillBefore("", '0', stationList.size() - 1);

通过hutool的EnumUtil得到结果

BigDecimal priceRate = EnumUtil.getFieldBy(TrainTypeEnum::getPriceRate, TrainTypeEnum::getCode, trainType);
SeatTypeEnum seatTypeEnum = EnumUtil.getBy(SeatTypeEnum::getCode, seatTypeCode);

通过hutoool的copyToList得到列表

BeanUtil.copyToList(list, PassengerQueryResp.class)

13章,配置Nacos

本质上就是将IDEA中的一些本地配置文件的信息,由Nacos代替。这样可以做到不修改代码更改配置。

14章,缓存

MyBatis的一级、二级缓存

一些高频接口添加缓存。有大量的读,并且写操作很少。

会话一关闭,一级缓存就消失了。

加上事务,还是一个会话,就有一级缓存了。有效的减少数据库的访问。

但是如果是select,update,select。则一级缓存则不能查出最新的数据。

如何关闭一级缓存:

# 配置为statement,即关闭一级缓存

mybatis.configuration.local-cache-scope=statement

开启二级缓存:需要在mapper.xml的下面一行加个,对应的实体类要实现序列化接口。

每一个mapper文件对应一个二级缓存。如果sql一样但是不在一个Mapper文件爱里,也不行。

只要我们对mapper的命名空间有增删改的时候,二级缓存就会失效。

余票的变化很快,所以实际项目中二级缓存用的很少。而且不支持分布式节点的一些同步。

springboot内置缓存

对接口进行缓存,springboot的内置缓存。

org.springframework.boot

spring-boot-starter-cache

在对应的子模块的启动类上加注解

@EnableCaching

再在对应的service方法上加注解(value随意取个名字就好)

// @Cacheable(value = “DailyTrainTicketService.queryList”)

这个注解会开辟一块空间,请求参数为key,缓存值为value.请求参数要有hashcode和equals函数。去算哈希值

这个注解自始至终都是读缓存

要是想刷新缓存,需要用@CachePut注解

同时springboot的缓存也不支持分布式多节点。这个也不咋用。

redis缓存

共享的分布式缓存

spring.cache.type=redis

把一些spring内置的缓存放到redis里,这样要实现序列化

使用Redis解决了两个问题:
1.访问速度。mysql单机QPS约为2000,redis约10万

2.解决多节点共享缓存,机器重启也不会丢缓存数据。

缓存击穿:

60s的超时时间,热点Key失效,大量请求打到数据库上。

针对失效:可以在失效前主动更新缓存。—》定时任务。每隔30s去调用

缓存里就是没有:100个请求进来,只让一个请求进入。分布式锁,快速失败。

缓存穿透:

一些本身就没有的数据,比如学号为-1

应该是布隆过滤器,或者直接把空列表放到缓存里。进入数据库里也要加分布式锁。

结合spring的返回值的缓存,进行配置

spring.cache.redis.cache-null-values=true

也可以解决缓存穿透问题,会把一个null值放到缓存里,默认配置就是true

缓存雪崩:
很多key同时失效。

不让其同时失效ttl+随机数。选择合适的缓存过期策略。把热点的key定时的去查询。加限流。

共享缓存:节点1和节点2是一样的访问结果

案例分析

使用本地缓存,1min有效

结果fullgc频繁,导致短时间内大量请求失败

解决方案:去掉本地缓存,使用线程本地变量。

15章,分布式事务

TC-seata-server

TM-一个个子模块business,member

RM-数据库等

用seata的话,可以使用global全局事务注解

16章,加锁

加synchronized可以解决超卖,但是仅限于单机。多节点情况下会导致超卖。而且吞吐量会很低。

所有节点都能读到的数据。Redis。

stringRedisTemplate.opsForValue().setIfAbsent的本质是setnx,要设置一个超时时间.而且还要要求保证只有锁的持有者才可以释放锁,要使用唯一id标记每个客户端。

使用Redisson看门狗解决锁超时:只要主线程的业务没执行完,我就给你这个锁一直延时

守护线程也会随着主线程的结束而结束。

// waitTime – the maximum time to acquire the lock 等待获取锁时间(最大尝试获得锁的时间),超时返回false
 //   leaseTime – lease time 锁时长,即n秒后自动释放锁
//   time unit – time unit 时间单位
//  
boolean tryLock = lock.tryLock(30, 10, TimeUnit.SECONDS); // 不带看门狗
boolean tryLock = lock.tryLock(0, TimeUnit.SECONDS); // 带看门狗

未获得锁可以等待30s,得到锁后可以持有10s

第二行的0,不等待,拿得到就拿得到,拿不到就立刻返回false. 不需要定义持有时间,因为有看门狗,其初始会持有30s.等剩20s的时候会再刷新。

 //只有当前线程才可以删除这个锁。
 if(null!=lock && lock.isHeldByCurrentThread()){
    lock.unlock();
}

如果redis宕机了,换了一个Redis主节点。会导致,A线程能拿到锁,B线程也能拿到锁。

为了解决上面的问题,可以使用红锁。

五个节点,我要拿到半数以上(即3个)的节点的锁,才能说明我拿到锁了。

红锁成本太高,要使用好多台redis.而且三个线程同时进来后,5个节点可能不够分,所以要按固定的顺序拿redis节点的锁。性能也会慢一些。

17章,限流

流量的控制是在被调用方A

A调用方去熔断B服务,因为B不稳定。熔断是调用方去熔断别的服务。

不同的插槽有不同的职能。

FlowSlot,流量控制; DegradeSlot,熔断降级

将功能定义为资源,并为其进行限流操作。

简单进行限流

sentinel不保存规则,一旦应用重启,之前设置的规则就全没了,要结合nacos做个持久化。

流控效果:快速失败,warm up(预热,避免一开始流量过大JIT编译不充分导致问题),排队等待(短时的高峰,让他等1min(自己配个超时时间)慢慢消费掉)

流控模式:直接,关联,链路。关联:下单和支付两个关联性很高的应用。必须是支付出现限流之后,下单的限流才会生效。并且这两个接口同时在跑。链路要改代码,关闭一个配置,让链路去分开,默认的不生效。hello->hello2,hello1->hello2,只对后者这条链路做限流。

Sentinel+Feign熔断后的降级处理。

A服务调用B服务,B服务太垃圾了,熔断掉。然后执行后面的。如果后面的用到了B服务的值,则用planB去赋值(降级)

熔断:熔断之后后面的请求都拒绝了。这样会导致一堆异常。降级,创造类BusinessFeginFallback,然后在fegin接口上添加

@FeignClient(value = "business", fallback = BusinessFeignFallback.class)
public interface BusinessFeign {

business服务挂掉也是走这个。因为熔断就相当于认为他挂掉了。

熔断策略:慢调用比例,异常比例,异常数。

慢调用:业务的响应时长(RT)大于指定时长的请求认定为慢调用请求。在指定时间内,如果请求数量超过设定的最小数量,慢调用比例大于设定的阈值,则触发熔断

异常比例或异常数:统计指定时间内的调用,如果调用次数超过指定请求数,并且出现异常的比例达到设定的比例阈值(或超过指定异常数),则触发熔断。

18章,令牌大闸

有人做过博客

100个请求,里面有机器人刷票。

令牌:令牌里有用户的信息,如果用户已经拿过令牌了,就短时间内不能再拿了。

大闸:没有余票时,需要查库存才知道没票。会影响性能,不如查令牌余量。

拿到令牌,才能去做后面的事情。拿到令牌后,要去锁这个用户5s,防止他刷票。

用Redis做了一层缓存,来存令牌。

后面还做了一个图形验证码。

19章,MQ削峰

博客

拿到锁以后就可以扔给异步线程。说明用户就能成功买到票。

轮询查询购票结果,可以搞个定时任务。

MQ的生产者和消费者一般放在两个不同的节点,两个不同的模块。

分布式锁必须跟购票逻辑放一起

有一个问题,拿锁的时候有可能失败,没拿到锁的会快速失败,会抛异常。正确的方法是让订单更新成失败,用户查询到失败会重新发起购票。

但是拿不到锁还会使令牌消耗过大,拿到令牌后就有买票的资格,不能因为没抢到车次锁就买票失败,因此要有排队功能

新的时序图

一个车次一个锁。

不要把数据库里一万条数据同时查出来,可以使用分页插件多查几次。不然一次性全放到内存里,压力太大了。

要保证库存超卖和库存卖完的情况。

20章,压力测试

-Xms2048m -Xmx2048m 改变内存,变化不高,说明内存不是瓶颈

吞吐量:450

18080端口用在了Sentinel

18181端口给了RocketMQ的控制台

22章项目优化


文章作者: 爱敲代码の鱼儿
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 爱敲代码の鱼儿 !
  目录