# redis

参考文档 (opens new window)

# redis 简介

Redis 是完全开源免费的,遵守BSD协议,是一个高性能的key-value数据库。

# redis的优势

  • 性能极高 – Redis能读的速度是110000次/s,写的速度是81000次/s 。
  • 丰富的数据类型 – Redis支持二进制的字符串、列表、哈希值、集合和有序集合等数据类型操作。
  • 原子性 – Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。
  • 单个操作是原子性的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来。
  • 丰富的特性 – Redis还支持 发布/订阅, 通知, key 过期等等特性。

# 数据类型

  • 字符串
  • 哈希值
  • 链表
  • 集合
  • 有序列表

# 字符串(String)

String是Redis最基本的类型,你可以理解成与Memcached一模一样的类型,一个key对应一个value。String类型是二进制安全的。意味着Redis的string可以包含任何数据。比如jpg图片或者序列化的对象。String类型是Redis最基本的数据类型,一个Redis中字符串value最多可以是512M。

# SET 设置值

# SET key value  设置指定key的值
set name '褚鹏飞'

# GET key 获取指定key的值
get name

# SETEX key seconds value 设置指定key的值,并将 key 的过期时间设为 seconds 秒
set age "18" 

# 只有在 key 不存在时  设置 key 的值
SETNX key value

SETNX userId "001"

SETNX userId "002"

# 这里拿到的值是 "001"
GET userId 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

这里有一个点需要注意,如果我们对同一个key进行两次set,值会被覆盖。

# GET 获取值

get name
1

当我们给相同的key重复设置值,就会把之前的值覆盖掉。

# GETRANGE 获取子串

Redis Getrange 命令用于获取存储在指定 key 中字符串的子字符串。字符串的截取范围由 start 和 end 两个偏移量决定(包括 start 和 end 在内)。

// GETRANGE key start end 
getrange name 1 2
"鹏飞"
1
2
3

# INCR 递增

SET page_view 20
INCR page_view 
GET page_view  // "21" 数字值在 redis 中以字符串的形式保存 
1
2
3

# 键的常用操作

DEL key  删除某一个键 
DEL user // 删除 user 这个键

EXISTS key 判断一个key是否存在
EXISTS user // 删除之后返回0

EXPIRE key seconds 设置过期时间
EXPIRE user 10  // 设置user 这个键10秒钟 就过期

TTL key 以秒为单位返回给定key的剩余生存时间
TTL user // 查看user的还有多久过期

TYPE key 返回key所存储的值的类型
TYPE user // 返回string

// 通用命令
keys * 

// 检查给定 key 是否存在
EXISTS 001

// 返回 key 所储存的值的类型
TYPE 001

// 返回给定 key 的剩余生存时间(TTL, time to live),以秒为单位 返回-1永久
TTL 001

// 该命令用于在 key 存在是删除 key
DEL name

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

还有一些常用的处理键值对的方法: Redis字符串命令 (opens new window)

# 哈希值(Hash)

主要用于存储对象

# HSET HMSET 设置值

# HSET key field value 	将哈希表 key 中的字段 field 的值设为 value
# HGET key field 	获取存储在哈希表中指定字段的值
# HDEL key field	删除存储在哈希表中的指定字段
# HKEYS key 		获取哈希表中所有字段
# HVALS key 		获取哈希表中所有值
# HGETALL key 		获取在哈希表中指定 key 的所有字段和值

// 设置用户 的名字
HSET 001 name 小明

// 设置用户 的年龄
HSET 001 age 10

// 获取用户名字
HGET 001 name

// 获取用户年龄
HGET 001 age

// 删除年龄
HDEL 001 age

// 获取指定key中的所有字段
HKEYS 001

// 获取指定key的所有的value
HVALS 001

// 获取所有字段和值
HGETALL 001

// 获取不存在的key 返回空
HGET 002 age
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# 列表(List)

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

它的底层实际是个双向链表,对两端的操作性能很高,通过索引下标的操作中间的节点性能会较差。

# LPUSH key value1 [value2] 	将一个或多个值插入到列表头部
# LRANGE key start stop 		获取列表指定范围内的元素
# RPOP key 			            移除并获取列表最后一个元素
# LLEN key 			            获取列表长度
# BRPOP key1 [key2 ] timeout 	移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止

redis 127.0.0.1:6379> LPUSH runoobkey redis
(integer) 1
redis 127.0.0.1:6379> LPUSH runoobkey mongodb
(integer) 2
redis 127.0.0.1:6379> LPUSH runoobkey mysql
(integer) 3
redis 127.0.0.1:6379> LRANGE runoobkey 0 10

1) "mysql"
2) "mongodb"
3) "redis"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

在以上实例中我们使用了 LPUSH 将三个值插入了名为 runoobkey 的列表当中。

# 列表命令

常见的列表命令 (opens new window)

# 集合(Set)

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

集合对象的编码可以是 intset 或者 hashtable。

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

# SADD key member1 [member2] 	向集合添加一个或多个成员
# SMEMBERS key 		            返回集合中的所有成员
# SCARD key 			        获取集合的成员数
# SINTER key1 [key2] 		    返回给定所有集合的交集
# SUNION key1 [key2] 		    返回所有给定集合的并集
# SDIFF key1 [key2] 		    返回给定所有集合的差集
# SREM key member1 [member2] 	移除集合中一个或多个成员

redis 127.0.0.1:6379> SADD runoobkey redis
(integer) 1
redis 127.0.0.1:6379> SADD runoobkey mongodb
(integer) 1
redis 127.0.0.1:6379> SADD runoobkey mysql
(integer) 1
redis 127.0.0.1:6379> SADD runoobkey mysql
(integer) 0
redis 127.0.0.1:6379> SMEMBERS runoobkey

1) "mysql"
2) "mongodb"
3) "redis"

// Redis 操作 set
// 添加三个元素
SADD myset redis mongo node

// 查看所有的成员 可以看到返回的顺序和加入的顺序是不一样的
SMEMBERS myset

// 获取集合的成员数量 3
SCARD myset

// 返回给定所有集合的交集
SADD myset2 redis mongo java ts

// 返回 redis 和 mongo
SINTER myset myset2

// 返回给定的所有集合的并集 
SUNION myset myset2

// 返回给定所有集合的差集 node 
// 不同的顺序获得的结果是不一样的
SDIFF myset myset2
SDIFF myset2 myset
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

在以上实例中我们通过 SADD 命令向名为 runoobkey 的集合插入的三个元素。

# 集合中的常见命令:

  • 查看集合中的所有成员 SMEMBERS key
  • 获取集合的成员数量 SCARD key
  • 返回给定所有集合的交集 SINTER key1 [key2]
  • 返回所有给定集合的并集 SUNION key1 [key2]
  • 返回第一个集合与其他集合之间的差异。SDIFF key1 [key2]

# 有序集合(Zset)

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

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

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

# ZADD key score1 member1 [score2 member2] 	向有序集合添加一个或多个成员,或者更新已存在成员的分数
# ZRANGE key start stop [WITHSCORES] 		通过索引区间返回有序集合中指定区间内的成员
# ZINCRBY key increment member 			    有序集合中对指定成员的分数加上增量 increment
# ZREM key member [member ...] 			    移除有序集合中的一个或多个成员

redis 127.0.0.1:6379> ZADD runoobkey 1 redis
(integer) 1
redis 127.0.0.1:6379> ZADD runoobkey 2 mongodb
(integer) 1
redis 127.0.0.1:6379> ZADD runoobkey 3 mysql
(integer) 1
redis 127.0.0.1:6379> ZADD runoobkey 3 mysql
(integer) 0
redis 127.0.0.1:6379> ZADD runoobkey 4 mysql
(integer) 0
redis 127.0.0.1:6379> ZRANGE runoobkey 0 10 WITHSCORES

1) "redis"
2) "1"
3) "mongodb"
4) "2"
5) "mysql"
6) "4"


// Redis操作zset
ZADD zsetkey 1 redis

ZADD zsetkey 2 mongo

// 获取所有成员 返回从小到大的排列
ZRANGE zsetkey 0 -1 withscores

// 会放在 redis 和 mongo 中间
ZADD zsetkey 1.5 node

// redis 加上5 变成了 6
ZINCRBY zsetkey 5 redis

// 移除元素
ZREM zsetkey redis
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

# express 中使用ioredis

const express = require('express');
const Redis = require('ioredis');

const app = express();
const port = 3000;

// 创建一个连接到本地 Redis 服务器的客户端
const redis = new Redis();

app.use(express.json());

// 设置一个示例路由,将数据存储到 Redis 中
app.post('/set-data', async (req, res) => {
  try {
    const { key, value } = req.body;

    // 使用 SET 命令将数据存储到 Redis 中
    await redis.set(key, value);

    res.status(200).json({ success: true, message: 'Data stored successfully.' });
  } catch (error) {
    console.error(error);
    res.status(500).json({ success: false, message: 'Internal Server Error' });
  }
});

// 设置一个示例路由,从 Redis 中获取数据
app.get('/get-data/:key', async (req, res) => {
  try {
    const key = req.params.key;

    // 使用 GET 命令从 Redis 中获取数据
    const value = await redis.get(key);

    if (value !== null) {
      res.status(200).json({ success: true, data: { key, value } });
    } else {
      res.status(404).json({ success: false, message: 'Data not found.' });
    }
  } catch (error) {
    console.error(error);
    res.status(500).json({ success: false, message: 'Internal Server Error' });
  }
});

app.listen(port, () => {
  console.log(`Server  on http://localhost:${port}`);
});

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

# redis 配置文件

redis的配置文件是:redis.conf

# 网络相关的配置

bind=127.0.0.1 只能接受本机的访问请求, 不写的情况下,无限制接受任何ip地址的访问, 生产环境肯定要写你应用服务器的地址;服务器是需要远程访问的,所以需要将其注释掉。

protected-mode: 本机访问限制 默认是yes,设置为no。

port: 端口 默认是6379,这个没有什么可说的,就是redis的默认配置。

timeout: 一个空闲的客户端维持多少秒会关闭, 默认值是0,表示永远不关闭

tcp-keepalive:对访问客户端的一种心跳检测,每个n秒检测一次,单位为秒,如果设置为0,则不会进行Keepalive检测,建议设置成60 。

loglevel: 指定日志记录级别,Redis总共支持四个级别:debug、verbose、notice、warning,默认为notice。

密码:访问密码的查看、设置和取消,在命令中设置密码,只是临时的。重启redis服务器,密码就还原了。永久设置,需要再配置文件中进行设置。

maxclients:设置redis同时可以与多少个客户端进行连接。默认是10000个

maxmemory:建议必须设置,否则,将内存占满,造成服务器宕机,设置redis可以使用的内存量。一旦到达内存使用上限,redis将会试图移除内部数据。移除规则可以通过maxmemory-policy来指定。

maxmemory-policy:

  • volatile-lru:使用LRU算法移除key,只对设置了过期时间的键;(最近最少使用)
  • allkeys-lru:在所有集合key中,使用LRU算法移除key
  • volatile-random:在过期集合中移除随机的key,只对设置了过期时间的键
  • allkeys-random:在所有集合key中,移除随机的key
  • volatile-ttl:移除那些TTL值最小的key,即那些最近要过期的key
  • noeviction:不进行移除。针对写操作,只是返回错误信息

# Redis模拟手机验证码场景

  • 输入手机号,点击发送后随机生成6位数字的验证码 2分钟有效

  • 输入验证码,点击验证,返回成功或者失败

  • 每个手机号每天只能输入3次

  • 1、生成现有的验证码库生成一个6位数的验证码

  • 2、把验证码放入redis中,设置过期时间为120秒

  • 3、判断验证码是否一致,从redis中获取验证码和输入的验证码进行比较

  • 4、每个手机每天只能发送3次。可以使用incr操作。做次数限制。

上面的设计实现中,将发送三次和验证码存入到不同的key中。

package com.atguigu.jedis;

import redis.clients.jedis.Jedis;

import java.util.Random;

public class PhoneCode {

    public static void main(String[] args) {
        //模拟验证码发送
        verifyCode("13678765435");
        //模拟验证码校验
        //getRedisCode("13678765435","4444");
    }

    //3 验证码校验
    public static void getRedisCode(String phone,String code) {
        //从redis获取验证码
        Jedis jedis = new Jedis("192.168.44.168",6379);
        //验证码key
        String codeKey = "VerifyCode"+phone+":code";
        String redisCode = jedis.get(codeKey);
        //判断
        if(redisCode.equals(code)) {
            System.out.println("成功");
        }else {
            System.out.println("失败");
        }
        jedis.close();
    }

    //2 每个手机每天只能发送三次,验证码放到redis中,设置过期时间120
    public static void verifyCode(String phone) {
        //连接redis
        Jedis jedis = new Jedis("192.168.44.168", 6379);

        //拼接key
        //手机发送次数key
        String countKey = "VerifyCode"+phone+":count";
        //验证码key
        String codeKey = "VerifyCode"+phone+":code";

        //每个手机每天只能发送三次
        String count = jedis.get(countKey);
        
        if(count == null) {
            //没有发送次数,第一次发送
            //设置发送次数是1
            jedis.setex(countKey,24*60*60,"1");
        } else if(Integer.parseInt(count)<=2) {
            //发送次数+1
            jedis.incr(countKey);
        } else if(Integer.parseInt(count)>2) {
            //发送三次,不能再发送
            System.out.println("今天发送次数已经超过三次");
            jedis.close();
        }

        //发送验证码放到redis里面
        String vcode = getCode();
        jedis.setex(codeKey,120,vcode);
        jedis.close();
    }

    //1 生成6位数字验证码
    public static String getCode() {
        Random random = new Random();
        String code = "";
        for(int i=0;i<6;i++) {
            int rand = random.nextInt(10);
            code += rand;
        }
        return code;
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76

# Redis事务

Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。

Redis事务的主要作用就是串联多个命令防止别的命令插队。

# 事务中三个命令 Multi、Exec、discard

从输入Multi命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入Exec后,Redis会将之前的命令队列中的命令依次执行。

组队的过程中可以通过discard来放弃组队。

redis事务

# 事务中的错误处理

事务中错误分为两种情况:

  • 在组队阶段出现错误了
  • 在执行阶段出现错误了

如果组队中某个命令出现了错误,执行的时候整个的所有队列都会被取消。 redis错误

如果是执行阶段某个命令报出了错误,则只有报错的命令不会执行,而其他的命令都会执行,不会回滚。 redis错误

# redis中的事务冲突问题

想象一个场景,很多人有你的账户,同时去参加双十一的抢购,但是你的账户里只有1万块钱。

  • 一个请求想给金额减8000
  • 一个请求想给金额减5000
  • 一个请求想给金额减1000

如果大家都扣款成功,那账户余额肯定是不够用的。 redis事务冲突

为了解决这个问题,我们可以使用锁的机制:

  • 悲观锁
  • 乐观锁

悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

redis悲观锁

乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的。

redis乐观锁

# 乐观锁的操作:

  • watch 在执行multi之前,先执行watch key1 [key2],可以监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。

  • unwatch 取消 WATCH 命令对所有 key 的监视。如果在执行 WATCH 命令之后,EXEC 命令或DISCARD,命令先被执行了的话,那么就不需要再执行UNWATCH了。

# redis事务的三个特性

  • 单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
  • 没有隔离级别的概念:队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行
  • 不保证原子性:事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚
最后更新时间: 1/14/2024, 12:02:34 PM