介绍Redis中的字符串键
字符串
字符串建是 Redis 最基本的键值对类型, 这种类型的键值对会在数据库中把单独的一个值关联起来, 被关联的键和值可以为文本, 也可以是图片, 视屏, 音频等二进制数据.
- SET: 为字符串键设置值 O(1)
SET key value
```Redis
SET number "10086"
> OK
SET book "Redis in action"
> OK
```
对于已经存在的 key, 再次赋值会覆盖原值, 若不想覆盖后面添加参数 NX, 相反, 默认 XX 允许覆盖
```Redis
SET key "10086" NX
> (nil)
SET key "10086" XX
> OK
```
- GET: 获取字符串键的值 O(1)
GET key
```Redis
GET number
> "10086"
```
对于不存在的值, 返回空
```Redis
GET key_new
> (nil)
```
- GETSET: 获取旧值并更新值 O(1)
GETSET key new_value
```Redis
GETSET key "123456"
> "10086"
```
示例: 缓存
对数据进行缓存是Redis最常见的用法之一, 将数据存储在内存比存储在硬盘要快得多 首先定义缓存
class Cache:
def __init__(self, client):
self.client = client
def set(self, key, value):
self.client.set(key, value)
def get(self, key):
return self.client.get(key)
def update(self, new_value, key):
return self.client.getset(key, new_value) # 设置新值, 返回旧值
然后缓存文本数据
client = Redis(decode_responses=True) # 使用文本编码方式打开客户端
cache = Cache(client)
cache.set("web_page", "<html><p>hello world</p></html>")
print(cache.get("web_page"))
print(cache.update("web_page", "<html><p>update<p></html>"))
print(cache.get("web_page"))
下面是存储一个二进制图片的缓存示例
client = Redis() # 二进制编码打开客户端
cache = Cache(client)
image = open("DailyBing.jpg", "rb") # 二进制只读方式打开图片
data = image.read() # 读取文件内容
image.close() # 关闭文件
cache.set("daily_bing.jpg", data) # 将二进制图片缓存到键 daily_bing.jpg 中
print(cache.get("daily_bing.jpg")[:20]) # 读取二进制数据的前20字节
b’\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x01\x00\x00\x00\x00\x00\x00'
示例: 锁
锁是一种同步机制, 用于保证一种资源任何时候只能被一个进程使用. 一个锁的实现通常有获取 (acquire) 和释放 (relase) 这两种操作.
- 获取操作用于获取资源的独占使用权, 任何时候只能有一个进程取得锁, 此时, 取得锁的进程称为锁的持有者.
- 释放操作用于放弃资源的独占使用权, 一般由持有者调用.
from redis import Redis
VALUE_OF_LOCK = "locking"
class Lock:
def __init__(self, client, key):
self.client = client
self.key = key
def acquire(self):
result = self.client.set(self.key, VALUE_OF_LOCK, nx=True)
return result is True
def relase(self):
return self.client.delete(self.key) == 1
client = Redis(decode_responses=True)
lock = Lock(client, 'test-lock')
print("第一次获取锁:", lock.acquire())
print("第二次获得锁:", lock.acquire())
print("取消锁:", lock.relase())
print("第三次获得锁:", lock.acquire())
第一次获取锁: True 第二次获得锁: False 取消锁: True 第三次获得锁: True
若要设置锁的时间 SET key value NX EX time
这样是原子性语法, 删除操作对应命令是 DEL key
, 返回0表示 key 不存在, 返回1~N表示删除key的数量.
NX 确保锁只有在没有值时加锁成功, 若有值则返回 None, 通过检查 result 是否为 True 来判断是否获得了锁.
- MSET: 一次为多个字符串键设置值 O(N)
MSET key value [key value …]
同 SET 命令, MSET 执行成功后返回 OK, 并且会用新值覆盖旧值. 由于执行多条 SET 命令要客户端和服务端之间多次进行网络通讯, 因此 MSET 能减少程序执行操作的时间
- MGET: 一次获取多个字符串键的值 O(N)
MGET key [key …]
- MSETNX: 只在键不存在的情况下, 一次为多个键设置值
MSETNX key value [key value …]
若有任意一次键存在值, 则会取消所有操作, 并返回0. 只有所有键都没有值的时候, 执行才成功, 返回1.
示例: 存储文章信息
在构建应用程序的时候, 经常会需要批量设计和获取多项信息, 以博客为例:
- 当用户注册博客时, 程序将用户名字、帐号、密码、注册时间等存储起来, 并在登陆时查取这些信息.
- 当编写一篇博客文章时, 就要将博客标题、内容、作者、发表时间存储起来, 并在用户阅读的时候取出这些信息.
通过 MSET、MSETNX、MGET 命令, 可以实现上面提到的这些批量设置和批量获取操作
from redis import Redis
from datetime import datetime
class Article:
def __init__(self, client, article_id):
"""根据id创建文章id"""
self.client = client
self.id = str(article_id)
self.title_key = "article::" + self.id + "::title"
self.content_key = "article::" + self.id + "::content"
self.author_key = "article::" + self.id + "author"
self.create_at_key = "article::" + self.id + datetime.now()
def create(self, title, content, author):
"""创建文章"""
article_data = {
self.title_key: title,
self.content_key: content,
self.author_key: author,
self.create_at_key: datetime.now(),
}
return self.client.msetnx(article_data)
def get(self):
"""获取文章信息"""
result = self.client.mget(
self.title_key,
self.content_key,
self.author_key,
self.create_at_key,
)
return {
"id": self.id,
"title": result[0],
"content": result[1],
"author": result[2],
"create_at_key": result[3],
}
def update(self, title=None, content=None, author=None):
"""更新文章"""
article_data = {}
if title is not None:
article_data[self.title_key] = title
if content is not None:
article_data[self.content_key] = content
if author is not None:
article_data[self.author_key] = author
return self.client.mset(article_data)
client = Redis(decode_responses=True)
article = Article(client, 10086)
# 创建文章
print(article.create("message", "hello world", "sx"))
# 获取文章信息
print(article.get())
# 更新文章作者
print(article.update(author="join"))
上面程序使用了多个字符串键存储文章信息: article::<id>::<attribute>
- STRLEN: 获取字符串的字节长度 O(1)
STRLEN key
对于存在的键, 返回字节长度信息. 对于不存在的键, 返回0
- GETRANGE: 获取字符串值指定索引范围上的内容 O(N)
GETRANGE key start end
SET message "hello world"
GETRANG message 0 4
> hello
GETRANGE message -5 -1
> world
- SETRANGE: 修改字符串索引范围的值 O(N)
SETRANGE key index subsitute
set message "hello world"
SETRANGE message 6 Redis
> (integer) 11
GET message
> hello Redis
当用户给定的新内容比被替换内容长的时候, SETRANGE 会自动扩展被修改的字符串值
SETRANGE message 5 ", this is a message"
> (integer) 24
GET message
> "hello, this is a message"
当用户给出的索引长度超出被替换字符长度时, 字符串末尾到 index-1 之间部分将使用空字符串填充为0
SET greeting "hello"
SETRANGE greeting 10 "hello"
> (integer) 15
GET greeting
> "hello\x00\x00\x00\x00\x00world"
示例: 给文章存储程序加上文章长度计数功能和文章御览功能给
- 文章长度计数功能: 显示文章长度, 用于估计阅读时长
- 文章预览功能: 显示文章开头一部分内容, 帮助读者快速了解文章
class Article:
...
def get_content_len(self):
return self.client.strlen(self.content_key)
def get_content_perview(self, preview_len):
start_index = 0
end_index = preview_len - 1
return self.client.getrange(self.content, start_index, end_index)
- APPEND: 追加新内容到值的末尾
APPEND key suffix
若用户给定的 key 不存在, 则相当于 SET key suffix
示例: 存储日志
很多程序运行的时候会产生日志, 日志记录了程序的运行状态以及执行过的重要操作. 若每条日志存储一个键值对, 则会消耗很多资源, 且分散在数据库中, 需要额外的时间查找日志, 这里将不同日志拼接在同一个值里面.
from redis import Redis
LOG_SEPERATOR = "\n"
class Log:
def __init__(self, client, key):
self.client = client
self.key = key
def add(self, new_log):
new_log += LOG_SEPERATOR
self.client.append(self.key, new_log)
def get_all(self):
all_logs = self.client.get(self.key)
if all_logs is not None:
log_list = all_logs.split(LOG_SEPERATOR)
log_list.remove("") # 删除默认多余的空字符串
return log_list
else:
return []
数字值
下面介绍使用字符串键存储数字值:
每当存储一个值到字符串键里面的时候, 有下面两种情况
- C 语言 long long int 类型的整数, 取值范围为 -2^63 ~ 2^63-1 (超出范围会被当成字符串)
- C 语言 long double 类型的浮点数
为了方便地处理字符串键的值, Redis 提供了一系列加法和减法操作命令, 下面介绍这些命令
- INCRBY, DECRBY: 对整数执行加法和减法操作 O(1)
INCRBY key increment DECRBY key increment
如果类型为浮点数, 使用上面方法会报错 (key的值 和 increment 都必须为整数)
当该命令遇到**不存在的键**时, 会将键的值初始化为0, 然后再执行操作
- INCR, DECR: 对整数执行加1和减1操作 O(1)
INCR key DECR key
- INCRBYFLOAT: 对数字值执行浮点数加减法操作
INCRBYFLOAT key increment
INCRBYFLOAT 命令即执行加法操作, 也可以执行加法操作, 并且操作对象和 increment 都既可以为整数也可以为浮点数
虽然 Redis 没有限制字符串键存储浮点数的小数位数, 但是 INCRBYFLOAT 最多只会保留小数点后的17位数字, 超出部分将被截断
示例: ID 生成器
identifier 标识符, 经常在程序中使用, 通常以数字形式出现, 并通过递增的方法创建新的ID.
from redis import Redis
class IdGenerator:
def __init__(self, client, key):
self.client = client
self.key = key
def produce(self):
"""生成下一个id"""
return self.client.incr(self.key)
def reserve(self, n):
"""初始化"""
result = self.client.set(self.key, n, nx=1) # key 不存在才行
return result is True
client = Redis(decode_responses=True)
id_generator = IdGenerator(client, "user::id")
print(id_generator.reserve(1000000)) # 保留100万个ID -> True
print(id_generator.produce()) # 生成ID, 均大于100万
print(id_generator.reserve(1000)) # 已存在 -> False
示例: 计数器
除了ID生成器, 计数器也是常用的组件之一, 例如点赞回复数量, 播放量等.
from redis import Redis
class Counter:
def __init__(self, client, key):
self.client = client
self.key = key
def increase(self, n=1):
return self.client.incr(self.key, n)
def decrease(self, n=1):
return self.client.decr(self.key, n)
def get(self):
value = self.client.get(self.key)
if value in None:
return 0
else:
return int(value)
def reset(self):
old_value = self.client.getset(self.key)
if old_value is None:
return 0
else:
return(old_value)
client = Redis(decode_responses=True)
counter = Counter(client, "counter::page_viewed")
print(counter.increase()) # +1
print(counter.increase())
print(counter.increase(10)) # +10
print(counter.decrease()) # -1
print(counter.decrease(5)) # -5
print(counter.reset()) # 重置计数器
print(counter.get()) # 返回计数器当前值
注: 在 redis-py 中 INCR 和 INCRBY 都使用 .incr() 方法
示例: 限速器
为了保障系统的安全性和性能, 并保证重要资源不被滥用, 应用程序需要对用户的行为进行限制
- 防止网络爬虫: 限制每个IP地址在固定时间段内访问的页面数量
- 防止爆力破解: 当用户多次输入错误的密码, 会帐号进行冻结
上面机制的实现可以使用限速器, 下面是一个限速器示例代码, 该程序将操作最大可执行次数存储在一个字符串里面, 每次用户进行该操作后就将其减1
from redis import Redis
class Limter:
def __init__(self, client, key):
self.client = client
self.key = key
def set_max_execute_times(self, max_execut_time):
self.client.set(self.key, max_execut_time)
def still_valid_to_execute(self):
num = self.client.decr(self.key)
return (num >= 0)
def remaining_execute_times(self):
num = int(self.client.get(self.key))
if num < 0:
return 0
else:
return num
client = Redis(decode_responses=True)
limter = Limter(client, "wrong_password_limter")
print(limter.set_max_execute_times(5)) # 最多5次输入错误密码
print(limter.still_valid_to_execute()) # 前5次 True, 之后 False