2
上一篇文章:Python--Redis实战:第五章:使用Redis构建支持程序:第3节:查找IP所属城市以及国家

随着我们越来越多地使用Redis以及其他服务,如何存储各项服务的配置信息将变成一个棘手的问题:对于一个Redis服务器、一个数据库服务器以及一个Web服务器来说,存储它们的配置信息并不困难;但如果我们使用了一个拥有好几个从服务器的Redis主服务器,或者为不同的应用程序设置了不同的Redis服务器,甚至为数据库也设置了主服务器和从服务器的话,那么存储这些服务器的配置信息将变成一件让人头痛的事情。

用于连接其他服务器以及服务器的配置信息一般都是以配置文件的形式存储在硬盘里面,每当机器下线、网络连接断开或者某些需要连接其他服务器的情况出现时,程序通常需要一次性地对不同服务器中的多个配置文件进行更新。而这一节要介绍的就是如何将大部分配置信息从文件转移到Redis里面,使得应用程序可以自己完成绝大部分配置工作。

使用Redis存储配置信息

为了展示配置管理方面的难题是多么的常见,来看一个非常简单的配置例子:假设现在我们要用一个标志flag来表示web服务器是否正在进行维护,如果服务器正在进行维护,那么它就不应该发送数据库请求,而是应该向访问们返回一条简短的【抱歉,我们正在进行维护,请稍后重试】的信息;相反,如果服务器并没有进行维护,那么它就应该按照既定的程序来运行。

在通常情况下,即使只更新配置中的一个标志,也会导致更新后的配置文件被强制推送至所有Web服务器,收到更新的服务器可能需要重新载入配置、甚至可能还要重启应用程序服务器。

与其尝试为不断增多的服务写入和维护配置文件,不如让我们直接配置写入Redis里面。只要将配置信息存储在Redis里面,并编写应用程序来获取这些信息,我们就不用再编写工具来向服务器推送配置信息了,服务器和程序也不用再通过载入配置文件的方式 来更新配置信息了。

为了实现这个简单的功能,让我们假设自己已经构建了一个中间层或者插件,这个中间层额作用在于:当is_under_maintenance()函数返回True时,它将向用户显示维护页面;与此相反,如何is_under_maintenance()函数返回False,它将如常地处理用户的访问请求。其中is_under_maintenance()函数通过检查一个名为is-under-maintenance的键来判断服务器是否正在进行维护:如果is-under-maintenance键非空,那么函数返回True;否则返回False,另外,因为访客在看见维护页面的时候通常都会不耐烦的频繁刷新页面,所以为了尽量降低Redis在处理高访问量Web服务器时的负载,is_under_maintenance()函数最多只会每秒更新一次服务器维护信息。

下面代码展示了is_under_maintenance()函数的具体定义:

import time

LAST_CHECKED=None
IS_UNDER_MAINTENANCE=False

def is_under_maintenance(conn):
    #将连个变量设置为全局变量以便在之后对它们进行写入
    global LAST_CHECKED,IS_UNDER_MAINTENANCE
    #距离上次检查是否以及超过1秒?
    if LAST_CHECKED<time.time()-1:
        #更新最后检查时间
        LAST_CHECKED=time.time()
        #检查系统是否正在进行维护
        IS_UNDER_MAINTENANCE=bool(conn.get('is-under-maintenance'))
    #返回一个布尔值,用于表示系统是否正在进行维护。
    return IS_UNDER_MAINTENANCE

通过将is_under_maintenance()函数插入应用程序的正确位置上,我们可以在1秒内改变数以千计Web服务器的行为。为了降低Redis在处理高访问量web服务器时的负载,is_under_maintenance()函数将服务器维护状态信息的更新频率限制为最多每秒1次,但如果有需要的话,我们也可以加快信息的更新频率,甚至直接移除函数里面限制更新速度的那些代码。虽然is_under_maintenance()函数看上去似乎并不实用,但它的确展示了将配置信息存储在一个普通可访问位置的威力。

接下来我们要考虑的是,怎样才能将更复杂的配置选项存储到Redis里面呢?

为每个应用程序组件分别配置一个Redis服务器

在我们越来越多地使用Redis的过程中,无数的开发者已经发现,最终在某个时间点上,只使用一台Redis服务器将不能满足我们的需要。因为我们可能需要记录更多信息,可能需要更多用于缓存的空间,还可能会使用本书之后的章节会介绍的、使用Redis构建的高级服务。但不管何种原因,我们都需要用到更多Redis服务器。

为了平滑地从单台服务器过渡到多台服务器,用户最好还是为应用程序中的每个独立部分都分别运行一个Redis服务器,比如说,一个专门负责记录日志、一个专门负责记录统计数据、一个专门负责进行缓存、一个专门负责存储cookies等。别忘了,一台机器是可以运行多个Redis服务器的,只要这些服务器使用的端口号各不同就可以了。除此之外,在一个Redis服务器里面使用多个【数据库】,也可以减少系统管理的工作量。以上提到的两种方法,都是通过将不同数据划分至不同键空间的方式,来或多或少的简化迁移至更大或更多服务器时所需的工作。但遗憾的是,随着Redis服务器的数量或者Redis数据库的数量不断增多,为所有Redis服务器管理和分发配置信息的工作将变得越来越烦琐和无趣。

在上一节中,我们用了Redis来存储表示服务器是否正在进行维护的标志,并通过这个标志来决定是否需要向访客显示维护页面。而这一次,我们同样可以使用Redis来存储与其他Redis服务器有关的信息。说的更详细一点,我们可以把一个已知的Redis服务器用作配置信息字典,然后通过这个字典存储的配置信息来连接为不同应用或服务组件提供数据的其他Redis服务器。此外,这个字典还会在配置出现变更时,帮助客户端连接至正确的服务器。字典的具体实现比这个例子所要求的更为通用一些,因为我敢肯定,当你开始使用这个字典来获取配置信息的时候,你很快就会把它应用到其他服务器以及其他服务上面,而不仅仅用于获取Redis服务器的配置信息。

我们将构建一个函数,该函数可以从一个键里面取出一个JSON编码的配置值,其中,存储配置值的键由服务的类型以及使用该服务的应用程序命名。举个例子,如何我们想要获取连接存储统计数据的Redis服务器所需的信息,那么就需要获取config:redis:statistics键的值。下面函数展示了设置配置值的具体方法:

def set_config(conn,type,component,config):
    conn.set('config:%s:%s'%(type,component))
    json.dumps(config)

通过这个函数,我们可以随心所欲的设置任何JSON编码的配置信息。因为get_config()函数和前面介绍过的is_under__maintenance()函数具有相似的结构,所以我们只要在语义上稍作修改,就可以使用get_config()函数来替代is__under_maintenance()函数。下面代码列出了与set_config()相对应的get_config()函数,这个函数可以按照用户的需要,对配置信息进行0秒、1秒或者10秒的局部缓存。

import json
import time

CONFIGS={}
CHECKED={}

def get_config(conn,type,component,wait=1):
    key='config:%s:%s'%(type,component)
    #检查是否需要对这个组件的信息进行更新
    if CHECKED.get(key)<time.time()-wait:
        #有需要对配置进行更新,记录最后一次检查这个连接的时间
        CHECKED[key]=time.time()
        #取得Redis存储的组件配置
        config=json.loads(conn.get(key) or '{}')
        #将潜在的Unicode关键字参数转换为字符串的关键字参数
        config=dict((str(k),config[k]) for k in config)
        #取得组件正在使用的配置
        old_cofig=CONFIGS.get(key)
        #如果两个配置并不相同
        if config!=old_cofig:
            #那么对组件的配置进行更新
            CONFIGS[key]=config

    return CONFIGS.get(key)

在拥有了配置信息和获取配置信息的两个函数之后,我们还可以在此之上更近一步。我们在前面一直考虑的都是怎样存储和获取配置信息以便连接各个不同的Redis服务器,但直到目前为止,我们编写的绝大多数函数和第一个参数都是一个连接参数。因此,为了不再需要手动获取我们正在使用的各项服务的连接,下面让我们来构建一个能够帮助我们自动连接这些服务的方法。

自动Redis连接管理

手动创建和传递Redis连接并不是一件容易地事情,这不仅是因为我们需要重复查阅配置信息,还有一个原因就是,即使使用了 上一节介绍的配置管理函数,我们还是需要获取配置、连接Redis,并在使用完连接之后关闭连接。为了简化连接的管理操作,我们将编写一个装饰器,让它负责连接除配置服务器之外的所有其他Redis服务器。

装饰器

Python提供了一种语法,用于将函数X传入另一个函数Y的内部,其中函数Y就被成为装饰器。装饰器给用户提供了一个修改函数X行为的机会。有些装饰器可以用于校验参数,而有些装饰器则可以用于注册回调函数,甚至还有一些装饰器可以用于管理连接:就像我们接下来要做的那样。

下面代码展示了我们定义的装饰器,它接受一个指定的配置作为参数并生成一个包装器,这个包装器可以包裹一个函数,使得之后对被包裹函数的调用可以自动连接至正确的Redis服务器,并且连接Redis服务器所使用的那个连接会和用户之后提供的其他参数一同传递至包裹的函数:

REDIS_CONNECTIONS={}

#将应用组件的名字传递给装饰器
def redis_connection(component,wait=1):
    #因为函数每次被调用都需要获取这个配置键,所以我们干脆把它缓存起来
    key='config:redis:'+component
    #包装器接受一个函数作为参数,并使用另一个函数来包裹这个函数。
    def wrapper(function):
        #将被包裹函数的一些有用的元数据复制给配置处理器。
        @function.wraps(function)
        def call(*args,**kwargs):#创建负责管理连接信息的函数
            #如果有就配置存在,那么获取它
            old_config=CONFIGS.get(key,object())
            #如果有新配置存在,那么获取它
            _config=get_config(config_connection,'redis',component,wait)

            config={}
            #对配置进行处理并将其用于创建Redis连接
            for k,v in _config.iteritems():
                config[k.encode('utf-8')]=v

            #如果新旧配置并不相同,那么创建新的连接
            if config!=old_config:
                REDIS_CONNECTIONS[key]=redis.Redis(**config)

            #将Redis连接以及其他匹配的参数传递给包裹函数,然后调用该函数并返回它的执行结果。
            return function(REDIS_CONNECTIONS.get(key),*args,**kwargs)
        #返回被包裹的函数
        return call
    #返回用于包裹Redis函数的包装器
    return wrapper
同时使用*args和**kwargs

在Python中,函数定义的args变量用于获取所有位置参数,而kwargs变量则用于获取所有命令出纳和素,这两种参数传递方式都可以将给定的参数传入被调用的函数里面。

上面战术的一系列嵌套函数初看上去可能会让人感动头昏目眩,但它们实际上并没有想象中的那么复杂。redis_connection()装饰器接受一个应用组件的名字作为参数并返回一个包装器。这个包装器接受一个我们想要将连接传递给它的函数为参数,然后对函数进行包裹并返回被包裹函数的调研器。这个调用器负责处理所有获取配置信息的工作,除此之外,它还负责连接Redis服务器并调用被包裹的函数。尽管redis_connecition()函数描述起来相当复杂,但实际使用起来却是非常方便的,下面代码就展示了怎样将redis_connection()函数应用到之间介绍的log_recent()函数上面。


@redis_connection('logs')
def log_recent(conn,app,message,severity=logging.INFO,pipe=None):
    # 尝试将日志的安全级别准还为简单的字符串
    severity = str(SEVERITY.get(severity, severity)).lower()
    # 创建负责存储消息的键
    destination = 'recent:%s:%s' % (name, severity)
    # 将当前时间添加到消息里面,用于记录消息的发送时间
    message = time.asctime() + '  ' + message
    # 使用流水线来将通信往返次数降低为一次
    pipe = pipe or conn.pipeline()
    # 将消息添加到日志列表的最前面
    pipe.lpush(destination, message)
    # 对日志列表进行修建,让它只包含最新的100条消息
    pipe.ltrim(destination, 0, 99)
    # 执行两个命令
    pipe.execute()

log_recent('main','User 235 logged in')

现在你已经看到怎样使用redis_connection()来装饰log_recent()函数,这个装饰器还是蛮有用的,不是吗?通过使用这个改良后的方法来处理链接和配置,我们几乎可以把我们要调用的所有函数的代码都删去好几行。

作为练习,请尝试使用redis_connection()去装饰之前介绍的access_time()上下文管理器,使得这个上下文管理器可以在不必手动传递Redis服务器连接的情况下执行。

本章小结

本章介绍的所有主题都直接或间接地用于对应用程序进行帮助和支持,这里展示的函数和装饰器都旨在帮助读者学会如何使用Redis来支撑应用程序的不用部分:日志、计数器以及统计数据可以帮助用户直观地了解应用程序的性能,而IP所属地查找程序则可以告诉你客户所在的地点。除此之外,存储服务的发现和配置信息可以帮助我们减少大量需要手动处理连接的工作。

现在我们已经知道了怎样使用Redis来对应用程序进行支持了,在接下来的第6章,我们将学习如何使用Redis来构建应用程序组件。

上一篇文章:Python--Redis实战:第五章:使用Redis构建支持程序:第3节:查找IP所属城市以及国家

Mark
662 声望344 粉丝

talk is cheap,show me the code