1. 基础介绍

1.1 什么是Redis

Redis是一种键值型NoSQL数据库,即数据以键(key)值(value)对的形式存在。多用于对数据的缓存处理,是一种内存数据结构存储器。

1.2 SQL与NOSQL

NoSql可以翻译做Not Only Sql(不仅仅是SQL),或者是No Sql(非Sql的)数据库。是相对于传统关系型数据库而言,有很大差异的一种特殊的数据库,因此也称之为 非关系型数据库

1.2.1结构化与非结构化

传统关系型数据库是结构化数据,每张表在创建的时候都有严格的约束信息,如字段名、字段数据类型、字段约束等,插入的数据必须遵循这些约束

而NoSQL则对数据库格式没有约束,可以是键值型,也可以是文档型,甚至是图格式。

1.2.2关联与非关联

SQL的数据,表与表之间通常具有关联性,如 外键约束。

而非关系型的的数据库关系维护要么通过代码之间的业务逻辑,要么通过数据之间的耦合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
id: 1,
name: "张三",
orders: [
{
id: 1,
item: {
id: 10, title: "荣耀6", price: 4999
}
},
{
id: 2,
item: {
id: 20, title: "小米11", price: 3999
}
}
]
}

例如通过上述结构存储张三与他的各个订单,不可避免的会出现订单信息的冗余,推荐使用业务逻辑来维护数据关系。

1.2.4 语法结构的统一性

关系型数据库通常具有统一的语法格式,如:

1
SELECT id,name,age FROM tb_user WHERE id=1;

而非关系型数据库语法格式不固定,不同的库语法可能完全不同。

1
2
3
Redis: get user:1
MongoDB: db.use.find({_id : 1})
elasticsearch: GET http://localhost:9200/user/1

1.2.5 事务处理上

关系型通常需要满足 ACID特性,即原子性,一致性,隔离性,持久性。

而非关系型数据库要么没有事务,要么无法满足强一致性,只能做一些基本的满足。

SQL NoSQL
数据结构 结构化(Structured) 非结构化
数据关联 关联(Relational) 无关联的
查询方式 SQL 非SQL
事务处理 ACID BASE
存储方式 磁盘 内存
扩展性 垂直 水平
使用场景 1)数据结构稳定
2)相关业务对数据安全性、一致性要求较高
1)数据结构不固定
2)对一致性、安全性要求不高 
3)对性能需求大

1.3 Redis

Redis : Remote Dictionary Serer 远程字典服务器,诞生于2009年,是一个基于内存的键值型NoSQL数据库。

作者:antirez

特征:

  • 键值型 : value支持多种不同的数据类型.
  • 单线程:所有命令串行执行,因此其线程是安全的。每个命令具有原子性。
  • 低延迟,速度快(基于内存,IO多路复用,良好的编码)。
  • 支持数据持久化
  • 支持主从集群,分片集群
  • 支持多语言客户端

1.4 配置准备

Redis 并未给出官方的windows版本,可以通过虚拟机技术模拟linux系统环境。

由于 centOS已经停止维护,故采用了 VM虚拟机 + ubuntu+finalshell 的配置完成本技术的学习。配置过程不在这里赘述。

1.5 Redis客户端

Redis提供了三种客户端方式用于访问Redis

  • 命令行客户端
  • 图形化桌面客户端
  • 编程客户端

1.5.1 命令行客户端

redis-cli 自带的命令行客户端,使用方式如下:

1
redis-cli [optings] [commonds]

其中常见的options有:

  • -h 127.0.0.1 : 指定要连接到的redis的节点的IP,默认为 127.0.0.1
  • -p 6379 : 要连接的redis的端口,默认6379
  • -a passwords : 访问密码

而commonds即为操作命令,如

  • ping :与服务器做心跳测试,服务端正常会返回pong
  • set,get等方法

不指定commonds时,会进入 redis-cli的交互控制台,

1776252950390

可以使用AUTH password 键入密码,不推荐 -a 接密码,有安全风险

1.5.2 图形化客户端

下载链接: 链接 链接2

Redis默认有16个库,不能改名,但能通过配置文件修改库的数量

在命令行中可以通过 SELECT 0 的命令连接到0号数据库

1
2
SELSET 1 
#连接到1号库

2 常用命令

2.1 Redis数据结构

已经知道,Redis是Key-Value 型数据库,而key通常为 String类型,value任意,常见的有:

数据类型 格式 前五个为基础类型,后三个为特殊类型
String ahfia
Hash {name:”david”,age:”21”}
List [A->B->C->D] 链表
Set {A,B,C} 无序集合,不能重复
SortedSet {A:1,B:2,C:3} 有序,不能重复
GEO {A :(120.3,  35.5)}
BitMap 001101011
HyperLog 001101010

redis通用命令

可以通过 help [commond] 查询命令的用法

  • KEYS pattern : 查看符合模板的所有key,pattern通常可以是 ,a等格式,不建议在生产环境设备上使用,redis单线程,数据量极大时会阻塞
  • DEL : 删除指定的key
  • MSET :批量插入键值对
  • EXISTS : 判断key是否存在,返回0/1
  • EXPIRE: 给键设置一个有效期,到期后key会被自动删除
    • EXPIREAT name 20 : 设置name 有效期为20s
  • TTL : 查看一个key的剩余有效期,未设置返回-1 ,已被移除返回-2

String类型

根据不同的字符串格式,可分为String,int,float三种格式,不管是哪种格式,底层都是字节数组形式存储,只不过是编码方式不同,字符串类型的最大空间不能超过512M

String类型的常见命令有:

命令 用法 3
SET 添加或修改一个已经存在的String键值对
GET 根据key获取String类型的value
MSET 批量添加多个String类型的键值对
MGET 批量获取多个key的value
INCR 令一个整型的key自增1
INCRBY 指定步长的自增,INCRBY num 2 令num自增2 可以INCRBY key -1 代替DECRBY实现自减
INCRBYFOLOAT 指定步长的浮点数自增
SETNX 添加一个String类型的键值对,key必需未存在,是真正的**新增**
SETEX 添加String类型的键值对并指定有效期

key的层级结构

Redis的key允许有多个单词形成层级结构,单词间用:隔开,例如格式:项目名:业务名:类型:id

如果value是一个Java对象,则可以将其序列化为JSON字符串后储存

key: yzxmm:user:1 value: {“id”:1,”name”:”jack”,”age”:”21”}

key: yzxmm:produce:1 value: {“id”:1,”name”:”iphone17”,”price”:7000}

Hash类型

Hash类型,也叫散列,其value是一个无序字典

  • Hash结构可以将对象中的每个字段独立存储,可以针对单个字段做CRUD
    Hash
  • Hash的常用命令有
命令 描述
HSET key field value 添加或者修改hash类型key的field的值
HGET key field 获取一个hash类型key的field的值
HMSET 批量添加多个hash类型key的field的值    #4.0后已视为弃用
HMGET 批量获取多个hash类型key的field的值
HGETALL 获取一个hash类型的key中的所有的field和value
HKEYS 获取一个hash类型的key中的所有的field
HINCRBY 让一个hash类型key的字段值自增并指定步长
HSETNX 添加一个hash类型的key的field值,前提是这个field不存在,否则不执行

List类型

  • Redis中的List类型与Java中的LinkedList类似,可以看做是一个双向链表结构。既可以支持正向检索和也可以支持反向检索。
    特征也与LinkedList类似 :
    • 有序
    • 元素可以重复
    • 插入删除快
    • 查询速度一般
  • 基于其特点,可以用来存储诸如朋友圈点赞,评论列表等有序数列,
  • List常见命令有:
命令 用法
LPUSH key element… 向列表左侧插入一个或多个元素
LPOP key 移除并返回列表左侧的第一个元素,没有则返回nil
RPUSH key element… 向列表右侧插入一个或多个元素
RPOP key 移除并返回列表右侧的第一个元素
LRANGE key start end 返回一段角标范围的所有元素
BLPOP与BRPOP 与LPOP和RPOP类似,但在没有元素时会等待指定的时间,不是直接返回nil

Set类型

依旧与java的HashSet类似,可以当做value为null的HasgMap。是一个Hsah表,因此具备与HashSet类似的特征:

  • 无序
  • 元素不可重复
  • 查找快
  • 支持交集,并集,差集功能
  • Set的常见命令有:
命令 描述
SADD key member… 向set中添加一个或多个元素
SREM key member … 移除set中的指定元素
SCARD key 返回set中元素的个数
SISMEMBER key member 判断一个元素是否存在于set中
SMEMBERS 获取set中的所有元素
SINTER key1 key2 … 求key1与key2的交集
SUNION key1 key2 … 求key1与key2的并集
SDIFF key1 key2 … 求key1与key2的差集

练习题:

1.用set集合存储

1
2
3
4
192.168.23.128:6379> sadd zhangsan lisi wangwu zhaoliu
(integer) 3
192.168.23.128:6379> sadd lisi wangwu mazi ergou
(integer) 3
  • 计算张三的好友有几个
1
2
192.168.23.128:6379> scard zhangsan
(integer) 3
  • 计算张三和李四有哪些共同好友
1
2
192.168.23.128:6379> sinter zhangsan lisi
1) "wangwu"
  • 查询哪些是张三的好友却不是李四的好友
1
2
3
192.168.23.128:6379> sdiff zhangsan lisi
1) "zhaoliu"
2) "lisi" #李四不是自己的好友 bushi
  • 查询张三和李四的好友共有那些人
1
2
3
4
5
6
192.168.23.128:6379> sunion zhangsan lisi
1) "mazi"
2) "zhaoliu"
3) "wangwu"
4) "lisi"
5) "ergou"
  • 判断李四是否是张三的好友
1
2
192.168.23.128:6379> sismember zhangsan lisi
(integer) 1
  • 判断张三是否是李四的好友
1
2
192.168.23.128:6379> sismember lisi zhangsan
(integer) 0
  • 将李四从张三的好友列表移除
1
2
3
4
5
6
192.168.23.128:6379> srem zhangsan lisi
(integer) 1
#查询zhangsan好友已经没有lisi
192.168.23.128:6379> smembers zhangsan
1) "wangwu"
2) "zhaoliu"

SortedSet类型

可排序的set集合,与java中的TreeSet有些相似,但底层数据结构差别非常大。

SortedSet中每一个元素都有一个score属性,可以通过这个score属性对元素进行排序,底层实现是一个 跳表 (SkipList) 加一个Hash表。

SortedSet具备以下属性:

  • 可排序
  • 元素不重复
  • 查询速度快

因其可排序的特性,经常用来实现排行榜这样的功能。

SortedSet类型的常见命令有:

命令 用法
ZADD key score member 添加一个或多个元素到sorted set,已存在则更新score的值
ZREM key member 删除一个元素
ZSCORE key member 获取sorted set 中的指定元素的score值
ZRANK key member 获取指定元素的排名
ZCRAD key 获取元素个数
ZCOUNT key min max 统计score值在给定范围内的所有元素的个数
ZINCRBY key increment member 令指定元素自增,步长为increment
ZRANGE key min max 按score排序后,获取指定排名范围内的元素
ZRANGEBYSCORE 按score排序后,获取指定score范围内的元素
ZUNION,ZINTER,ZDIFF 求并集,交集,差集

所有排名默认是 升序,降序为在命令Z后加上 REV

练习题:

  • 将以下数据存入SortedSet:

jack 85, Rose 89, Tom 95,Jerry 78,Amy 92, Miles 76

1
2
192.168.23.128:6379> zadd stus 85 jack 89 Lucy 82 Rose 95 Tom 78 Jerry 92 Amy 76 Miles
(integer) 6

实现下列功能:

  • 删除Tom同学
1
2
192.168.23.128:6379> zrem stus Tom
(integer) 1
  • 获取Amy的分数

    1
    2
    192.168.23.128:6379> zscore stus Amy
    "92"
  • 获取Rose的排名

    1
    2
    3
    4
    5
    6
    192.168.23.128:6379> zrank stus Rose
    (integer) 2
    #默认升序排列且从0开始,故升序返回2
    #降序排名
    192.168.23.128:6379> zrevrank stus Rose
    (integer) 3
  • 查询80分以下的有几个学生

    1
    2
    192.168.23.128:6379> zcount stus 0 80
    (integer) 2
  • 给Amy同学加2分

    1
    2
    192.168.23.128:6379> zincrby stus 2 Amy
    "94"
  • 查出成绩前三名的同学

    1
    2
    3
    4
    192.168.23.128:6379> zrevrange stus 0 2
    1) "Amy"
    2) "Lucy"
    3) "jack"
  • 查出成绩80分及以下的所有同学

    1
    2
    3
    192.168.23.128:6379> zrangebyscore stus 0 80
    1) "Miles"
    2) "Jerry"

3.Redis的java客户端

jedis

redis/jedis: Redis Java client

1.idea导入jedis的meavn坐标,以及单元测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependencies>
<dependency>
<!--Jedis -->
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>7.4.0</version>
</dependency>
<!--单元测试 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.7.0</version>

</dependency>
</dependencies>

2.建立链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.example.test;

import org.junit.jupiter.api.BeforeEach;
import redis.clients.jedis.Jedis;

public class JedisTest {
private Jedis jedis;

@BeforeEach
void setUp() {
//建立连接
jedis=new Jedis("192.168.23.128",6379);
//设置密码
jedis.auth("Zxhxywh1p1.");
//选择库
jedis.select(0);
}
}

3.测试

1
2
3
4
5
6
7
8
9
10
@Test
void testString() {
//存
String result= jedis.set("name","777");
System.out.println("result= "+result);
//取
String name = jedis.get("name");
System.out.println("name = " + name);

}

4.关闭

1
2
3
4
5
6
7
8
    //释放连接
@AfterEach
void tearDown() {
if(jedis!=null){
jedis.close();
}
}
}

连接池

  • Jedis本身是线程不安全的,并且频繁的创建和销毁连接会有性能损耗,因此推荐使用Jedis连接池代替Jedis的直连方式。

新建com.example.jedis.uitl包用来存放工具类,新建JedisConnectionFactory类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class JedisConnectionFactory {
private static JedisPool jedisPool ;

static {
//配置连接池
JedisPoolConfig poolConfig = new JedisPoolConfig();
//最大连接数
poolConfig.setMaxTotal(8);
//最大空闲连接数
poolConfig.setMaxIdle(8);
//最小空闲连接
poolConfig.setMinIdle(0);
//最大等待时间
poolConfig.setMaxWait(Duration.ofMillis(1000)); //当连接池没有连接可用时是否等待

//创建连接池对象
jedisPool = new JedisPool(poolConfig, "192.168.23,128", 6379, 1000, "Zxhxywh1p1.");

}

public static Jedis getJedis(){
return jedisPool.getResource();
}
}

这样之前的测试类就可以直接从连接池获取连接

1
2
3
4
5
6
7
8
9
10
11
@BeforeEach
void setUp() {
//建立连接
//jedis=new Jedis("192.168.23.128",6379);
//从连接池获取连接
jedis= JedisConnectionFactory.getJedis();
//设置密码
jedis.auth("Zxhxywh1p1.");
//选择库
jedis.select(0);
}

SpringDataRedis

SpringData是Spring中数据操作的模块,包含对各种数据库的集成,对Redis的集成就叫做SpringDataRedis

官网 https://spring.io/projects/spring-data-redis

  • 提供了对不同Reids客户端的整合(LettuceJedis)
  • 提供了RdisTemplate统一API来操作Redis
  • 支持Redis的发布订阅模型
  • 支持Redis哨兵和Redis集群
  • 支持基于Lettuce的响应式编程
  • 支持基于JDK,JSON,字符串,Spring对象的数据序列化及反序列化
  • 支持基于Redis的JDKCollection实现
  • SpringDataRedis中提供了RedisTemplate工具类,其中封装了各种对Redis的操作。并且将不同数据类型的操作API封装到了不同的类型中:
API 返回值类型 说明
redisTemplate.opsForValue() ValueOperations 操作String类型数据
redisTemplate.opsForHash() HashOperations 操作Hash类型数据
redisTemplate.opsForList() ListOperations 操作List类型数据
redisTemplate.opsForSet() SetOperations 操作Set类型数据
redisTemplate.opsForzSet() ZSetOperations 操作SortedSet类型数据
redisTemplate 通用的命令

快速入门

SpringBoot已经提供了对SpringDataRedis的支持

1.引入依赖

1
2
3
4
5
6
7
8
9
10
11

<!-- redis依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- common-pool-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>

2.配置文件

配置redis的地址信息

1
2
3
4
5
6
7
8
9
10
11
12
spring:
data:
redis:
host: 192.168.23.128
port: 6379
password: Zxhxywh1p1.
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
max-wait: 1000ms

3.注入RedisTemlpate

1
2
@Autowired
private RedisTemplate<Object,Object> redisTemplate;

4.编写测试

1
2
3
4
5
6
7
8
9
@Test
void testString() {

//写入String数据
redisTemplate.opsForValue().set("name","已修改");
//获取数据
Object name=redisTemplate.opsForValue().get("name");
System.out.println("name = " + name);
}

运行发现name=”已修改”,但在虚拟机运行仍然是之前set的”777”,这是因为没有序列化。

reidsTemplate 的set方法接收的参数不是字符串,而是 Object ,其会将Object序列化为字节形式,又由于默认采用JDK序列化,比如上例的key是 name,name会被序列化,相当于重新存了一个新的键值对,在命令行查询结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
192.168.23.128:6379> get name
"777"
192.168.23.128:6379> keys *
1) "yzxmm:user:2"
2) "s1"
3) "s2"
4) "lisi"
5) "users"
6) "user:1"
7) "yzxmm:produce:1"
8) "name"
9) "age"
10) "yzxmm:user:1"
11) "stus"
12) "\xac\xed\x00\x05t\x00\x04name"
13) "yzxmm:user:3"
14) "zhangsan"
192.168.23.128:6379> get "\xac\xed\x00\x05t\x00\x04name"
"\xac\xed\x00\x05t\x00\t\xe5\xb7\xb2\xe4\xbf\xae\xe6\x94\xb9"

缺点

  • 可读性差
  • 内存占用大

Spring Boot 的测试默认会在测试方法执行完毕后回滚事务,以保证测试之间互不影响,不会产生脏数据。
linux中直接存和用redistemplate存,其实是存了两个,因为这俩序列化方式不一样,所以出现了两个

因此需要改变redisTemplate的序列化方式

自定义RedisTemplate的序列化方式
  • com/example/redis/config/下新建RedisConfig.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @Configuration
    public class RedisConfig {
    @Bean
    public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory connectionFactory){
    //创建RedisTemplate对象
    RedisTemplate<String, Object> stringObjectRedisTemplate = new RedisTemplate<>();
    //设置连接工厂
    stringObjectRedisTemplate.setConnectionFactory(connectionFactory);
    //创建JSON序列化工具
    GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
    //设置key的序列化
    stringObjectRedisTemplate.setKeySerializer(RedisSerializer.string());
    stringObjectRedisTemplate.setHashKeySerializer(RedisSerializer.string());
    //设置value的序列化
    stringObjectRedisTemplate.setValueSerializer(jsonRedisSerializer);
    stringObjectRedisTemplate.setHashValueSerializer(jsonRedisSerializer);

    return stringObjectRedisTemplate;
    }
    }
  • 在test中修改

    1
    2
    @Autowired
    private RedisTemplate<String,Object> redisTemplate;

    成功修改正确的name

  • 测试value

    1
    2
    3
    4
    5
    6
    7
    8
    @Test
    void testSaveUser(){
    //写入数据
    redisTemplate.opsForValue().set("user:7",new User("用户7",21));
    //获取数据
    User user7 = (User) redisTemplate.opsForValue().get("user:7");
    System.out.println("user7 = " + user7);
    }
    • idea返回

      1
      user7 = User(name=用户7, age=21)
    • RESP中查看

      1
      2
      3
      4
      5
      {
      "@class": "com.example.redis.test.User",
      "name": "用户7",
      "age": 21
      }

这样能将Java对象自动的序列化为JSON字符串,并且查询时能自动把JSON反序列化为Java对象。不过,其中记录了序列化时对应的class名称,目的是为了查询时实现自动反序列化,这会带来额外的内存开销。

简化处理:统一使用String序列化器。

string序列化器

要求只能存储String类型的keyh和value。当需要存储java对象时,手动完成对象的序列化和反序列化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private static final ObjectMapper mapper= new ObjectMapper();
//ObjectMapper 是springMVC中默认使用的JSON处理工具
@Test
void testSaveUser() throws JsonProcessingException {
//创建对象
User user7 = new User("用户7", 21);
//手动序列化
String s = mapper.writeValueAsString(user7);
//写入数据
stringRedisTemplate.opsForValue().set("user:7",s);
//获取数据
String s1 = stringRedisTemplate.opsForValue().get("user:7");
//手动反序列化
User user1 = mapper.readValue(s1, User.class);

System.out.println("user1 = " + user1);
}

没了…