Redis实战(三)

Redis数据安全与性能保障

内存数据存储到硬盘中的主要原因是重用数据,为了防止系统故障而将数据备份,降低重复计算的成本。

持久化方法方式
快照snapshootting将存在于某一时刻的全部数据都存入到硬盘里
只追加文件AOF(append-only file)在执行写命令时,将被执行的命令复制到硬盘里面
save 60 1000                        # 快照持久化选项。
stop-writes-on-bgsave-error no      #
rdbcompression yes                  #
dbfilename dump.rdb                 #

appendonly no                       # 只追加文件持久化选项。
appendfsync everysec                #
no-appendfsync-on-rewrite no        #
auto-aof-rewrite-percentage 100     #
auto-aof-rewrite-min-size 64mb      #

dir ./                              # 共享选项,这个选项决定了快照文件和只追加文件的保存位置。
快照持久化

依据配置,快照将被写入dbfilename指定的文件里并存储在dir选项指定的路径上。

创建快照的方式
客户端发送BGSAVE命令,redis会调用fork来创建一个子进程,然后子进程负责将快照写入硬盘而父进程继续处理命令
客户端发送SAVE命令,redis在快照创建完成之前不再相应其他命令(通常在没有足够内存去执行BGSAVE的情况下或者等待持久化操作完毕也无所谓的情况下执行)
配置SAVE 60 10000意为60秒内有10000此请求时redis自动触发BGSAVE命令
redis通过SHUTDOWN命令接收到关闭服务器的请求时或收到标准term信号(TERM是请求彻底终止某项执行操作.它期望接收进程清除自给的状态并退出)的时候会执行SAVE操作,执行完毕后关闭服务器
一台redis服务器连接另一台redis服务器,并向对方发送sync命令来一次复制操作的时候,主服务器就会来一次BGSAVE操作

若系统崩溃,用户将丢失最近一次生成快照之后更改的所有数据。

快照持久化案例
  • 个人开发服务器

    为尽可能降低资源消耗。如果服务器距离上次生成快照已经超过了900秒并至少有一次写入操作,就来一次BGSAVE操作。

  • 对日志进行聚合操作

    通过将日志的处理进度记录到redis里面,程序可以在系统崩溃之后依据进度记录继续执行之前未完成的处理工作

    # 日志处理函数接受的其中一个参数为回调函数,
    # 这个回调函数接受一个Redis连接和一个日志行作为参数,
    # 并通过调用流水线对象的方法来执行Redis命令。
    def process_logs(conn, path, callback):
        # 获取文件当前的处理进度。
        current_file, offset = conn.mget( 
            'progress:file', 'progress:position') 
        pipe = conn.pipeline()
    
        # 通过使用闭包(closure)来减少重复代码
        def update_progress():    
            # 更新正在处理的日志文件的名字和偏移量。
            pipe.mset({
                'progress:file': fname,
                'progress:position': offset 
            })
            # 这个语句负责执行实际的日志更新操作,
            # 并将日志文件的名字和目前的处理进度记录到Redis里面。
            pipe.execute()
    
        # 有序地遍历各个日志文件。
        for fname in sorted(os.listdir(path)): 
            # 略过所有已处理的日志文件。
            if fname < current_file:
                continue
            inp = open(os.path.join(path, fname), 'rb')
            # 在接着处理一个因为系统崩溃而未能完成处理的日志文件时,略过已处理的内容。
            if fname == current_file:
                inp.seek(int(offset, 10)) 
            else:
                offset = 0
            current_file = None
            # 枚举函数遍历一个由文件行组成的序列,
            # 并返回任意多个二元组,
            # 每个二元组包含了行号lno和行数据line,
            # 其中行号从0开始。
            for lno, line in enumerate(inp):
                # 处理日志行。
                callback(pipe, line)
                # 更新已处理内容的偏移量。
                offset += int(offset) + len(line) 
                # 每当处理完1000个日志行或者处理完整个日志文件的时候,
                # 都更新一次文件的处理进度。
                if not (lno+1) % 1000: 
                    update_progress()
            update_progress()
            inp.close()
    
  • 大数据

    当redis占用内存比较大的时候,快照时间和不同OS的开销都非常大

AOF持久化

简单来说aof持久化会将被执行的写命令写到aof文件的末尾,以此来记录数据发生的变化。

文件写入:file.write() ->文件内容存储到缓存区->OS在某一时刻写入硬盘/调用file.flush()尽快写入

我们可以通过appendonly yes配置打开AOF持久化,下图是appendsync配置选项对AOF文件的同步频率的影响

选项同步频率
always每个redis写明亮都要写入硬盘
everysec每秒执行一次同步,显示地将多个写命令同步到硬盘
no让OS来决定
重写/压缩AOF文件

BGREWRITEAOF命令会移除AOF文件里的冗余命令,和BGSAVE类似,是通过子进程实现的。

复制
  • 复制可以让其他服务器拥有一个不断更新的数据副本,从而使得拥有数据副本的服务器可以处理客户端发送的读请求
命令描述
slaveof host port依据指定的主机和端口号连接主服务器
slaveof no one对于正在运行的redis服务器,使其终止复制,不接受主服务器数据更新
  • 复制的启动过程
步骤主服务器从服务器
1(等待命令进入)连接(重连接)主服务器,发送sync命令
2执行BGSAVE,并用缓存记录BGSAVE之后执行的所有写命令依据配置选项来决定是继续使用现有的数据处理客户端的命令请求还是向发送请求的客户端返回错误
3BGSAVE执行完毕,向从服务器发送快照,并用缓存记录发送期间被执行的写命令丢弃旧数据,开始载入快照
4快照发送完毕,发送缓存中记录的写命令完成对快照的解释,开始接受命令请求
5缓存中写命令发送完毕,现在开始每执行一个写命令就向服务器发送相同的写命令执行发送过来的写命令,现在开始接受并执行主服务器传来的每个写命令
  • 主从复制负载问题及解决方案

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c89AaOEg-1585137335234)(C:\Users\zhousheng42\AppData\Roaming\Typora\typora-user-images\image-20200120151610363.png)]

  • 检验硬盘写入

    检查info命令的输出结果中aof_pending_bio_fsync属性的值是否为0,如果是的活就表明服务器已经将已知的所有数据都保存到硬盘里了。info命令非常有用。

def wait_for_sync(mconn, sconn):
    identifier = str(uuid.uuid4())
    # 将令牌添加至主服务器。
    mconn.zadd('sync:wait', identifier, time.time()) 

    # 如果有必要的话,等待从服务器完成同步。
    while sconn.info()['master_link_status'] != 'up': 
        time.sleep(.001)

    # 等待从服务器接收数据更新。
    while not sconn.zscore('sync:wait', identifier): 
        time.sleep(.001)

    # 最多只等待一秒钟。
    deadline = time.time() + 1.01  
    while time.time() < deadline: 
        # 检查数据更新是否已经被同步到了磁盘。
        if sconn.info()['aof_pending_bio_fsync'] == 0:
            break
        time.sleep(.001)

    # 清理刚刚创建的新令牌以及之前可能留下的旧令牌。
    mconn.zrem('sync:wait', identifier)
    mconn.zremrangebyscore('sync:wait', 0, time.time()-900) 
处理系统故障
  • 验证快照文件和AOF文件

    • 无论是快照持久化或者是AOF持久化,redis-check-aof和redis-check-dump可以在系统故障后检查aof文件和快照文件的状态,并在有需要的情况下对文件进行修复。

    • 若给定–fix参数,系统将对aof文件进行修复,修复的方式是扫描文件直到发现第一个错误命令,然后删除掉错误命令及之后的所有命令。

    • 出错的快照无法被修复

    • 用户在进行数据恢复的时候,通过计算快照文件的SHA1散列值和SHA256散列值来对内容进行验证

  • 更换故障主服务器

    • A主B从,A炸了,C替换A作为主服务器。首先向B发送一个SAVE命令,然后把快照发送给C,最后B成为C的从服务器即可
    • 下面展示一下更换服务器时用到的各个命令
user@vpn-master ~:$ ssh root@machine-b.vpn                          # 通过VPN网络连接机器B。
Last login: Wed Mar 28 15:21:06 2012 from ...                       #
root@machine-b ~:$ redis-cli                                        # 启动命令行Redis客户端来执行几个简单的操作。
redis 127.0.0.1:6379> SAVE                                          # 执行SAVE命令,
OK                                                                  # 并在命令完成之后,
redis 127.0.0.1:6379> QUIT                                          # 使用QUIT命令退出客户端。
root@machine-b ~:$ scp \\                                           # 将快照文件发送至新的主服务器——机器C。
> /var/local/redis/dump.rdb machine-c.vpn:/var/local/redis/         #
dump.rdb                      100%   525MB  8.1MB/s   01:05         #
root@machine-b ~:$ ssh machine-c.vpn                                # 连接新的主服务器并启动Redis。
Last login: Tue Mar 27 12:42:31 2012 from ...                       #
root@machine-c ~:$ sudo /etc/init.d/redis-server start              #
Starting Redis server...                                            #
root@machine-c ~:$ exit
root@machine-b ~:$ redis-cli                                        # 告知机器B的Redis,让它将机器C用作新的主服务器。
redis 127.0.0.1:6379> SLAVEOF machine-c.vpn 6379                    #
OK                                                                  #
redis 127.0.0.1:6379> QUIT
root@machine-b ~:$ exit
user@vpn-master ~:$
# <end id="master-failover"/>
#A Connect to machine B on our vpn network
#B Start up the command line redis client to do a few simple operations
#C Start a SAVE, and when it is done, QUIT so that we can continue
#D Copy the snapshot over to the new master, machine C
#E Connect to the new master and start Redis
#F Tell machine B's Redis that it should use C as the new master
#END
Redis事务
  • MULTI ---- EXEC

  • 多个事务处理一个对象需要用到二阶提交(two-phase commit)

  • 流水线pipeline,通过减少客户端与redis服务器之间的网络通信次数来提升redis在执行多个命令时的性能

  • redis没有像关系型数据库对被访问的数据加锁,因为持有锁的客户端运行越慢等待解锁的客户端被阻塞的时间就越长。因此redis并不会对watch加锁,只会在数据已经被其他客户端抢先修改了的情况下,通知执行了watch命令的客户端(即乐观锁)。客户端不必花时间去等待第一个获得锁的客户端——它们只需要在自己的事务执行失败时进行重试就可以了。

    乐观锁:认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让用户返回错误的信息。让用户决定如何去做。

    悲观锁:对数据的冲突采取一种悲观的态度,也就是说假设数据肯定会冲突,所以在数据开始读取的时候就把数据锁定住。【数据锁定:数据将暂时不会得到修改】

  • 大部分redis客户端库都提供了某种级别的内置连接池(connection pool),有部分管理连接的功能。

  • 事务案例分析

    • 上架商品时,添加商品至在售商品的有序集合,并且在添加的过程中监视卖家的包裹以确保被销售的商品的确存在于卖家的包裹中

      def list_item(conn, itemid, sellerid, price):
          inventory = "inventory:%s"%sellerid
          item = "%s.%s"%(itemid, sellerid)
          end = time.time() + 5
          pipe = conn.pipeline()
          while time.time() < end:
              try:
                  # 监视用户包裹发生的变化。
                  pipe.watch(inventory)
                  # 验证用户是否仍然持有指定的物品。
                  if not pipe.sismember(inventory, itemid):
                      # 如果指定的物品不在用户的包裹里面,
                      # 那么停止对包裹键的监视并返回一个空值。
                      pipe.unwatch() 
                      return None
                  # 将指定的物品添加到物品买卖市场里面。
                  pipe.multi()
                  pipe.zadd("market:", item, price) 
                  pipe.srem(inventory, itemid)
                  # 如果执行execute方法没有引发WatchError异常,
                  # 那么说明事务执行成功,
                  # 并且对包裹键的监视也已经结束。
                  pipe.execute()   
                  return True
              # 用户的包裹已经发生了变化;重试。
              except redis.exceptions.WatchError: 
                  pass
          return False
      
    • 购买一件商品

      def purchase_item(conn, buyerid, itemid, sellerid, lprice):
          buyer = "users:%s"%buyerid
          seller = "users:%s"%sellerid
          item = "%s.%s"%(itemid, sellerid)
          inventory = "inventory:%s"%buyerid
          end = time.time() + 10
          pipe = conn.pipeline()
      
          while time.time() < end:
              try:
                  # 对物品买卖市场以及买家账号信息的变化进行监视。
                  pipe.watch("market:", buyer)
      
                  # 检查指定物品的价格是否出现了变化,
                  # 以及买家是否有足够的钱来购买指定的物品。
                  price = pipe.zscore("market:", item) 
                  funds = int(pipe.hget(buyer, "funds"))  
                  if price != lprice or price > funds:  
                      pipe.unwatch()   
                      return None
      
                  # 将买家支付的货款转移给卖家,并将卖家出售的物品移交给买家。
                  pipe.multi()
                  pipe.hincrby(seller, "funds", int(price)) 
                  pipe.hincrby(buyer, "funds", int(-price)) 
                  pipe.sadd(inventory, itemid)  
                  pipe.zrem("market:", item)  
                  pipe.execute()      
                  return True
              # 如果买家的账号或者物品买卖市场出现了变化,那么进行重试。
              except redis.exceptions.WatchError:
                  pass
          return False
      
    • 用流水线(非事务型)完善更新token方法

      # 不采用流水线方式
      def update_token(conn, token, user, item=None):
          # 获取时间戳。
          timestamp = time.time()
          # 创建令牌与已登录用户之间的映射。
          conn.hset('login:', token, user)
          # 记录令牌最后一次出现的时间。
          conn.zadd('recent:', token, timestamp)
          if item:
              # 把用户浏览过的商品记录起来。
              conn.zadd('viewed:' + token, item, timestamp) 
              # 移除旧商品,只记录最新浏览的25件商品。
              conn.zremrangebyrank('viewed:' + token, 0, -26) 
              # 更新给定商品的被浏览次数。
              conn.zincrby('viewed:', item, -1) 
      
      # 采用流水线方式
      def update_token_pipeline(conn, token, user, item=None):
          timestamp = time.time()
          # 设置流水线。
          pipe = conn.pipeline(False)                         #A
          pipe.hset('login:', token, user)
          pipe.zadd('recent:', token, timestamp)
          if item:
              pipe.zadd('viewed:' + token, item, timestamp)
              pipe.zremrangebyrank('viewed:' + token, 0, -26)
              pipe.zincrby('viewed:', item, -1)
          # 执行那些被流水线包裹的命令。
          pipe.execute()                                      #B
      
      # 让我们对比测试一下采用流水线前后的区别
      def benchmark_update_token(conn, duration):
          # 测试会分别执行update_token()函数和update_token_pipeline()函数。
          for function in (update_token, update_token_pipeline): 
              # 设置计数器以及测试结束的条件。
              count = 0                                               #B
              start = time.time()                                     #B
              end = start + duration                                  #B
              while time.time() < end:
                  count += 1
                  # 调用两个函数的其中一个。
                  function(conn, 'token', 'user', 'item')             #C
              # 计算函数的执行时长。
              delta = time.time() - start                             #D
              # 打印测试结果。
              print function.__name__, count, delta, count / delta    #E
      
  • 性能测试

    命令描述
    redis-benchmarkredis附带的性能测试程序
  • 问题排查

    性能或错误可能的原因解决方法
    单个客户端的性能达到redis-bnchmark的50%~60%不使用流水线的预期性能
    单个客户端的性能达到redis-bnchmark的25%~30%对于每个命令或每组命令都创建了新的连接重用已有的连接
    客户端返回错误“Cannot assign requested address”对于每个命令或每组命令都创建了新的连接重用已有的连接

    在2.4GHz处理器上运行redis-benchmark

    $ redis-benchmark  -c 1 -q                               # 给定“-q”选项可以让程序简化输出结果,
    PING (inline): 34246.57 requests per second              # 给定“-c 1”选项让程序只使用一个客户端来进行测试。
    PING: 34843.21 requests per second
    MSET (10 keys): 24213.08 requests per second
    SET: 32467.53 requests per second
    GET: 33112.59 requests per second
    INCR: 32679.74 requests per second
    LPUSH: 33333.33 requests per second
    LPOP: 33670.04 requests per second
    SADD: 33222.59 requests per second
    SPOP: 34482.76 requests per second
    LPUSH (again, in order to bench LRANGE): 33222.59 requests per second
    LRANGE (first 100 elements): 22988.51 requests per second
    LRANGE (first 300 elements): 13888.89 requests per second
    LRANGE (first 450 elements): 11061.95 requests per second
    LRANGE (first 600 elements): 9041.59 requests per second
    # <end id="redis-benchmark"/>
    #A We run with the '-q' option to get simple output, and '-c 1' to use a single client
    #END
    

使用redis构建支持程序

使用redis记录日志
一般记录日志的方式描述
文件随时间流逝不断添加日志行到文件里
syslog514号tcp和udp端口上接受其他程序发来的日志消息并存储到硬盘的文件里
  • 将最新的日志记录到redis里
# 设置一个字典,它可以帮助我们将大部分日志的安全级别转换成某种一致的东西。
SEVERITY = {                                                   
    logging.DEBUG: 'debug',                                    
    logging.INFO: 'info',                                      
    logging.WARNING: 'warning',                                
    logging.ERROR: 'error',                                    
    logging.CRITICAL: 'critical',                              
}                                                              
SEVERITY.update((name, name) for name in SEVERITY.values())    

def log_recent(conn, name, 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()      
  • 记录并轮换最常见的轮换日志消息的方法

    def log_common(conn, name, message, severity=logging.INFO, timeout=5):
        # 设置日志的级别。
        severity = str(SEVERITY.get(severity, severity)).lower() 
        # 负责存储最新日志的键。
        destination = 'common:%s:%s'%(name, severity)
        # 因为程序每小时需要轮换一次日志,所以它使用一个键来记录当前所处的小时数。
        start_key = destination + ':start'
        pipe = conn.pipeline()
        end = time.time() + timeout
        while time.time() < end:
            try:
                # 对记录当前小时数的键进行监视,确保轮换操作可以正确地执行。
                pipe.watch(start_key)
                # 取得当前时间。
                now = datetime.utcnow().timetuple()
                # 取得当前所处的小时数。
                hour_start = datetime(*now[:4]).isoformat() 
    
                existing = pipe.get(start_key)
                # 创建一个事务。
                pipe.multi()
                # 如果目前的常见日志列表是上一个小时的……
                if existing and existing < hour_start: 
                    # ……那么将旧的常见日志信息进行归档。
                    pipe.rename(destination, destination + ':last')
                    pipe.rename(start_key, destination + ':pstart') 
                    # 更新当前所处的小时数。
                    pipe.set(start_key, hour_start)
    
                # 对记录日志出现次数的计数器执行自增操作。
                pipe.zincrby(destination, message) 
                # log_recent()函数负责记录日志并调用execute()函数。
                log_recent(pipe, name, message, severity, pipe) 
                return
            except redis.exceptions.WatchError:
                # 如果程序因为其他客户端在执行归档操作而出现监视错误,那么重试。
                continue
    
  • 计数器和统计数据

    • 更新计数器

      # 以秒为单位的计数器精度,分别为1秒钟、5秒钟、1分钟、5分钟、1小时、5小时、1天——用户可以按需调整这些精度。
      PRECISION = [1, 5, 60, 300, 3600, 18000, 86400]         #A
      
      def update_counter(conn, name, count=1, now=None):
          # 通过取得当前时间来判断应该对哪个时间片执行自增操作。
          now = now or time.time() 
          # 为了保证之后的清理工作可以正确地执行,这里需要创建一个事务型流水线。
          pipe = conn.pipeline() 
          # 为我们记录的每种精度都创建一个计数器。
          for prec in PRECISION:
              # 取得当前时间片的开始时间。
              pnow = int(now / prec) * prec
              # 创建负责存储计数信息的散列。
              hash = '%s:%s'%(prec, name)
              # 将计数器的引用信息添加到有序集合里面,
              # 并将其分值设置为0,以便在之后执行清理操作。
              pipe.zadd('known:', hash, 0)
              # 对给定名字和精度的计数器进行更新。
              pipe.hincrby('count:' + hash, pnow, count) 
          pipe.execute()
      
    • 读取计数器

      def get_counter(conn, name, precision):
          # 取得存储着计数器数据的键的名字。
          hash = '%s:%s'%(precision, name)
          # 从Redis里面取出计数器数据。
          data = conn.hgetall('count:' + hash) 
          # 将计数器数据转换成指定的格式。
          to_return = []
          for key, value in data.iteritems():
              to_return.append((int(key), int(value))) 
          # 对数据进行排序,把旧的数据样本排在前面。
          to_return.sort() 
          return to_return
      
    • 清理旧计数器

      • expire命令的一个限制是它只能应用于整个键而不能只对键的某一部分数据进行过期处理。
      • 任何时候都会有新的计数器被添加进来
      • 同一时间可能会有多个不同的清理操作在执行
      • 对于一个每天更新一次的计数器,每分钟一次的频率清理这个计数器就太浪费了
      • 空计数器不应被清理
      def clean_counters(conn):
          pipe = conn.pipeline(True)
          # 为了平等地处理更新频率各不相同的多个计数器,程序需要记录清理操作执行的次数。
          passes = 0
          # 持续地对计数器进行清理,直到退出为止。
          while not QUIT:
              # 记录清理操作开始执行的时间,用于计算清理操作执行的时长。
              start = time.time()
              # 渐进地遍历所有已知的计数器。
              index = 0
              while index < conn.zcard('known:'):
                  # 取得被检查计数器的数据。
                  hash = conn.zrange('known:', index, index)
                  index += 1
                  if not hash:
                      break
                  hash = hash[0]
                  # 取得计数器的精度。
                  prec = int(hash.partition(':')[0])
                  # 因为清理程序每60秒钟就会循环一次,
                  # 所以这里需要根据计数器的更新频率来判断是否真的有必要对计数器进行清理。
                  bprec = int(prec // 60) or 1
                  # 如果这个计数器在这次循环里不需要进行清理,
                  # 那么检查下一个计数器。
                  # (举个例子,如果清理程序只循环了三次,而计数器的更新频率为每5分钟一次,
                  # 那么程序暂时还不需要对这个计数器进行清理。)
                  if passes % bprec:
                      continue
      
                  hkey = 'count:' + hash
                  # 根据给定的精度以及需要保留的样本数量,
                  # 计算出我们需要保留什么时间之前的样本。
                  cutoff = time.time() - SAMPLE_COUNT * prec 
                  # 获取样本的开始时间,并将其从字符串转换为整数。
                  samples = map(int, conn.hkeys(hkey))
                  # 计算出需要移除的样本数量。
                  samples.sort()
                  remove = bisect.bisect_right(samples, cutoff) 
      
                  # 按需移除计数样本。
                  if remove:
                      conn.hdel(hkey, *samples[:remove]) 
                      # 这个散列可能已经被清空。
                      if remove == len(samples):
                          try:
                              # 在尝试修改计数器散列之前,对其进行监视。
                              pipe.watch(hkey)
                              # 验证计数器散列是否为空,如果是的话,
                              # 那么从记录已知计数器的有序集合里面移除它。
                              if not pipe.hlen(hkey):
                                  pipe.multi()
                                  pipe.zrem('known:', hash)  
                                  pipe.execute()
                                  # 在删除了一个计数器的情况下,
                                  # 下次循环可以使用与本次循环相同的索引。
                                  index -= 1
                              else:
                                  # 计数器散列并不为空,
                                  # 继续让它留在记录已有计数器的有序集合里面。
                                  pipe.unwatch()
                          # 有其他程序向这个计算器散列添加了新的数据,
                          # 它已经不再是空的了,继续让它留在记录已知计数器的有序集合里面。
                          except redis.exceptions.WatchError:
                              pass
      
              # 为了让清理操作的执行频率与计数器更新的频率保持一致,
              # 对记录循环次数的变量以及记录执行时长的变量进行更新。
              passes += 1 
              duration = min(int(time.time() - start) + 1, 60)  
              # 如果这次循环未耗尽60秒钟,那么在余下的时间内进行休眠;
              # 如果60秒钟已经耗尽,那么休眠一秒钟以便稍作休息。
              time.sleep(max(60 - duration, 1))   
      
    • 存储统计数据

      def update_stats(conn, context, type, value, timeout=5):
          # 设置用于存储统计数据的键。
          destination = 'stats:%s:%s'%(context, type) 
          # 像common_log()函数一样,
          # 处理当前这一个小时的数据和上一个小时的数据。
          start_key = destination + ':start'
          pipe = conn.pipeline(True)
          end = time.time() + timeout
          while time.time() < end:
              try:
                  pipe.watch(start_key) 
                  now = datetime.utcnow().timetuple() 
                  hour_start = datetime(*now[:4]).isoformat() 
      
                  existing = pipe.get(start_key)
                  pipe.multi()
                  if existing and existing < hour_start:
                      pipe.rename(destination, destination + ':last') 
                      pipe.rename(start_key, destination + ':pstart') 
                      pipe.set(start_key, hour_start)
      
                  tkey1 = str(uuid.uuid4())
                  tkey2 = str(uuid.uuid4())
                  # 将值添加到临时键里面。
                  pipe.zadd(tkey1, 'min', value)
                  pipe.zadd(tkey2, 'max', value)                     
                  # 使用合适聚合函数MIN和MAX,
                  # 对存储统计数据的键和两个临时键进行并集计算。
                  pipe.zunionstore(destination,                     
                      [destination, tkey1], aggregate='min')          
                  pipe.zunionstore(destination,                      
                      [destination, tkey2], aggregate='max')        
      
                  # 删除临时键。
                  pipe.delete(tkey1, tkey2)                           
                  # 对有序集合中的样本数量、值的和、值的平方之和三个成员进行更新。
                  pipe.zincrby(destination, 'count')                  
                  pipe.zincrby(destination, 'sum', value)            
                  pipe.zincrby(destination, 'sumsq', value*value)    
      
                  # 返回基本的计数信息,以便函数调用者在有需要时做进一步的处理。
                  return pipe.execute()[-3:]                        
              except redis.exceptions.WatchError:
                  # 如果新的一个小时已经开始,并且旧的数据已经被归档,那么进行重试。
                  continue       
      
    • 程序取出统计数据

      def get_stats(conn, context, type):
          # 程序将从这个键里面取出统计数据。
          key = 'stats:%s:%s'%(context, type)                            
          # 获取基本的统计数据,并将它们都放到一个字典里面。
          data = dict(conn.zrange(key, 0, -1, withscores=True))            
          # 计算平均值。
          data['average'] = data['sum'] / data['count']                     
          # 计算标准差的第一个步骤。
          numerator = data['sumsq'] - data['sum'] ** 2 / data['count']       
          # 完成标准差的计算工作。
          data['stddev'] = (numerator / (data['count'] - 1 or 1)) ** .5      
          return data
      
  • 查找IP所属城市及国家

    # 这个视图(view)接受一个Redis连接以及一个生成内容的回调函数为参数。
    def process_view(conn, callback):             
        # 计算并记录访问时长的上下文管理器就是这样包围代码块的。
        with access_time(conn, request.path):     
            # 当上下文管理器中的yield语句被执行时,这个语句就会被执行。
            return callback()                      
    
    def ip_to_score(ip_address):
        score = 0
        for v in ip_address.split('.'):
            score = score * 256 + int(v, 10)
        return score
    
    # 这个函数在执行时需要给定GeoLiteCity-Blocks.csv文件所在的位置。
    def import_ips_to_redis(conn, filename):             
        csv_file = csv.reader(open(filename, 'rb'))
        for count, row in enumerate(csv_file):
            # 按需将IP地址转换为分值。
            start_ip = row[0] if row else ''             
            if 'i' in start_ip.lower():
                continue
            if '.' in start_ip:                            
                start_ip = ip_to_score(start_ip)           
            elif start_ip.isdigit():                       
                start_ip = int(start_ip, 10)               
            else:
                # 略过文件的第一行以及格式不正确的条目。
                continue                                  
    
            # 构建唯一城市ID。
            city_id = row[2] + '_' + str(count)            
            # 将城市ID及其对应的IP地址分值添加到有序集合里面。
            conn.zadd('ip2cityid:', city_id, start_ip) 
    
    # 这个函数在执行时需要给定GeoLiteCity-Location.csv文件所在的位置。
    def import_cities_to_redis(conn, filename):  
        for row in csv.reader(open(filename, 'rb')):
            if len(row) < 4 or not row[0].isdigit():
                continue
            row = [i.decode('latin-1') for i in row]
            # 准备好需要添加到散列里面的信息。
            city_id = row[0]                          
            country = row[1]                           
            region = row[2]                            
            city = row[3]                             
            # 将城市信息添加到Redis里面。
            conn.hset('cityid2city:', city_id, 
                json.dumps([city, region, country])) 
    
    def find_city_by_ip(conn, ip_address):
        # 将IP地址转换为分值以便执行ZREVRANGEBYSCORE命令。
        if isinstance(ip_address, str):                        #A
            ip_address = ip_to_score(ip_address)               #A
    
        # 查找唯一城市ID。
        city_id = conn.zrevrangebyscore(                       #B
            'ip2cityid:', ip_address, 0, start=0, num=1)       #B
    
        if not city_id:
            return None
    
        # 将唯一城市ID转换为普通城市ID。
        city_id = city_id[0].partition('_')[0]                 #C
        # 从散列里面取出城市信息。
        return json.loads(conn.hget('cityid2city:', city_id))  #D
    
  • 服务的发现与配置

    • 检查系统是否正在维护

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

      def set_config(conn, type, component, config):
          conn.set(
              'config:%s:%s'%(type, component),
              json.dumps(config))
      
    • 读取配置

      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_config = CONFIGS.get(key)                  
      
              # 如果两个配置并不相同……
              if config != old_config:                    
                  # ……那么对组件的配置进行更新。
                  CONFIGS[key] = config                     
      
          return CONFIGS.get(key)
      
    • 将应用组件的名字传递给装饰器

      def redis_connection(component, wait=1):                        #A
          # 因为函数每次被调用都需要获取这个配置键,所以我们干脆把它缓存起来。
          key = 'config:redis:' + component                           #B
          # 包装器接受一个函数作为参数,并使用另一个函数来包裹这个函数。
          def wrapper(function):                                      #C
              # 将被包裹函数里的一些有用的元数据复制到配置处理器。
              @functools.wraps(function)                              #D
              # 创建负责管理连接信息的函数。
              def call(*args, **kwargs):                              #E
                  # 如果有旧配置存在,那么获取它。
                  old_config = CONFIGS.get(key, object())             #F
                  # 如果有新配置存在,那么获取它。
                  _config = get_config(                               #G
                      config_connection, 'redis', component, wait)    #G
      
                  config = {}
                  # 对配置进行处理并将其用于创建Redis连接。
                  for k, v in _config.iteritems():                    #L
                      config[k.encode('utf-8')] = v                   #L
      
                  # 如果新旧配置并不相同,那么创建新的连接。
                  if config != old_config:                            #H
                      REDIS_CONNECTIONS[key] = redis.Redis(**config)  #H
      
                  # 将Redis连接以及其他匹配的参数传递给被包裹函数,然后调用函数并返回执行结果。
                  return function(                                    #I
                      REDIS_CONNECTIONS.get(key), *args, **kwargs)    #I
              # 返回被包裹的函数。
              return call                                             #J
          # 返回用于包裹Redis函数的包装器。
          return wrapper                                              #K
      
    • 装饰后的

      @redis_connection('logs')                   # redis_connection()装饰器非常容易使用。
      def log_recent(conn, app, message):         # 这个函数的定义和之前展示的一样,没有发生任何变化。
          'the old log_recent() code'
      
      log_recent('main', 'User 235 logged in')    # 我们再也不必在调用log_recent()函数时手动地向它传递日志服务器的连接了。
      
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值