Redis实战2 - 使用Redis构建支持程序

使用Redis构建支持程序

构建的不是应用程序,而是记录应用程序信息、记录访客信息、为应用程序提供配置信息等手段来帮助和支持应用程序。

使用Redis记录日志

如何用Redis存储与世界紧密相关的日志

最新日志

将最新消息以列表形式存储在Redis中

查看代码
 def log_recent(conn, name, message, level, pipe=None):
    dest = 'rescent:%s:%s'%(name, level)
    message = time.asctime() + ' ' + message
    pipe = pipe or conn.pipeline()
    pipe.lpush(dest, message)
    pipe.ltrim(dest, 0, 99)
    pipe.execute()

最常见日志

查看代码
def log_common(conn:redis.Redis, name, msg, level, timeout=5):
    dest = 'common:%s:%s'%(name, level) # 存储近期常见日志消息的键
    start_key = dest + ':start'         # 统计的是每小时内最常见日志,故使用一个键保存当前小时数
    pipe = conn.pipeline()
    end = time.time() + timeout
    while time.time() < end:
        try:
            pipe.watch(start_key)       # 监视当前小时数,确保轮换操作可正确执行
            hour_start = datetime.datetime.now().hour
            existing = int(str(pipe.get(start_key)))
            pipe.multi()
            if existing and existing < hour_start: # 将前一个小时的日志归档
                pipe.rename(dest, dest+':last')
                pipe.rename(start_key, dest + ':pstart')
                pipe.set(start_key, hour_start)
            elif not existing:
                pipe.set(start_key, hour_start)
            pipe.zincrby(dest, 1, msg)   # 更新日志出现次数
            log_recent(pipe, name, msg, level, pipe)
            return
        except redis.exceptions.WatchError:
            continue

使用Redis实现计数器并进行数据统计

将计数器存储到Redis

计数器:

已有计数器:

查看代码
PERCISION = [1, 5, 60, 300, 3600, 18000, 86400]
def update_counter(conn:redis.Redis, name, count=1, now=None):
    now = now or time.time()
    pipe = conn.pipeline()
    for prec in PERCISION:
        pnow = int(now / prec) * prec  # 获取时间片的开始
        hash = '%s:%s'%(prec, name)
        pipe.zadd('known:', {hash: 0}) # 添加到已知计数器
        pipe.hincrby('count:' + hash, str(pnow), count)  # 对指定名字和精度的计数器进行更新
    pipe.execute()
    

def get_counter(conn:redis.Redis, name, precision):
    hash = '%s:%s'%(precision, name)
    data = conn.hgetall('count:' + hash)
    to_return = []
    for key, value in data.items():
        to_return.append((int(key), int(value)))
    to_return.sort()
    return to_return

# 60秒一次遍历有序集合中的计数器,只清理应该清理的计数器
def clean_counters(conn:redis.Redis):
    '''
    任何时候都可能有新计数器加入
    同一时间可能有多个不同清理操作在执行
    对于一个每天只更新一次的计数器,每分钟尝试清理一次是浪费资源
    计数器不包含数据,那么久不应该清理
    '''
    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]) # 计数器精度
            bprec = int(prec // 60) or 1       # 每60秒循环一次,判断本次计算是否要进行清理
            if passes % bprec:
                continue
            hkey = 'count:' + hash
            cutoff = time.time() - SAMPLE_COUNT * prec # 根据给定精度与要保留的样本数量,计算出要保留什么时间点前的样本
            samples = map(int, conn.hkeys(hkey))
            sorted(samples)
            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('know:', hash)
                            pipe.execute()
                            index -= 1
                        else:
                            pipe.unwatch()
                    except redis.exceptions.WatchError:
                        pass
        passes += 1
        duration = min(int(time.time() - start) + 1, 60)
        time.sleep(max(60 - duration, 1))

使用Redis存储统计数据

使用有序集合进行保存,方便交并集运算

查看代码
 def update_status(conn:redis.Redis, context, type, value, timeout=5):
    dest = 'status:%s:%s'%(context, type) # 负责存储统计数据的键
    start_key = dest + ':start'
    pipe = conn.pipeline(True)
    end = time.time() + timeout
    while time.time() < end:
        try:
            pipe.watch(start_key)
            hour_start = datetime.datetime.now().hour
            existing = int(str(pipe.get(start_key)))
            pipe.multi()
            if existing and existing < hour_start: # 每一个小时数据备份一份
                pipe.rename(dest, dest+':last')
                pipe.rename(start_key, dest + ':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})
            pipe.zunionstore(dest, [dest, tkey1], aggregate='MIN')
            pipe.zunionstore(dest, [dest, tkey2], aggregate='MAX')
            pipe.delete(tkey1, tkey2)

            pipe.zincrby(dest, 1, 'count')
            pipe.zincrby(dest, value, 'sum')
            pipe.zincrby(dest, value*value, 'sumsq')

            return pipe.execute()[-3:]
        except redis.exceptions.WatchError:
            continue


def get_status(conn:redis.Redis, context, type):
    key = 'status:%s:%s'%(context, type)
    data = dict(conn.zrange(key, 0, -1, withscores=True))
    data['average'] = data['sum'] / data['count']
    numerator = data['numsq'] - data['num'] ** 2 / data['count']
    data['stddev'] = (numerator / (data['count'] -1 or 1)) ** .5
    return data

简化统计数据的记录与发现

 

查看代码
 @contextlib.contextmanager                                  # 将这个生成器用在上下文管理器
def access_time(conn, context):
    start = time.time()                                     # 记录代码执行前的时间
    yield                                                   # 运行被包裹的代码

    delta = time.time() - start                             # 计算执行时长
    stats = update_stats(conn, context, 'AccessTime', delta)#
    average = stats[1] / stats[0]                           # 计算平均时长

    pipe = conn.pipeline(True)
    pipe.zadd('slowest:AccessTime', context, average)       # 
    pipe.zremrangebyrank('slowest:AccessTime', 0, -101)     # 
    pipe.execute()
    
def process_view(conn, callback):
    with access_time(conn, request.path):  # 打开上下文并计算时间
        return callback()

查询IP地址所属的城市和国家

将一个IP所属城市载入Redis中,然后通过搜索数据库发现玩家位置。

一个表记录IP地址段和城市ID zset实现 ;另一个表保存城市ID和城市信息 hash 存储

查看代码
 def ip_to_score(ip_address):
    score = 0
    for v in ip_address.split('.'):
        score = score * 256 + int(v, 10)
    return score

def import_ips_to_redis(conn, filename):                #A
    csv_file = csv.reader(open(filename, 'rb'))
    for count, row in enumerate(csv_file):
        start_ip = row[0] if row else ''                #B
        if 'i' in start_ip.lower():
            continue
        if '.' in start_ip:                             #B
            start_ip = ip_to_score(start_ip)            #B
        elif start_ip.isdigit():                        #B
            start_ip = int(start_ip, 10)                #B
        else:
            continue                                    #C

        city_id = row[2] + '_' + str(count)             #D
        conn.zadd('ip2cityid:', city_id, start_ip)      #E

        
def import_cities_to_redis(conn, filename):         #A
    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]                            #B
        country = row[1]                            #B
        region = row[2]                             #B
        city = row[3]                               #B
        conn.hset('cityid2city:', city_id,          #C
            json.dumps([city, region, country]))    #

查找IP所属城市

查看代码
 def find_city_by_ip(conn, ip_address):
    if isinstance(ip_address, str):                        #A
        ip_address = ip_to_score(ip_address)               #A

    city_id = conn.zrevrangebyscore(                       #B
        'ip2cityid:', ip_address, 0, start=0, num=1)       #B

    if not city_id:
        return None

    city_id = city_id[0].partition('_')[0]                 #C
    return json.loads(conn.hget('cityid2city:', city_id))  #D

 

服务的发现和配置

存储配置信息

将配置写入redis,应用程序获取这些信息,这样就不用推送

查看代码
 LAST_CHECKED = None
IS_UNDER_MAINTENANCE = False

def is_under_maintenance(conn):
    '''
    在redis中检测键的值并返回
    '''
    global LAST_CHECKED, IS_UNDER_MAINTENANCE   #A

    if LAST_CHECKED < time.time() - 1:          # 为了降低负载,控制访问频率
        LAST_CHECKED = time.time()              #C
        IS_UNDER_MAINTENANCE = bool(conn.get('is-under-maintenance'))   #D

    return IS_UNDER_MAINTENANCE                 #E

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

 构建一个函数,可以从一个键中取出一个json编码的配置值,键由服务类型、应用程序名等命名,如:config:redis:statistics

 

查看代码
 def set_config(conn, type, component, config):
    conn.set('config:%s:%s'%(type, component),json.dumps(config))
    
CONFIGS = {}
CHECKED = {}

def get_config(conn, type, component, wait=1):
    key = 'config:%s:%s'%(type, component)
    
    # 检查是否要对配置信息更新
    if CHECKED.get(key) < time.time() - wait:           #A
        # 记录最后一次更新时间
        CHECKED[key] = time.time()                      #B
        # 获取redis存储的配置
        config = json.loads(conn.get(key) or '{}')      #C
        config = dict((str(k), config[k]) for k in config)#G
        # 取得正在使用的配置
        old_config = CONFIGS.get(key)                   #D
        # 不同时合并
        if config != old_config:                        #E
            CONFIGS[key] = config                       #F

    return CONFIGS.get(key)

自动Redis连接管理

编写一个装饰器,自动连接除配置服务以外的所有其他Redis服务

查看代码
 REDIS_CONNECTIONS = {}

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(config_connection, 'redis', component, wait)    #G

            config = {}
            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

            return function(                                    #I
                REDIS_CONNECTIONS.get(key), *args, **kwargs)    #I
        return call                                             #J
    return wrapper                                              #K

@redis_connection('logs')
def log_recent(conn, app, message):
    pass

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

 

posted @ 2022-11-26 08:31  某某人8265  阅读(63)  评论(0编辑  收藏  举报