《redis in action》笔记

motivation

  • get familiar with python
  • go over redis's commands & be practised
  • cases
  • dive deeper

初识redis

工具会极大地改变人们解决问题的方式

只有关系型数据库时,做聚合统计的计算需要将数据插入到临时的统计表中,再定期扫描这些数据行收集聚合数据,再更新。

四 数据安全与性能保障

# snapshot快照持久化选项
save 60 1000 #60秒内有1000次写入,  过于频繁会造成资源浪费,过于稀疏又可能有丢失大量数据的隐患
stop-write-on-bgsave-error no  # 在创建快照失败后是否任然继续执行写命令
rdbcompression yes
dbfilename dump.rdb

# aof持久化选项 
appendonly no
appendfsync everysec/always/no  #同步频率 
#always:每个redis写命令都同步写入硬盘。 会严重降低Redis的速度
#everysec: 每秒执行一次同步,显示地将多个写命令同步到硬盘 (推荐)
#no: 让操作系统来觉得应该合适进行同步

no-appendfsync-on-rewrite no
# 当aof文件体积大于64MB,并且aof文件的体积比上一次重写之后的体积大了至少一倍的时候,Redis将执行GBREWRITEAOF命令
auto-aof-rewrite-percentage 100  
auto-aof-rewrite-min-size 64mb 

# 共享选项, 这个选项决定快照文件和AOF文件的保存位置
dir ./  

mac 下redis配置文件路径:??

快照

BGSAVE是通过fork子进程来创建快照的; SAVE不会创建子进程,所以不会抢占资源,创建快照的速度会比BGSAVE快,但是会一直阻塞Redis进程。

在真实的硬件、vmware虚拟机或kvm虚拟机,Redis进程每占用一个GB的内存,创建该进程的子进程所需时间就要增加10 ~ 20毫秒;而对于Xen虚拟机来说,根据配置的不同,Redis进程每占用一个GB的内存,创建该进程的子进程所需的时间就要增加200 ~ 300毫秒。

AOF持久化

AOF既可以将丢失数据的时间窗口降低至1秒,又可以在极端的时间内完成定期的持久化操作。

但是AOF记录了所有的写命令,如果文件的体积非常大,那么还原操作执行的时间就可能非常长;可以用BGREWRITEAOF命令来移除AOF文件中的冗余命令,尽可能缩小AOF文件的体积。但是这个命令和快照一样是通过创建一个子进程来处理的,而且AOF的体积比快照文件的体积大好几倍,重写并删除旧AOF文件的时候也可能导致性能问题和内存占用问题。

事务

watch/unwatch , multi exec, discard

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)
            pipe.execute() #如果exec执行没有引发WatchError说明事务执行成功
            return True
        except redis.exception.WatchError: #如果库存发生变化 5秒内重试
            pass
        
    return False
                

性能

  • 非事务型流水线减少不必要的通信往返次数
  • 连接池(避免每个命令或每组命令都创建新的连接)
  • redis-benchmark redis内置性能测试命令

五 使用Redis构建支持程序

5.1 记录日志

syslog服务

最近日志,常见日志

5.2 计数器和统计数据

// 存储统计数据
stats:ProfilePage:AccessTime -- zset
    min     | 0.035
    max     | 4.958
    sumsq   | 194.269
    sum     | 258.974
    count   | 2323
    
// 慢页面收集
slowest:Access -- zset
    pageName | time
    

5.3 查找ip所属城市以及国家

// 查找ip所属城市以及国家
ip2cityid: -- zset
    cityId_count | ip2score

cityid2city:cityId -- hash
    cityId | cityJson
    
def find_city_by_ip(conn, ip_address):
    ip_address = ip_to_score(ip_address)
    # 利用zrevrangebyscore获取分值最大的ip和所对应的城市id
    cityid = conn.zrevrangebyscore('ip2cityid:', ip_address,0,start=0,num=1)
    ...
    

5.4 服务发现和配置

locally and centralized in redis

间隔时间同步机制

# check for maintenance
LAST_CHECKED = None
IS_UNDER_MAINTENANCE = False

def is_under_maintenance(conn):
    global LAST_CHECKED, IS_UNDER_MAINTENANCE
    
    # affect all web servers within 1 second
    if LAST_CHECKED < time.time() - 1:
        LAST_CHACKED = time.time()
        IS_UNDER_MAINTENANCE = bool(conn.get("is-under-maintenance"))
    
    return IS_UNDER_MAINTENANCE
    
# config
type:service:component
config:redis:statistics

---

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()
        config = json.loads(conn.get(key) or '{}')
        config = dict((str(k), config[k]) for k in config)
        old_config = CONIFGS.get(key)
        
        if config != old_config:
            CONFIGS[key] = config
        
    return CONFIGS.get(key)
        
graph TD
    A[Fetch Config] --> B{Sync Interval}
    B --> |Y| D[Fetch Remote Config]
    D --> F[Update Local Config]
    B --> |N| E[return Local Config]
    F --> E

六 使用Redis构建应用程序组件

6.1.1 自动补全最近联系人

list没提供范围搜索的功能,只能在程序中进行模糊匹配; 此方法不适合非常大的列表

def add_update_contact(conn, user, contact):
    ac_list = 'recent:' + user
    pipeline = conn.pipeline(True) 
    pipeline.lrem(ac_list, contact) # 从列表中删除当前联系人
    pipeline.lpush(ac_list, contact) # 将联系人推入到列表最前端
    pipeline.ltrime(ac_list, 0, 99) 
    pipeline.execute()

def fetch_autocomplete(conn, user, prefix):
    candidates = conn.lrange('recent:' + user, 0, -1)
    matches = []
    for candidate in candidates:
        if candidate.lower().startswith(prefix):
            matches.append(candidate)
    return matches

6.1 通讯录自动补全

当所有成员的分值都相同时,有序集合将根据成员的名字来进行排序。

6.2 分布式锁

acquire -> do work -> release

要解决的问题:

  • A process acquired a lock, operated on data, but took too long, and the lock was
    automatically released. The process doesn’t know that it lost the lock, or may
    even release the lock that some other process has since acquired.
  • A process acquired a lock for an operation that takes a long time and crashed.
    Other processes that want the lock don’t know what process had the lock, so can’t
    detect that the process failed, and waste time waiting for the lock to be released.
  • One process had a lock, but it timed out. Other processes try to acquire the lock

simultaneously, and multiple processes are able to get the lock.

  • Because of a combination of the first and third scenarios, many processes now
    hold the lock and all believe that they are the only holders.
  • 持有锁的进程因为操作时间过长而导致锁被自动释放,但是进程本身并不知晓这一点,甚至还可以会错误的释放掉了其他进程持有的锁(锁超时,do work )
  • 一个持有所并打算长时间操作的进程已经奔溃,但其他想要获取锁的进程并不知道哪个进程持有着锁,也无法检测到持有锁的进程已经崩溃,只能白白地浪费时间等待锁被释放(死等, wait acquire)
  • 在一个进程持有锁过期之后,其他多个进程同时尝试去获取锁,并且都获得了锁(锁竞争, concurrent acquiring)
  • 上面第一种和第三种情况同时出现,导致有多个进程都获得了锁,而每个进程都以为自己是唯一一个获得锁的
# 释放锁
def release_lock(conn, lockname, identifier):
    pipe = conn.pipeline(True)
    lockname = 'lock:' + lockname
    
    while True:
        try:
            pipe.watch(lockname)
            if pipe.get(lockname) == identifier:  # 检查进程是否任然持有锁
                pipe.multi()
                pipe.delete(lockname)
                pipe.execute()
                return True
            
            pipe.unwatch()
            break
        
        except  redis.exceptions.WatchError: # 如果有其他客户端修改了锁,重试
            pass
        
    return False
    
# 获取锁,带超时时间 不能解决处理时间过长导致锁超时后的并发问题
def acquire_lock_with_timeout(conn, lockname, acquire_timeout = 10, lock_timeout = 10):
    identifier = str(uuid.uuid4())
    lockname = 'lock:' + lockname
    lock_timeout = int(math.ceil(lock_timeout))
    
    end = time.time() + acquire_timeout
    while time.time() < end:
        if conn.setnx(lockname, identifier):
            conn.expire(lockname, lock_timeout)
            return identifier
        elif not conn.ttl(lockname):   #检查过期时间,并在有需要的时候对其进行更新(防止setnx,expire两个命令之间时 客户端奔溃导致超时设置失败)
            conn.expire(lockname, lock_timeout)
        
        time.sleep(.001)  # 未获取成功锁,则等待
    
    return False

6.3 计数信号量 semaphore

多主机环境下,每个进程的系统时间是有差异的

公平信号量

// 超时有序集合
semaphore:remote -- zset
    uuid | timestampe
    
// semaphore拥有者有序集合
semaphore:remote:owner -- zset
    uuid | counter

// 计数器
semaphore:remote:counter -- string
    counter

// 长时操作内部要不时的刷新令牌超时时间
refresh 
    zadd

6.4 Task queues

  1. FIFO queue: rpush, blpop high-q, medium-q, low-q
  2. Delayed tasks
delayed: -- zset
    [guid,methodname, args] | timestamp to exec

high-q -- list
medium-q -- list
low-q -- q

6.5 消息拉取

zset 可以很方便的获取一个范围内(排序、分值)的值,也可以和其他有序集合做交集补集操作

def blockingFetch(conn):
    while not quit:
        packed = conn.blpop('queue',30) # if queue is empty, blocking for it timedout in 30 seconds
        if not packed:
            continue
        
        try:
            # process with packed
        except:
            # handle error
    

def nonBlockingFetch(conn):
    while not quit:
        packed = conn.lpop('queue')
        if not packed:
            sleep(.01)  
        try:
            # process with packed
        except:
            # handle error
        

七 基于搜索的应用

7.1

分词(parsing) -> 去除非用词 -> 标记(tokenization) -> 反向索引(inverted index)

# 结构
idx:wordA -- set
    docA
    dobB

idx:wordB -- set
    docA

# 再根据关键字word 匹配文档doc, 同时可以利用redis的集合操作取交集、并集、差集
  

八 简单的社交网站

hash: user
hash: message/status
zset: followers:{userid}
zset: following:{userid}
zset: profile:{userid}  用户状态集合(主页)
zset: home:{userid} 用户主页状态集合

九 降低内存占用

短结构

list-max-ziplist-entries 512
list-max-ziplist-value 54

hash-max-ziplist-entries 512
hash-max-ziplist-value 64

zset-max-ziplist-entries 128
zset-max-zipplist-value 64

set-max-intset-entries 512

# 超过限制后,存储结构变化 不可逆
ziplist -> linkedlist
intsest -> hashtable

9.2 分片结构

对散列进行分片,合理设置redis的配置 xx-max-ziplist-entries 和 xx-max-ziplist-value 就可利用redis的短结构来节约内存

9.3 字符串分片

redis字符串类型的值最大只能存储512MB的数据

如何查看单个key占用的内存:

清空redis, flushall后执行 info memory 查询redis内存使用情况, 然后插入一个值,再执行info memory

further:

  • 性能测试, 监控
  • 使用场景,瓶颈和解决方案
  • 如何降低内存
  • 《redis设计与实现》 数据结构如何节约存储空间的? 垃圾回收的几种方式?

reference:

推荐阅读更多精彩内容

  • 导读 这本书有点厉害,看完读懂了,就大致知道构建类微博的亿级社交平台的大部分秘密。 微博及Twitter这两大社交...
    cajan2阅读 344评论 0 1
  • 这本书涵盖Redis的使用。读者不要求了解Redis,但是必须有python(版本2.7)基础。 书中介绍的示例都...
    cajan2阅读 591评论 1 0
  • NOSQL类型简介键值对:会使用到一个哈希表,表中有一个特定的键和一个指针指向特定的数据,如redis,volde...
    MicoCube阅读 1,654评论 3 24
  • 1.1 资料 ,最好的入门小册子,可以先于一切文档之前看,免费。 作者Antirez的博客,Antirez维护的R...
    JefferyLcm阅读 15,104评论 1 43
  • 安全性 设置客户端连接后进行任何其他指令前需要使用的密码。 警告:因为redis 速度相当快,所以在一台比较好的服...
    OzanShareing阅读 701评论 1 8