Elasticsearch

Elasticsearch中文官网

Kibana-可视化工具

名词解释
节点(Node)一个运行中的Elasticsearch实例称为一个节点。节点可以通过集群名称加入到一个集群中。
集群(Cluster)集群是由一个或多个节点组成的集合,它们共同承载整个数据集并提供联合索引和搜索功能。每个集群有一个唯一的名称。
索引(Index)索引是具有相似特征的文档集合。索引的名称必须是全部小写的,并且一个集群中可以创建多个索引。
类型(Type)在Elasticsearch 7.x中已废弃,在早期版本中,类型用于区分索引中的不同类别数据。每个索引可以包含一个或多个类型。
文档(Document)文档是Elasticsearch中的基本数据单元,通常是以JSON格式表示。每个文档都存储在一个索引中,并且具有一个类型和一个ID。
映射(Mapping)映射是定义文档及其字段如何存储和索引的过程。映射可以包括字段的数据类型、是否分析、是否存储等信息。
碎片(Shard)索引可以分解为多个碎片,每个碎片是一个独立的Lucene实例。碎片允许水平扩展,并且可以分布在集群中的不同节点上。
副本(Replica)副本是碎片的副本,用于提供数据冗余和增加搜索性能。每个碎片可以有一个或多个副本。
查询DSL(Domain Specific Language)Elasticsearch提供了一种查询DSL,这是一种非常灵活的、基于JSON的查询语言,允许你执行复杂的查询。
聚合(Aggregation)聚合提供了从数据中提取洞察力的能力,类似于SQL中的GROUP BY和聚合函数。Elasticsearch提供了多种聚合类型,如桶聚合、度量聚合等。
主节点(Master Node)负责集群管理的节点,例如创建或删除索引、添加或移除节点等。
数据节点(Data Node)存储数据的节点,负责索引和搜索操作。
协调节点(Coordinating Node)处理客户端请求,将请求路由到合适的节点,并组合响应返回给客户端。

Nginx

基本使用

  • 安装sudo apt-get install nginx(Debian/Ubuntu)
  • 配置文件:通常位于/etc/nginx/nginx.conf
  • 基本命令
    • 启动:sudo systemctl start nginx
    • 停止:sudo systemctl stop nginx
    • 重载:sudo systemctl reload nginx

动静分离

  • 定义:将动态请求和静态请求分开处理。

  • 优势:提高服务器效率和性能。

  • 配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    server {
    location / {
    proxy_pass http://backend;
    }
    location ~* \.(jpg|png|gif|js|css)$ {
    expires 30d;
    gzip on;
    gzip_types image/jpeg image/png application/javascript text/css;
    root /path/to/static;
    }
    }

JMeter

基本使用

  • 安装:下载和解压JMeter。
  • 测试计划:创建线程组、添加HTTP请求、设置断言和监听器。
  • 录制:使用JMeter代理录制HTTP请求。
  • 参数化:使用CSV数据文件配置器导入参数。
  • 分布式测试:配置远程启动JMeter服务器和客户端。

JVM

  • JVM结构:包括类加载器、执行引擎、内存区域等。
  • 类加载机制:Bootstrap ClassLoader、Extension ClassLoader、Application ClassLoader。
  • 内存模型:堆(Heap)、栈(Stack)、方法区(Method Area)、程序计数器(Program Counter)。
  • 垃圾回收:标记-清除(Mark-Sweep)、复制(Copy)、标记-整理(Mark-Compact)等算法。
  • 监控和调优:使用jconsolejvisualvm等工具。

jconsole

轻量、方便

  • 启动:jconsole

  • 监控:查看内存、线程、类、垃圾回收等信息。

jvisualvm

更专业、支持插件

  • 启动:jvisualvm

  • 监控:除了jconsole的功能外,还可以查看更详细的性能图表。

  • 案例:使用jvisualvm分析内存泄漏,通过堆转储(Heap Dump)定位问题代码。

本机压测

压测内容压测线程数吞吐量/s90%响应时间(ms)99%响应时间(ms)
Nginx508440917
Gateway508460929
简单服务503805324
首页渲染5099893272
首页渲染(开缓存)50215631120
三级分类数据获取502034994182
三级分类数据获取(Redis缓存)5015724280
Gateway+简单服务5063341234
全链路5020712942

结论

  1. 中间件越多,性能损失越大,大多损失在网络交互

  2. 影响因子:DB、模板渲染速度、静态资源

缓存

SpringBoot整合Redis

  1. 导入依赖,lettuce客户端有对外内存溢出的问题,可切换为jedis

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <dependency><!-- 引入Redis -->
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <exclusions>
    <exclusion><!-- 排除lettuce -->
    <groupId>io.lettuce</groupId>
    <artifactId>lettuce-core</artifactId>
    </exclusion>
    </exclusions>
    </dependency>

    <dependency><!-- 用于替换lettuce -->
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    </dependency>
  2. 配置Redis地址

    1
    2
    3
    4
    5
    spring:
    redis:
    host: 192.168.114.141
    #password:
    port: 6379
  3. 测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Test
    void redisTest() {
    ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
    ops.set("hello", "world_" + UUID.randomUUID());
    System.out.println(ops.get("hello"));
    }

缓存失效

缓存穿透、缓存雪崩和缓存击穿是分布式缓存系统中常见的问题,它们都会导致缓存系统的性能下降,甚至影响到后端存储系统的稳定性。下面我会分别解释这三种现象:

  1. 缓存穿透(Cache Penetration)

    缓存穿透是指客户端请求的数据在缓存中不存在,需要穿透缓存去后端存储系统(如数据库)查询,但后端存储系统中也没有该数据的情况。

    这种情况通常发生在恶意攻击或错误的应用程序逻辑导致的频繁无效请求。

    解决方案:

    • 对无效请求进行过滤,例如使用布隆过滤器快速判断一个key是否在数据库中存在。

    • 设置合理的缓存穿透阈值,当超过阈值时,可以将请求放入异步队列中,慢慢处理。

    • 对返回的空结果也进行缓存,但设置较短的过期时间。

  2. 缓存雪崩(Cache Avalanche)

    缓存雪崩是指缓存系统中大量的缓存项几乎同时过期,导致大量请求直接到达后端存储系统,造成系统压力突然增加的现象。

    这种情况类似于多个请求同时击中缓存系统的“空白区域”。

    解决方案:

    • 避免设置大量缓存项在同一时间过期,可以通过设置不同的过期时间来打散缓存项的过期时间。

    • 使用锁或队列来控制对后端存储系统的请求,防止过载。

    • 提高后端存储系统的性能和扩展能力,以应对突发的请求量。

  3. 缓存击穿(Cache Breakdown)

    缓存击穿是指一个热点缓存项过期,导致大量请求在缓存失效的瞬间同时请求该数据,这些请求都会穿透缓存到达后端存储系统。

    这种情况通常发生在高并发场景下对热点数据的请求。

    解决方案:

    • 对热点数据使用永不过期或较长的过期时间。

    • 使用锁或分布式锁,确保同一时间只有一个请求能够穿透到后端存储系统。

    • 预热缓存,提前将热点数据加载到缓存中。

分布式锁

即所有的服务到同一个地方占坑,可以使用Redis实现,案例:

1
set lock yes NX

lua脚本:整合Redis操作为一个原子操作,完整案例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {
// 占分布式锁
UUID token = UUID.randomUUID();
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", token.toString(), 300, TimeUnit.SECONDS);
if (lock) {
// 加锁成功,执行业务
Map<String, List<Catelog2Vo>> dataFromDb;
try {
dataFromDb = getDataFromDb();
} finally {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end)";
stringRedisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Collections.singletonList("lock"), token);// 删除锁
}
return dataFromDb;
} else {
try {
// 防止栈溢出
Thread.sleep(200);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return getCatalogJsonFromDbWithRedisLock(); // 自旋方式
}
}

注意事项

  1. 加锁保持原子性
  2. 删锁保持原子性

Redission

优点

  1. 分布式锁
  2. 没有死锁问题(看门狗,锁自动续期,默认30秒)

redis-distributed-locks Redis分布式锁

redisson

使用步骤

  1. 导入Maven坐标

    1
    2
    3
    4
    5
    6
    <!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
    <dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.12.5</version>
    </dependency>
  2. 配置Redisson,可以参考Redisson-配置方法Redisson-第三方框架整合

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    import org.redisson.Redisson;
    import org.redisson.api.RedissonClient;
    import org.redisson.config.Config;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import java.io.IOException;

    @Configuration
    public class MyRedissonConfig {
    /**
    * 所有对Redisson的操作都是通过RedissonClient对象
    * @return
    * @throws IOException
    */
    @Bean(destroyMethod="shutdown")
    public RedissonClient redissonClient() throws IOException {
    // 创建配置
    Config config = new Config();
    config.useSingleServer().setAddress("redis://192.168.114.141:6379");
    // 根据配置创建出RedissonClient实例
    return Redisson.create(config);
    }
    }
  3. 使用案例,可参考Redisson-分布式锁和同步器

    基本使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    import org.redisson.api.RLock;
    import org.redisson.api.RedissonClient;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.ResponseBody;

    @Controller
    public class IndexController {
    @Autowired
    private RedissonClient redissonClient;

    @GetMapping("/hello")
    @ResponseBody
    public String hello() {
    // 获取一把锁,锁名字一样就是同一把锁
    RLock lock = redissonClient.getLock("my-lock");
    // 加锁
    lock.lock(); // 阻塞式等待
    try {
    // 执行业务
    System.out.println("加锁成功,执行业务……" + Thread.currentThread().getId());
    Thread.sleep(3000);
    } catch (InterruptedException e) {
    throw new RuntimeException(e);
    } finally {
    System.out.println("释放锁……" + Thread.currentThread().getId());
    // 解锁
    lock.unlock();
    }

    return "hello world";
    }
    }

    读写锁

    1. 写锁(排他锁):独占性,即同一时间只有一个线程可以持有该锁。
    2. 读锁(共享锁):共享性,即同一时间可以有多个线程持有该锁。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    @GetMapping("/write")
    @ResponseBody
    public String writeValue() {
    RReadWriteLock lock = redissonClient.getReadWriteLock("rw-lock");
    String s;
    RLock rLock = lock.writeLock();
    try {
    // 改数据加写锁,读数据加读锁
    rLock.lock();
    s = UUID.randomUUID().toString();
    Thread.sleep(30000);
    stringRedisTemplate.opsForValue().set("writeValue", s);
    } catch (InterruptedException e) {
    throw new RuntimeException(e);
    } finally {
    rLock.unlock();
    }

    return s;
    }

    @GetMapping("/read")
    @ResponseBody
    public String readValue() {
    RReadWriteLock lock = redissonClient.getReadWriteLock("rw-lock");
    String s = "";
    // 加读锁
    RLock rLock = lock.readLock();
    rLock.lock();
    try {
    s = stringRedisTemplate.opsForValue().get("writeValue");
    } catch (Exception e) {
    e.printStackTrace();
    } finally {
    rLock.unlock();
    }

    return s;
    }

    闭锁(计数锁)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @GetMapping("/lockDoor")
    @ResponseBody
    public String lockDoor() throws InterruptedException {
    RCountDownLatch door = redissonClient.getCountDownLatch("door");
    door.trySetCount(5);
    door.await(); // 等待闭锁都完成
    return "放假了……";
    }

    @GetMapping("/go/{id}")
    @ResponseBody
    public String go(@PathVariable("id") Long id) {
    RCountDownLatch door = redissonClient.getCountDownLatch("door");
    door.countDown(); // 计数器减一
    return id + "班的人都走了";
    }

缓存数据一致性

都有脏数据的问题,考虑加锁或不将经常修改的数据加入缓存

  1. 双写模式:数据库与缓存一起更改
  2. 失效模式:数据库改完将缓存删除

可以使用Alibaba-Canal解决数据异构。

Spring Cache

Spring Cache中文指引

使用步骤

  1. 导入Mave坐标:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <dependency><!-- Spring缓存 -->
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>

    <dependency><!-- 如使用Redis做缓存则需要引入Redis -->
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
  2. 写配置:CacheAutoConfiguration会导入RedisCacheConfiguration,自动配置好了缓存管理器RedisCacheManager,在application.yml中写入以下内容即可使用Redis作为缓存:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    spring:
    # redis配置
    redis:
    host: 192.168.114.141
    #password:
    port: 6379
    # 缓存配置
    cache:
    type: redis

    在启动类上加上@EnableCaching注解以开启缓存。

  3. 使用缓存,注解说明与案例代码如下:

    注解说明
    @EnableCaching启用Spring Cache支持。通常在配置类上使用。
    @Cacheable标记一个方法的返回值应该被缓存。如果缓存中已经有值,则直接返回缓存中的值;否则执行方法,并将返回值存入缓存。
    @CachePut用于更新缓存。每次调用该方法时,都会执行方法,并将返回值更新到缓存中。(双写模式
    @CacheEvict用于清除缓存。可以在方法执行前或执行后清除缓存。(失效模式
    @Caching用于组合多个缓存操作。可以同时应用多个@Cacheable@CachePut@CacheEvict
    @CacheConfig用于类级别,提供缓存相关的配置,如缓存名称等。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 当前方法的结果需要缓存,如果缓存中有方法不需要调用。
    // 如果缓存中没有,会调用方法,最后将方法的结果放入缓存当中。
    // 每一个需要缓存的数据,都需要指定要放到哪个名字的缓存。(缓存分区,可以按照业务类型分区)
    @Cacheable({"category"})
    @Override
    public List<CategoryEntity> getLevel1Categories() {
    return baseMapper.selectList(new LambdaQueryWrapper<CategoryEntity>()
    .eq(CategoryEntity::getParentCid, 0));
    }

    Redis缓存默认规则

    1. Key规则:缓存名::SimpleKey []
    2. 使用JDK序列化规则存储
    3. 永不过期

    自定义规则

    1. 指定生成的缓存使用的Key

      1
      @Cacheable(value = {"category"}, key = "'level1Categories'")
    2. 指定缓存数据的生存时间

      1
      2
      3
      4
      5
      spring:
      cache:
      redis:
      # 一个小时过期
      time-to-live: 3600000
    3. 将数据保存为JSON格式

      需要自定义RedisCacheConfiguration(application.yml中的配置就会失效),创建一个自定义配置类,可以命名为MyCacheConfig

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      import org.springframework.boot.autoconfigure.cache.CacheProperties;
      import org.springframework.boot.context.properties.EnableConfigurationProperties;
      import org.springframework.cache.annotation.EnableCaching;
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      import org.springframework.data.redis.cache.RedisCacheConfiguration;
      import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
      import org.springframework.data.redis.serializer.RedisSerializationContext;
      import org.springframework.data.redis.serializer.StringRedisSerializer;

      @EnableConfigurationProperties(CacheProperties.class)
      @EnableCaching
      @Configuration
      public class MyCacheConfig {
      @Bean
      public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
      RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
      // 更改Key序列化机制
      config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
      // 更改Value序列化机制
      config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
      // 导入配置文件的配置
      CacheProperties.Redis redisProperties = cacheProperties.getRedis(); // 获取Redis配置
      if (redisProperties.getTimeToLive() != null) {
      config = config.entryTtl(redisProperties.getTimeToLive());
      }
      if (redisProperties.getKeyPrefix() != null) {
      config = config.prefixCacheNameWith(redisProperties.getKeyPrefix());
      }
      if (!redisProperties.isCacheNullValues()) {
      config = config.disableCachingNullValues();
      }
      if (!redisProperties.isUseKeyPrefix()) {
      config = config.disableKeyPrefix();
      }
      return config;
      }
      }

    Redis缓存扩展配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    spring:
    # 缓存配置
    cache:
    type: redis
    redis:
    # 一个小时过期
    time-to-live: 3600000
    # Key前缀,如果没有指定前缀,则使用缓存名字作为Key前缀
    key-prefix: CACHE_
    # 使用前缀
    use-key-prefix: true
    # 是否缓存空值,防止缓存穿透
    cache-null-values: true

    其他注解使用

    1. 缓存失效,执行该方法时使指定的缓存失效

      1
      2
      3
      4
      5
      6
      @CacheEvict(value = "category", key = "'level1Categories'")
      @Override
      public void updateCascade(CategoryEntity category) {
      this.updateById(category);
      categoryBrandRelationService.updateCategory(category.getCatId(), category.getName());
      }
    2. 同时进行多种缓存操作(多个缓存失效)

      1
      2
      3
      4
      @Caching(evict = {
      @CacheEvict(value = "category", key = "'getLevel1Categories'"),
      @CacheEvict(value = "category", key = "'getCatalogJson'")
      })
    3. 指定删除某个分区下所有的数据

      1
      @CacheEvict(value = "category", allEntries = true)
    4. 加锁解决击穿问题(只有Cacheable有)

      1
      @Cacheable(value = {"category"}, key = "#root.method.name", sync = true)

    总结:常规数据(读多写少,即时性、一致性要求不高的数据)完全可以使用Spring-Cache,特殊数据则需要特殊设计。