Python-Redis系列之-基本操作和分布式缓存以及分布式锁

1,442 阅读9分钟

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在当今互联网架构中的位置

image.png

从上图中可以看出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 启动服务端

image.png 图中标记1的位置表示是启动命令redis-server,图中标记2的位置表示启动的端口是6379,进程是53382

2.3.2、 执行: redis-cli 使用客户端连接

image.png 图中标记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

image.png

3.2.2、 get命令

get:获取key对应的值,可参考文档 http://redisdoc.com/string/get.html
命令模板: get name

image.png

3.2.3、 setnx命令

key不存在的情况下设置1个值,如果存在就不操作,可参考文档 http://redisdoc.com/string/setnx.html
命令模板: setnx key value
举例: setnx learn tencent

image.png 从上图中可以看出,设置key为learn的值是tencent,再次通过setnx设置key为learn的值为alibaba,查看设置失败了

3.2.4、 INCR命令

key自增1,如果key不存在会先初始化1个为0key,然后在自增1,可参考文档 http://redisdoc.com/string/incr.html
命令模板: INCR key
举例: INCR dianzan_username_tencent
使用场景说明,比如博文点赞等

image.png

3.2.5、 INCRBY命令

给对应的key自增1个特定的数,可参考文档 http://redisdoc.com/string/incrby.html
命令模板: INCRBY key increment
举例: INCRBY dianzan_username_tencent 4
使用场景说明,比如库存加了4

image.png

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

image.png

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、 如何理解分布式缓存

为了理解分布式缓存,我们通过如下例子来说明

程序员张三的接到一个需求,要求系统提供新增用户和查看用户的功能,举例说明

image.png

image.png

5.1.2、 张三如何设计这个系统

张三会如何设计这个系统呢,于是有了如下对话

张三: 我把用户存到数据库中,新增用户的时候我往数据库中insert一条数据, 查询的时候我select
老板: 接口性能速度能在10ms以下吗?
张三: ...不能(滚)
老板: 必须10ms以内
张三: 那我把数据存到服务器内存中, 这样读取速度快, 应该能到10ms以内
老板: ...(傻逼)
老板: 如果我是一个服务器集群, A机器能读到B机器的缓存吗?
张三: 不能...
张三: 那我用分布式缓存缓存吧, 我把数据存储到redis中, 无论多少台机器都能读取到

那么我们梳理以上对话得出结论, 张三的系统设计分成了3个阶段,如下图

image.png

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瓶,还剩下多少瓶?

image.png

6.2.2、 分别点击购买, 查看结果

image.png 从上面结果来看, 实际上已经不正确了, 因为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中

image.png

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

image.png

当前剩余都是100, 分别点击购买, 查看结果

image.png 购买后剩下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>