Idea - 编程中的tips

github网页查看方式

首先进入 https://github.com/klc407073648/cpp-notes

然后,键盘输入句号

编程注意事项

  • 引入一个库时,先写测试类,即小模块功能验证后使用。例如,使用POCO库的LocalDateTime等
  • 缓存的使用,一定要设置过期时间,不能无限期地使用,得到资源释放,重复利用的功能。
  • 分布式锁等复杂逻辑,尽量采用现有的框架实现,不要自己写!!!
  • 工作规划 ———— 写文档,列出需求背景、需求分析、设计方案选择、具体实现(库表设计、增删改查、业务逻辑(PO,重点需求))、以及各部分工作时间的安排
  • 为什么需要请求参数包装类(DTO)?
    1. 请求参数名称 / 类型和实体类不一样
    2. 有一些参数用不到,如果要自动生成接口文档,会增加理解成本
    3. 对个实体类映射到同一个对象
  • 为什么需要包装类?
    • 可能有些字段需要隐藏,不能返回给前端。或者有些字段某些方法是不关心的

数据查询慢

用缓存:提前把数据取出来保存好(通常保存到读写更快的介质,比如内存),就可以更快地读写。

缓存

作用

提前把数据取出来保存好(通常保存到读写更快的介质,比如内存),就可以更快地读写。

分类

  • Redis(分布式缓存)———— 本质:key-value存储系统
  • memcached(分布式)
  • Etcd(云原生架构的一个分布式存储,存储配置,扩容能力)
  • 本地缓存(内存 Map)

设计缓存key

在项目使用过程中,往往多个模块都会用到缓存功能,可以使用 systemId:moduleId:func:options 来区分。

举例:

  • yupao:user:recommed:userId
  • hmdp:user:login:token

redis 内存不能无限增加,一定要设置过期时间!!!

缓存预热

问题:第一个用户访问还是很慢,也能一定程度上保护数据库

优点:

  1. 解决上面的问题,可以让用户始终访问很快

缺点:

  1. 增加开发成本(需要额外的开发、设计)
  2. 预热的时机和时间如果错了,有可能你缓存的数据不对或者太老
  3. 需要占用额外空间

怎么缓存预热?

  1. 定时
  2. 模拟触发(手动触发)

实现

用定时任务,每天刷新所有用户的推荐列表

注意点:

  1. 缓存预热的意义(新增少、总用户多)
  2. 缓存的空间不能太大,要预留给其他缓存空间
  3. 缓存数据的周期(此处每天一次)

分析优缺点的时候,要打开思路,从整个项目从 0 到 1 的链路上去分析

控制定时任务的执行

前文提及定时任务的执行,需要进行额外的控制

  1. 浪费资源,想象 10000 台服务器同时 “打鸣”
  2. 脏数据,比如,定时任务用于插入数据,会引发重复插入

要控制定时任务在同一时间只有 1 个服务器能执行

  1. 分离定时任务程序和主程序,只在 1 个服务器运行定时任务。————成本太大

  2. 写死配置,每个服务器都执行定时任务,但是只有 ip 符合配置的服务器才真实执行业务逻辑,其他的直接返回。————成本最低;但是服务器的 IP 可能是不固定的,把 IP 写的太死了。

  3. 动态配置,配置是可以轻松的、很方便地更新的(代码无需重启),但是只有 ip 符合配置的服务器才真实执行业务逻辑。

    • 可以采用
      • 数据库
      • Redis
      • 配置中心(Nacos、Apollo、Spring Cloud Config)
    • 问题:服务器多了、IP 不可控还是很麻烦,还是要人工修改
  4. 分布式锁,只有抢到锁的服务器才能执行业务逻辑。

    • 坏处:增加成本,例如Redis服务器成本,人工编码成本;
    • 好处:不用手动配置,多少个服务器都一样。
  5. Zookeeper 实现(不推荐)

注意事项:单机就会存在单点故障。

有限资源的情况下,控制同一时间(段)只有某些线程(用户 / 服务器)能访问到资源。

  • Java 实现锁:synchronized 关键字、并发包的类 ———— 问题:只对单个 JVM 有效
  • C++ 实现锁:进程中启动多线程,通过标志位控制 ———— 问题:只对单个服务进程有效

分布式锁

为什么需要分布式锁?

  1. 有限资源的情况下,控制同一时间(段)只有某些线程(用户 / 服务器)能访问到资源
  2. 单个锁只对单个 JVM / 单个进程 有效

分布式锁实现的关键

抢锁机制

如何保证同一时间只有 1 个服务器能抢到锁?

核心思想 :先来的人先把数据改成自己的标识(服务器 ip),后来的人发现标识已存在,就抢锁失败,继续等待。等先来的人执行方法结束,把标识清空,其他的人继续抢锁。

实现的几种方式

  • MySQL 数据库:select for update 行级锁(最简单),但是数据库操作时间会慢一些

*(乐观锁)

  • ✔ Redis 实现:内存数据库,读写速度快 。支持 setnx、lua 脚本,比较方便实现分布式锁。
    • setnx:set if not exists 如果不存在,则设置;只有设置成功才会返回 true,否则返回 false

注意事项

  1. 用完锁要释放(腾地方)

  2. 锁一定要加过期时间

  3. 如果方法执行时间过长,锁提前过期了?

    • 问题:
      • 连锁效应:释放掉别人的锁
      • 这样还是会存在多个方法同时执行的情况 ​ * 解决方案:续期
boolean end = false;

new Thread(() -> {
    if (!end)}{
    续期
})

end = true;
1
2
3
4
5
6
7
8
  1. 释放锁的时候,有可能先判断出是自己的锁,但这时锁过期了,最后还是释放了别人的锁

    // 原子操作
    if(get lock == A) {
        // set lock B
        del lock
    }
    
    1
    2
    3
    4
    5

    Redis + lua 脚本实现

  2. Redis 如果是集群(而不是只有一个 Redis),如果分布式锁的数据不同步怎么办?

分布式锁的实现

  • 拒绝从0到1自己写,一般而言不够高效
  • 采用现成的库,功能齐全,而且即使有BUG,也会持续修复

Redisson 实现分布式锁

特点:

  • Java 客户端,数据网格
  • 实现了很多 Java 里支持的接口和数据结构

Redisson 是一个 java 操作 Redis 的客户端,提供了大量的分布式数据集来简化对 Redis 的操作和使用,可以让开发者像使用本地集合一样使用 Redis,完全感知不到 Redis 的存在。

种引入方式

  1. spring boot starter 引入(不推荐,版本迭代太快,容易冲突)
  2. 直接引入:
    • [redisson]https://github.com/redisson/redisson#quick-start

定时任务 + 锁

  1. waitTime 设置为 0,只抢一次,抢不到就放弃
  2. 注意释放锁要写在 finally 中

看门狗机制

redisson 中提供的续期机制

开一个监听线程,如果方法还没执行完,就帮你重置 redis 锁的过期时间。

原理:

  1. 监听当前线程,默认过期时间是 30 秒,每 10 秒续期一次(补到 30 秒)
  2. 如果线程挂掉(注意 debug 模式也会被它当成服务器宕机),则不会续期