Redis实战1 - redis使用

Redis

5种结构:STRING LIST SET HASH ZSET

常用命令

https://redis.io/commands/

通用命令

DEL key
DUMP key
EXISTS key

# 过期时间相关
PERSIST key                # 移除过期的键
EXPIRE key secnods
EXPIREAT key timestamp
PEXPIRE key milliseconds
PEXPIREAT key millisecond-timestamp
PTTL key                   # 毫秒形式返回剩余时间
TTL key                    # 以秒返回剩余时间

KEYS pattern
MOVE key db
RANDOMKEY
RENAME key newkey
RENAMENX key newkey        # 当newkey不存在时,将key改为newkey
SCAN cursor [MATCH pattern] [COUNT count]   # 迭代key
TYPE key

STRING

GET key
SET key value
SETEX key seconds value        # 将value关联到key,并将过期时间设置为seconds
SETNX key value                # 不存在时设置

GETRANGE key start end
SETRANGE key offset value
GETSET key value               # 返回旧值并设置新值
STRLEN key

GETBIT key offset              # 获取指定偏移位置的bit
SETBIT key offset value        # 设置指定偏移位置的bit
BITCOUNT key [start end]
BITOP AND|OR|XOR|NOT dest-key key1 [key2...]

MGET key1 [key2...]
MSET key value [key value ...]
MSETNX key value [key value...]
PSETEX key milliseconds value
APPEND key value

INCR key
INCRBY key increment
INCRBYFLOAT key increment
DECR key
DECRBY key decrement

LIST 最多包含 2^23 - 1 个元素

BLPOP key1 [key2] timeout      # 获取最左边的元素,阻塞超时模式
BRPOP key1 [key2] timeout
BRPOPLPUSH src dest timeout    # 从src列表弹出并插入dest列表并返回它,阻塞超时
RPOPLPUSH src dest             # 移除src最后一个元素并添入另一个列表,并返回

LINDEX key index
LINSERT key BEFORE|AFTER pivot value  # 例:LINSERT mylist BEFORE "World" "There"
LLEN key

LPOP key
LPUSH key value1 [value2]
LPUSHX key value               # 插入已存在的列表头部
RPOP key
RPUSH key value1 [value2]
RPUSHX key value

LRANGE key start stop
LREM key count value
LSET key index value
LTRIM key start stop           # trim操作,只保留指定区间元素

SET

SADD key member1 [member2]
SCARD key                    # 获取集合成员数
SISMEMBER dest member
SMEMBERS key
SMOVE src dest member
SREM key member1 member2
SPOP key                     # 返回一个随机数
SRANDMEMBER key [count]      # 返回count个随机数
SSCAN key cursor [MATCH pattern] [COUNT count]

SDIFF key1 [key2]            # 返回差集
SDIFFSTORE dest key1 [key2]  # 存储差集到 dest
SINTER key1 [key2]           # 返回交集
SINTERSTORE dest key1 [key2]
SUNION key1 [key2]
SUNIONSTORE dest key1 [key2]

HASH

HDEL key field1 [field2]
HEXISTS key field
HKEYS key
HLEN key
HVALS key
HGETALL key

HGET key field
HGETALL key
HSET key field value
HSETNX key field value
HMGET key field1 [field2]
HMSET key field1 value1 [field2 value2]

HINCRBY key field increment
HINCRBYFLOAT key field increment
HSCAN key cursor [MATCH pattern] [COUNT count]

ZSET

ZADD key score1 member1 [score2 member2]
ZCARD key
ZCOUNT key min max
ZSCORE key member
ZINCRBY key increment member
ZINTERSTORE dest numkeys key [key...]
ZUNIONSTORE dest numkeys key [key...]
ZLEXCOUNT                                 # 指定字典区间内成员数量
ZSCAN key cursor [MATCH pattern] [COUNT count]

ZRANGE key start stop [WITHSCORES]
ZRANGEBYLEX key min max [LIMIT offset count]    # 通过字典区间返回有序集合的成员
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT]  # 通过分数返回有序集合指定区间内的成员
ZRANK key member

ZREM key member [member...]
ZREMRANGEBYLEX key min max
ZREMRANGEBYRANK key start stop
ZREMRANGEBYSCORE key min max

ZREVRANGE key start stop [WITHSCORES]           # 指定区间成员,通过索引,分数从高到低
ZREVRANGEBYSCORE key max min [WITHSCORES]       # 返回有序集中指定分数区间内的成员,分数从高到低排序
ZREVRANK key member                             # 递减顺序返回所有

发布订阅

listener订阅channel,publisher向channel发送二进制字符串消息。每当有消息到达channel时,对应的所有订阅者都会接收到消息。订阅者可同时收听多个电台,发送者可在任何电台发送消息。

SUBSCRIBE channel [channel1...]
UNSUBSCRIBE [channel...]
PUBLISH channel message
PSUBSCRIBE pattern [pattern...]   # 订阅与给定模式相匹配的所有频道
PUNSUBSCRIBE [pattern [pattern...]]

其他命令

排序

类似sql中 order by 语句

# 根据给定的选项,对输入列表、集合、zset 进行排序,然后返回或存储排序结果
SORT src-key [BY pattern] [LIMIT offset count] [GET pattern [GET pattern ...]] [ASC|DESC] [ALPHA] [STORE dst-key]

rpush sort-input 23 15 110 7
sort sort-input
# 1) "7"
# 2) "15"
# 3) "23"
# 4) "110"
hset d-7 field 5
hset d-15 field 1
hset d-23 field 9
hset d-110 field 3
sort sort-input BY d-*->field
# 1) "15"
# 2) "110"
# 3) "7"
# 4) "23"
sort sort-input BY d-*->field get d-*->field
# 1) "1"
# 2) "3"
# 3) "5"
# 4) "9"

持久化

配置文件

# rdb
save 60 1000                    # 60秒内1000次写入就备份
stop-writes-on-bgsave-error no
rdbcompression yes
dbfilename dump.rdb

# aof
appendonly yes
appendfsync always/everysec/no
no-appendfsync-on-rewrite no
# 当AOF体积大于64MB,且AOF文件体积比上一次重写后大了至少 100% 时,Redis执行BFREWRITEAOF命令。
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

dir ./ # 快照和AOF文件保存位置

快照

某一时刻将所有数据写入硬盘

可通过客户端发送 BGSAVE SAVE 手动启动

在接收到 SHUTDOWN 命令或接收到 TERM 信号时执行一个 SAVE 命令,阻塞所有客户端不再执行任何命令,并在 SAVE 命令执行完成后关闭服务器

当一个redis-server向另一个redis-server发送 SYNC 命令后开始一次复制操作,如果不是正在执行BGSAVE 操作,则立即执行

只追加文件

执行命令时将写命令复制到硬盘,Redis在启动时将AOF文件读取并执行,会消耗时间。BGREWRITEAOF 通知移除AOF文件中冗余部分,解决AOF文件体积不断增大的问题。因为也创建一个子进程所以也会导致性能问题和内存占用问题。

复制(replication)

主从结构,master处理写请求,slave处理读请求。当从服务器连接主服务器的时候,主服务器会执行BGSAVE 操作。因此为了正确地使用复制特性,用户需要保证主服务器已经正确地设置了dir 选项和dbfilename 选项。

在slave配置文件中指定 slaveof host port 连接master,或对正在运行的redis执行 SLAVEOF host port 让其复制新主服务器;执行 SLAVEOF no one 可让slave终止复制操作。

复制启动过程

步骤 master slave
1 (等待) 连接master,发送SYNC命令
2 开始执行BGSAVE,并使用缓冲区记录BGSAVE之后的命令 由配置项决定:使用现有数据回复client,还是返回错误
3 BGSAVE执行完毕,向slave发送快照,并记录写命令到缓冲区 丢弃旧数据,加载快照
4 快照发送完毕,发送缓冲区记录的被执行写命令 加载快照文件,正常接收命令请求
5 缓冲区存储的写命令发送完毕;每执行一个写命令也向slave发送 接收master发来的写命令,并执行

Redis在复制时也尽可能处理命令请求。最好让master只使用50%~65%内存,剩余的用于执行BGSAVE命令和创建记录写命令缓冲区。

slave进行同步时会清空自己的所有数据,替换为master发来的数据。

多个slave连接同一个master时

master接收到sync命令时的操作 master的操作
步骤3未执行 所有slave收到相同快照和缓冲区写命令
步骤3正执行或执行完毕 与前一个slave完成5个步骤后再执行一次5个步骤

slave可以有slave,从而形成主从链。唯一的区别是子节点在执行步骤4时将断开与孙节点的连接,导致孙节点需要重新连接并重新同步。当读请求远大于写请求时同步大量slave会对master造成很大的性能影响。可以通过 (master/slave node) 中间层来分担主服务器的复制工作。

检验磁盘写入

通过向master写入一个唯一的虚构值,然后检验slave中是否存在以判断写数据是否到达slave。

判断是否保存到硬盘则难很多。检查 INFO 命令中 aof_pending_bio_fsync 属性值是否为0,如果是0则表示已知数据已保存在硬盘中。

检查是否同步
 def wait_for_sync(mconn, sconn):
    identifier = str(uuid.uuid4())
    mconn.zadd('sync:wait', identifier, time.time()) # 将唯一的令牌添加至master

    while not sconn.info()['master'] != 'up':        # 如有必要,等待slave完成同步
        time.sleep(.001)

    while not sconn.zscore('sync:wait', identifier): # 等待从服务器接收数据更新
        time.sleep(.001)
    
    deadline = time.time() + 1.01                    # 最多等待1秒
    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持久化,用户可以增强Redis对于系统崩溃的抵抗能力。

故障处理

为了保证Redis数据完整性要多做一些工作

验证RDB和AOF

使用 redis-check-aof.exe [--fix|--truncate-to-timestamp $timestamp] <file.aof> 和 redis-check-rdb.exe <rdb-file-name> 两个工具。
aof 的 --fix 参数是当AOF文件出错时从错误命令开始进行删除,只保留正确命令。
快照文件无法修复,最好是保留多个备份,通过散列值进行验证。

更换故障主服务器

A为master,B为slave。A故障后B原地待命不会改变状态。使用新的机器C作为新的Redis master。

流程:先向B发送 SAVE 命令保存快照,然后发给C,在C上启动Redis,再让B成为C的slave。

或者将B变为master,C作为新的slave

流程:B使用 slaveof no one 变为master。启动c,然后使用 slaveof host port

注意:可能要相应的修改客户端代码,且可能要更新持久化配置

事务

多个事务同时处理一个对象通常用到二阶段提交

延迟执行事务有助于提升性能,很多客户端都在所有命令出现后将MULTI命令、事务中一系列命令、EXEC命令一起发送给Redis,然后等待接收回复。这称为流水线(pipelining)。

游戏商城应用

出售

用户信息的hash和用户包裹set:

商品ID和卖家ID拼接起来放入ZSET,分值为价格。通过价格排序

WATCH
UNWATCH  # 在 WATCH 和 MULTI 之间执行,对连接重置
DISCARD  # 在 MULTI 和 EXEC 之间执行,对连接重置
MULTI
EXEC     # 当被WATCH的key被写入后,执行EXEC时返回错误

在使用WATCH监视多个键后使用MULTI开始一个新事务,并将多个命令入队到事务队列后,可通过DISCARD命令取消WATCH并清空入队的命令

出售到市场
def list_item(conn:redis.Redis, itemid, sellerid, price):
    inventory = "inventory:{}".format(sellerid)
    item = "{}.{}".format(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()
            return True
        except redis.exceptions.WatchError: # 包裹发生了变化,重试
            pass
    return False

 购买

转移商品和钱
 def purchase(conn:redis.Redis, buyerid, itemid, sellerid, prospective_price):
    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 = int(str(pipe.zscore("market:", item)))
            funds = int(str(pipe.hget(buyer, "funds")))
            if price != prospective_price 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
        except Exception as e: # 还可能存在类型错误
            print(e)
            return False
    return False

 Redis中基于watch的使用乐观锁,MySQL中使用悲观锁。

 性能

  • 使用流水线
    在python中使用 pipe = conn.pipeline() 创建流水线时默认参数为True,客户端会使用MULTI和EXEC包裹起所有命令,如果传入False则可以在不使用事务的情况下使用流水线
  • 使用连接池
    python客户端默认使用连接池
  • 使用正确的数据结构

降低使用内存

短结构

list、set、hash、zset有一些配置选项可以以更节约的方式存储较短的结构。在list、hash、zset较小时会使用压缩列表(ziplist)存储。ziplist以序列化方式存储数据,每次读取时要解码,写入时局部重新编码且可能移动内存数据。

ziplist

entries表示最大元素数量,value指每个节点最大体积。两个条件中任意一个被突破时会将其转换为相应的其他结构。当结构体积减小后不会再变为ziplist。

集合的整数集合编码

如果整数包含的所有成员都可以被解释为十进制整数,而这些整数又处于平台的有符号整数范围之内,并且集合成员的数量又足够少的话,那么Redis就会以有序整数数组的方式存储集合,这种存储方式又被称为整数集合 (intset)。

长压缩列表和大整数集合带来的性能问题

突破限制后,Redis将底层结构转为更典型的结构。因为随着紧凑结构的体积变得越来越大,操作这些结构的速度也会变得越来越慢。

建议ziplist长度1024之内,每个元素体积64字节之内。

此外,让键名保持简短。

分片结构

分片本质上就是基于某些简单的规则将数据划分为更小的部分,然后根据数据所属的部分来决定将数据发送到哪个位置上面。程序不再是将值X存储到键Y里面,而是将值X存储到键Y:<shardid> 里面。

对列表分片:应当使用Lua

对有序集合分片:range操作要在所有分片上进行,所以速度不快,作用不大。

分片式散列

选一个方法划分数据,可以把散列存储的键用作一个信息源,并用散列函数计算出一个数字散列值。

# 给出散列名字、存到分片散列的键、预计的元素总数、请求的分片数量
def shard_key(base, key, total_elements, shard_size):
    if isinstance(key, (int, long)) or key.isdigit():   # 看上去是整数则直接分片
        shard_id = int(str(key), 10) // shard_size      # 使用二进制的高位
    else:
        shards = 2 * total_elements // shard_size       # 计算分片总数量
        shard_id = binascii.crc32(key) % shards         # 获得分片id
    return "%s:%s"%(base, shard_id)                     # 得到分片键


def shard_hset(conn, base, key, value, total_elements, shard_size):
    shard = shard_key(base, key, total_elements, shard_size)    #A
    return conn.hset(shard, key, value)                         #B

def shard_hget(conn, base, key, total_elements, shard_size):
    shard = shard_key(base, key, total_elements, shard_size)    #C
    return conn.hget(shard, key)                                #D

分片集合

计算网站的唯一访客可使用一个基于set的访客计数器。对其实现分片。对每个用户生成唯一的UUID,将其前15个十六进制数转为十进制数存储,只要占用8字节内存。

def shard_sadd(conn, base, member, total_elements, shard_size):
    # 使用之前的 shared_key ,因成员并非连续ID,所以先转为字符串再分片
    shard = shard_key(base,
        'x'+str(member), total_elements, shard_size)            #A
    return conn.sadd(shard, member)                             #B

SHARD_SIZE = 512                        #B
def count_visit(conn, session_id):
    # 通过当天日期生成唯一访客的键
    today = date.today()                                #C
    key = 'unique:%s'%today.isoformat()                 
    # 获取或计算唯一访客
    expected = get_expected(conn, key, today)           
 
    id = int(session_id.replace('-', '')[:15], 16)
    # 如果访客不存在则加1
    if shard_sadd(conn, key, id, expected, SHARD_SIZE):
        conn.incr(key)
        
        
DAILY_EXPECTED = 1000000                                #I
EXPECTED = {}                                           # 本地存储一份副本
# 计算每天预计的访问客人数。Web页面的访客数量总是会随着时间发生变化,而每天都维持相同分片数量的做法将无法适应访客人数增多的情况,也无法在访客数量少于100万人次的时候缩减分片的数量。
def get_expected(conn, key, today):
    # 已存在则直接获取
    if key in EXPECTED:                                 #B
        return EXPECTED[key]                            #B
    # 其他客户端已算出则直接获取
    exkey = key + ':expected'
    expected = conn.get(exkey)                          #C
 
    if not expected:
        # 获取昨天唯一访客数,不存在则用默认值
        yesterday = (today - timedelta(days=1)).isoformat() #D
        expected = conn.get('unique:%s'%yesterday)          #D
        expected = int(expected or DAILY_EXPECTED)          #D
        # 预计明天访客人数比今天多50%
        expected = 2**int(math.ceil(math.log(expected*1.5, 2)))
        # 写入redis,供其他程序使用
        if not conn.setnx(exkey, expected):
            # 如果已存在,使用已有的
            expected = conn.get(exkey)                      #G
 
    EXPECTED[key] = int(expected)                       #H
    return EXPECTED[key]                                #H

 

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