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的内置缓存。
在对应的子模块的启动类上加注解
@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的控制台