1、Redis简介
Redis是一个使用ANSI C编写的开源、支持网络、基于内存、分布式、可选持久性的键值对存储数据库。从2015年6月开始,Redis的开发由Redis Labs赞助,而2013年5月至2015年6月期间,其开发由Pivotal赞助。[2]在2013年5月之前,其开发由VMware赞助。[3][4]根据月度排行网站DB-Engines.com的数据,Redis是最流行的键值对存储数据库
1.1、redis与mysql的关系
在实际的工作场景中,redis和mysql大致是一样的,为后端服务提供数据存储功能,我们通过如下一张图看看redis和mysql在当今互联网架构中的位置
从上图中可以看出redis是属于存储层,主要用于数据存储,用户在APP,小程序,web上产生数据后将会存储到redis中
那么,有mysql为什么还需要redis呢,首先mysql因为数据存储在磁盘中,而redis存储在内存中,mysql没有redis查询速度快,同时也正因为如此,内存一定是有限的,所以不能所有数据都存储到redis中,一般情况,我们会将一些热点数据,排名数据存储到redis中
- 热点数据,比如微博热搜
- 排名数据,点赞数量,收藏数量,转发数量
- 固定的数据,比如淘宝首页的商品信息,通常价格信息禁止使用redis
- 特定的时候也会出现全量缓存的情况
2、Redis的安装
2.1、 macOS使用命令安装
brew install redis
2.1、 windows在github上下载安装包
https://github.com/tporadowski/redis/releases
下载完成后进入redis的目录下执行命令
redis-server.exe redis.windows.conf
2.3、 启动过程
2.3.1、 执行: redis-server 启动服务端
图中标记1的位置表示是启动命令redis-server,图中标记2的位置表示启动的端口是6379,进程是53382
2.3.2、 执行: redis-cli 使用客户端连接
图中标记1的位置表示执行命令redis-cli,图中标记2的位置表示当前连接的服务器是127.0.0.1,端口是6379
其中redis-cli -h redis服务器地址 -p 服务器启动端口号
3、 Redis的常用操作
3.1、 Redis总共有5种数据类型
- string,字符类型,也是使用非常常见的一种类型
- hash,哈希类型,也就是python中的字典类型
- list,列表类型,也就是python中的list类型
- set,集合类型,类似python中的set类型
- zset,有序集合类型 本次只介绍string类型,完成基本的工作已经足够了
3.2、 Redis中string类型的基本操作
3.2.1、 set命令
set:给key设置一个值,可参考文档 http://redisdoc.com/string/set.html
命令模板: set key value ex 1
3.2.2、 get命令
get:获取key对应的值,可参考文档 http://redisdoc.com/string/get.html
命令模板: get name
3.2.3、 setnx命令
在key不存在的情况下设置1个值,如果存在就不操作,可参考文档 http://redisdoc.com/string/setnx.html
命令模板: setnx key value
举例: setnx learn tencent
从上图中可以看出,设置key为learn的值是tencent,再次通过setnx设置key为learn的值为alibaba,查看设置失败了
3.2.4、 INCR命令
给key自增1,如果key不存在会先初始化1个为0的key,然后在自增1,可参考文档 http://redisdoc.com/string/incr.html
命令模板: INCR key
举例: INCR dianzan_username_tencent
使用场景说明,比如博文点赞等
3.2.5、 INCRBY命令
给对应的key自增1个特定的数,可参考文档 http://redisdoc.com/string/incrby.html
命令模板: INCRBY key increment
举例: INCRBY dianzan_username_tencent 4
使用场景说明,比如库存加了4件
4、使用python操作redis
4.1、 在python操作redis之前我们需要先准备好环境
- 安装好redis server,按照序号2中的步骤
- 安装好python环境,百度安装步骤
- 安装redis包
pip3 install redis
4.2、 启动redis server
redis-server
4.3、 验证python redis包是否正常
import redis
4.4、 redis基本的get和set操作
设置key的值为value,如果存在就覆盖
from redis import Redis
# 连接redis
redis_cli = Redis("127.0.0.1", 6379)
# 设置name为tencent
redis_cli.set("name", "tencent")
# 获取name的值
print(redis_cli.get("name"))
# 打印的值b'tencent'
4.5、 redis使用setnx操作
如果key存在就不设置值,如果不存在就设置值
# setnx操作
# 设置learn的值为ten
key = "learn"
redis_cli.setnx(key, "ten")
# 再次设置learn的值为cent
redis_cli.setnx(key, "cent")
print(redis_cli.get(key))
# 打印的值为b'ten'
4.6、redis使用INCR操作
给key自增1,注意如果key已经存在且存储的是string,就不能自增1
# 使用INCR做计数器
incr_key = "total"
# 给total这个key自增1
redis_cli.incr(incr_key)
print(redis_cli.get(incr_key))
4.7、 redis使用INCRBY操作
# 使用INCRBY自增特定的值
incr_by_key = "total_incrby"
redis_cli.incrby(incr_by_key, 3)
# 打印total_incrby的值
print(redis_cli.get(incr_by_key))
# 打印结果为b'3'
4.8、 redis使用lock进行分布式锁加锁操作
# 使用redis lock进行分布式锁操作
buy_lock = redis_cli.lock("buy_lock")
print(buy_lock.acquire())
print("执行到这里了")
# 结果只会执行到这, 下面的代码会被阻塞
print(buy_lock.acquire())
print("执行到这里了吗?")
4.9、 redis使用lock进行释放锁
# 使用redis lock进行分布式锁操作
buy_lock = redis_cli.lock("buy_lock")
# 释放锁
print(buy_lock.release())
5、 结合flask进行分布式缓存数据存储
5.1、 如何理解分布式缓存
为了理解分布式缓存,我们通过如下例子来说明
程序员张三的接到一个需求,要求系统提供新增用户和查看用户的功能,举例说明
5.1.2、 张三如何设计这个系统
张三会如何设计这个系统呢,于是有了如下对话
张三: 我把用户存到数据库中,新增用户的时候我往数据库中insert一条数据, 查询的时候我select
老板: 接口性能速度能在10ms以下吗?
张三: ...不能(滚)
老板: 必须10ms以内
张三: 那我把数据存到服务器内存中, 这样读取速度快, 应该能到10ms以内
老板: ...(傻逼)
老板: 如果我是一个服务器集群, A机器能读到B机器的缓存吗?
张三: 不能...
张三: 那我用分布式缓存缓存吧, 我把数据存储到redis中, 无论多少台机器都能读取到
那么我们梳理以上对话得出结论, 张三的系统设计分成了3个阶段,如下图
5.1.3、分布式缓存代码实现
from flask import Flask, request
from redis import Redis
import json
app = Flask(__name__)
@app.route("/add/user", methods=["POST"])
def add_user():
# 获取到body中的参数,包含username和password
data = request.get_json()
# 获取参数中的username
username = data.get("username")
# 获取参数的中password
password = data.get("password")
# 连接redis
redis_cli = Redis("localhost", 6379)
# 定义存用户的key为user_username
key = "user_" + username
# 定义存用户的数据结构,定义为一个字典
user = {
"username": username,
"password": password
}
# 将用户存储到redis中
redis_cli.set(key, json.dumps(user))
return {
"code": 200,
"message": "请求成功",
"data": "添加成功"
}
@app.route("/get/user")
def get_user():
# 获取get接口中的username参数
username = request.args.get("username")
# 连接redis
redis_cli = Redis("localhost", 6379)
# 定义redis的key
key = "user_" + username
# 从redis中获取到存储的用户信息
user = redis_cli.get(key)
# redis返回的都是byte类型,转成string类型
user_string = str(user, encoding="utf-8")
return {
"code": 200,
"message": "请求成功",
# 通过json转成dict后返回给接口
"data": json.loads(user_string)
}
if __name__ == "__main__":
app.run()
6、redis分布式锁的使用
6.1、 场景分析
以最常见的电商场景为例子, 通用例子
用户购买商品 -> 生成订单 -> 扣减库存
问题思考: 如果现在有10瓶可乐, A和B同时购买了2瓶, 那么还剩下多少瓶?
这个问题应该很快大家都有答案了, 结果是还剩下6瓶, 但是在并发编程的世界里, 情况往往并不是你想的那么乐观
6.2、 未使用分布式锁时出现的问题
6.2.1、 首先从图中可以查看,目前可乐剩下100瓶,如果2个人同时都购买1瓶,还剩下多少瓶?
6.2.2、 分别点击购买, 查看结果
从上面结果来看, 实际上已经不正确了, 因为2个人分别购买一瓶后, 应该是还剩下98瓶
6.2.3、 无分布式锁的代码都在下面
"""
无分布式锁使用demo
"""
from flask import Flask, request
from redis import Redis
import time
app = Flask(__name__)
@app.route("/reduce", methods=["POST"])
def reduce():
# 获取参数
data = request.get_json()
# 获取商品名称
sku_name = data.get("skuName")
# 获取购买的商品数量
num = data.get("num")
# 连接redis
redis_cli = Redis("localhost", 6379)
total = int(str(redis_cli.get(sku_name), encoding="utf-8"))
remain_num = total - num
time.sleep(3)
# 购买后更新缓存
redis_cli.set(sku_name, remain_num)
return {
"code": 200,
"message": "请求成功",
"data": None
}
@app.route("/get")
def get():
# 获取到商品名称
sku_name = request.args.get("skuName")
# 创建redis连接
redis_cli = Redis("localhost", 6379)
# 剩余数量
remain_total = redis_cli.get(sku_name)
# 通过接口返回
return {
"code": 200,
"message": "请求成功",
"data": str(remain_total, encoding="utf-8")
}
# 跨域问题设置, 不需要理解, 这是固定值
@app.after_request
def cors(environ):
environ.headers['Access-Control-Allow-Origin']='*'
environ.headers['Access-Control-Allow-Method']='*'
environ.headers['Access-Control-Allow-Headers']='x-requested-with,content-type'
return environ
if __name__ == "__main__":
app.run(port=5001)
6.2.4、 分析问题出现的原因
无锁场景: A和B都是获取到总数100后分别减去1, 剩下99存储到redis中
有锁场景: A和B, 先获得锁的线程获取总数100后减去1, 剩下99存储到redis中, 然后在释放锁, B一直在等待锁, 等A执行完成后, B获得锁, 获取总数为99, 减去1, 剩下98存储到redis中
6.2.5、 给代码加上分布式锁
"""
分布式锁使用demo
"""
from flask import Flask, request
from redis import Redis
import time
app = Flask(__name__)
@app.route("/reduce", methods=["POST"])
def reduce():
# 获取参数
data = request.get_json()
# 获取商品名称
sku_name = data.get("skuName")
# 获取购买的商品数量
num = data.get("num")
# 连接redis
redis_cli = Redis("localhost", 6379)
'''
total = int(str(redis_cli.get(sku_name), encoding="utf-8"))
remain_num = total - num
time.sleep(3)
# 购买后更新缓存
redis_cli.set(sku_name, remain_num)
'''
# 创建一个购买锁
buy_lock = redis_cli.lock("buy")
# 获取这个锁, 这里如果没有获取到锁, 代码或阻塞在这, 一直等待获取到锁为止
if buy_lock.acquire():
# 获取到这个商品的数量
total = int(str(redis_cli.get(sku_name), encoding="utf-8"))
# 本次购买后的剩余数量
remain_num = total - num
# 为了更好的展示数据并发问题, 这里sleep 1s
time.sleep(3)
# 购买后更新缓存
redis_cli.set(sku_name, remain_num)
# 释放锁
buy_lock.release()
return {
"code": 200,
"message": "请求成功",
"data": None
}
@app.route("/get")
def get():
# 获取到商品名称
sku_name = request.args.get("skuName")
# 创建redis连接
redis_cli = Redis("localhost", 6379)
# 剩余数量
remain_total = redis_cli.get(sku_name)
# 通过接口返回
return {
"code": 200,
"message": "请求成功",
"data": str(remain_total, encoding="utf-8")
}
# 跨域问题设置, 不需要理解, 这是固定值
@app.after_request
def cors(environ):
environ.headers['Access-Control-Allow-Origin']='*'
environ.headers['Access-Control-Allow-Method']='*'
environ.headers['Access-Control-Allow-Headers']='x-requested-with,content-type'
return environ
if __name__ == "__main__":
app.run(port=5001)
6.2.6、使用前端购买商品来证明有效性
在redis客户端中初始化可乐为100瓶
127.0.0.1:6379> set 可乐 100
当前剩余都是100, 分别点击购买, 查看结果
购买后剩下98, 说明结果正确, 分布式锁是正确的
7、 前端代码
前端代码不熟悉, 全靠手敲
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script>
function sub(){
current = document.getElementsByClassName("quantity")[0].value
current = Number(current - 1)
document.getElementsByClassName("quantity")[0].value = current
}
function add(){
current = document.getElementsByClassName("quantity")[0].value
current = Number(current) + 1
document.getElementsByClassName("quantity")[0].value = current
}
function buy(){
console.log("开始购买")
var xhr = new XMLHttpRequest()
//设置请求行
xhr.open("POST", "http://localhost:5001/reduce")
//设置请求头(POST)
xhr.setRequestHeader("Content-Type","application/json")
//设置请求体
num = document.getElementsByClassName("quantity")[0].value
data = JSON.stringify({"skuName": "可乐", "num": Number(num)})
xhr.send(data)
xhr.onreadystatechange = function () {
if (xhr.readyState==4 &&xhr.status==200) {
//步骤五 如果能够进到这个判断 说明 数据 完美的回来了,并且请求的页面是存在的
alert("购买成功")
}
}
}
window.onload = function(){
console.log("页面刷新")
var xhr = new XMLHttpRequest()
//设置请求行
xhr.open("get", "http://localhost:5001/get?skuName=可乐")
//设置请求头(POST)
xhr.setRequestHeader("Content-Type","application/json")
xhr.send()
xhr.onreadystatechange = function () {
if (xhr.readyState==4 &&xhr.status==200) {
//步骤五 如果能够进到这个判断 说明 数据 完美的回来了,并且请求的页面是存在的
res = JSON.parse(xhr.response)
document.getElementsByTagName("span")[0].innerText = res.data
}
}
}
</script>
</head>
<body>
<div class="order-item">
<img class="buy-item" src="./可乐.jpeg">
剩余: <span></span>
<div class="p-quantity">
<input type="button" class="decrease" value="-" onclick="sub()">
<input type="text" class="quantity" value="1"/>
<input type="button" class="increase" value="+" onclick="add()">
</div>
<div>
<button onclick="buy()">购买</button>
</div>
</div>
</body>
<style>
.order-item{
width: 100px;
height: 100px;
}
.buy-item{
width: 100px;
height: 100px;
}
.quantity{
width: 50px;
}
.p-quantity{
display: flex;
}
</style>
</html>