avatar

目录
高并发解决方案(1):扩容&缓存

[TOC]

第11章 扩容思路

为什么要扩容

每个线程都有自己的工作内存, 占用内存大小取决于工作内存里变量的多少与大小 , 单个线程占用内存通常不会很大

但是随着并发的线程不断的增加 , 从成百上千, 甚至几十万 , 占用的内存就会越来越多.这时候可能就要考虑给系统扩容了 , 简单点的 升级内存, 复杂点的 , 增加服务器 , 分担压力.

两种方式

  • 垂直扩容:提高系统部件能力。但会增大单个服务中其他软件设施的依赖与管理、服务内部复杂度
  • 水平扩容:增加更多系统成员。但会增加网络、数据库IO开销、管理多个服务器的难度

扩容-数据库

  • 读操作扩展:假如网站是读操作比较多,比如博客这类。通过通过关系型数据库进行垂直扩展是个不错的选择,并且结合memcathe、redis、CDN等构建一个健壮的缓存系统。如果系统超负荷运行,将更多的数据放在缓存中来缓解系统的读压力。采用水平扩容没有太大的意义,因为性能的瓶颈不在写操作,所以不需要实时去完成,用更多的服务器来分担压力性价比太低。所以针对单个系统去强化它的读性能就可以了
  • 写操作扩展:假如写操作比较多,比如大型网站的交易系统,可考虑可水平扩展的数据存储方式,比如Cassandra、Hbase等。和大多数的关系型数据库不同,这种数据存储会随着增长增加更多的节点。也可以考虑垂直扩容提升单个数据库的性能,但会发现资金与硬盘的IO能力是有限的,所以需要增加更多数据库来分担写的压力。

第12章 缓存思路

12-1 特征、场景及组件介绍

应用需要支撑大量并发量,但数据库的性能有限,所以使用缓存来减少数据库压力与提高访问性能。

![屏幕快照 2018-10-31 上午11.04.47](20181031100154042/屏幕快照 2018-10-31 上午11.04.47.png)

缓存的使用可以出现在1到4的各个环节中,每个环节的方案他们都各有特点。

特征

  • 命中率 = 命中数 / (命中数 + 没有命中数)

  • 最大空间:缓存最大空间一旦缓存中元素数量超过这个值(或者缓存数据所占空间超过其最大支持空间),那么将会触发缓存启动清空策略根据不同的场景合理的设置最大元素值往往可以一定程度上提高缓存的命中率,从而更有效的时候缓存。

  • 清空策略:FIFO/LFU/LRU/过期时间/随机

    FIFO:最先进入缓存的数据,在缓存空间不足时被清除,为了保证最新数据可用,保证实时性

    LFU(Least Frequently Used):最近最不常用,基于访问次数,去除命中次数最少的元素,保证高频数据有效性

    LRU(Least Recently Used):最近最少使用,基于访问时间,在被访问过的元素中去除最久未使用的元素,保证热点数据的有效性

影响缓存命中率的因素

  1. 业务场景和业务需求

    缓存通常适合读多写少的业务场景,反之的使用意义并不多,命中率会很低。业务需求也决定了实时性的要求,直接影响到过期时间和更新策略,实时性要求越低越适合缓存。

  2. 缓存的设计(策略和粒度)

    通常情况下缓存的粒度越小,命中率越高。比如说缓存一个用户信息的对象,只有当这个用户的信息发生变化的时候才更新缓存,而如果是缓存一个集合的话,集合中任何一个对象发生变化都要重新更新缓存。

    当数据发生变化时,直接更新缓存的值比移除缓存或者让缓存过期它的命中率更高,不过这个时候系统的复杂度过高。

  3. 缓存的容量和基础设施

    缓存的容量有限就会容易引起缓存的失效和被淘汰。目前多数的缓存框架和中间件都采用LRU这个算法。同时采用缓存的技术选型也是至关重要的,比如采用本地内置的应用缓存,就比较容易出现单机瓶颈。而采用分布式缓存就更加容易扩展。所以需要做好系统容量规划,系统是否可扩展。

缓存分类

根据缓存和应用的耦合度

  • 本地缓存:编程实现(成员变量、局部变量、静态变量)、Guava Cache

    本地缓存最大的优点在于它在应用进程的内部,请求缓存非常的快速,没有过多的网络开销。在单应用中不需要集群支持,集群的情况下各节点不需要互相通知的情况下使用本地缓存比较合适。

    缺点是因为本地缓存跟应用程序耦合,多个应用程序无法直接共享缓存,各应用节点都需要维护自己单独的缓存,有时也是对内存的一种浪费。

  • 分布式缓存:Memcache、Redis

    分布式缓存指的是应用分离的缓存服务,最大的优点就是自身就是一个独立的应用,与本地应用是隔离的,多个应用直接共享缓存。

根据缓存介质

虽然从硬件介质上来看,无非就是内存和硬盘两种,但从技术上,可以分成内存、硬盘文件、数据库。

  • 内存:将缓存存储于内存中是最快的选择,无需额外的I/O开销,但是内存的缺点是没有持久化落地物理磁盘,一旦应用异常break down而重新启动,数据很难或者无法复原。
  • 硬盘:一般来说,很多缓存框架会结合使用内存和硬盘,在内存分配空间满了或是在异常的情况下,可以被动或主动的将内存空间数据持久化到硬盘中,达到释放空间或备份数据的目的。
  • 数据库:前面有提到,增加缓存的策略的目的之一就是为了减少数据库的I/O压力。现在使用数据库做缓存介质是不是又回到了老问题上了?其实,数据库也有很多种类型,像那些不支持SQL,只是简单的key-value存储结构的特殊数据库(如BerkeleyDB和Redis),响应速度和吞吐量都远远高于我们常用的关系型数据库等。

缓存组件介绍

本地缓存Guava Cache

Guava Cache是Google开源的Java重用工具集库Guava里的一款缓存工具,它的设计灵感是CuncurentHashMap

![屏幕快照 2018-11-01 下午6.55.09](20181031100154042/屏幕快照 2018-11-01 下午6.55.09.png)

Guava Cache继承了ConcurrentHashMap的思路,使用多个segments方式的细粒度锁,在保证线程安全的同时,支持高并发场景需求。Cache类似于Map,它是存储键值对的集合,不同的是它还需要处理evict、expire、dynamic load等算法逻辑,需要一些额外信息来实现这些操作。对此,根据面向对象思想,需要做方法与数据的关联封装.

其主要实现的缓存功能有:

  • 自动将entry节点加载进缓存结构中;
  • 当缓存的数据超过设置的最大值时,使用LRU算法移除;
  • 具备根据entry节点上次被访问或者写入时间计算它的过期机制;
  • 缓存的key被封装在WeakReference引用内;
  • 缓存的Value被封装在WeakReference或SoftReference引用内;
  • 统计缓存使用过程中命中率、异常率、未命中率等统计数据。
java
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package com.machine.concurrency.example.cache;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

@Slf4j
public class GuavaCacheExample1 {

public static void main(String[] args) {

LoadingCache<String, Integer> cache = CacheBuilder.newBuilder()
.maximumSize(10) // 最多存放10个数据
.expireAfterWrite(10, TimeUnit.SECONDS) // 缓存10秒
.recordStats() // 开启记录状态数据功能
.build(new CacheLoader<String, Integer>() {
@Override
public Integer load(String key) throws Exception {
return -1;
}
});

log.info("{}", cache.getIfPresent("key1")); // null
cache.put("key1", 1);
log.info("{}", cache.getIfPresent("key1")); // 1
cache.invalidate("key1");
log.info("{}", cache.getIfPresent("key1")); // null

try {
log.info("{}", cache.get("key2")); // -1
cache.put("key2", 2);
log.info("{}", cache.get("key2")); // 2

log.info("{}", cache.size()); // 1

for (int i = 3; i < 13; i++) {
cache.put("key" + i, i);
}
log.info("{}", cache.size()); // 10

log.info("{}", cache.getIfPresent("key2")); // null

Thread.sleep(11000);

log.info("{}", cache.get("key5")); // -1

log.info("{},{}", cache.stats().hitCount(), cache.stats().missCount());

log.info("{},{}", cache.stats().hitRate(), cache.stats().missRate());
} catch (Exception e) {
log.error("cache exception", e);
}
}
}

Memcache

memcached是应用较广的开源分布式缓存产品之一,它本身其实不提供分布式解决方案。在服务端,memcached集群环境实际就是一个个memcached服务器的堆积,环境搭建较为简单;cache的分布式主要是在客户端实现,通过客户端的路由处理来达到分布式解决方案的目的。客户端做路由的原理非常简单,应用服务器在每次存取某key的value时,通过某种算法把key映射到某台memcached服务器nodeA上,因此这个key所有操作都在nodeA上

左: memcached客户端路由图

右:memcached一致性hash示例图

![屏幕快照 2018-11-01 下午7.03.24](20181031100154042/屏幕快照 2018-11-01 下午7.03.24.png)

memcache客户端采用一致性hash算法作为路由策略,相对于一般hash(如简单取模)的算法,一致性hash算法除了计算key的hash值外,还会计算每个server对应的hash值,然后将这些hash值映射到一个有限的值域上(比如0~2^32)。通过寻找hash值大于hash(key)的最小server作为存储该key数据的目标server。如果找不到,则直接把具有最小hash值的server作为目标server。同时,一定程度上,解决了扩容问题,增加或删除单个节点,对于整个集群来说,不会有大的影响。最近版本,增加了虚拟节点的设计,进一步提升了可用性。

一致性哈希算法原理

memcache内存管理机制

![屏幕快照 2018-11-01 下午7.09.51](20181031100154042/屏幕快照 2018-11-01 下午7.09.51.png)

  • memcached是一个高效的分布式内存cache,了解memcached的内存管理机制,才能更好的掌握memcached,让我们可以针对我们数据特点进行调优,让其更好的为我所用。我们知道memcached仅支持基础的key-value键值对类型数据存储。在memcached内存结构中有两个非常重要的概念:slab和chunk。
  • 每个page的默认大小是1M,trunk是真正存放数据的地方,memcache会根据value值的大小找到接近大小的slab

Redis

Redis是一个远程内存数据库(非关系型数据库),性能强劲,具有复制特性以及解决问题而生的独一无二的数据模型。它可以存储键值对与5种不同类型的值之间的映射,可以将存储在内存的键值对数据持久化到硬盘,可以使用复制特性来扩展读性能,还可以使用客户端分片来扩展写性能。

![屏幕快照 2018-11-01 下午7.46.18](20181031100154042/屏幕快照 2018-11-01 下午7.46.18.png)

Redis具备以下特点:

  • 异常快速: Redis数据库完全在内存中,因此处理速度非常快,每秒能执行约11万集合,每秒约81000+条记录。
  • 数据持久化: redis支持数据持久化,可以将内存中的数据存储到磁盘上,方便在宕机等突发情况下快速恢复。
  • 支持丰富的数据类型: 相比许多其他的键值对存储数据库,Redis拥有一套较为丰富的数据类型。
  • 数据一致性: 所有Redis操作是原子的,这保证了如果两个客户端同时访问的Redis服务器将获得更新后的值。
  • 多功能实用工具: Redis是一个多实用的工具,可以在多个用例如缓存,消息,队列使用(Redis原生支持发布/订阅),任何短暂的数据,应用程序,如 Web应用程序会话,网页命中计数等。

适用场景:

  1. 取最新N个数据的操作

  2. 排行榜类似的应用

  3. 精准设定过期时间的应用

  4. 计数器的应用

  5. 唯一性检查

  6. 实时系统,队列系统,最基础的缓存功能

12-2 redis的使用

参考我的文章:

redis学习笔记 bootdo已实现(1)

12-3 高并发场景问题及实战

高并发场景常见问题

  • 缓存一致性
  • 缓存并发问题
  • 缓存穿透问题
  • 缓存的雪崩现象

1、缓存一致性问题

当数据时效性要求很高时,需要保证缓存中的数据与数据库中的保持一致,而且需要保证缓存节点和副本中的数据也保持一致,不能出现差异现象。这就比较依赖缓存的过期和更新策略。一般会在数据发生更改的时,主动更新缓存中的数据或者移除对应的缓存。

包含4种情况:

![屏幕快照 2018-11-01 下午10.10.06](20181031100154042/屏幕快照 2018-11-01 下午10.10.06.png)

2、缓存并发问题

缓存过期后将尝试从后端数据库获取数据,这是一个看似合理的流程。但是,在高并发场景下,有可能多个请求并发的去从数据库获取数据,对后端数据库造成极大的冲击,甚至导致 “雪崩”现象。此外,当某个缓存key在被更新时,同时也可能被大量请求在获取,这也会导致一致性的问题。那如何避免类似问题呢?我们会想到类似“锁”的机制,在缓存更新或者过期的情况下,先尝试获取到锁,当更新或者从数据库获取完成后再释放锁,其他的请求只需要牺牲一定的等待时间,即可直接从缓存中继续获取数据。

![屏幕快照 2018-11-01 下午10.13.51](20181031100154042/屏幕快照 2018-11-01 下午10.13.51.png)

3、缓存穿透问题

真正的缓存穿透应该是这样的:

在高并发场景下,如果某一个key被高并发访问,没有被命中,出于对容错性考虑,会尝试去从后端数据库中获取,从而导致了大量请求达到数据库,而当该key对应的数据本身就是空的情况下(查询null时没有缓存),这就导致数据库中并发的去执行了很多不必要的查询操作,从而导致巨大冲击和压力。

![屏幕快照 2018-11-01 下午10.19.34](20181031100154042/屏幕快照 2018-11-01 下午10.19.34.png)

可以通过下面的几种常用方式来避免缓存传统问题

  • 缓存空对象

    对查询结果为空的对象也进行缓存,如果是集合,可以缓存一个空的集合(非null),如果是缓存单个对象,可以通过字段标识来区分。这样避免请求穿透到后端数据库。同时,也需要保证缓存数据的时效性。这种方式实现起来成本较低,比较适合命中不高,但可能被频繁更新的数据。

  • 单独过滤处理

    对所有可能对应数据为空的key进行统一的存放,并在请求前做拦截,这样避免请求穿透到后端数据库。这种方式实现起来相对复杂,比较适合命中不高,但是更新不频繁的数据。

4、缓存颠簸问题

缓存的颠簸问题,有些地方可能被成为“缓存抖动”,可以看做是一种比“雪崩”更轻微的故障,但是也会在一段时间内对系统造成冲击和性能影响。一般是由于缓存节点故障导致。业内推荐的做法是通过一致性Hash算法来解决。这里不做过多阐述。

5、缓存的雪崩现象

缓存雪崩就是指由于缓存的原因,导致大量请求到达后端数据库,从而导致数据库崩溃,整个系统崩溃,发生灾难。导致这种现象的原因有很多种,上面提到的“缓存并发”,“缓存穿透”,“缓存颠簸”等问题,其实都可能会导致缓存雪崩现象发生。这些问题也可能会被恶意攻击者所利用。还有一种情况,例如某个时间点内,系统预加载的缓存周期性集中失效了,也可能会导致雪崩。为了避免这种周期性失效,可以通过设置不同的过期时间,来错开缓存过期,从而避免缓存集中失效。

![屏幕快照 2018-11-01 下午10.30.16](20181031100154042/屏幕快照 2018-11-01 下午10.30.16.png)

从应用架构角度,我们可以通过限流、降级、熔断等手段来降低影响,也可以通过多级缓存来避免这种灾难。

此外,从整个研发体系流程的角度,应该加强压力测试,尽量模拟真实场景,尽早的暴露问题从而防范。

高并发缓存应用案例-股票分时线

![屏幕快照 2018-11-01 下午10.37.55](20181031100154042/屏幕快照 2018-11-01 下午10.37.55.png)

  • 利用guava cache缓存最近几分钟内所有股票的分时数据,key是时间点(单位到分钟),一分钟内有多次变动时使用最后的数据覆盖,保证每只股票每分钟只缓存一条数据
  • 启动一个定时任务,每分钟将最近几分钟的数据都写到redis里,保证redis里的数据一直是最新的。保存使用的是redis hash结构,key是时间点。用户访问分时线时只需从redis取出当前股票缓存数据。

参考:Redis在股票分时K线图计算的实践

文章作者: Machine
文章链接: https://machine4869.gitee.io/2018/10/31/20181031100154042/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 哑舍
打赏
  • 微信
    微信
  • 支付宝
    支付宝

评论