返回介绍

5.5 memcached 和它的伙伴们

发布于 2023-05-19 13:36:37 字数 28471 浏览 0 评论 0 收藏 0

在程序的实现中,经常会忽略程序的运行时间。即便采用类似的实现方法,有时候运行速度也会相差很多。大多数情况下,这一速度上的差异是由数据访问速度的差异所导致的。

在程序中,虽然数据访问所消耗的时间看上去差不多,但实际上却有很大的差别。这是因为,数据访问所需要的时间,与数据存放的位置有很大关系。例如,内存中的数据与硬盘上的数据,其访问所需的时间可以相差数百万倍之多。

以机械旋转方式工作的硬盘,从驱动磁头到将盘片旋转到适当的扇区,需要几毫秒的时间,从 CPU 的速度来看这些时间都是需要等待的,而对内存的访问仅仅需要几纳秒的时间。相比之下,硬盘简直就像停止不动一样。此外,和位于外部的内存相比,位于 CPU 内部的寄存器和高速缓存的访问速度又能快上几倍。

用于高速访问的缓存

话虽如此,但内存的访问速度再快,要准备和硬盘同等容量的内存,将所有数据都保存在内存中,目前来看还是不现实的1

1 不过,现在的服务器所配置的内存容量,已经超过了过去主流硬盘的容量。此外,比硬盘访问速度快几倍的闪存式 SSD(Solid State Drive,固态硬盘)也已经开始普及。说不定在不久的将来,现在的“常识”就会被颠覆。(原书注)

所幸的是,对数据的访问具备“局部性”的特点。也就是说,一个操作中所访问的数据大多是可以限定范围的。即便数据的量很大,但大多数的操作都是仅仅对一部分数据进行频繁的访问,而几乎不会去碰其余的数据。

既然如此,如果将这些频繁访问的数据复制到一个可以高速访问的地方,平时就在那个地方进行操作的话,就有可能为性能带来大幅度的改善。像这样“能够高速访问的数据存放地点”,被称为缓存(cache)。“缓存”这个词的英文 cache,和“现金”的英文 cash 发音相同,但拼写方法是不同的。cache 一词来自法语,原来是“储藏地”、“仓库”的意思。最近的 CPU 中为了高速访问数据和指令,都配备了一定的高速缓存,但缓存一词本身,应该是泛指所有用于加速数据访问的手段。

提到缓存,往往会包含以下含义:

· 可以高速访问。

· 以改善性能为目的。

· 仅用于临时存放数据,当空间已满时可以任意丢弃多出来的数据。

· 数据是否存放在缓存中,不会产生除性能以外的其他影响。

数据库的职责是用来永久存储数据,不可能将数据任意丢弃。缓存则不同,它具有较大的随意性。

memcached

在提供缓存功能的软件中,比较流行的是 memcached。memcached 是由美国 Danga Interactive 公司在 Brad Fitzpatrick(1980— )的带领下开发的一种“内存型键 - 值存储”软件,主要面向 Web 应用程序,对数据库查询的结果进行缓存处理。

当对数据库进行查询时,数据库服务器会执行下列操作:①解析 SQL 语句;②访问数据;③提取数据;④(根据需要)对数据进行更新等操作。考虑到数据库中的数据大多存放在磁盘上(当然,数据库服务器本身也有一定的缓存机制),这样的操作开销是非常大的。

另一方面,近年来,随着服务器及内存价格的下降,相比购买昂贵的高性能服务器来说,将查询结果缓存在内存中就可以以低成本实现高性能。因此,memcached 并不是一个真正意义上的键 - 值存储数据库,而是主要着眼于以缓存的方式来改善数据访问的性能。

用于实现缓存功能的 memcached 具备以下特征:

· 以字符串为对象的键 - 值存储。

· 数据只保存在内存中,重启 memcached 服务器将导致数据丢失。

· 可以对数据设置有效期。

· 达到一定容量后将清除最少被访问的数据。

· 键的长度上限为 250B,值的长度上限为 1MB。

memcached 能够接受的命令如表 1 所示。

表1 memcached命令

名 称

功 能

set

设置数据

add

插入新数据

replace

更新现有数据

append

在值之后添加数据

prepend

在值之前添加数据

get

获取数据

gets

获取数据(有唯一ID)

cas

更新数据(指定唯一ID)

delete

删除数据

incr

将数据视为数值并累加

decr

将数据视为数值并减少

stats

获取统计数据

flush_all

清空数据

version

版本信息

verbosity

设置日志级别

quit

结束连接

memcached 非常好用,应用也非常广泛。根据其主页上的介绍,表 2 所示的这些服务都使用了 memcached。这些都是非常著名的网站或公司,当然,在很多没有那么有名的网站中,memcached 的使用也十分广泛。

表2 使用memcached的服务及企业

服务名称或企业名称

Bebo,Craigslist,Digg,Flickr,LiveJournal,mixi,Typepad,Twitter,Wikipedia,Wordpress,Yellowbot,YouTube

示例程序

memcached 可以通过 C、C++、PHP、Java、Python、Ruby、Perl、Erlang、Lua 等语言来访问。此外,memcached 的通信协议是由简单的文本形式构成的,使用如 telnet 等方式也很容易进行访问,要开发新的客户端也非常容易。除了上述列举的语言之外,还有很多语言也提供了 memcached 客户端库。

下面我们来看看访问 memcached 客户端程序的示例吧(图 1)。有很多库都提供了用 Ruby 访问 memcached 的功能,在这里我们用的是一个叫做 memcache 的库。正如图 1 最下方的注释所写的,对于同时执行查询和更新操作的数据进行缓存时一定要注意,否则一不小心就容易造成缓存和数据库之间的数据不匹配。通过 memcache 库也可以很容易地实现事务机制。下面我们来重新实现一下 prepend 命令吧(图 2)。

require 'memcache'

# 连接memcached
MCache = Memcache.new(:server => "localhost:11211")

# 对userinfo进行带缓存查询
def userinfo(userid)
  # 使用“user:<userid>”为键来访问缓存
  result = MCache.get("user:" + userid)

  # 如果不存在于缓存中则返回nil
  unless result
    # 缓存中不存在,直接查询数据库
    result = DB.select("SELECT * FROM users WHERE userid = ?", userid)
    # 将返回结果存放在缓存中,以便下次从缓存中查询
    MCache.add("user:" + userid, result)
  end
  result
end

# 如果对userinfo进行更新,需要同时更新缓存
图 1 Ruby 编写的 memcached 客户端

def mc_prepend(key, val)
  loop do
    # :设置:cas标志并获取唯一ID
    v = MCache.get(key, :cas => true)
    # 修改值并用cas命令设置
    # 唯一ID可通过memcache_cas获取
    v = MCache.cas(key, val + v, :cas => v.memcache_cas)
    # 设置失败则返回nil(循环)
    return v unless v.nil?
  end
end
图 2 memcached 事务示例

对 memcached 的不满

和其他大多数软件一样,随着使用的不断增加,memcached 也遇到了当初从未设想过的应用场景,从而也招来了很多不满。对 memcached 的不满主要有下列这些:

数据长度:对键和值的长度分别限制在 250B 和 1MB 过于严格。越是大的数据查询起来就要花很长的时间,从这个角度来说,对这样的数据进行缓存的需求反而比较高。

分布:一台服务器的内存容量是有限的,如果能将缓存分布到多台服务器上就可以增加总的数据容量。然而 memcached 并没有提供分布功能。

持久性:顾名思义,memcached 只是一个缓存,重启服务器数据就会丢失,且为了保持一定的缓存大小,还会自动舍弃旧数据。

不过,当对数据库进行访问来填充缓存的开销超过一定程度时,缓存的损失就要付出较高的代价。为了克服这一问题(,即便舍弃缓存这一最初的目的)便产生了对数据进行持久化的需求。

像最后的“持久性”这一点,虽然已经完全脱离了缓存的范畴,但随着其应用领域的扩展,还是会产生各种各样类似的要求。

在 memcached 的范围内,对这些问题,主要是采取在客户端方面进行努力来应对。例如,Ruby 的 memcache 库中,提供了一个叫做“SegmentedServer”的功能,通过将键所对应的值分别存放到多台服务器中来支持长度很大的值。此外,还可以根据由键计算出的某种散列值,在客户端级别上实现对分布式数据存储的支持。其算法如图 3 所示。

# 表示算法的简单Ruby代码
class DistMemcache
  def add(key, value)
    # 由键计算散列值
    hash = hash_func(key)
    # 根据散列值选择服务器
    server = @servers[hash % @servers.size]
    # 向选择的服务器发送命令
    server.add(key, value)
  end
  # 对其他的命令采用同样的方法
end
图 3 memcache 在客户端级别实现分布

对于持久性这一点,在客户端级别上恐怕是无能为力了。不过新版本的 memcached 中正在对数据存储功能进行抽象化,从而实现将数据以文件及数据库等形式进行保存,重启服务器也不会丢失。

memcached 替代服务器

刚才已经讲过,memcached 的协议是基于文本的,非常简单2,因此大多数键 - 值存储数据库都可以支持 memcached 协议。下面我们来介绍其中的几种数据库软件。

2 为了提高效率,也提供了二进制协议。(原书注)

1. memcachedb

名字只差了一个字母,很容易搞混。这是一款用 Berkeley DB 来保存数据的 memcached 替代服务器。memcachedb 原本是由 memcached 改造开发而来,目的是为了应对 memcached 不支持数据持久性的问题。

2. ROMA

我所参与的乐天技术研究所开发的键 - 值存储 ROMA,也支持通过 memcached 协议来进行访问。ROMA 是一种重视可扩展性的键 - 值存储,其数据库是由 P2P 方式的节点集合所构成的。数据在多个节点上保存副本,即便由于一些故障导致一个节点退出服务,也不会造成数据的丢失。memcached 只是在客户端一侧实现了分布,相对而言,ROMA 则是在服务器一侧实现了分布和冗余化。

ROMA 服务器是用 Ruby 进行开发的,采用了可插式(pluggable)架构。通过用 Ruby 编写插件,并配置到服务器上,就可以很容易地实现数据保存方法的选择、服务器端命令的追加等。

采用 Ruby 进行实现,大家可能会担心性能方面的问题。ROMA 是在乐天内部的各种场景下运用的,由于采用多个节点分担负荷,实际运用中并没有发生性能方面的问题。其实,在乐天这样大规模数据中心的应用中,发生硬件故障、访问量集中导致进程终止等问题几乎是家常便饭,因此比起性能来说,ROMA 更加重视实现实际运用中的稳定性和灵活性。

3. Flare

Flare 是由 GREE3的 CTO 藤本真树领导开发的一种 memcached 替代键 - 值存储。其特征如下:

3 GREE 是日本的一家社交网站(http://gree.jp),与中国的“格力电器”无关。

o 使用 Tokyo Cabinet 实现数据持久性

o 将数据复制到多台服务器上实现数据分区

o 无需停止系统就可以添加服务器的动态重组机制

o 节点监控+故障转移(failover)

o 可支持大于 256B 的键和大于 1MB 的值

根据文档,GREE 使用的 Flare 由 12 个节点(6 个主节点、6 个从节点)构成,目前正在对超过 2000 万个键和 1GB 级别的数据,以峰值每秒 500 ~ 1000 次访问的频率进行试运行。在这样的条件下,系统基本上没有什么负担,而且在运行中频繁更换服务器,也没有对系统的运转产生任何问题。

4. Tokyo Tyrant

Tokyo Tyrant 是由平林干雄开发的一种网络型键 - 值存储。很多键 - 值存储都在使用的 DBM 库 Tokyo Cabinet 也是平林先生开发的。相对而言,Tokyo Tyrant 提供了通过网络进行访问的功能。Tokyo Tyrant 支持 memcached 协议及 HTTP 协议,从这个角度来看,也可以视为 memcached 的替代服务。

Tokyo Tyrant 虽然需要写入磁盘,但却能够实现与 memcached 同等的性能。

5. kumofs

kumofs 是由筑波大学的古桥贞之(现供职于美国 Treasure Date 公司)开发的一种键 - 值存储,实现了持久性和冗余故障容忍性。

用 C++ 编写的 kumofs,仅用 1 个节点就能够实现与 memcached 几乎同等的性能,还可以通过增加节点来进一步提高性能。此外,它还能够在不停止系统运行的情况下,进行节点增加、恢复等操作,从而对访问量的增加等情况作出灵活的应对。

另一种键 - 值存储 Redis

看了上面这些例子,我们可以总结一下,大家对 memcached 的不满主要体现在以下几个方面:

1. 缺乏持久性

2. 不支持分布

3. 数据长度有限制

于是,为了解决这些不满,就出现了各种各样的替代服务器软件。然而,这里希望大家不要误会,这些不满决不等于是 memcached 的缺点。顾名思义,memcached 原本是作为数据缓存服务器诞生的,因此,缺乏持久性、不支持分布,以及对数据长度的限制,都是作者原本的意图。可以说,之所以会出现这些不满,都是试图将 memcached 用在超出原本目的的场景中所导致的结果。这些不满的存在表明,大多数的使用案例中,用户所需要的不仅仅是一个缓存数据库,而是一个支持持久性和分布式计算的真正意义上的键 - 值存储数据库。而之所以选择了原本并不适合这一目的的 memcached,无非是被 memcached 的高速性所吸引的结果。

也就是说,对同时满足:

1. 高速

2. 支持分布

3. 持久性

这些特性的键 - 值存储的需求,是真实存在的。而且,如果不但支持单纯的字符串键值对,而是能够以更加丰富的数据结构作为值来使用的话,大家也一定是非常喜欢的。能满足上述这些需求的,就是 Redis。Redis 是意大利程序员 Salvatore Sanfilippo 开发的一种内存型键 - 值存储,其特征如下:

内存型:数据库的操作基本都在内存中进行,速度非常快。

支持永久化:上面提到数据库的操作是在内存中进行的,但是它提供了异步输出文件的功能。发生故障时,最后输出的文件之后的变更会丢失。虽然并不具备严格的可靠性,但却可以避免数据的完全丢失。

支持分布:现行版本和 memcached 一样支持在客户端一侧实现对分布的支持。Redis Cluster 分布层的开发还在计划中。具备服务器端复制功能。

除字符串之外的数据结构:除了字符串,Redis 还支持列表(list,字符串数组)、集(set,不包含重复数据的集合)、有序集(sorted set,值经过排序的集合)和散列表(hash,键 - 值组合)。在 memcached 中必须强制转换为字符串才能存放的数据结构,在 Redis 中可以直接存放。

高速:全面使用 C 语言编写的 Redis 速度非常快。根据测试数据,在 Xeon 2.5GHz 的 Linux 计算机上,每秒能够处理超过 5 万个请求。在另一个测试中,对单节点采用 60 个线程分别产生 10000 个请求的调用,Redis 实现的每秒请求数量达到了 memcached 的两倍。而同样一个测试中,memcached 的成绩几乎是 MySQL 的 10 倍,从这个角度来看,Redis 的性能着实令人惊叹。

原子性:由于 Redis 内部是采用单线程实现的,因此各命令都具备原子性。像用 incr 命令对值进行累加而干扰其他请求的执行这样的问题是不会发生的。

不兼容 memcached 协议:Redis 拥有自己的数据结构,功能也比较丰富,因此没有采用 memcached 协议。访问 Redis 需要借助客户端库,但 Redis 协议也是基于文本的简单协议(实际上和 memcached 协议很相似),因此无论各种语言都很容易支持,包括 Ruby 在内,很多语言都提供了用于访问 Redis 的库的功能(表 3),数量上不输给 memcached。

表3 提供Redis访问的语言

语言名称

C,C#,Clojure,Haskell,Io,Erlang,Java,JavaScript,Go,Perl,Lua,PHP,Python,Ruby,Scala,Tcl

Redis 的主页上列出了一些实际采用 Redis 的企业名单,如表 4 所示。其中,Engine Yard 和 GitHub 都是 Ruby 开发者耳熟能详的公司。

表4 采用Redis的企业

企业名称

Boxcar,craigslist,Dark Curse,Engine Yard,GitHub,guardian,LLOOGG,OKNOtizie,RubyMinds,Superfeedr,Vidiowiki,Virgilio Film,Wish Internet Consulting,Zoombu

Redis 的数据类型

之前已经讲过,Redis 中的值,除了字符串以外,还支持列表、集、有序集、散列等数据结构。

字符串:将字符串作为值的操作和 memcached 几乎是一样的。不知道为什么,Redis 中没有提供在值之前添加字符串的 prepend 命令,也许是因为很少使用吧。实在需要这个功能的话,也可以通过事务来实现。

列表:相当于字符串数组。Redis 不支持如数组的数组这样的嵌套数据结构,因此数组的元素仅限于字符串。

列表是可以对左右两端进行 PUSH(添加一个元素)和 POP(取出并删除一个元素)操作的,因此可以作为队列和栈来使用。

:相当于不允许出现重复元素的集合。通过 Redis 的 SADD 命令添加元素,用 SREM 命令删除元素。由于没有定义元素的顺序,因此使用取出一个元素的 SPOP 命令不知道会取出集中的哪个元素。

集合之间也可以进行运算,例如 SINTER 可以得到两个集中都包含的元素(交集),SUNION 则可以得到两个集中属于任意一个集的元素(合集)。此外,还可以用 SINTERSTORE/SUNIONSTORE 命令将运算结果保存到另一个键中。

有序集:Redis 从 1.1 版开始提供的一种有序的集(sorted set,Redis 术语中称为 ZSET),在这种集中会将元素都视为数值并进行排序。说实话我不知道这样的数据结构应该用在哪里,也许在某些场合中用起来会很方便吧。

散列表:散列表就是通过键来查找值的一种表。在 Redis 中,散列表的键和值都限定为字符串。

将上述特征总结一下,与只能存放字符串键值对的 memcached 相比,Redis 是一种高速、拥有丰富的数据结构,且支持异步快照功能的键 - 值存储。不过,Redis 也并非万能的数据库,(至少截至到目前来说)还不具备服务器端 Sharding(分布)和动态重组功能,也没有实现非常高的可靠性。

在数据不要求有很高的可靠性(也就是说,万一丢掉一些数据也不会产生严重后果)的场合,Redis 就不是一个非常理想的数据库。从 memcached 的使用实例来看,这样的领域还是非常广阔的。

Redis 的命令与示例

Redis 的命令如表 5 所示。虽然形式非常类似,但数量却远远多于 memcached 命令。

表5 Redis命令一览

命 令

概 要

连接管理

QUIT

结束连接

AUTH

简单认证(可选)

数据库操作

DBSIZE

当前DB中的键数量

SELECT index

数据库选择

MOVE key index

将键移动到index 的DB

FLUSHDB

删除当前DB中的全部键

FLUSHALL

删除全部DB中的全部键

适用于所有值类型的命令

EXISTS key

检查key 是否存在

DEL key

删除key

TYPE key

key 的类型

KEYS pattern

列出与pattern相匹配的键

RANDOMKEY

随机获取一个键

RENAME old new

重命名键(new已存在则舍弃)

RENAMENX old new

重命名键(new已存在则忽略)

EXPIRE key sec

设置有效期

TTL key

剩余有效期

适用于字符串值的命令

SET key val

将val(字符串)赋值给key

GET key

获取key 相对应的值

GETSET key val

将val 赋值给key 并获取更新前的原始值

MGET key1 key2 ... keyN

获取多个key 相对应的值

SETNX key val

key 不存在时更新val

SETEX key time val

带有效期的SET

MSET key1 value1 key2 value2... keyN valueN

对多个key 和val 进行更新(原子操作)

MSETNX key1 value1 key2 value2 ... keyN valueN

对多个k e y 和v a l 进行更新(原子操作,其中任何k e y 都不能是事先存在的)

INCR key

对值加1

INCRBY key int

对值加int

DECR key

对值减1

DECRBY key int

对值减int

APPEND key val

在值末尾追加val

SUBSTR key start end

获取值字符串的一部分

适用于列表值的命令

RPUSH key val

向列表末尾追加val

LPUSH key val

向列表开头追加val

LLEN key

列表长度

LRANGE key start end

列表中指定范围内的元素

LTRIM key start end

将列表按指定范围切割

LINDEX key index

指定位置的元素

LSET key index val

向指定位置写入val

LREM key count val

从列表开头删除count 个val(0 为删除全部,负数为从末尾开始删除)

LPOP key

获取并删除列表开头的元素

RPOP key

获取并删除列表末尾的元素

BLPOP key1 key2 ... keyN timeout

带超时的LPOP

BRPOP key1 key2 ... keyN timeout

带超时的RPOP

RPOPLPUSH key1 key2

从key1 列表中RPOP,然后LPUSH到key2 列表中

适用于集值的命令

SADD key member

向集添加元素

SREM key member

从集中删除元素

SPOP key

从集中随机删除一个元素

SMOVE key1 key2 member

将member从key1 集移动到key2 集(原子操作)

SCARD key

集的元素数

SISMEMBER key member

key 集中是否包含元素member

SINTER key1 key2 ... keyN

属于所有指定集的元素

SINTERSTORE dstkey key1 key2 ... keyN

将属于所有指定集的元素的集赋值给dstkey

SUNION key1 key2 ... keyN

属于任一指定集的元素

SUNIONSTORE dstkey key1 key2 ... keyN

将属于任一指定集的元素的集赋值给dstkey

SDIFF key1 key2 ... keyN

key1 与Key2 及其之后的集之间的差异元素

SDIFFSTORE dstkey key1 key2 ... keyN

将key1 与Key2 及其之后的集之间的差异元素赋值给dstkey

SMEMBERS key

集中所有的元素

SRANDMEMBER key

随机获取集中一个元素

适用于有序集(ZSET)的命令

ZADD key score member

添加成员(已经存在则更新score)

ZREM key member

删除member

ZINCRBY key inc member

将member的得分增加inc

ZRANK key member

member的排位

ZREVRANK key member

member的排位(倒序)

ZRANGE key start end

获取范围内的元素

ZREVRANGE key start end

获取范围内的元素(倒序)

ZRANGEBYSCORE key min max

获取得分为min 到max 之间的元素

ZCARD key

有序集的元素数

ZSCORE key member

member的得分

ZREMRANGEBYRANK key min max

获取排位为min 到max 之间的元素

ZREMRANGEBYSCORE key min max

删除得分为min 到max 之间的元素

ZINTERSTORE dstkey N key1 ... keyN

将指定有序集的交集赋值给dstkey

ZUNIONSTORE dstkey N key1 ... keyN

将指定有序集的合集赋值给dstkey

适用于散列表的命令

HSET key field val

对key 指定的散列表的field 设置val

HGET key field

获取key 指定的散列表的field

HMGET key field1 ... fieldN

获取多个field 对应的值

HMSET key field1 value1 ... fieldN valueN

设置多个field(原子操作)

HINCRBY key field int

将field 的值增加int

HEXISTS key field

判断散列表中是否包含field

HDEL key field

删除field

HLEN key

散列表的field 数

HKEYS key

获取散列表的所有field

HVALS key

获取散列表的所有value

HGETALL key

获取散列表的所有field 和value

排 序

SORT key

对列表、集、有序集进行排序

事 务

MULTI/EXEC/DISCARD/WATCH/UNWATCH

Redis的原子性事务

Publish/Subscribe

SUBSCRIBE/UNSUBSCRIBE/PUBLISH

Redis Publish/Subscribe通信

持久性控制命令

SAVE

同步保存

BGSAVE

异步保存

LASTSAVE

最后保存时间

SHUTDOWN

同步保存后停止服务器

BGREWRITEAOF

替换日志文件

远程服务器控制命令

INFO

服务器信息

MONITOR

请求转储(调试用)

SLAVEOF

复制的设置

CONFIG

Redis设置(可变更)

我们将图 1 的 memcached 客户端改造成了 Redis 客户端(图 4)。Ruby 的 memcache 库可以将对象自动转换成字符串,不过 Redis 库却不会进行这样的自动转换。因此,我们需要用 Marshal 显式地转换成字符串。除了这一点和 memcached 客户端有区别以外,其他方面基本上是相同的。在这里我们只使用了字符串,不过用 Redis 的散列来表达 userinfo 可能也挺有意思的。

require 'redis'

# 连接Redis
RD = Redis.new(:host => "localhost", :port => 6379)

# 对userinfo进行带缓存查询
def userinfo(userid)
  # 使用“user:<userid>”为键来访问缓存
  result = RD.get("user:" + userid)

  # 如果不存在于缓存中则返回nil
  unless result
    # 缓存中不存在,直接查询数据库
    result = DB.select("SELECT * FROM users WHERE userid = ?", userid)
    # 将返回结果存放在缓存中,以便下次从缓存中查询
    RD.set("user:" + userid, Marshal.dump(result))
  else
    # 将缓存还原成对象
    # redis库不会进行自动字符串转换
    result = Marshal.load(result)
  end
  result
end
图 4 Ruby 编写的 Redis 客户端

Redis 中没有像 memcached 的 prepend 这样的命令,要进行这样的原子性操作就需要用到事务。memcached 和 Redis 在事务的结构方面是有很大差异的。Redis 的事务是由包裹在 multi 命令和 exec 命令之间的部分构成的。当遇到 multi 命令时,其后面的所有命令都只进行参数检查,然后记录到日志中,随后当遇到 exec 命令时,再一次性(原子性)地执行所有的记录下来的命令集。如果需要像 prepend 这样对现有 key 进行变更,则需要事先用 watch 命令指定要变更的 key。

图 5 是一个通过 Redis 的事务来实现 prepend 的例子。不过,在编写这个程序的时候,Redis 的事务功能还处于开发阶段,因此无法使用 WATCH 命令。

def mc_prepend(key, val)
  loop do
    RD.watch(key)
    v = RD.get(key) + val
    RD.multi
    RD.set(key, v)
    unless RD.exec.nil?
      return v
    end
  end
end
图 5 Redis 事务示例

如果没有事务功能的话,可能很多人会感觉非常别扭。但是,在采用 Redis 的场景中,并不一定需要事务功能,因此我觉得开发和完善这个功能的优先级并不高。大家可以回想一下,在 Web 应用程序中广泛使用的 MySQL 数据库,也是在曾经很长一段时间内都不支持事务的。

小结

memcached 以及对其“不满”的应对,似乎都是云计算网络环境改变了对软件的要求所导致的结果。环境的变化,必然会加速软件的进化。

“支撑大数据的数据存储技术”后记

历史上,对于数据存储的重大革新,可以说非 RDB(关系型数据库)莫属了。具备关系代数理论背景的 RDB,虽然在 1970 年诞生之初遭到了无数批评,称其毫无用武之地,然而现在 RDB 却几乎已经成为了数据库的代名词。

不过,在进入云计算时代之后,除 RDB 以外的其他方案开始受到越来越多的关注,本章中也对其中的 MongoDB、memcached 和 Redis 等进行了介绍。由于这些数据库系统并非采用 SQL 的 RDB,因此经常被称为 NoSQL。它们之所以备受关注,应该说是因为在大量节点构成的云计算系统中,数据库服务器逐渐成为了整个系统的瓶颈。而且,大多以通用数据库服务器形式提供的 RDB,出于各种各样的原因,很难解决这一瓶颈问题。

当然,要解决这一问题,也可以采用复制(对多个数据库进行同步,并将请求分配到多个数据库服务器上)、分割(按照一定的标准将数据库分割成多个,例如将编号为偶数和奇数的会员分别保存到不同的数据库中)等技术,但这些技术都需要在客户端一侧提供一定的支持,而从结果来说,有很多场景只是需要通过键来获取值这样简单的操作,并不一定要动用 RDB。

在这样的背景下,就出现了只管理键值对并将数据完全保存在内存中的缓存系统 memcached。同时,由于缓存数据丢失后还是需要访问数据库,与其产生这样的开销还不如自己来管理文件写入操作,于是就诞生了 ROMA、Flare 等软件。

除此之外,随着对系统灵活性的需求不断提高,固定的数据库结构(schema)已经无法应对各种变化,于是便出现了追求数据库结构灵活性的 MongoDB 和 Redis。另一方面,RDB 也认识到其在速度和灵活性方面的问题,同时为了解决这些问题而进行着持续的进化。本章中介绍的 VoltDB 正是其中的一种尝试。

在这样的对峙中,数据存储的基础,即存储架构本身也在不断发生变化。速度缓慢的硬盘(HDD)正在被淘汰,数据存储逐步过渡到采用以闪存为基础的固态硬盘(SSD),同时,MRAM、FeRAM 等下一代内存也开始崭露头角,据说可以实现和现有 DRAM 同等的速度、能够与闪存相媲美的容量,而且还能实现永久性数据存储(断电后数据不会丢失)。这样的话,数据库这一概念就有可能会从根本上被颠覆。

在下一代内存得到广泛运用的时代,曾经像 Smalltalk、Lisp 那样将内存空间直接保存下来的模型,说不定会东山再起。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文