Redis
一:Redis引入:
1.1.什么是redis:
Redis是一个开源的内存数据库,也被称为数据结构服务器。它支持多种数据结构,如字符串、哈希表、列表、集合、有序集合等,并提供了丰富的操作命令来对这些数据结构进行操作。Redis具有高性能、高可用性和可扩展性的特点,被广泛应用于缓存、会话管理、消息队列等场景。它是一个非常流行的NoSQL数据库,被许多互联网公司和开发者广泛使用。
1.2.NoSQL:
NoSQL(Not Only SQL)是一种非关系型数据库管理系统,用于存储和检索大量的非结构化或半结构化数据。与传统的关系型数据库不同,NoSQL数据库通常不遵循传统的表格结构,而是采用键值对、文档存储、列存储或图形数据库等形式来存储数据。
NoSQL数据库的设计目标是解决传统关系型数据库在大规模数据存储和高并发访问场景下的性能瓶颈和扩展性问题。NoSQL数据库通常具有以下特点:
-
高性能:NoSQL数据库通常采用分布式架构和水平扩展技术,能够快速存储和检索大量数据。
-
高可用性:NoSQL数据库支持多副本备份、自动故障转移等机制,保证系统的可用性。
-
灵活的数据模型:NoSQL数据库支持多种数据模型,如键值对、文档存储、列存储等,可以根据应用场景选择合适的数据模型。
-
水平扩展:NoSQL数据库支持水平扩展,可以通过增加节点来扩展系统的存储容量和处理能力。
-
适用于大数据场景:NoSQL数据库适用于大规模数据存储和高并发访问场景,可以处理海量数据和复杂查询需求。
常见的NoSQL数据库包括MongoDB、Cassandra、Redis、HBase等,它们被广泛应用于互联网、大数据、物联网等领域,为应对大规模数据存储和处理挑战提供了一种新的解决方案。
它们的主要不同如下:
1.2.1.结构化:
传统关系型数据库是结构化数据,每一张表都有严格的约束信息:字段名.字段数据类型.字段约束等等信息,插入的数据必须遵守这些约束,而NoSql则对数据库格式没有严格约束,往往形式松散,自由。
![image-20240418175943527](https://img-blog.csdnimg.cn/img_convert/7c29d9001d359a92aa03d54e4b4f2173.png)
1.2.2.关联:
传统数据库的表与表之间往往存在关联,例如外键:
![image-20240418201536714](https://img-blog.csdnimg.cn/img_convert/92ab863999b239e15b89184412ee6173.png)
而非关系型数据库不存在关联关系,要维护关系要么靠代码中的业务逻辑,要么靠数据之间的耦合:
{
id: 1,
name: "张三",
orders: [
{
id: 1,
item: {
id: 10, title: "荣耀6", price: 4999
}
},
{
id: 2,
item: {
id: 20, title: "小米11", price: 3999
}
}
]
}
此处要维护“张三”的订单与商品“荣耀”和“小米11”的关系,不得不冗余的将这两个商品保存在张三的订单文档中,不够优雅。还是建议用业务来维护关联关系。
1.2.3.查询方式:
传统关系型数据库会基于Sql语句做查询,语法有统一标准,而不同的非关系数据库查询语法差异极大,五花八门各种各样。
![image-20240418203929646](https://img-blog.csdnimg.cn/img_convert/9f5d982ec07b5fe0aaa2cd7202c02761.png)
1.2.4.事务
传统关系型数据库能满足事务ACID的原则。而非关系型数据库往往不支持事务,或者不能严格保证ACID的特性,只能实现基本的一致性。
ACID是数据库事务的四个特性的缩写,分别是原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。这四个特性是保证数据库事务正确执行和数据完整性的重要标准。
- 原子性(Atomicity):原子性要求一个事务中的所有操作要么全部执行成功,要么全部失败回滚,不允许事务只执行部分操作。即事务是一个不可分割的工作单位,要么全部执行,要么全部不执行。
- 一致性(Consistency):一致性要求事务执行前后数据库的状态必须是一致的。即事务执行前后,数据库从一个一致的状态转换到另一个一致的状态,不会破坏数据的完整性和约束条件。
- 隔离性(Isolation):隔离性要求多个事务并发执行时,每个事务的操作应该与其他事务隔禂开来,互不干扰。即每个事务的操作应该看起来是独立的,不会受到其他事务的影响。
- 持久性(Durability):持久性要求一旦事务提交,其对数据库的改变应该是永久性的,即数据的修改不能因为系统故障或断电而丢失。数据库系统需要保证事务的提交后,数据的改变被永久保存。
ACID是数据库事务的基本特性,保证了数据库操作的正确性和数据的完整性。数据库管理系统通过实现ACID特性来保证事务的可靠性和一致性,确保数据的安全性和可靠性。
1.1.5.总结
除了上述四点以外,在存储方式.扩展性.查询性能上关系型与非关系型也都有着显著差异,总结如下:
- 存储方式
- 关系型数据库基于磁盘进行存储,会有大量的磁盘IO,对性能有一定影响
- 非关系型数据库,他们的操作更多的是依赖于内存来操作,内存的读写速度会非常快,性能自然会好一些
- 扩展性
- 关系型数据库集群模式一般是主从,主从数据一致,起到数据备份的作用,称为垂直扩展。
- 非关系型数据库可以将数据拆分,存储在不同机器上,可以保存海量数据,解决内存大小有限的问题。称为水平扩展。
- 关系型数据库因为表之间存在关联关系,如果做水平扩展会给数据查询带来很多麻烦
1.2.认识Redis:
Redis诞生于2009年全称是Remote Dictionary Server 远程词典服务器,是一个基于内存的键值型NoSQL数据库。
特征:
- 键值(key-value)型,value支持多种不同数据结构,功能丰富
- 单线程,每个命令具备原子性
- 低延迟,速度快(基于内存.IO多路复用.良好的编码)。
- 支持数据持久化
- 支持主从集群.分片集群
- 支持多语言客户端
作者:Antirez
Redis的官方网站地址:https://redis.io/
二:Redis命令:
Redis是一个key-value的数据库,key一般是String类型,不过value的类型多种多样:
![image-20240418220307505](https://img-blog.csdnimg.cn/img_convert/1572b8d7ad10a71dea406ea5463c902f.png)
Redis为了方便我们学习,将操作不同数据类型的命令也做了分组,在官网( https://redis.io/commands )可以查看到不同的命令。
或者我们也可以通过命令行的方式:
help @
一直按tap就可以切换指定的帮助:
![image-20240418222130163](https://img-blog.csdnimg.cn/img_convert/9119f2cc7bb1fd1a81369981a280d8d7.png)
2.1.通用命令:
2.1.1.查看所有符合的key:
举例:
KEYS *
KEYS a*
注意,模糊查询不建议使用,因为效率太低。
补充:
![image-20240418222605027](https://img-blog.csdnimg.cn/img_convert/c9a08857128c919e62a33a2ab95ad838.png)
2.1.2.删除:DEL:
DEL name
DEL k1 k2 k3 k4
会返回删除的数量。
2.1.3.EXISTS,判断是否存在:
EXISTS K1
2.1.4.EXPIPE,设置key有效期,过期自动删除:
EXPIPE age 20
单位为s。
2.1.5.TTL,查看KEY剩余有效期:
TTL age
大于0表示有效期,-2表示已经删除,-1表示永久有效。
2.2.String:
![image-20240419163616186](https://img-blog.csdnimg.cn/img_convert/53039b39082bd1820e6cc2edaa86613b.png)
举例:
set name k1
set name k2
get k1
MSET k1 k2 k3
set age 18
INCR age
INCRBY age -4
SETNX hh k6
SETEX name 10 jack
2.3.Key的层级:
举例:
set heima:user:1 '{"id":1, "name": "Jack", "age": 21}'
set heima:user:2 '{"id":2, "name": "Jackson", "age": 31}'
一旦我们向redis采用这样的方式存储,那么在可视化界面中,redis会以层级结构来进行存储,形成类似于这样的结构,更加方便Redis获取数据。
2.4.Hash类型:
![image-20240419165735449](https://img-blog.csdnimg.cn/img_convert/3073ab1e18f54b846b188d3193e59d3d.png)
- HSET和HGET
127.0.0.1:6379> HSET heima:user:3 name Lucy//大key是 heima:user:3 小key是name,小value是Lucy
(integer) 1
127.0.0.1:6379> HSET heima:user:3 age 21// 如果操作不存在的数据,则是新增
(integer) 1
127.0.0.1:6379> HSET heima:user:3 age 17 //如果操作存在的数据,则是修改
(integer) 0
127.0.0.1:6379> HGET heima:user:3 name
"Lucy"
127.0.0.1:6379> HGET heima:user:3 age
"17"
- HMSET和HMGET
127.0.0.1:6379> HMSET heima:user:4 name HanMeiMei
OK
127.0.0.1:6379> HMSET heima:user:4 name LiLei age 20 sex man
OK
127.0.0.1:6379> HMGET heima:user:4 name age sex
1) "LiLei"
2) "20"
3) "man"
- HGETALL
127.0.0.1:6379> HGETALL heima:user:4
1) "name"
2) "LiLei"
3) "age"
4) "20"
5) "sex"
6) "man"
- HKEYS和HVALS
127.0.0.1:6379> HKEYS heima:user:4
1) "name"
2) "age"
3) "sex"
127.0.0.1:6379> HVALS heima:user:4
1) "LiLei"
2) "20"
3) "man"
- HINCRBY
127.0.0.1:6379> HINCRBY heima:user:4 age 2
(integer) 22
127.0.0.1:6379> HVALS heima:user:4
1) "LiLei"
2) "22"
3) "man"
127.0.0.1:6379> HINCRBY heima:user:4 age -2
(integer) 20
- HSETNX
127.0.0.1:6379> HSETNX heima:user4 sex woman
(integer) 1
127.0.0.1:6379> HGETALL heima:user:3
1) "name"
2) "Lucy"
3) "age"
4) "17"
127.0.0.1:6379> HSETNX heima:user:3 sex woman
(integer) 1
127.0.0.1:6379> HGETALL heima:user:3
1) "name"
2) "Lucy"
3) "age"
4) "17"
5) "sex"
6) "woman"
2.5.List类型:
- LPUSH和RPUSH
127.0.0.1:6379> LPUSH users 1 2 3
(integer) 3
127.0.0.1:6379> RPUSH users 4 5 6
(integer) 6
- LPOP和RPOP
127.0.0.1:6379> LPOP users
"3"
127.0.0.1:6379> RPOP users 1
"6"
- LRANGE
127.0.0.1:6379> LRANGE users 1 2
1) "1"
2) "4"
2.6.Set类型:
具体命令
127.0.0.1:6379> sadd s1 a b c
(integer) 3
127.0.0.1:6379> smembers s1
1) "c"
2) "b"
3) "a"
127.0.0.1:6379> srem s1 a
(integer) 1
127.0.0.1:6379> SISMEMBER s1 a
(integer) 0
127.0.0.1:6379> SISMEMBER s1 b
(integer) 1
127.0.0.1:6379> SCARD s1
(integer) 2
案例
- 将下列数据用Redis的Set集合来存储:
- 张三的好友有:李四.王五.赵六
- 李四的好友有:王五.麻子.二狗
- 利用Set的命令实现下列功能:
- 计算张三的好友有几人
- 计算张三和李四有哪些共同好友
- 查询哪些人是张三的好友却不是李四的好友
- 查询张三和李四的好友总共有哪些人
- 判断李四是否是张三的好友
- 判断张三是否是李四的好友
- 将李四从张三的好友列表中移除
127.0.0.1:6379> SADD zs lisi wangwu zhaoliu
(integer) 3
127.0.0.1:6379> SADD ls wangwu mazi ergou
(integer) 3
127.0.0.1:6379> SCARD zs
(integer) 3
127.0.0.1:6379> SINTER zs ls
1) "wangwu"
127.0.0.1:6379> SDIFF zs ls
1) "zhaoliu"
2) "lisi"
127.0.0.1:6379> SUNION zs ls
1) "wangwu"
2) "zhaoliu"
3) "lisi"
4) "mazi"
5) "ergou"
127.0.0.1:6379> SISMEMBER zs lisi
(integer) 1
127.0.0.1:6379> SISMEMBER ls zhangsan
(integer) 0
127.0.0.1:6379> SREM zs lisi
(integer) 1
127.0.0.1:6379> SMEMBERS zs
1) "zhaoliu"
2) "wangwu"
2.7.SortedSet:
注意:所有的排名默认都是升序,如果要降序则在命令的Z后面添加REV即可,例如:
- 升序获取sorted set 中的指定元素的排名:ZRANK key member
- 降序获取sorted set 中的指定元素的排名:ZREVRANK key memeber
三:Jedis:
3.1.Jedis快速入门:
Jedis是一个用Java语言编写的Redis客户端库,可以用来与Redis服务器进行交互。它提供了一些简单易用的API,可以方便地操作Redis数据库,比如设置和获取键值对、执行各种命令等。Jedis支持连接池、分片等功能,可以帮助开发者更好地利用Redis的功能。它是目前使用最广泛的Redis客户之一。
它的使用步骤如下:
1)引入依赖:
<!--jedis-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.7.0</version>
</dependency>
<!--单元测试-->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.7.0</version>
<scope>test</scope>
</dependency>
2)建立连接
新建一个单元测试类,内容如下:
private Jedis jedis;
@BeforeEach
void setUp() {
// 1.建立连接
// jedis = new Jedis("192.168.150.101", 6379);
jedis = JedisConnectionFactory.getJedis();
// 2.设置密码
jedis.auth("123321");
// 3.选择库
jedis.select(0);
}
3)测试:
@Test
void testString() {
// 存入数据
String result = jedis.set("name", "虎哥");
System.out.println("result = " + result);
// 获取数据
String name = jedis.get("name");
System.out.println("name = " + name);
}
@Test
void testHash() {
// 插入hash数据
jedis.hset("user:1", "name", "Jack");
jedis.hset("user:1", "age", "21");
// 获取
Map<String, String> map = jedis.hgetAll("user:1");
System.out.println(map);
}
4)释放资源
@AfterEach
void tearDown() {
if (jedis != null) {
jedis.close();
}
}
3.2.Jedis连接池
Jedis本身是线程不安全的,并且频繁的创建和销毁连接会有性能损耗,因此我们推荐大家使用Jedis连接池代替Jedis的直连方式。
public class JedisConnectionFacotry {
private static final JedisPool jedisPool;
static {
//配置连接池
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(8);
poolConfig.setMaxIdle(8);
poolConfig.setMinIdle(0);
poolConfig.setMaxWaitMillis(1000);
//创建连接池对象
jedisPool = new JedisPool(poolConfig,
"192.168.150.101",6379,1000,"123321");
}
public static Jedis getJedis(){
return jedisPool.getResource();
}
}
代码说明:
-
1) JedisConnectionFacotry:工厂设计模式是实际开发中非常常用的一种设计模式,我们可以使用工厂,去降低代的耦合,比如Spring中的Bean的创建,就用到了工厂设计模式
-
2)静态代码块:随着类的加载而加载,确保只能执行一次,我们在加载当前工厂类的时候,就可以执行static的操作完成对 连接池的初始化
-
3)最后提供返回连接池中连接的方法.
然后我们可以这么使用:
1.在我们完成了使用工厂设计模式来完成代码的编写之后,我们在获得连接时,就可以通过工厂来获得,而不用直接去new对象,降低耦合,并且使用的还是连接池对象。
2.当我们使用了连接池后,当我们关闭连接其实并不是关闭,而是将Jedis还回连接池的。
@BeforeEach
void setUp(){
//建立连接
/*jedis = new Jedis("127.0.0.1",6379);*/
jedis = JedisConnectionFacotry.getJedis();
//选择库
jedis.select(0);
}
@AfterEach
void tearDown() {
if (jedis != null) {
jedis.close();
}
}
四:SpringDataRedis:
SpringData是Spring中数据操作的模块,包含对各种数据库的集成,其中对Redis的集成模块就叫做SpringDataRedis,官网地址:https://spring.io/projects/spring-data-redis
- 提供了对不同Redis客户端的整合(Lettuce和Jedis)
- 提供了RedisTemplate统一API来操作Redis
- 支持Redis的发布订阅模型
- 支持Redis哨兵和Redis集群
- 支持基于Lettuce的响应式编程
- 支持基于JDK.JSON.字符串.Spring对象的数据序列化及反序列化
- 支持基于Redis的JDKCollection实现
SpringDataRedis中提供了RedisTemplate工具类,其中封装了各种对Redis的操作。并且将不同数据类型的操作API封装到了不同的类型中。
4.1.快速入门:
4.1.1.导入pom依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.7</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.heima</groupId>
<artifactId>redis-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>redis-demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!--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>
<!--Jackson依赖-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
4.1.2 .配置文件
spring:
redis:
host: 192.168.150.101
port: 6379
password: 123321
lettuce:
pool:
max-active: 8 #最大连接
max-idle: 8 #最大空闲连接
min-idle: 0 #最小空闲连接
max-wait: 100ms #连接等待时间
4.1.3.测试代码
@SpringBootTest
class RedisDemoApplicationTests {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Test
void testString() {
// 写入一条String数据
redisTemplate.opsForValue().set("name", "虎哥");
// 获取string数据
Object name = redisTemplate.opsForValue().get("name");
System.out.println("name = " + name);
}
}
贴心小提示:SpringDataJpa使用起来非常简单,记住如下几个步骤即可
SpringDataRedis的使用步骤:
- 引入spring-boot-starter-data-redis依赖
- 在application.yml配置Redis信息
- 注入RedisTemplate
4.2.序列化:
问题如下:
在我们不指定数据类型时,java会把它当成对象处理并且进行序列化,因为其他的序列化器初始为null,当我们不处理时,会默认使用jdk的序列化器,即用ObjectOutputStream转换成字节写入redis,就形成了乱码。所以我们需要自己来定义序列化器。
在Redis中,key和value的序列化确实有所区别,这主要体现在它们的使用方式和存储需求上。在Spring Data Redis中,你可以分别为key和value设置不同的序列化器。以下是你提供的代码段中key与value序列化区别的详细解释:
Key的序列化
template.setKeySerializer(RedisSerializer.string());
template.setHashKeySerializer(RedisSerializer.string());
对于key的序列化,通常推荐使用简单的字符串序列化器,因为Redis的key通常用于快速查找和定位数据,它们应该尽可能简单、短小并且易于阅读。在大多数情况下,key是简单的字符串或字符串模式,因此使用字符串序列化器已经足够。此外,字符串序列化器确保了key的跨语言兼容性,因为几乎所有语言都能够处理字符串。
Value的序列化
GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
template.setValueSerializer(jsonRedisSerializer);
template.setHashValueSerializer(jsonRedisSerializer);
对于value的序列化,情况则有所不同。value可能包含复杂的Java对象,这些对象需要被转换为Redis可以存储的格式。使用像GenericJackson2JsonRedisSerializer
这样的JSON序列化器,你可以将Java对象转换为JSON格式的字符串,这样它们就可以被存储在Redis中了。JSON是一种轻量级的数据交换格式,易于阅读和编写,同时也易于机器解析和生成。
总之:
JDK序列化器是将Java对象转换为字节序列(即二进制格式)进行存储或传输,String序列化器则是将对象转换为字符串格式进行存储或传输,json同理,所以可读性高。
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory){
// 创建RedisTemplate对象
RedisTemplate<String, Object> template = new RedisTemplate<>();
// 设置连接工厂
template.setConnectionFactory(connectionFactory);
// 创建JSON序列化工具
GenericJackson2JsonRedisSerializer jsonRedisSerializer =
new GenericJackson2JsonRedisSerializer();
// 设置Key的序列化
template.setKeySerializer(RedisSerializer.string());
template.setHashKeySerializer(RedisSerializer.string());
// 设置Value的序列化
template.setValueSerializer(jsonRedisSerializer);
template.setHashValueSerializer(jsonRedisSerializer);
// 返回
return template;
}
}
现在,无论是哪种类型,都可以显示正确,即使是对象,整体可读性有了很大提升,并且能将Java对象自动的序列化为JSON字符串,并且查询时能自动把JSON反序列化为Java对象。不过,其中记录了序列化时对应的class名称,目的是为了查询时实现自动反序列化。这会带来额外的内存开销。
4.3.StringRedisTemplate:
为了减少内存的消耗,我们可以采用手动序列化的方式,换句话说,就是不借助默认的序列化器,而是我们自己来控制序列化的动作,同时,我们只采用String的序列化器,这样,在存储value时,我们就不需要在内存中就不用多存储数据,从而节约我们的内存空间.
这种用法比较普遍,因此SpringDataRedis就提供了RedisTemplate的子类:StringRedisTemplate,它的key和value的序列化方式默认就是String方式。
省去了我们自定义RedisTemplate的序列化方式的步骤,而是直接使用:
@SpringBootTest
class RedisStringTests {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Test
void testString() {
// 写入一条String数据
stringRedisTemplate.opsForValue().set("verify:phone:13600527634", "124143");
// 获取string数据
Object name = stringRedisTemplate.opsForValue().get("name");
System.out.println("name = " + name);
}
private static final ObjectMapper mapper = new ObjectMapper();
@Test
void testSaveUser() throws JsonProcessingException {
// 创建对象
User user = new User("虎哥", 21);
// 手动序列化
String json = mapper.writeValueAsString(user);
// 写入数据
stringRedisTemplate.opsForValue().set("user:200", json);
// 获取数据
String jsonUser = stringRedisTemplate.opsForValue().get("user:200");
// 手动反序列化
User user1 = mapper.readValue(jsonUser, User.class);
System.out.println("user1 = " + user1);
}
}
此时我们再来看一看存储的数据,就会发现那个class数据已经不在了。
4.4.Hash结构操作:
@SpringBootTest
class RedisStringTests {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Test
void testHash() {
stringRedisTemplate.opsForHash().put("user:400", "name", "虎哥");
stringRedisTemplate.opsForHash().put("user:400", "age", "21");
Map<Object, Object> entries = stringRedisTemplate.opsForHash().entries("user:400");
System.out.println("entries = " + entries);
}
}