当前Query cache是所有session共享的,也就是说同一条SELECT语句 + database + flag(包含影响执行结果的所有环境变量)构成的Key如果已经存储在Query cache中了,任何session都可以从Query cache中获取想要的结果集。所有session共享Query cache,那如何处理并发呢?当前Query cache只支持查询,插入,删除操作,不支持更新。下面我们将对这三种操作的并发原理进行分析。

    在对三种操作进行分析之前,我们先来看看Query cache 并发处理的方式。Query cache的并发处理,同样是利用锁。对于Query cache对象自身的所有操作使用一把mutex锁来进行并发控制。Query_cache在其初始化,即调用Query_cache::init的时候,会初始化如下锁变量:

    说明

    以及key_COND_cache_status_changed这两个变量是用来处理Query cache与PSI(Performance schema instrumentation interface)相关的并发控制,这里我们不对其进行介绍,如果有兴趣可以参考PSI的相关介绍。

    m_cache_lock_status控制当前Query cache所处的状态。该变量有3个值:

    Query cache中一个重要的控制并发的函数是Query_cache::try_lock,也就是加锁过程,算法实现如下:

    1. bool Query_cache::try_lock(bool use_timeout)
    2. {
    3. mysql_mutex_lock(&structure_guard_mutex); //首先试图获取mutex
    4. while(1)
    5. {
    6. if (m_cache_lock_status == Query_cache::UNLOCKED)
    7. {
    8. m_cache_lock_status= Query_cache::LOCKED; //如果Query cache未被锁定,那么我们修改其状态为锁定状态。利用mutex进行加锁。
    9. break;
    10. }
    11. else if (m_cache_lock_status == Query_cache::LOCKED_NO_WAIT)
    12. {
    13. interrupt= TRUE; //这里表示Query cache正在被Flush或者处于关闭状态,没有必要再加锁继续进行操作。遇到这种状态,需要加锁的操作将直接返回。
    14. break;
    15. }
    16. else
    17. {
    18. if (use_timeout) //这个参数是控制是否需要超时处理。
    19. set_timespec_nsec(waittime,(ulong)(50000000L)); // 50微秒超时
    20. int res= mysql_cond_timedwait(&COND_cache_status_changed,
    21. &structure_guard_mutex, &waittime);
    22. }
    23. else
    24. {
    25. mysql_cond_wait(&COND_cache_status_changed,
    26. &structure_guard_mutex);
    27. }
    28. }
    29. }
    30. }

    Query cache的记录查询,插入都需要先使用Query_cache::try_lock加锁。使用Query_cache::try_lock加锁的主要原因是可以检查Query cache所处的锁定状态,如果Query cache正在FLUSH或者关闭,记录查询或者插入都将没有意义,因此检查到锁定状态为Query_cache::LOCKED_NO_WAIT就可以直接返回了。

    对于删除Query cache中的记录,操作前进行的锁定是Query_cache::lock。该函数与Query_cache::try_lock的唯一区别就是不再检查Query_cache::LOCKED_NO_WAIT状态,一直等待直到获取Query cache锁。

    1. Query_cache::send_result_to_client(…)
    2. {
    3. If (!SELECT语句)
    4. return;
    5. if (try_lock())
    6. return;
    7. 构造Query cacheKey值(Key值包含了query + database + flag(包含影响执行结果的所有环境变量));
    8. return
    9. if (query_block->result_type == Query_cache_block::RESULT) // 这里的条件是用来判断与该条Query相关的结果集是否已经被完全的写入了Query cache中。如果结果集没有全部写入,显然我们也不能返回结果集。
    10. {
    11. RD_lock (query_block); //这个Query_cache_block的块锁应该没什么用处,因为所有操作都需要Query cache的全局mutex。
    12. if (表的权限检查成功)
    13. 返回结果集;
    14. RD_unlock(query_block); //释放Query_cache_block的Read锁。
    15. }
    16. unlock(); // 释放Query cache的全局mutex。
    17. }

    目前插入流程如下:

    1. Query_cache::invalidate_table(…)
    2. {
    3. lock();
    4. // 这里使用lock而非try_lock,是因为我们需要强制失效所有与table相关的Query_cache_block。
    5. // 而try_lock会在Query cache的状态为Query_cache::LOCKED_NO_WAIT的时候直接返回。
    6. invalidate_table_internal(); //失效所有与指定表相关的Query cache。
    7. unlock(); //释放全局mutex。
    8. }

    对于Query cache的失效部分,目前的处理方式非常暴力,任何对表数据的修改,包括UPDATE/INSERT/DELETE操作,都会将该表相关的所有Query cache记录实效掉,这种实效方式影响非常大。建议增加对于WHERE,HAVING等过滤条件的判断,如果Query cache中的记录涉及的结果集与当前UPDATE/INSERT/DELETE所涉及的数据没有交集,我们完全没有必要实效掉这样的记录。比如:

    1. SELECT * FROM t WHERE t.a > 10;

    我们对于这样一条SELECT语句进行结果集的缓存。对于如下的INSERT/UPDATE/DELETE 语句来说,我们完全没有必要去失效与这条SELECT语句相关的结果集缓存,因为下面这几条语句操作的数据集和SELECT的结果集没有发生任何交集。

    对于DDL其实我们也可以做的更好,比如对于下面这条SELECT语句的结果集缓存记录来说:

      1. ALTER TABLE t ADD COLUMN c INT;

      总而言之,Query cache的并发处理的粒度比较大,几乎所有的操作都需要拿到Query cache的全局mutex。如果可以对Query cache的全局状态变量使用Free lock,只对于存储分配使用mutex,对Query_cache_block进行加锁处理会对性能有所改进。