缓存管理的基本问题

来源:互联网 发布:程序员简历中项目经验 编辑:程序博客网 时间:2024/05/05 09:56

     所谓缓存,本质上是同一块数据在不同空间的存储。这样的例子很多:

※ CPU和内存之间的高速缓存;

※ 同时映射到两个进程地址空间的数据,典型的是共享内存;

※ 同时位于数据库和内存中的同一个业务数据,如O-R映射;

※ 位于两台服务器的数据,典型的例子是集群全局配置信息。

一、访问缓存可能发生的简单问题:

    1、在同一个存储空间中,不同进程/线程间的共享数据一致性问题。

例如,假设有一个表示姓名的数据结构被两个线程A、B共享:

struct name {

  char* first;

  char* last;

}  

假设线程A要修改姓名,步骤如下:

第一步,修改name.first

第二步,修改name.last

    如果线程B在第一步和第二步之间读取name,那么它将获得的是不匹配的“姓”和“名”。

   2、在不同存储空间中,数据的一致性问题。

仍然以上面的数据为例,这次假设同样的数据在内存和数据库中各有一份。如果线程A要修改姓名,步骤如下:

第一步:修改内存中的name数据;

第二步:将数据保存在数据库中(修改数据库中的name数据)。

    同样,如果在第一步和第二步之间,B先从数据库中读取了name,再与内存中的name比较,就会发现数据不一致问题。

     虽然上述两个问题看起来都是由于不同存储空间(内存的不同区域、内存和数据库)造成的数据一致性。其实,这两个问题本质是不同的,问题1是两份有关联的数据间的一致性问题;问题2是同一份数据在不同存储空间中的一致性问题。

 

二、解决思路

    1、Copy-on-Write方法

         对于被多个线程共享的数据来说,如果一个线程要修改数据,步骤如下:

         第一步:将要求改的数据D复制一份,作为线程私有数据D’;

         第二步:对D’进行所需的修改;

         第三步:查看D从第一步到现在是否未被被其他线程修改过。如果是,则用D’替换D;否则重新从第一步开始。

         第三步其实是一个CAS过程(Compare and Set),要求是一个原子操作。上述过程常用循环来模拟:

         while (true) {

             D’ = copy(D);

             modify(D’);

             if CAS( !modified(D) , D’)

                break;

         }

    2、加锁方法

         对于被多个线程共享的数据来说,如果一个线程要修改数据,步骤如下:

         第一步:对要修改的数据D加排他锁(X锁),阻止其他线程访问;

         第二步:修改数据D;

         第三步:解除数据D上的锁。       

     方法1正是所谓无锁结构(lock-free),相比方法2并发要性能高一些,但是复杂性也相对高一些。而且,方法1通常也可用锁机制来模拟,只是加锁时间短暂一些,通常被称为spin-lock(旋转锁)。

     上述两个方法的根本,有其共同之处,就是要保证:在共享数据被修改完成以前,不能被其他线程访问。

 

三、稍微复杂一些的缓存管理问题

    1、层次结构的数据一致性问题。

        例如下面的数据结构中,结构name是结构person的成员:

struct name {

  char* first;

  char* last;

}  

        stuct person {

            int id;

            struct name full_name;

        }

        如果我们要通过加锁的方式更新person的数据,就要考虑是否对person.full_name也加锁。

    2、写缓存的时机

        对于需要在两种存储介质中存储的某种信息,必须考虑何时同步这两种存储中的数据。例如,内存和数据库中的数据同步;内存和磁盘之间的数据同步;CPU和内存之间的数据同步。

    3、缓冲的透明性

这就首先要考虑下面两个问题。

        1)冲机制的引入,与没有缓冲相比,是否需要对外暴露更多的接口?

     考虑为保证一致性而引入锁机制,对编程接口的影响。最直观的方案,就是要求程序员必须显式的进行“加锁/解锁”操作:

     lock(d);

     *d = new_value;

     unlock(d);

     另一种方案,就是对外不暴露锁机制,由框架内部完成:

     change(data* d, data new_value) {

         lock(d);

         *d = new_value;

         unlock(d);

     }

     虽然后一种方案看起来先进一些,但其实对于比较复杂的数据结构来说,要求实现被修改数据实现深拷贝(deep copy)接口。

        2)是不是每次修改数据,都要同步数据?

           对于一个较复杂的数据结构来说,我们可能常常一次只修改其中的一部分数据。例如:

           name.first = “Tom”;

           /* 进行其他操作 */

           /* … */

           name.last = “Smith”;

     有时,如果修改每一部分数据都进行同步操作,分散写入数据,不但步骤比较繁琐,而且效率不如集中写入高。如果让缓冲管理机制来决定何时写入数据,可能即方便又高效。但是,这样做也有新的问题,一方面实现起来比较复杂,另一方面,如果系统崩溃,可能造成缓冲中为写入的数据丢失。

     这就引出了其他机制,回写、直写、先写日志和检查点等各种新的概念和技术。