外观
Speing-Could 学习笔记
分布式/微服务方向的起步学习(熟练度 1/5)。当前进度:先学好 MyBatis-Plus(它是微服务里数据访问层的事实标准),再进入 Nacos、Gateway、Sentinel 等微服务组件。
1. 学习Mybatis-Plus-基础
【重点要点】
- 定位:MyBatis 的增强工具,只增强不改变——保留 MyBatis 的 XML/注解写法,额外提供单表 CRUD、条件构造器等开箱能力,免写大量样板 SQL。
BaseMapper<T>:Mapper 接口继承它即自带insert / deleteById / updateById / selectById / selectList等单表方法。- 通用 Service:接口继承
IService<T>、实现类继承ServiceImpl<M, T>,得到save / getById / list / page等批量方法。 - 条件构造器:
QueryWrapper(字段名写字符串)与LambdaQueryWrapper(方法引用,编译期查错、更推荐);配合eq / like / orderByDesc / in / between / last("limit 1")组合查询。 - 分页插件:注册
MybatisPlusInterceptor+PaginationInnerInterceptor后,Page对象直接分页(物理分页)。 - 代码生成器:
FastAutoGenerator一键生成 Entity/Mapper/Service/Controller。 - 易错点:
selectPage必须配置分页插件否则全表查;Lambda 条件类字段写错会按错列查;实体驼峰字段与库下划线列默认自动映射。
java
// 1. Mapper:继承 BaseMapper,自带单表 CRUD
@Mapper
public interface UserMapper extends BaseMapper<User> { }
// 2. Service:IService + ServiceImpl
public interface UserService extends IService<User> { }
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
implements UserService { }java
// 3. LambdaQueryWrapper:类型安全的条件查询
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getStatus, 1)
.like(User::getName, "张")
.orderByDesc(User::getCreateTime)
.last("limit 10");
List<User> users = userService.list(wrapper);
// 分页
Page<User> page = userService.page(new Page<>(1, 10), wrapper);
page.getRecords(); page.getTotal(); page.getPages();java
// 4. 分页插件配置
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}java
// 5. 代码生成器(3.5.1+)
FastAutoGenerator.create("jdbc:mysql://localhost:3306/blog", "root", "123456")
.globalConfig(b -> b.author("me").outputDir("src/main/java"))
.packageConfig(b -> b.parent("com.example"))
.execute();2. 学习Mybatis-Plus-扩展功能
【重点要点】
- 逻辑删除:实体字段加
@TableLogic,删除变 update 软删;查询自动加deleted = 0条件(解决物理删除无法恢复、关联数据断裂问题)。 - 乐观锁:实体字段加
@Version+ 注册OptimisticLockerInnerInterceptor,更新时自动带版本号比对(防止并发覆盖)。 - 自动填充:实现
MetaObjectHandler,配合@TableField(fill = FieldFill.INSERT / INSERT_UPDATE)自动写入创建时间/更新时间/操作人等字段。 - 防全表更新:注册
BlockAttackInnerInterceptor,拦截不带 WHERE 的 update/delete,防止误操作清库。 - 多租户(SaaS):
TenantLineInnerInterceptor+ 配置租户字段,SQL 自动追加租户条件,一套代码多租户隔离。 - 性能分析:注册
SqlExplainInterceptor(分析 SQL 执行计划)或开启 SQL 日志(configuration.log-impl: StdOutImpl调试用)。 - 易错点:逻辑删除与唯一索引冲突(软删后再插同值主键会撞唯一索引);乐观锁更新失败(影响行数 0)要提示用户重试;自动填充只在 MP 内置方法生效,自定义 SQL 需手动赋值;多租户插件要放在拦截器链最内层。
java
// 逻辑删除 + 乐观锁 + 自动填充字段
@Data
public class User {
private Long id;
private String name;
@TableLogic
private Integer deleted; // 0 正常 / 1 已删
@Version
private Integer version; // 乐观锁版本号
@TableField(fill = FieldFill.INSERT)
private Date createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
}java
// 自动填充实现
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", Date.class, new Date());
this.strictInsertFill(metaObject, "updateTime", Date.class, new Date());
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
}
}java
// 完整拦截器链:分页 + 乐观锁 + 防全表更新
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); // 分页
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); // 乐观锁
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); // 防全表更新
return interceptor;
}3. 学习业务的拆分
【重点要点】
- 为什么要拆:单体应用代码膨胀后,团队协作冲突多、扩展只能整体扩(扛不住单模块高并发)、故障互相拖累;微服务按"业务边界"把大系统拆成一个个独立部署的小服务。
- 拆分原则:按业务域拆(如 用户服务/订单服务/商品服务/支付服务),不是按技术层拆(controller/service/dao 拆开没意义);每个服务自治:自己的数据库、自己的接口、独立部署。
- 拆分维度:业务划分 + 数据库划分(各服务独立库,从"一个库里多张表"变成"各服务各自库")。
- 拆分后的新问题:服务间如何通信(HTTP/OpenFeign)、服务如何被发现(注册中心)、如何统一入口(网关)、数据一致性(分布式事务,后续再学)、链路追踪。
- 易错点:拆分不是越细越好(服务多了运维/联调成本高,小项目单体反而合适);严禁服务间共享数据库表(会退化成"分布式单体");接口设计要稳定,避免频繁破坏性变更。
text
单体架构 微服务架构
┌──────────────┐ ┌──────────┐ ┌──────────┐
│ 用户/订单/商品 │ → │ 用户服务 │ │ 订单服务 │
│ 同一进程同一库 │ │ 独立库 │ │ 独立库 │
└──────────────┘ └────┬─────┘ └────┬─────┘
│ HTTP/OpenFeign 通信 │
└───────┴───────────────┘
注册中心(Nacos)发现问题4. 学习了解nacos注册中心
【重点要点】
- 注册中心作用:服务提供者启动时注册自己的地址(IP:端口)到注册中心;服务消费者从注册中心发现可用实例列表,再调用——解决"服务地址写死/实例变化"问题。
- Nacos 是阿里开源的一体化平台:注册中心 + 配置中心二合一;启动默认端口 8848,管理台
/nacos。 - 服务注册:服务端加
spring-cloud-starter-alibaba-nacos-discovery,配置spring.cloud.nacos.discovery.server-addr(注意:旧版用spring.cloud.nacos.server-addr,新版用 discovery/config 前缀)。 - 服务发现:OpenFeign/RestTemplate 按服务名调用,由负载均衡器从注册中心取实例;心跳/健康检查剔除不健康实例。
- 配置中心:把配置挪到 Nacos 上,
@RefreshScope+bootstrap.yml(旧版)/spring.config.import=nacos:(新版)实现改配置不重启。 - 易错点:服务名要一致(消费者按 name 找);nacos 与 Spring Boot 版本要匹配(Alibaba 版本号对应表);默认命名空间 public,多环境用命名空间/分组隔离。
yaml
# 服务提供者 & 消费者都要有
spring:
application:
name: user-service # 注册进 Nacos 的服务名
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848java
// 消费者用 @LoadBalanced RestTemplate 按服务名调用(注册中心自动发现)
@Configuration
public class RestConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
// 调用:http://user-service/user/1 而不是写死 IP
restTemplate.getForObject("http://user-service/user/1", User.class);5. 学习gateway网关,springcould提供
【重点要点】
- 网关职责:所有请求的统一入口——路由转发(按路径分发到对应服务)、鉴权、限流、跨域、日志,前置一层"门户"。
- Spring Cloud Gateway 基于 WebFlux(响应式,非阻塞),核心三要素:
Route(路由:id + uri + 断言集合)、Predicate(断言:匹配规则,如按 Path/Header/时间)、Filter(过滤器:请求/响应加工)。 - 配置路由:
spring.cloud.gateway.routes:id路由名、uri目标(lb://服务名走负载均衡发现)、predicates断言(Path=/user/**)、filters(如StripPrefix=1去前缀)。 - 常用断言:
Path、Method、Header、Query、Cookie、After/Before/Between(时间窗)。 - 常用过滤器:
StripPrefix(剥掉路径段数)、AddRequestHeader、自定义 GlobalFilter(做 JWT 鉴权通常写一个全局过滤器)。 - 易错点:
lb://需要配合注册中心/负载均衡;gateway 不在 spring-web 下,别把它当普通 web 依赖(排除 starter-web);路由不匹配返回 404 先查 predicates;跨域在网关统一配置AddResponseHeader或CorsGlobalFilter。
yaml
spring:
cloud:
gateway:
routes:
- id: user-route
uri: lb://user-service # 负载均衡到注册中心的服务
predicates:
- Path=/user/** # 匹配 /user/xx 的请求
filters:
- StripPrefix=1 # 去掉第一段 /user,再转发给服务
- id: order-route
uri: lb://order-service
predicates:
- Path=/order/**java
// 全局过滤器:统一鉴权示例(Gateway + WebFlux)
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getHeaders().getFirst("Authorization");
if (token == null || token.isBlank()) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete(); // 401 拦截
}
return chain.filter(exchange); // 放行
}
@Override
public int getOrder() { return -100; } // 优先级:数字越小越先执行
}6. 学习openFeign统一管理clent服务
【重点要点】
- OpenFeign:声明式 HTTP 客户端——把"调别人服务"的 REST 调用写成接口,省去手写 RestTemplate/HTTP 模板代码。
- 使用三步:接口加
@FeignClient(name="user-service")→ 方法上写 SpringMVC 注解(@GetMapping等,路径与目标服务一致)→ 启动类/配置类加@EnableFeignClients。 - 负载均衡:Spring Cloud LoadBalancer 自动集成(Feign 按服务名走注册中心选实例,替代已废弃的 Netflix Ribbon)。
- 参数映射:GET 用
@RequestParam/@PathVariable,POST 对象用@RequestBody(和 Controller 写法一致);返回类型直接声明为目标 DTO。 - 超时与容错:
spring.cloud.openfeign.client.config.default.connect-timeout/read-timeout配超时;远程异常默认抛 FeignException,可配 fallback(CircuitBreaker,进阶)。 - 易错点:
@FeignClient的 name 必须与目标服务在注册中心的 name 一致;Feign 接口的方法签名要和对方 Controller 完全对应(路径/参数类型/返回类型),否则运行时 404/反序列化失败;默认同步阻塞,别在接口里写复杂业务。
java
// 1. 声明 Feign 客户端:订单服务调用用户服务
@FeignClient(name = "user-service")
public interface UserClient {
@GetMapping("/user/{id}")
User getUserById(@PathVariable("id") Long id);
}java
// 2. 启用 Feign
@SpringBootApplication
@EnableFeignClients // 扫描 @FeignClient 接口
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}java
// 3. 业务中使用:像调本地方法一样调远程
@Service
public class OrderServiceImpl {
@Autowired
private UserClient userClient;
public OrderDetail getById(Long id) {
User user = userClient.getUserById(order.getUserId()); // 远程调用自动完成
return new OrderDetail(order, user);
}
}yaml
# 超时配置(读/连接超时,默认较保守)
spring:
cloud:
openfeign:
client:
config:
default:
connect-timeout: 3000
read-timeout: 50007. 学习MQ消息队列
【重点要点】
- 为什么用 MQ:服务间异步解耦(下单后发消息通知库存/积分,不用同步等)、流量削峰(秒杀高并发先入队)、日志/广播场景。
- 常见产品:RabbitMQ(AMQP,路由灵活,课程用这个)、Kafka(高吞吐日志流)、RocketMQ(阿里,事务消息)。
- RabbitMQ 核心模型:交换机(Exchange)→ 队列(Queue)→ 消费者;生产者发到交换机,交换机按**路由键(routing key)和绑定(binding)**把消息路由到队列。
- 四种交换机:
direct(路由键精确匹配)、fanout(广播到所有绑定队列)、topic(通配符匹配#/*)、headers(按头匹配,少用)。 - SpringAMQP:
RabbitTemplate.convertAndSend(exchange, routingKey, msg)发送;@RabbitListener(queues="queue")消费;队列/交换机用@Bean声明(或@RabbitListener自动声明)。 - 可靠性:发送方
confirm回调确认;消费方手动 ack + 重试(spring.rabbitmq.listener.simple.acknowledge-mode: manual);死信队列处理失败消息。 - 易错点:队列/交换机名字或路由键不匹配 → 消息"神秘消失"(发到了没人绑定的交换机);
topic的#匹配零个或多个词、*匹配一个词;消费端要幂等(消息可能重发);默认自动 ack 异常会丢消息。
yaml
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guestjava
// 发送消息
@Autowired
private RabbitTemplate rabbitTemplate;
public void orderCreated(Order order) {
// 发到 exchange.order 交换机,路由键 order.created
rabbitTemplate.convertAndSend("exchange.order", "order.created", order);
}java
// 消费消息
@Component
public class OrderListener {
@RabbitListener(queues = "queue.order")
public void onOrder(Order order) {
// 业务:扣库存、发短信、更新统计
System.out.println("收到订单:" + order.getId());
}
}java
// 声明交换机与队列(topic 模式示例)
@Configuration
public class MqConfig {
@Bean
public TopicExchange orderExchange() {
return new TopicExchange("exchange.order");
}
@Bean
public Queue orderQueue() {
return new Queue("queue.order");
}
@Bean
public Binding orderBinding() {
return BindingBuilder.bind(orderQueue())
.to(orderExchange())
.with("order.#"); // 匹配 order.created / order.paid 等
}
}8. 学习ES搜索引擎
【重点要点】
- ES(Elasticsearch)是什么:基于 Lucene 的分布式搜索引擎,核心是倒排索引——把文档拆成词条,记录"词条 → 文档"映射,搜索时按词条查文档(比 LIKE 快得多)。
- 核心概念对比 MySQL:索引 Index ↔ 数据库;类型(已废弃)↔ 表;文档 Document ↔ 行;字段 Field ↔ 列。
- 分词:中文用 IK 分词器(ik_max_word 最细粒度 / ik_smart 粗粒度),自定义词典可加业务词。
- 索引映射(Mapping):建索引时定义字段类型(text 可分词 / keyword 精确 / 数值 / 日期)、
analyzer分词器。 - DSL 查询:
match(分词匹配)、term(精确)、range(范围)、bool(组合 must/should/filter)、match_all。 - Java 客户端:RestHighLevelClient(旧)/
co.elastic.clients(新);或 Spring Data Elasticsearch。 - 数据同步:MySQL 数据 → ES 常用三种方案:同步双写(业务里同时写)、异步 MQ(发消息由监听器写 ES,课程用这种)、Canal 监听 binlog。
- 易错点:text 字段默认被分词,做精确匹配/聚合要用 keyword;查询 DSL 的 bool 里
filter不参与打分更快;ES 字段一旦建好不能随便改类型(要重建索引);高亮/分页/聚合易出细节问题。
json
// 索引映射:定义字段与分词器
PUT /item
{
"mappings": {
"properties": {
"id": { "type": "keyword" },
"name": { "type": "text", "analyzer": "ik_max_word" },
"price": { "type": "double" },
"brand": { "type": "keyword" }
}
}
}json
// DSL:多条件查询(品牌精确 + 名称分词匹配 + 价格范围)
GET /item/_search
{
"query": {
"bool": {
"must": [{ "match": { "name": "手机" } }],
"filter": [
{ "term": { "brand": "华为" } },
{ "range": { "price": { "gte": 1000 } } }
]
}
}
}9. 学习Redis高级-缓存与分布式锁
【重点要点】
- 缓存三大问题及解法(面试高频):
- 缓存穿透:查不存在的数据 → 每次打数据库。解决:缓存空值(null 也缓存短时间)/ 布隆过滤器。
- 缓存击穿:热点 key 过期瞬间大量请求打库。解决:互斥锁(重建缓存加锁)/ 逻辑过期(不设物理过期,缓存值里带过期时间,过期后异步重建,课程秒杀场景用这个)。
- 缓存雪崩:大量 key 同时过期或 Redis 宕机。解决:过期时间加随机值 / 集群高可用。
- 缓存一致性:更新数据库后删缓存(延迟双删:先删缓存→更新库→延迟再删);或用 Canal 监听 binlog 删缓存;先更库再删缓存,避免旧缓存覆盖新值。
- 分布式锁:多个服务实例抢同一把锁,不能用本地锁。Redis 实现:
SETNX key value+ 过期时间原子设置(防死锁)+ 释放时校验(防误删别人锁);更专业用 Redisson(RLock,看门狗自动续期,防锁过期问题)。 - 秒杀/扣库存场景:
lua 脚本保证"查库存 + 扣减"原子性;或 Redis 预扣 + 异步同步数据库。 - 易错点:锁的 key 要区分业务(如
lock:order:用户id);设置过期时间要原子(SET key v EX n NX);删除锁要用 lua 比对 value 再删;Redisson 锁用完必须 finally unlock。
bash
# 原子设置锁:NX 不存在才设置,EX 过期时间
SET lock:order:1001 1 NX EX 10
# 释放锁要校验是自己的 value 再删(lua)
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
endjava
// Redisson 分布式锁
@Autowired
private RedissonClient redissonClient;
public void deductStock(Long id) {
RLock lock = redissonClient.getLock("lock:item:" + id);
try {
lock.lock(10, TimeUnit.SECONDS); // 看门狗默认 30s 自动续期
// 查库存、扣减、写库(业务)
} finally {
lock.unlock();
}
}java
// 缓存空值防穿透:查不到也缓存
public Item queryById(Long id) {
String json = redis.opsForValue().get("item:" + id);
if (json != null) {
if ("".equals(json)) return null; // 缓存空值
return JSON.parseObject(json, Item.class);
}
Item item = itemMapper.selectById(id);
if (item == null) {
redis.opsForValue().set("item:" + id, "", 5, TimeUnit.MINUTES); // 空值短缓存
return null;
}
redis.opsForValue().set("item:" + id, JSON.toJSONString(item));
return item;
}10. 学习Sentinel服务保护
【重点要点】
- 为什么需要:微服务链路长,一个服务慢/挂 → 上游线程被拖死 → 雪崩(如订单调用户,用户挂了订单线程池占满)。
- Sentinel 是阿里的流量控制/熔断降级组件(替代 Hystrix),控制台可视化配置规则。
- 三个核心能力:
- 流量控制(Flow):按 QPS/并发线程数限流,防止突发流量打爆;可配直接/关联/链路模式与排队等待。
- 熔断降级(Degrade):慢调用比例、异常比例、异常数达到阈值 → 熔断(快速失败/降级兜底),保护下游。
- 热点参数限流:对某个参数(如商品 id)单独限流。
- 集成:
spring-cloud-starter-alibaba-sentinel+ 控制台(默认 8080 端口,-Dcsp.sentinel.dashboard.server);@SentinelResource(value, blockHandler, fallback)标注资源并指定降级方法。 - 线程隔离(舱壁模式):给每个下游分配独立线程池/信号量,一个服务慢不拖垮其他(Sentinel 用信号量隔离;课程里和线程池隔离对比)。
- 易错点:懒加载——Sentinel 默认第一次访问才注册资源,冷启动规则看不到(可加
-Dcsp.sentinel.heartbeat.client.ip/ 预热);blockHandler 与 fallback 的方法签名要求;规则默认存内存,重启丢失(可配 Nacos 持久化)。
yaml
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8080 # Sentinel 控制台地址
eager: true # 关闭懒加载java
// 标记资源 + 降级兜底
@SentinelResource(
value = "item.queryById",
blockHandler = "queryByIdBlock", // 被限流/熔断时调用
fallback = "queryByIdFallback" // 业务异常时调用
)
public Item queryById(Long id) {
return itemService.queryById(id);
}
// 方法签名:原方法参数 + BlockException
public Item queryByIdBlock(Long id, BlockException e) {
return Item.fallback(); // 返回兜底数据
}
public Item queryByIdFallback(Long id, Throwable e) {
log.error("查询失败", e);
return null;
}11. 学习Seata分布式事务
【重点要点】
- 问题背景:微服务各自独立库,跨服务写操作无法用单库事务保证(下单:订单库扣减 + 库存库扣减,一个成功一个失败就数据不一致)。
- Seata 分布式事务模型:TC(事务协调器,独立服务) + TM(事务管理器,发起方) + RM(资源管理器,参与方)。
- AT 模式(最常用,自动补偿):
- 一阶段:各参与方本地提交 + 记录回滚日志(undo_log)(before/after 镜像);
- 二阶段:全部成功则删除 undo_log;有失败则由 TC 通知回滚,RM 按 undo_log 反向补偿。
- 优点:无侵入(普通 SQL 即可),适合大部分业务。
- 其他模式:XA(强一致,数据库支持,性能低)、TCC(Try-Confirm-Cancel 手动补偿,适合需要控制的场景)、SAGA(长事务)。
- 使用:业务入口方法加
@GlobalTransactional;部署 Seata Server(默认端口 8091)+ 参与方配seata.tx-service-group与 TC 地址;数据库加undo_log表。 - 易错点:AT 模式要求数据库支持事务(InnoDB);undo_log 表每库都要建;本地事务必须能被回滚(别 try-catch 吞异常);全局锁冲突时性能下降明显,长事务慎用;与 Spring Cloud 版本/注册中心配合要配置正确。
sql
-- 每个参与事务的数据库都要建 undo_log 表
CREATE TABLE IF NOT EXISTS `undo_log` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`branch_id` BIGINT(20) NOT NULL,
`xid` VARCHAR(100) NOT NULL,
`context` VARCHAR(128) NOT NULL,
`rollback_info` LONGBLOB NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;yaml
# 参与方(订单/库存服务)配置
seata:
enabled: true
tx-service-group: hmall_tx_group # 事务分组(与 server 配置一致)
registry:
type: nacos # 注册中心类型
nacos:
server-addr: 127.0.0.1:8848
service:
vgroup-mapping:
hmall_tx_group: default # 分组映射到集群java
// 发起全局事务:订单服务入口方法
@GlobalTransactional // 开启分布式事务
public Long createOrder(Order order) {
orderMapper.insert(order); // 订单库
itemClient.deductStock(order.getItemId(), order.getNum()); // 远程扣库存(参与方)
return order.getId(); // 任一步失败,整体回滚
}