Redis快速入门指南(内附分布式锁实现过程)

Redis快速入门指南

在这里插入图片描述

一、redis 安装与配置

在这里插入图片描述

  • 进入终端使用命令下载redis

    wget http://download.redis.io/releases/redis-6.0.1.tar.gz
    
  • 进入文件下载的目录解压压缩包

    tar -zxvf redis-6.0.1.tar.gz -C ./
    # 创建一个软连接
    ln -s redis-6.0.1 redis
    
  • 进入解压后的目录使用make命令进行编译

    make MALLOC=libc
    

    注:如提示make命令不存在,可使用 sudo apt install make 自行安装,编译过程中如果少什么依赖根据报错提示自行安装即可。

  • 安装redis到指定目录

     cd ./src
     make install
     ```
    
    注:一定要先进入redis的src目录才可以执行make install 命令
    
    
  • 配置redis

    为了方便后续的管理,我们在redis的根目录中新建两个文件夹

    mkdir etc
    mkdir bin
    

    将 redis/redis.conf 文件移动到新建的etc目录中

    mv redis.conf etc/
    

    进入src目录将常用的二进制可执行文件拷贝到新建的bin目录中

    cd redis/src
    ls -l | grep "^-rwx" | awk '{print $9}' | xargs -i cp {
          
          } ../bin/
    

    进入刚建的 etc 目录,然后打开编辑 redis.conf 文件

    cd redis/etc
    vim redis.conf
    

    然后修改两个地方即可

    # 绑定的地址 (默认是127.0.0.1,如只是本地访问就不需要修改)
    # ifconfig 查看自己的ip地址绑定即可
    bind 192.168.96.131
    # 配置启动方式为守护进程的方式启动,也就是后台启动
    daemonize yes
    # 配置redis持久化的文件(这个目录需要自己提前创建)
    dir /home/hadoop/opt/redis/data
    # 配置连接密码(可要可不要,真实生产环境肯定还是必要的,但学习时可以图方便不设)
    requirepass 123456
    # 配置文件中还有很多配置,可以自行按照需求对照官网的参数解释进行配置
    
  • 启动后redis-server

    # 使用刚才配置好的配置文件进行启动
    redis-server etc/redis.conf
    # 查看服务是否启动成功
    ps -xu | grep redis-server
    

    至此redis的环境配置完成

二、常用命令

  • 先来看看自带的几个可执行文件的作用
文件名 主要功能
redis-server Redis服务的启动
redis-cli Redis的命令行客户端
redis-benchmark Redis性能测试客户端
redis-check-aof AOF文件修复工具
redis-check-rdb RDB文件检查工具
redis-sentinel Sentinel服务器的启动
  • 先来介绍一下最常用的两个运维命令吧

    (1)redis-server

    刚在在安装时我们已经使用过这个命令了,我们先来来介绍一下它的一些常用参数;

    # 就会罗列出这个命令的使用帮助
    redis-server --help
    # 指定端口启动(redis 默认就是监听6379端口,可以自行更改)
    redis-server --port 6379
    # 使用配置文件的方式启动
    redis-server redis.conf
    

    (2)redis-cli

    这个命令就比较重要了,使用它可以连接到我们的redis服务器。它提供了一个可交互的命令行窗口,我们可以在命令行做一些测试以及修改配置关闭服务器等等,总之非常的强大。

    # 先列举一些简单的运维命令
    
    # 连接前面我们启动的redis服务器 (如果端口和地址都是默认其实不填参数也行,如果设有密码加 -a 参数即可)
    redis-cli -h 192.168.96.131 -p 6379
    # 关闭redis-server(平时建议使用此命令进行关闭服务,
    # 因为暴力关闭的化可能会造成数据还没来得急持久化,
    # 服务就被关闭了从而导致数据丢失)
    # 客户端连接后再输入命令
    > SHUTDOWN
    # 查看连接是否正常
    > PING
    # 打印一段文本类容
    > ECHO 'hello world!'
    # 退出客户端(注意这里和关闭服务时有区别的,这里先当与只是断开了连接)
    > quit
    

    在这里插入图片描述

三、redis常见数据类型以及用法

1. 字符串类型(string)

string是redis最基本的类型,你可以理解成与Memcached一模一样的类型,一个key对应一个value。

string类型是二进制安全的。意思是redis的string可以包含任何数据。比如jpg图片或者序列化的对象 。

string类型是Redis最基本的数据类型,一个键最大能存储512MB。

在需要使用命令前先提示一句(Rredis 的命令是不区分大小写的,但为了和键值进行区分还是把命令写成大写比较直观)

用法:

  • 创建一个字符串类型的键

用法 SET [key] [value] [EX seconds | PX milliseconds]

> SET key1 "hello world!"
OK
  • 读取刚才新建的这个键所对应的值
> GET key1
"hello world!"
  • 再次设置同一个键就会自动覆盖前面的值
> SET key1 "override"
OK
  • 设置键的过期时间
# 秒为单位:
> SET key1 "hello world!" EX 1
# 等一秒之后再获取值 (这个键时间已到期就被删除了)
> GET key1
(nil)

# 毫秒为单位:
> SET key1 "hello world!" PX 1000
OK
# 也是等一秒后再获取值,键也是时间到期就被删除
> GET key1
(nil)
  • 将某个键对应的数组自增1(注意这个键对应的值必须是可以转为整数类型的)

用法: INCR [key]

> SET key1 1
OK
> INCR key1
(integer) 2
  • 将某个键对应的数值自减1

用法: DECR [key]

> DECR
(integer) 1
  • 将某个键的值增加指定的整数值

用法: INCRBY [key] [increment]

> INCRBY key1 10
(integer) 11
  • 将某个键的值减少指定的整数值

用法: DECRBY [key] [decrement]

> DECRBY key1 10
(integer) 1
  • 将某个键的值增加指定的浮点数值

用法: INCRBYFLOAT [key] [increment]

注意: 没有减少的方法,如需减少只需要再增长数值前加负号即可

> INCRBYFLOAT key1 0.5
"1.5"
> INCRBYFLOAT key1 -0.5
"1"
  • 在字符串后面追加

用法: APPEND [key] [value]

# 返回值为追加字符串后,字符串的总长度
> APPEND key1 name
(integer) 5
> GET key1
"1name"

这里也只是笼统的介绍了一下常用的一些命令,命令还有很多就不依次列举了

2.哈希类型(hash)

Redis hash 是一个string类型的field和value的映射表,hash特别适合用于存储对象。

Redis 中每个 hash 可以存储 2 32 − 1 2^{32}-1 2321个键值对(40多亿)。

  • 一次性创建多个键值对在一个实体中

用法: HMSET key field value [field value …]

# 创建一个学生'对象',姓名为Nick,年龄:18,性别:M
> HMSET student1 name "Nick" age 18 gender "M"
OK
  • 获取刚创建的这个实体

用法: HGETALL key

> HGETALL student1
1) "name"
2) "Nick"
3) "age"
4) "18"
5) "gender"
6) "M"
  • 获取实体的某一个属性值

用法: HMGET key field [field …]

> HMGET student1 name
1) "Nick

3.列表类型(list)

Redis列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素导列表的头部(左边)或者尾部(右边)

一个列表最多可以包含 2 32 − 1 2^{32} - 1 2321 个元素 (4294967295, 每个列表超过40亿个元素)。

  • 像一个键对应的列表的头部添加一个或多个元素(如果键不存在会自动创建)

用法: LPUSH key element [element …]

# 返回的整数代表,加入元素后列表的总长度
> LPUSH color red blue yellow green
(integer) 4
  • 获取列表中的元素

用法: LRANGE key start stop

# 即使超过了列表的长度,也没关系
> LRANGE color 0 10
1) "green"
2) "yellow"
3) "blue"
4) "red"
  • 从列表尾部插入一个或多个元素

用法: RPUSH key element [element …]

> RPUSH color white black pink
(integer) 7
> LRANGE color 0 10
1) "green"
2) "yellow"
3) "blue"
4) "red"
5) "white"
6) "black"
7) "pink"

4.集合类型(set)

Redis的Set是string类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。

Redis 中集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。

集合中最大的成员数为 2 32 − 1 2^{32} - 1 2321 (4294967295, 每个集合可存储40多亿个成员)。

  • 向集合中添加元素

用法: SADD key member [member …]

# 返回的整数代表添加的个数
> SADD zoo1 tiger lion snake
(integer) 3
# 添加重复的元素看看
> SADD zoo1 snake
(integer) 0
  • 显示集合中的所有元素

用法: SMEMBERS key

SMEMBERS1 zoo
1) "snake"
2) "tiger"
3) "lion"
  • 获取多个集合的交集

用法: SINTER key [key …]

# 先在创建一个集合了来
> SADD zoo2 tiger lion cat dog
(integer) 4
# 求两个集合的交集
SINTER zoo1 zoo2
1) "tiger"
2) "lion"
  • 获取多个集合的并集

用法: SUNION key [key …]

SUNION zoo1 zoo2
1) "lion"
2) "cat"
3) "snake"
4) "tiger"
5) "dog"
  • 获取多个集合的差集

用法: SDIFF key [key …]

> SDIFF zoo1 zoo2
1) "snake"

4.有序集合(sorted set)

Redis 有序集合和集合一样也是string类型元素的集合,且不允许重复的成员。

不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。

有序集合的成员是唯一的,但分数(score)却可以重复。

集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。 集合中最大的成员数为 2 32 − 1 2^{32} - 1 2321 (4294967295, 每个集合可存储40多亿个成员)。

  • 创建一个有序集合并添加元素

用法: ZADD key score member [score member …]

# 返回的也是添加的个数
> ZADD math_grade 88 "Nick" 99 "Alice" 55 "Tom" 
(integer) 3
  • 返回指定索引区间内的元素

用法: ZRANGE key start stop [WITHSCORES]

# WITHSCORES 的作用是将分数一并显示出来,可以不加
> ZRANGE math_grade 0 10 WITHSCORES
1) "Tom"
2) "55"
3) "Nick"
4) "88"
5) "Alice"
6) "99"
  • 返回某个元素的分数

用法: ZSCORE key member

> ZSCORE math_grade Tom 
"55"
  • 计算指定分数段内的元素个数

用法: ZCOUNT key min max

> ZCOUNT math_grade 0 59
(integer) 1
  • 通过分数返回有序集合指定区间内的元素

用法: ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]

> ZRANGEBYSCORE math_grade 0 59
1) "Tom"

三、对键(key)的整体操作

  • 查看已有的键

用法: KEYS pattern

pattern 支持模糊匹配,遵循glob风格

通配符 含义
? 匹配一个任意字符
* 匹配任意多个任意字符
[] 匹配括号间的一个字符,可以用 ‘-’ 表示一个范围
\ 用于转义使用,如果要匹配 “?” 就可以使用 '?" 进行匹配
# 查看当前全部已经存在的键
> KEYS *
1) "zoo1"
2) "key1"
3) "math_grade"
4) "color"
5) "key2"
6) "zoo2"
7) "student1"
  • 检查给定的 Key 是否存在

用法: EXISTS key [key …]

# 键存在返回 1,不存在返回 0
> EXISTS zoo1
(integer) 1
> EXISTS zoo3
(integer) 0
  • 删除给定键

用法: DEL key [key …]

# 返回值为删除成功的键的个数
> DEL zoo1
(integer) 1
  • 为某个键设置过期时间

用法: 秒:EXPIRE key seconds,毫秒:PEXPIRE key milliseconds

# 如果重复设置会覆盖之前的设置
# 返回值为 1 表示成功,0 为键不存在
> EXPIRE key1 60
(integer) 1

> PEXPIRE key1 1000
(integer) 1
  • 产看键的剩余时间(也就是还有好久过期)

用法: 秒: TTL key, 毫秒: PTTL key

# 秒为单位
> EXPIRE zoo2 600
(integer) 1
> TTL zoo2
(integer) 597

# 毫秒为单位
> PTTL zoo2
(integer) 590512
  • 清除过期时间,让键保持持久

用法: PERSIST key

> PERSIST zoo2
(integer) 1
# 返回负数表示不存在过期时间
> PTTL zoo2
(integer) -1
  • 产看键对应的值的类型

用法: TYPE key

> TYPE color
list
> TYPE student1
hash
  • 更改键名称

用法: RENAME key newkey

> keys *
1) "math_grade"
2) "color"
3) "key2"
4) "zoo2"
5) "student1"

> RENAME color colors
OK

> keys *
1) "colors"
2) "math_grade"
3) "key2"
4) "zoo2"
5) "student1"

四、使用python操作Redis

1.安装第三方包

安装非常简单只需要一条命令即可完成安装

pip install redis

2.连接到Redis服务器

redis 提供两个类 Redis 和 StrictRedis, StrictRedis 用于实现大部分官方的命令,Redis 是 StrictRedis 的子类,用于向后兼用旧版本。

  • 方法一:
import redis

# decode_responses 的作用是将返回值进行解码,默认是返回字节类型
r = redis.Redis(host="192.168.96.131",
                port=6379, 
                password="123456", 
                decode_responses=True)

  • 方法二:
import redis

redis.StrictRedis(host="192.168.96.131",
                  port=6379, 
                  password="123456", 
                  decode_responses=True)

查看源码我们发现两个类的内部都实现了 "__enter__“和”__exit__"方法,也就意味着更推荐我们使用python的with方法来实例化连接对象,这样发生异常就会自动为我们断开连接,从不不占用连接数,示例如下:

import redis

with redis.StrictRedis(host="192.168.96.131",
                       port=6379,
                       password="123456",
                       decode_responses=True) as R:
    pass
  • redis 还提供了一个更高效的方法,也就是连接池 (这适用于高并发的系统,这样会减少很多连接和关闭带来的开销) 默认,每个Redis实例都会维护一个自己的连接池。可以直接建立一个连接池,然后作为参数传入Redis(或 StrictRedis),这样就可以实现多个 Redis 实例共享一个连接池。
import redis

pool = redis.ConnectionPool(host="192.168.96.131",
                            port=6379,
                            password="123456",
                            decode_responses=True)

with redis.StrictRedis(connection_pool=pool) as r:
    pass

3.调用 API 操作 Redis

  • python 的 redis 库操作redis很简单,几乎就和我们前面所学的命令行中的操作一致,就先讲一些基础常见的API使用方法吧,如有另外的需求可以去看官方文档;

    注: 下文的 R 就代表我们的连接Reidis后的对象

  • set() 方法

参数对照表

参数名 作用
name key 的名称
value key 对应的值
ex 过期时间(秒)
px 过期时间(毫秒)
nx 如果设置为True,则只有name不存在时,当前set操作才执行
xx 如果设置为True,则只有name存在时,当前set操作才执行

示例:

# 创建一个字符串类型的 key-value 3秒后过期
R.set(name="test_key1", value="test",ex=3)
  • exists(*names)方法

    传入一个键名或多个键名,返回是否存在这些键,如果存在就会返回True,如果不存在就会返回False

示例:

# 判断给定的键名是否已被创建
R.exists("test_key")
  • incr(name, amount=1)方法

    将对应传入的键名对应的值自增,增加的值为amount的值,如果是键不存在,就会创建这个键,然后初始值就是amount的值,最后整个函数返回自增后值

示例:

# 创建或者增增一个键,自增的值为10
R.incr("test_incr",amount=10)
  • delete(*name)

    根据传入的一个或者多个键名,然后再redis中删除这些键对应的键值对

示例:

# 删除两个键值对
R.delete("key1","key2")
  • expire(name,time)

    将传入的键对应的键值对设置超时时间,如果到达超时时间,那么这个键值对就会自动删除,time为超时时间,单位为秒

示例:

# 将某个键设置10秒的超时时间
R.expire("test_key",time=10)

五、利用redis实现分布式锁

1.分布式锁原理

示意图:

在这里插入图片描述

  • 我们的客户端首先发生并发任务,由于客户端需要的资源再整个集群中只有一件,这个资源在一个时间段内也只允许一个客户端使用.那么这些并发任务中的客户端就会去 ‘抢’ 这个资源,我们就要设定一个规则如果它们中有一个客户但一旦抢到了,那么就要通知其他的客户端资源已被占用,这时没抢到的客户端就会进入阻塞状态知道资源被释放,然后再开始竞争资源,整个锁的实现的详细流程就如下:

    (1) 每个客户端向Redis中创建代表自己的键值对.键为整个Redis数据库中唯一的,且键是自增的(也就是先到的键就要小一点),

    (2) 客户端再Redis中创建键值成功后,Redis会返回自己的键名给客户端,从图中可以看到:客户端1创建的键为002,客户端2创建的键为001,客户端3创建的键为003.

    (3) 然后客户端会进行’自我检查’,判断自己注册的键是否为最小的,如果是就表示自己抢夺资源成功,然后立刻就就要设置自己在Redis上注册的这个键的过期时间.

    (4) 其他没有抢夺成功的客户端就会监听比自己键小 1 的键,如果这个键消失了,就表示资源已被释放,那么自己就可以占用这个资源了.就图中而言:我们的锁被客户端2占用,这时客户端1监听客户端2,客户端3监听客户端1.

    (5) 一直往返执行步骤 (4),知道所有客户端都已使用资源

    (6) 此次并发任务结束

2.代码实现

import redis
import time
import threading

LOCK_ACCESS_ID = "lock:access:id"
LOCK_REGISTER_KEY = "lock:register:id:"

redis_pool = redis.ConnectionPool(host="192.168.96.131",
                                  port=6379,
                                  password="123456",
                                  decode_responses=True)


class DistributedLock(object):

    def __init__(self, pool):
        self.redis_connect = redis.StrictRedis(connection_pool=pool)
        self.my_key = None

    def get_lock(self, timeout=10):
        """
        desc: 获取锁

        :param timeout:获取锁之后的最大占用时间
        :return: None
        """
        # 向 redis 获取注册的 id (使用 lua 脚本执行从而实现原子性)
        register_lock_lua = """
                  local register_id = redis.call("incr", KEYS[1])
                  redis.call("set",KEYS[2]..register_id,register_id)
                  return register_id
            """
        # 初始化注册锁id的脚本
        register_lock_id_script = self.redis_connect.register_script(register_lock_lua)
        # 向脚本中传入参数,并执行脚本,返回注册的 锁id
        lock_id = register_lock_id_script(keys=[LOCK_ACCESS_ID, LOCK_REGISTER_KEY])
        # 初始化自己的锁对应的键名
        self.my_key = "".join([LOCK_REGISTER_KEY, str(lock_id)])
        # 当前自己需要监听的节点
        listen_key = "".join([LOCK_REGISTER_KEY, str(lock_id - 1)])

        # 判断监听的锁是否存在,如果已经消失则自己获得锁
        # 并将自己对应在redis上的键值对设置过期时间
        listen_lua = """
                local exists = redis.call("exists", KEYS[1])
                if exists then
                    redis.call("expire",KEYS[2],ARGV[1]) 
                end
                return exists
        """
        listen_key_script = self.redis_connect.register_script(listen_lua)
        # 判断比自己小 1 的键是否存在,如果存在就阻塞
        while listen_key_script(keys=[listen_key, self.my_key], args=[timeout]):
            time.sleep(1)
        # print("do something")

    def release(self):
        # 判断自己注册的键是否还存在
        if self.redis_connect.exists(self.my_key):
            # 如果存在就删除这个键,表示主动释放锁
            self.redis_connect.delete(self.my_key)
        else:
            # 如果键已经不存在了,那么表示我们的锁已经超时,资源被自动释放了
            print("超时释放")

    def close(self):
        self.redis_connect.close()


# 定义装饰器,是得我们的锁的使用更加灵活
def add_lock(pool):
    def wrapper1(func):
        def wrapper2(*args, **kwargs):
            dl = DistributedLock(pool)
            try:
                dl.get_lock()
                func(*args, **kwargs)
            finally:
                dl.release()
                dl.close()
        return wrapper2
    return wrapper1


# 调用装饰器,代表被装饰的这个函数需要锁
@add_lock(redis_pool)
def task(n):
    print(f"task{n}xec")
    time.sleep(2)


if __name__ == '__main__':
    # 使用对线程模拟分布式环境的并发
    task_pool = []
    for i in range(10):
        threading.Thread(target=task, args=(i,)).start()

y):
            # 如果存在就删除这个键,表示主动释放锁
            self.redis_connect.delete(self.my_key)
        else:
            # 如果键已经不存在了,那么表示我们的锁已经超时,资源被自动释放了
            print("超时释放")

    def close(self):
        self.redis_connect.close()


# 定义装饰器,使得我们的锁的使用更加灵活
def add_lock(pool):
    def wrapper1(func):
        def wrapper2(*args, **kwargs):
            dl = DistributedLock(pool)
            try:
                dl.get_lock()
                func(*args, **kwargs)
            finally:
                dl.release()
                dl.close()
        return wrapper2
    return wrapper1


# 调用装饰器,代表被装饰的这个函数需要锁
@add_lock(redis_pool)
def task(n):
    print(f"task{n}xec")
    time.sleep(2)


if __name__ == '__main__':
    # 使用对线程模拟分布式环境的并发
    task_pool = []
    for i in range(10):
        threading.Thread(target=task, args=(i,)).start()

猜你喜欢

转载自blog.csdn.net/qq_42359956/article/details/106179137