《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:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,099评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,473评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,229评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,570评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,427评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,335评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,737评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,392评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,693评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,730评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,512评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,349评论 3 314
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,750评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,017评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,290评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,706评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,904评论 2 335

推荐阅读更多精彩内容

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