数据库虚引用堆积问题

线上出现FGC占用时间过长的告警,查看GC日志确认FGC达到3.84s

  1. 问题描述

通过mat分析dump下来的日志发现,com.mysql.cj.jdbc.AbandonedConnectionCleanupThread 对象占了堆内存的大部分空间.

image2021-2-20 15_42_52

查看对象是com.mysql.cj.jdbc.AbandonedConnectionCleanupThread$ConnectionFinalizerPhantomReference(mysql的connection清理的虚引用)对象堆积达到2435个,初步判断长时间gc的问题是由于ConnectionFinalizerPhantomReference堆积引起的,该应用采用hikari的datasource.

image2021-2-20 16_3_51

同样选了一台采用druid的datasource的应用,也发现同样问题:

image2021-2-20 15_57_54

image2021-2-20 15_57_54

其中com.mysql.cj.jdbc.NonRegisteringDriver堆积对象达到1188个image2021-2-20 16_6_38

2. 问题分析

基本可以确定是mysql driver连接弱引用的问题,两个问题可以合并为同一个问题,其中mysql清理源代码这里略过不表,有兴趣可以去查看NonRegisteringDriver中connectionPhantomRefs的添加和清理逻辑。

这里结合项目中hikaricp数据配置和官方文档结合说明,Druid同理。

查阅hikaricp数据池的官网地址,看看部分属性介绍如下:

maximumPoolSize

This property controls the maximum size that the pool is allowed to reach, including both idle and in-use connections. Basically this value will determine the maximum number of actual connections to the database backend. A reasonable value for this is best determined by your execution environment. When the pool reaches this size, and no idle connections are available, calls to getConnection() will block for up to connectionTimeout milliseconds before timing out. Please read about pool sizing. Default: 10

maximumPoolSize控制最大连接数,默认为10

minimumIdle

This property controls the minimum number of idle connections that HikariCP tries to maintain in the pool. If the idle connections dip below this value and total connections in the pool are less than maximumPoolSize, HikariCP will make a best effort to add additional connections quickly and efficiently. However, for maximum performance and responsiveness to spike demands, we recommend not setting this value and instead allowing HikariCP to act as a fixed size connection pool. Default: same as maximumPoolSize

minimumIdle控制最小连接数,默认等同于maximumPoolSize,10。

⌚idleTimeout

This property controls the maximum amount of time that a connection is allowed to sit idle in the pool. This setting only applies when minimumIdle is defined to be less than maximumPoolSize. Idle connections will not be retired once the pool reaches minimumIdle connections. Whether a connection is retired as idle or not is subject to a maximum variation of +30 seconds, and average variation of +15 seconds. A connection will never be retired as idle before this timeout. A value of 0 means that idle connections are never removed from the pool. The minimum allowed value is 10000ms (10 seconds). Default: 600000 (10 minutes)

连接空闲时间超过idleTimeout(默认10分钟)后,连接会被抛弃

⌚maxLifetime

This property controls the maximum lifetime of a connection in the pool. An in-use connection will never be retired, only when it is closed will it then be removed. On a connection-by-connection basis, minor negative attenuation is applied to avoid mass-extinction in the pool. We strongly recommend setting this value, and it should be several seconds shorter than any database or infrastructure imposed connection time limit. A value of 0 indicates no maximum lifetime (infinite lifetime), subject of course to the idleTimeout setting. Default: 1800000 (30 minutes)

连接生存时间超过 maxLifetime(默认30分钟)后,连接会被抛弃.

回头看看项目的hikari配置

1619691072

  • 配置了minimumIdle = 10,maximumPoolSize = 10,没有配置idleTimeout和maxLifetime。所以这两项会使用默认值 idleTimeout = 10分钟,maxLifetime = 30分钟。

  • 假如数据库连接池已满,有10个连接,假如系统空闲(没有连接会在10分钟后(超过idleTimeout)被废弃);假如系统一直繁忙,10个连接会在30分钟后(超过maxLifetime)后被废弃。

  • 猜测问题产生的根源:

    每次新建一个数据库连接,都会把连接放入connectionPhantomRefs集合中。数据连接在空闲时间超过idleTimeout或生存时间超过maxLifetime后会被废弃,在connectionPhantomRefs集合中等待回收。由于连接资源一般存活周期长,经过多次Young GC,一般都能存活到老年代。如果这个数据库连接对象晋升到老年代,connectionPhantomRefs中的元素就会一直堆积,直到下次 full gc。如果等到full gc 的时候connectionPhantomRefs集合的元素非常多,那么该次full gc就会非常耗时。

3. 问题验证

  1. 线上模拟环境(待测试)

    为了验证问题,可以模拟线上环境,调整maxLifetime等参数~压测思路如下

  • 1.缓存系统模拟线上的配置,使用压测系统一段时间内持续压缓存系统,使缓存系统短时间创建/废弃大量数据库连接,观察 connectionPhantomRefs对象是否如期大量堆积,手动触发FGC,观察 connectionPhantomRefs对象是否被清理。
  • 2.调整maxLifetime 参数,观察相同的压测时间内 connectionPhantomRefs对象是否还发生堆积
  1. 不过可以结合GC日志分析:

    再结合我们生产的问题,假设我们每天14个小时高峰期(12:00 ~ 凌晨2:00),期间连接数10,10个小时低峰期,期间连接数10,每次 full gc 间隔4天,等到下次 full gc 堆积的 NonRegisteringDriver 对象为 10 24 2 * 4 = 1920,与问题dump里面connectionPhantomRefs对象的数量2435个基本吻合。

4. 问题解决方案

由上面分析可知,问题产生的根源是废弃的数据库连接对象堆积,最终导致 full gc 时间过长。可以从以下几个方面入手解决方案:

  • 1、减少废弃的数据连接对象的产生和堆积。
  • 2、优化full gc时间.

【调整hikari参数】

可以考虑设置 maxLifetime 为一个较大的值,用于延长连接的生命周期,减少产生被废弃的数据库连接的频率,等到下次 full gc 的时候需要清理的数据库连接对象会大大减少。

Hikari 推荐 maxLifetime 设置为比数据库的 wait_timeout 时间少 30s 到 1min。如果使用的是 mysql 数据库,可以使用 show global variables like ‘%timeout%’; 查看 wait_timeout,腾讯云默认为 1 小时。

  1. 下面开始验证,设置maxLifetime = 55分钟,其他条件不变。压测启动前观察jvisualvm,connectionPhantomRefs对象数量
  2. 1000s后,观察 connectionPhantomRefs对象

看看connectionPhantomRefs对象没有发生堆积

同时另外注意:minimumIdle和maximumPoolSize不要设置得太大,一般来说配置minimumIdle=10,maximumPoolSize=10~20即可。

我们这次问题产生的根源是数据库连接对象堆积,导致full gc时间过长。解决思路可以从以下三点入手:

  • 1、调整hikari配置参数。例如把maxLifetime设置为较大的值(比数据库的wait_timeout少30s),minimumIdle和maximumPoolSize值不能设置太大,或者直接采用默认值即可。
  • 2、采用G1垃圾回收器?(G1可以自定义STW时间,但R大推荐8g为分界线,8g以下CMS比较推荐)。
  • 3、建立巡查系统,在业务低峰期主动触发full gc。
分享到

ConcurrentHashMap源码解析

ConcurrentHashMap为Java中常用的并发容器,用于解决HashTable锁住整个hash表的问题,JDK8针对ConcurrentHashMap作了改进和优化,摒弃JDK7的分段锁机制,采用Node + CAS + synchronized保证并发安全。

源码分析

  • 类视图

    类视图

  • 类注释

  • 类常量

  • 插入

总结

分享到

HashMap源码解析

HashMap是Java中极其频繁的、非常重要的一个集合类,在JDK8改动也比较大,本文主要基于JDK8下HashMap的实现

源码分析

  • 类视图

    类视图

  • 类注释

    1. 允许NULL值,NULL
    2. 不要轻易改变负载因子,负载因子过高会导致链表过长,查找键值对时间复杂度就会增高,负载因子过低会导致hash桶的 数量过多,空间复杂度会增高
    3. Hash表每次会扩容长度为以前的2倍
    4. HashMap是多线程不安全的,在JDK1.7进行多线程put操作,之后遍历,直接死循环,CPU飙到100%,在JDK 1.8中进行多线程操作会出现节点和value值丢失,为什么JDK1.7JDK1.8多线程操作会出现很大不同,是因为JDK 1.8的作者对resize方法进行了优化不会产生链表闭环。这也是本章的重点之一,具体的细节大家可以去查阅资料。这里就不解释太多了
  • 类常量

    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
      /**
    * The default initial capacity - MUST be a power of two.
    */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    /**
    * The load factor used when none specified in constructor.
    */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    /**
    * The bin count threshold for using a tree rather than list for a
    * bin. Bins are converted to trees when adding an element to a
    * bin with at least this many nodes. The value must be greater
    * than 2 and should be at least 8 to mesh with assumptions in
    * tree removal about conversion back to plain bins upon
    * shrinkage.
    */
    static final int TREEIFY_THRESHOLD = 8;
    /**
    * The smallest table capacity for which bins may be treeified.
    * (Otherwise the table is resized if too many nodes in a bin.)
    * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
    * between resizing and treeification thresholds.
    */
    static final int MIN_TREEIFY_CAPACITY = 64;
    /**
    * The maximum capacity, used if a higher value is implicitly specified
    * by either of the constructors with arguments.
    * MUST be a power of two <= 1<<30.
    */
    static final int MAXIMUM_CAPACITY = 1 << 30;
  • 构造函数

    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
    public HashMap(int initialCapacity, float loadFactor) {                                                                   
    if (initialCapacity < 0)
    throw new IllegalArgumentException("Illegal initial capacity: " +
    initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
    initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
    throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
    this.loadFactor = loadFactor;
    //下面介绍一下这行代码的作用
    this.threshold = tableSizeFor(initialCapacity);
    }

    public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

    public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
    }

    HashMap有4个构造函数.

    重点介绍下tableSizeFor(initialCapacity)方法,该方法作用,将你传入的initialCapacity进行计算,返回一个大于等于initialCapacity最小的2的幂次方,比如输入6,结算结果为8,源码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    static final int tableSizeFor(int cap) {                                                                      
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }
  • 插入源码

    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
    public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
    }

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
    boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //当table为空时,这里初始化table,不是通过构造函数初始化,而是在插入时通过扩容初始化,有效防止了初始化HashMap没有数据插入造成空间浪费可能造成内存泄露的情况
    if ((tab = table) == null || (n = tab.length) == 0)
    n = (tab = resize()).length;
    //存放新键值对
    if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);
    else {
    Node<K,V> e; K k;
    //旧键值对的覆盖
    if (p.hash == hash &&
    ((k = p.key) == key || (key != null && key.equals(k))))
    e = p;
    //在红黑树中查找旧键值对更新
    else if (p instanceof TreeNode)
    e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
    else {
    //将新键值对放在链表的最后
    for (int binCount = 0; ; ++binCount) {
    if ((e = p.next) == null) {
    p.next = newNode(hash, key, value, null);
    //当链表的长度大于等于树化阀值,并且hash桶的长度大于等于MIN_TREEIFY_CAPACITY,链表转化为红黑树
    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
    treeifyBin(tab, hash);
    break;
    }
    //链表中包含键值对
    if (e.hash == hash &&
    ((k = e.key) == key || (key != null && key.equals(k))))
    break;
    p = e;
    }
    }
    //map中含有旧key,返回旧值
    if (e != null) {
    V oldValue = e.value;
    if (!onlyIfAbsent || oldValue == null)
    e.value = value;
    afterNodeAccess(e);
    return oldValue;
    }
    }
    //map调整次数加1
    ++modCount;
    //键值对的数量达到阈值需要扩容
    if (++size > threshold)
    resize();
    afterNodeInsertion(evict);
    return null;
    }

    上述代码总结如下:

    1. 首次插入进行hash表的初始化操作,扩容初始化,插入键值对
    2. 插入的键值对中key已经存在,更新键值对
    3. 插入链表,如果链表长度大于MIN_TREEIFY_CAPACITY(默认值为8),转化为红黑树,否则直接插入
    4. 检查是否需要扩容,当键值对的个数大于threshold阈值进行扩容操作,其中threshold=size*loadFactor
  • 扩容

    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
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    //如果旧hash桶不为空
    if (oldCap > 0) {
    //超过hash桶的最大长度,将阀值设为最大值
    if (oldCap >= MAXIMUM_CAPACITY) {
    threshold = Integer.MAX_VALUE;
    return oldTab;
    }
    //新的hash桶的长度2被扩容没有超过最大长度,将新容量阀值扩容为以前的2倍
    else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
    oldCap >= DEFAULT_INITIAL_CAPACITY)
    newThr = oldThr << 1; // double threshold
    }
    //如果hash表阈值已经初始化过
    else if (oldThr > 0) // initial capacity was placed in threshold
    newCap = oldThr;
    //如果旧hash桶,并且hash桶容量阈值没有初始化,那么需要初始化新的hash桶的容量和新容量阀值
    else {
    newCap = DEFAULT_INITIAL_CAPACITY;
    newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    //新的局部变量阀值赋值
    if (newThr == 0) {
    float ft = (float)newCap * loadFactor;
    newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
    (int)ft : Integer.MAX_VALUE);
    }
    //为当前容量阀值赋值
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    //初始化hash桶
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    //如果旧的hash桶不为空,需要将旧的hash表里的键值对重新映射到新的hash桶中
    if (oldTab != null) {
    for (int j = 0; j < oldCap; ++j) {
    Node<K,V> e;
    if ((e = oldTab[j]) != null) {
    oldTab[j] = null;
    //只有一个节点,通过索引位置直接映射
    if (e.next == null)
    newTab[e.hash & (newCap - 1)] = e;
    //如果是红黑树,需要进行树拆分然后映射
    else if (e instanceof TreeNode)
    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
    else {
    //如果是多个节点的链表,将原链表拆分为两个链表,两个链表的索引位置,一个为原索引,一个为原索引加上旧Hash桶长度的偏移量
    Node<K,V> loHead = null, loTail = null;
    Node<K,V> hiHead = null, hiTail = null;
    Node<K,V> next;
    do {
    next = e.next;
    //链表1
    if ((e.hash & oldCap) == 0) {
    if (loTail == null)
    loHead = e;
    else
    loTail.next = e;
    loTail = e;
    }
    //链表2
    else {
    if (hiTail == null)
    hiHead = e;
    else
    hiTail.next = e;
    hiTail = e;
    }
    } while ((e = next) != null);
    //链表1存于原索引
    if (loTail != null) {
    loTail.next = null;
    newTab[j] = loHead;
    }
    //链表2存于原索引加上原hash桶长度的偏移量
    if (hiTail != null) {
    hiTail.next = null;
    newTab[j + oldCap] = hiHead;
    }
    }
    }
    }
    }
    return newTab;
    }

    如下情况会产生扩容操作:

    1. 初始化HashMap,第一次进行put操作
    2. 当键值对的个数大于threshold阀值时产生扩容,threshold=size*loadFactor

    源码中关于红黑树的操作、旋转、着色,这里不做介绍,有兴趣可以另行查看

总结

  1. HashMap允许NULL值,NULL
  2. 不要轻易改变负载因子,负载因子过高会导致链表过长,查找键值对时间复杂度就会增高,负载因子过低会导致hash桶的数量过多,空间复杂度会增高
  3. Hash表每次会扩容长度为以前的2倍
  4. HashMap是多线程不安全的,我在JDK 1.7进行多线程put操作,之后遍历,直接死循环,CPU飙到100%,在JDK 1.8
    进行多线程操作会出现节点和value值丢失,为什么JDK1.7JDK1.8多线程操作会出现很大不同,是因为JDK 1.8的作者对resize
    方法进行了优化不会产生链表闭环。这也是本章的重点之一,具体的细节大家可以去查阅资料。这里就不解释太多了
  5. 尽量设置HashMap的初始容量,尤其在数据量大的时候,防止多次resize
  6. HashMapJDK 1.8在做了很好性能的提升,我看到过在JDK1.7JDK1.8 get操作性能对比JDK1.8是要优于JDK 1.7的,大家感兴趣的可以自己做个测试。
分享到

分片和一致性

最近整理总结了分布式常用数据结构和算法,特此记录下常用的分布式分片算法和一致性问题。

数据分片与路由

哈希分片(Hash Partition)

一种数据分片和路由的通用模型,可以将其看作是一个二级映射关系。第一级映射是key-partition映射,其将数据记录映射到数据分片空间,这往往是多对一的映射关系,即一个数据分片包含多条记录数据;第二级映射是partition-machine映射,其将数据分片映射到物理机中,这一般也是多对一映射关系,即一台物理机容纳多个数据分片。

  • Round Robin
    • 哈希取模法。H(key) = hash(key) mod K,如果物理机增加1台,则数据和物理机之间的映射关系全被打乱。
    • 该方法缺乏扩展灵活性,原因是该方法将物理机和数据分片两个功能点合二为一,即每台物理机对应一个数据分片,这样key-paitition映射和partition-machine映射也就两位一体。
  • 虚拟桶(Virtual Buckets)
    • 所有记录先通过哈希函数映射到对应的虚拟桶,记录和虚拟桶是多对一的映射关系,即一个虚拟桶包含多条记录信息;第二层映射是虚拟桶和物理机之间的映射关系,同样也是多对一映射,一个物理机可以容纳多个虚拟桶,其具体实现是通过查表实现。
    • 当加入新机器,将某些虚拟桶从原来分配的机器重新分配给新机器,只需修改partition-machine映射表中受影响的个别条目就能实现扩展。
  • 一致性哈希(Consistent Hashing)
    • 将哈希数值空间按照大小组成一个首尾相接的环状序列。对于每台机器,可以根据其ip和端口号经过哈希函数映射到哈希数值空间内,这样不同的机器就成了环状序列中的不同节点,而这台机器则负责存储落在一段有序哈希空间内的数据。
    • 路由问题:沿着有向环顺序查找,效率低;为加快查找速度,可以在每个机器节点配置路由表。

数据一致性

1. 基本原则

  • CAP
    • 强一致性(Consisitency):分布式系统中同一数据多副本情形下,对于数据的更新操作体现出的效果与只有单份数据是一样的
    • 可用性(Availability):客户端在任何时刻对大规模数据系统的读/写操作都应该保证在限定延时内完成
    • 分区容忍性(Partition Tolerance):分区间的机器无法进行网络通信的情况
  • BASE原则
    • 基本可用(Basically Available):大多数情况下系统可用,允许偶尔的失败
    • 软状态或柔性状态(Soft State):指数据状态不要求在任何时刻都完全保持同步
    • 最终一致性(Eventual Consistency):一种弱一致性,不要求任意时刻数据保持一致同步,但是要求在给定时间窗口内数据会达到一致状态

2. 副本更新策略

  • 同时更新
    • 多副本同时更新
  • 主从式更新
    • 对数据的更新操作首先提交到主副本,再由主副本通知从副本更新
  • 任意节点更新
    • 数据更新请求可能发给多副本中任意一个节点,再由这个节点来负责通知其他副本进行更新

3. 一致性协议

  • 两阶段提交(Tow-Phrase Commit, 2PC) - 表决阶段 - 提交阶段 - 如果协调者崩溃,则参与者会存在长时间阻塞的可能
  • 三阶段提交 - 将2PC的提交阶段再次分为两个阶段:预提交阶段和提交阶段,用于解决2PC长时间阻塞的问题。 - 实际使用很少,一方面是2PC发生阻塞情况很少;另一方面是3PC效率过低。
  • PaxosRaft一致性协议。

参考链接

分享到

缓存一致性

简介

本文转自:http://coolshell.cn/articles/17416.html 。文章是耗子大神关于缓存更新策略的总结,值得大家学习讨论。

背景

看到好些人在写更新缓存数据代码时,先删除缓存,然后再更新数据库,而后续的操作会把数据再装载的缓存中。然而,这个是逻辑是错误的。试想,两个并发操作,一个是更新操作,另一个是查询操作,更新操作删除缓存后,查询操作没有命中缓存,先把老数据读出来后放到缓存中,然后更新操作更新了数据库。于是,在缓存中的数据还是老的数据,导致缓存中的数据是脏的,而且还一直这样脏下去了。

我不知道为什么这么多人用的都是这个逻辑,当我在微博上发了这个贴以后,我发现好些人给了好多非常复杂和诡异的方案,所以,我想写这篇文章说一下几个缓存更新的Design Pattern(让我们多一些套路吧)。

这里,我们先不讨论更新缓存和更新数据这两个事是一个事务的事,或是会有失败的可能,我们先假设更新数据库和更新缓存都可以成功的情况(我们先把成功的代码逻辑先写对)。

更新缓存的的Design Pattern有四种:Cache aside, Read through, Write through, Write behind caching,我们下面一一来看一下这四种Pattern。

Cache Aside Pattern

这是最常用最常用的pattern了。其具体逻辑如下:

  • 失效:应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。
  • 命中:应用程序从cache中取数据,取到后返回。
  • 更新:先把数据存到数据库中,成功后,再让缓存失效。

Cache-Aside-Design-Pattern-Flow-Diagram-e1470471723210

Updating-Data-using-the-Cache-Aside-Pattern-Flow-Diagram-1-e1470471761402

一个是查询操作,一个是更新操作的并发,首先,没有了删除cache数据的操作了,而是先更新了数据库中的数据,此时,缓存依然有效,所以,并发的查询操作拿的是没有更新的数据,但是,更新操作马上让缓存的失效了,后续的查询操作再把数据从数据库中拉出来。而不会像文章开头的那个逻辑产生的问题,后续的查询操作一直都在取老的数据。

这是标准的design pattern,包括Facebook的论文《Scaling Memcache at Facebook》也使用了这个策略。为什么不是写完数据库后更新缓存?你可以看一下Quora上的这个问答《Why does Facebook use delete to remove the key-value pair in Memcached instead of updating the Memcached during write request to the backend?》,主要是怕两个并发的写操作导致脏数据。

那么,是不是Cache Aside这个就不会有并发问题了?不是的,比如,一个是读操作,但是没有命中缓存,然后就到数据库中取数据,此时来了一个写操作,写完数据库后,让缓存失效,然后,之前的那个读操作再把老的数据放进去,所以,会造成脏数据。

但,这个case理论上会出现,不过,实际上出现的概率可能非常低,因为这个条件需要发生在读缓存时缓存失效,而且并发着有一个写操作。而实际上数据库的写操作会比读操作慢得多,而且还要锁表,而读操作必需在写操作前进入数据库操作,而又要晚于写操作更新缓存,所有的这些条件都具备的概率基本并不大。

所以,这也就是Quora上的那个答案里说的,要么通过2PC或是Paxos协议保证一致性,要么就是拼命的降低并发时脏数据的概率,而Facebook使用了这个降低概率的玩法,因为2PC太慢,而Paxos太复杂。当然,最好还是为缓存设置上过期时间。

Read/Write Through Pattern

我们可以看到,在上面的Cache Aside套路中,我们的应用代码需要维护两个数据存储,一个是缓存(Cache),一个是数据库(Repository)。所以,应用程序比较啰嗦。而Read/Write Through套路是把更新数据库(Repository)的操作由缓存自己代理了,所以,对于应用层来说,就简单很多了。可以理解为,应用认为后端就是一个单一的存储,而存储自己维护自己的Cache

  • Read Through

Read Through 套路就是在查询操作中更新缓存,也就是说,当缓存失效的时候(过期或LRU换出),Cache Aside是由调用方负责把数据加载入缓存,而Read Through则用缓存服务自己来加载,从而对应用方是透明的。

  • Write Through

Write Through 套路和Read Through相仿,不过是在更新数据时发生。当有数据更新的时候,如果没有命中缓存,直接更新数据库,然后返回。如果命中了缓存,则更新缓存,然后再由Cache自己更新数据库(这是一个同步操作)

下图自来Wikipedia的Cache)词条。其中的Memory你可以理解为就是我们例子里的数据库。

460px-Write-through_with_no-write-allocation.svg_

Write Behind Caching Pattern

Write Behind 又叫 Write Back。一些了解Linux操作系统内核的同学对write back应该非常熟悉,这不就是Linux文件系统的Page Cache的算法吗?是的,你看基础这玩意全都是相通的。 所以,基础很重要,我已经不是一次说过基础很重要这事了。

Write Back套路,一句说就是,在更新数据的时候,只更新缓存,不更新数据库,而我们的缓存会异步地批量更新数据库。这个设计的好处就是让数据的I/O操作飞快无比(因为直接操作内存嘛 ),因为异步,write backg还可以合并对同一个数据的多次操作,所以性能的提高是相当可观的。

Write Back套路,一句说就是,在更新数据的时候,只更新缓存,不更新数据库,而我们的缓存会异步地批量更新数据库。这个设计的好处就是让数据的I/O操作飞快无比(因为直接操作内存嘛 ),因为异步,write backg还可以合并对同一个数据的多次操作,所以性能的提高是相当可观的。

但是,其带来的问题是,数据不是强一致性的,而且可能会丢失(我们知道Unix/Linux非正常关机会导致数据丢失,就是因为这个事)。在软件设计上,我们基本上不可能做出一个没有缺陷的设计,就像算法设计中的时间换空间,空间换时间一个道理,有时候,强一致性和高性能,高可用和高性能是有冲突的。软件设计从来都是取舍Trade-Off。

另外,Write Back实现逻辑比较复杂,因为他需要track有哪数据是被更新了的,需要刷到持久层上。操作系统的write back会在仅当这个cache需要失效的时候,才会被真正持久起来,比如,内存不够了,或是进程退出了等情况,这又叫lazy write。

在wikipedia上有一张write back的流程图,基本逻辑如下:

Write-back_with_write-allocation

再多唠叨一些

1)上面讲的这些Design Pattern,其实并不是软件架构里的mysql数据库和memcache/redis的更新策略,这些东西都是计算机体系结构里的设计,比如CPU的缓存,硬盘文件系统中的缓存,硬盘上的缓存,数据库中的缓存。基本上来说,这些缓存更新的设计模式都是非常老古董的,而且历经长时间考验的策略,所以这也就是,工程学上所谓的Best Practice,遵从就好了。

2)有时候,我们觉得能做宏观的系统架构的人一定是很有经验的,其实,宏观系统架构中的很多设计都来源于这些微观的东西。比如,云计算中的很多虚拟化技术的原理,和传统的虚拟内存不是很像么?Unix下的那些I/O模型,也放大到了架构里的同步异步的模型,还有Unix发明的管道不就是数据流式计算架构吗?TCP的好些设计也用在不同系统间的通讯中,仔细看看这些微观层面,你会发现有很多设计都非常精妙……所以,请允许我在这里放句观点鲜明的话——如果你要做好架构,首先你得把计算机体系结构以及很多老古董的基础技术吃透了

3)在软件开发或设计中,我非常建议在之前先去参考一下已有的设计和思路,看看相应的guideline,best practice或design pattern,吃透了已有的这些东西,再决定是否要重新发明轮子。千万不要似是而非地,想当然的做软件设计。

4)上面,我们没有考虑缓存(Cache)和持久层(Repository)的整体事务的问题。比如,更新Cache成功,更新数据库失败了怎么吗?或是反过来。关于这个事,如果你需要强一致性,你需要使用“两阶段提交协议”——prepare, commit/rollback,比如Java 7 的XAResource,还有MySQL 5.7的 XA Transaction,有些cache也支持XA,比如EhCache。当然,XA这样的强一致性的玩法会导致性能下降,关于分布式的事务的相关话题,你可以看看《分布式系统的事务处理》一文。

(全文完)

参考链接

分享到

ssr开机启动

本文主要介绍privoxy和ssr的安装和开机启动,运行系统为centos7.6,python版本为2.7.x;至于为何使用ssr,而不使用ss,主要得益于ssr具有的加密和混淆特性,其中的爱恨纠葛就不赘述,有兴趣的小伙伴可以自行搜索。

SSR Client

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
git clone git@github.com:shadowsocksr-backup/shadowsocksr.git
# 超链接
cd /usr/local/bin && ln -s ${pwd}/shadowsocksr/shadowsocks/local.py ssrlocal
# 配置shadowsocks.json
cat /etc/shadowsocks.json
{
"server":"your server ip",
"server_ipv6": "::",
"server_port":8388,
"local_address": "127.0.0.1",
"local_port":1080,
"password":"your password",
"timeout":300,
"udp_timeout": 60,
"method":"aes-256-cfb",
"protocol": "auth_aes128_md5",
"protocol_param": "",
"obfs":"tls1.2_ticket_auth",
"obfs_param": "",
"fast_open": false,
"workers": 1
}
# 配置启动脚本
vim /etc/systemd/system/shadowsocksr.service
[Unit]
Description=SSR
[Service]
TimeoutStartSec=0
ExecStart=/usr/local/bin/ssrlocal -c /etc/shadowsocks.json
[Install]
WantedBy=multi-user.target

# 设置开机启动
systemctl enable shadowsocksr
# 启动
systemctl start shadowsocksr
# 查看
systemctl status shadowsocksr
# 停止
systemctl stop shadowsocksr
# 取消开机启动
systemctl disable shadowsocksr

Privoxy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
yum install -y privoxy
# 修改配置
cat /etc/privoxy/config | grep -v "#"

listen-address 0.0.0.0:8118
...
forward-socks5 / 127.0.0.1:1080 .

# 设置开机启动
systemctl enable privoxy
# 启动
systemctl start privoxy
# 查看
systemctl status privoxy
# 停止
systemctl stop privoxy
# 取消开机启动
systemctl disable privoxy

参考链接

分享到

docker常用命令

docker常用命令
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
# 清理无效的images
docker rmi $(docker images -a -q)
# daemon启动指定的image
docker run -d fcoin:latest
# 进入container容器
docker exec -ti e7e325e08354 sh
# build 镜像
docker build -t fcoin:latest -f Dockerfile .
# 停止container
docker stop e7e325e08354

# 指定docker-compose.yml,设置project name,强制build
docker-compose -f docker-compose.yml -p docker up --build
# 查看日志
docker-compose logs -f
# 后台启动
docker-compose up -d
# 停止
docker-compose down

# 常用配置
cat /etc/docker/daemon.json
{
"insecure-registries": [
"doc.flyflyfish.com:8082"
],
"registry-mirrors": [
"http://doc.flyflyfish.com:8082"
],
"debug": true,
"experimental": false
}
分享到

rpm离线安装脚本

rpm离线安装脚本,主要从https://pkgs.org/下载

下载脚本
1
2
3
4
5
6
7
8
9
#!/usr/bin/env bash

RPM_ARRAY=(perl-5.16.3-293.el7.x86_64.rpm perl-Carp-1.26-244.el7.noarch.rpm perl-Encode-2.51-7.el7.x86_64.rpm perl-Exporter-5.68-3.el7.noarch.rpm perl-File-Path-2.09-2.el7.noarch.rpm perl-File-Temp-0.23.01-3.el7.noarch.rpm perl-Filter-1.49-3.el7.x86_64.rpm perl-Getopt-Long-2.40-3.el7.noarch.rpm perl-HTTP-Tiny-0.033-3.el7.noarch.rpm perl-PathTools-3.40-5.el7.x86_64.rpm perl-Pod-Escapes-1.04-293.el7.noarch.rpm perl-Pod-Perldoc-3.20-4.el7.noarch.rpm perl-Pod-Simple-3.28-4.el7.noarch.rpm perl-Pod-Usage-1.63-3.el7.noarch.rpm perl-Scalar-List-Utils-1.27-248.el7.x86_64.rpm perl-Socket-2.010-4.el7.x86_64.rpm perl-Storable-2.45-3.el7.x86_64.rpm perl-Text-ParseWords-3.29-4.el7.noarch.rpm perl-Time-HiRes-1.9725-3.el7.x86_64.rpm perl-Time-Local-1.2300-2.el7.noarch.rpm perl-constant-1.27-2.el7.noarch.rpm perl-libs-5.16.3-293.el7.x86_64.rpm perl-macros-5.16.3-293.el7.x86_64.rpm perl-parent-0.225-244.el7.noarch.rpm perl-podlators-2.5.1-3.el7.noarch.rpm perl-threads-1.87-4.el7.x86_64.rpm perl-threads-shared-1.43-6.el7.x86_64.rpm
)

for val in ${RPM_ARRAY}; do
echo ${val}
wget http://mirror.centos.org/centos/7/os/x86_64/Packages/${val}
done
参考链接
分享到

mysql5.7离线安装

本文主要介绍mysql5.7的离线安装,关于mysql的配置可以参考之前的博客Mysql配置,离线安装中需要的rpm包可以前往https://pkgs.org/下载

Centos 7.5

  • 下载安装mysql,下载mysql的安装包,下载地址,解压后rpm列表如下:
1
2
3
4
5
6
7
8
9
10
11
12
tar -xvf mysql-5.7.26-1.el7.x86_64.rpm-bundle.tar
# ls
mysql-community-client-5.7.26-1.el7.x86_64.rpm
mysql-community-common-5.7.26-1.el7.x86_64.rpm
mysql-community-devel-5.7.26-1.el7.x86_64.rpm
mysql-community-embedded-5.7.26-1.el7.x86_64.rpm
mysql-community-embedded-compat-5.7.26-1.el7.x86_64.rpm
mysql-community-embedded-devel-5.7.26-1.el7.x86_64.rpm
mysql-community-libs-5.7.26-1.el7.x86_64.rpm
mysql-community-libs-compat-5.7.26-1.el7.x86_64.rpm
mysql-community-server-5.7.26-1.el7.x86_64.rpm
mysql-community-test-5.7.26-1.el7.x86_64.rpm
  • 依赖包

    1. numactl软件包

      1
      2
      3
      numactl-2.0.9-4.el7_2.x86_64.rpm
      numactl-devel-2.0.9-4.el7_2.x86_64.rpm
      numactl-libs-2.0.9-4.el7_2.x86_64.rpm
    2. perl软件包

      1
      perl-Data-Dumper-2.145-3.el7.x86_64.rpm
      • perl相关

        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
        perl-5.16.3-293.el7.x86_64.rpm 
        perl-Carp-1.26-244.el7.noarch.rpm
        perl-Encode-2.51-7.el7.x86_64.rpm
        perl-Exporter-5.68-3.el7.noarch.rpm
        perl-File-Path-2.09-2.el7.noarch.rpm
        perl-File-Temp-0.23.01-3.el7.noarch.rpm
        perl-Filter-1.49-3.el7.x86_64.rpm
        perl-Getopt-Long-2.40-3.el7.noarch.rpm
        perl-HTTP-Tiny-0.033-3.el7.noarch.rpm
        perl-PathTools-3.40-5.el7.x86_64.rpm
        perl-Pod-Escapes-1.04-293.el7.noarch.rpm
        perl-Pod-Perldoc-3.20-4.el7.noarch.rpm
        perl-Pod-Simple-3.28-4.el7.noarch.rpm
        perl-Pod-Usage-1.63-3.el7.noarch.rpm
        perl-Scalar-List-Utils-1.27-248.el7.x86_64.rpm
        perl-Socket-2.010-4.el7.x86_64.rpm
        perl-Storable-2.45-3.el7.x86_64.rpm
        perl-Text-ParseWords-3.29-4.el7.noarch.rpm
        perl-Time-HiRes-1.9725-3.el7.x86_64.rpm
        perl-Time-Local-1.2300-2.el7.noarch.rpm
        perl-constant-1.27-2.el7.noarch.rpm
        perl-libs-5.16.3-293.el7.x86_64.rpm
        perl-macros-5.16.3-293.el7.x86_64.rpm
        perl-parent-0.225-244.el7.noarch.rpm
        perl-podlators-2.5.1-3.el7.noarch.rpm
        perl-threads-1.87-4.el7.x86_64.rpm
        perl-threads-shared-1.43-6.el7.x86_64.rpm
    3. 其他软件包

      libaio-0.3.109-13.el7.x86_64.rpm

    4. 查询并卸载系统自带的Mariadb(会与mysql冲突)

      1
      2
      3
      # 查询mariadb
      rpm -ga | grep mariadb
      rpm -e --nodeps 查询出来的版本
  • 安装mysql

    1
    2
    3
    4
    rpm -ivh libaio-0.3.109-13.el7.x86_64.rpm
    rpm -ivh numactl*
    rpm -ivh perl-*
    rpm -ivh mysql-community-*
  • 启动mysql服务

    1
    systemctl start mysqld
  • 设置开机启动

    1
    2
    3
    systemctl enable mysqld

    systemctl daemon-reload
  • 查看mysql root临时密码

    1
    2
    3
    4
    cat /var/log/mysqld.log | grep password
    # 修改密码
    # mysql -uroot -p
    # SET PASSWORD FOR ‘root’@’localhost’ = PASSWORD(‘newpass’);
  • 新建用户并授权

    1
    2
    3
    create user newuser@’%’ identified by ‘password’;
    grant all privileges on db.* to newuser;
    # grant all privileges on *.* to newuser;

参考链接

分享到

ubuntu移动mysql的datadir

Introduction

数据库会随着时间的推移而增长,有时会超出文件系统上的空间。当它们位于与操作系统其余部分相同的分区上时,您还可能遇到I/O争用。RAID、网络块存储和其他设备可以提供冗余和其他需要的特性。无论您是在添加更多空间、评估优化性能的方法,还是希望利用其他存储特性,本文都将指导您重新定位MySQL的数据目录。

Prerequisites

前置条件:

  • ubuntu16.04服务器,具有非根用户和sudo特权。
  • MySQL服务器。如果你还没有安装MySQL, 可以参考之前博文的mysql的安装

STEP

  • 登录mysql查看当前的datadir
1
2
3
4
mysql -u root -p
mysql> select @@datadir;
sudo systemctl stop mysql
sudo systemctl status mysql
  • 同步现有的datadir到新的目录:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# rsync
sudo rsync -av /var/lib/mysql /mnt/data01
sudo mv /var/lib/mysql /var/lib/mysql.bak
sudo vim /etc/mysql/mysql.conf.d/mysqld.cnf
$ cat /etc/mysql/mysql.conf.d/mysqld.cnf
...
datadir = /mnt/data01/mysql
...
# Configuring AppArmor Access Control Rules
sudo vim /etc/apparmor.d/tunables/alias
$ cat /etc/apparmor.d/tunables/alias
. . .
alias /var/lib/mysql/ -> /mnt/data01/mysql/
. . .
# Restart apparmor
sudo systemctl restart apparmor
  • 创建目录以通过/usr/share/mysql/mysql-systemd-start脚本的检测:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ cat /usr/share/mysql/mysql-systemd-start
...
datadir=$(get_mysql_option mysqld datadir "/var/lib/mysql")
if [ ! -d "${datadir}" ] && [ ! -L "${datadir}" ]; then
echo "MySQL data dir not found at ${datadir}. Please create one."
exit 1
fi

if [ ! -d "${datadir}/mysql" ] && [ ! -L "${datadir}/mysql" ]; then
echo "MySQL system database not found in ${datadir}. Please run mysqld --initialize."
exit 1
fi
...
# 创建目录,根据自己的datadir决定
sudo mkdir /var/lib/mysql/mysql -p
  • 验证是否更改成功
1
2
3
4
5
6
7
8
9
10
11
12
13
sudo systemctl start mysql
sudo systemctl status mysql
mysql -u root -p
mysql> select @@datadir;
+----------------------------+
| @@datadir |
+----------------------------+
| /mnt/data01/mysql/ |
+----------------------------+
1 row in set (0.01 sec)
# Restart Mysql
sudo systemctl restart mysql
sudo systemctl status mysql

参考链接:

分享到