七、Redis 缓存 —— 超详细操作演示!
- 七、Redis 缓存
- 7.1 Jedis 客户端
- 7.1.1 Jedis 简介
- 7.1.2 创建工程
- 7.1.3 使用 Jedis 实例
- 7.1.4 使用 JedisPool
- 7.1.5 使用 JedisPooled
- 7.1.6 连接 Sentinel 高可用集群
- 7.1.7 连接分布式系统
- 7.1.8 操作事务
- 7.2 金融产品交易平台
- 7.3 高并发问题
- 7.3.1 缓存穿透
- 7.3.2 缓存击穿
- 7.3.3 缓存雪崩
- 7.3.4 数据库缓存双写不一致
数据库系列文章:
关系型数据库:
- MySQL —— 基础语法大全
- MySQL —— 进阶
非关系型数据库:
- 一、Redis 的安装与配置
- 二、Redis 基本命令(上)
- 三、Redis 基本命令(下)
- 四、Redis 持久化
- 五、Redis 主从集群
- 六、Redis 分布式系统
七、Redis 缓存
7.1 Jedis 客户端
7.1.1 Jedis 简介
Jedis 是一个 基于 java 的 Redis 客户端连接工具 ,旨在提升性能与易用性 。 其 github 上的官网地址为: https://github.com/redis/jedis 。
7.1.2 创建工程
首先创建一个普通的 Maven 工程 01-jedis ,然后在 POM 文件中添加 Jedis 与 Junit 依赖。
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<!--jedis依赖-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.2.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
</dependencies>
7.1.3 使用 Jedis 实例
Jedis 基本使用十分简单, 其提供了非常丰富的操作 Redis 的方法,而这些方法名几乎与 Redis 命令相同。 在每次使用时直接创建 Jedis 实例即可。在 Jedis 实例 创建好之后, Jedis 底层实际会创建一个到指定 Redis 服务器的 Socket
连接 。所以 ,为 了节省系统资源与网络带宽 在每次使用完 Jedis 实例 之后,需要立即调用 close()
方法将 连接关闭。
首先,需要在工程的 src/test/java
下创建测试类 JedisTest
。
⭐️(1) value 为 String 的测试
@Test
public void test01() {
Jedis jedis = new Jedis("redis", 6379);
jedis.set("name", "张三");
jedis.mset("age", "23", "depart", "市场部");
System.out.println("name = " + jedis.get("name"));
System.out.println("age = " + jedis.get("age"));
System.out.println("depart = " + jedis.get("depart"));
jedis.close();
}
⭐️(2) value 为 Hash 的测试
@Test
public void test02() {
Jedis jedis = new Jedis("redis", 6379);
jedis.hset("emp", "name", "Tom");
Map<String, String> map = new HashMap<>();
map.put("age", "24");
map.put("depart", "行政部");
jedis.hset("emp", map);
System.out.println("emp.name = " + jedis.hget("emp", "name"));
List<String> emp = jedis.hmget("emp", "name", "age", "depart");
System.out.println("emp = " + emp);
jedis.close();
}
⭐️(3) value 为 List 的测试
// value为List的情况
@Test
public void test03() {
Jedis jedis = new Jedis("redis", 6379);
jedis.rpush("cities", "北京", "上海", "广州");
List<String> cities = jedis.lrange("cities", 0, -1);
System.out.println("cities = " + cities);
jedis.close();
}
⭐️(4) value 为 Set 的测试
// value为Set的情况
@Test
public void test04() {
Jedis jedis = new Jedis("redis", 6379);
jedis.sadd("courses", "Redis", "RocketMQ", "Zookeeper", "Kafka");
Set<String> courses = jedis.smembers("courses");
System.out.println("courese = " + courses);
jedis.close();
}
⭐️(5) value 为 Zset 的测试
// value为ZSet的情况
@Test
public void test05() {
Jedis jedis = new Jedis("redis", 6379);
jedis.zadd("sales", 50, "Benz");
jedis.zadd("sales", 40, "BMW");
jedis.zadd("sales", 30, "Audi");
jedis.zadd("sales", 80, "Tesla");
jedis.zadd("sales", 120, "BYD");
List<String> top3 = jedis.zrevrange("sales", 0, 2);
System.out.println("top3 = " + top3);
List<Tuple> sales = jedis.zrevrangeWithScores("sales", 0, -1);
for (Tuple sale : sales) {
System.out.println(sale.getScore() + ":" + sale.getElement());
}
jedis.close();
}
7.1.4 使用 JedisPool
如果应用非常 频繁地 创建和销毁 Jedis 实例 虽然节省了系统资源与网络带宽,但会大大降低系统性能。 因为 创建和销毁 Socket 连接
是比较耗时的 。 此时可以使用 Jedis 连接池 来解决该问题。
使用 JedisPool 与使用 Jedis 实例的区别是, JedisPool 是全局性的,整个类只需创建一次即可,然后每次需要操作 Redis 时,只需从 JedisPool 中拿出一个 Jedis 实例 直接使用即可。使用完毕后,无需释放 Jedis 实例,只需 返回 JedisPool 即可。
public class JedisPoolTest {
private JedisPool jedisPool = new JedisPool("redis", 6379);
// value为String的情况
@Test
public void test01() {
try(Jedis jedis = jedisPool.getResource()) {
jedis.set("name", "张三");
jedis.mset("age", "23", "depart", "市场部");
System.out.println("name = " + jedis.get("name"));
System.out.println("age = " + jedis.get("age"));
System.out.println("depart = " + jedis.get("depart"));
}
}
}
7.1.5 使用 JedisPooled
对于每次对 Redis 的操作都需要使用 try-with-resource
块是比较麻烦的,而使用 JedisPooled 则无需再使用该结构来自动释放资源了。
public class JedisPooledTest {
private JedisPooled jedis = new JedisPooled("192.168.216.128", 6379);
// value为String的情况
@Test
public void test01() {
jedis.set("name", "张三");
jedis.mset("age", "23", "depart", "市场部");
System.out.println("name = " + jedis.get("name"));
System.out.println("age = " + jedis.get("age"));
System.out.println("depart = " + jedis.get("depart"));
}
}
7.1.6 连接 Sentinel 高可用集群
对于 Sentinel
高可用集群的连接,直接使用 JedisSentinelPool 即可。在该客户端只需注册所有 Sentinel
节点
及其监控的 Master
的名称即可,无需出现 master -slave
的任何地址信息。其采用的也是 JedisPool ,使用完毕的 Jedis 也需要通过 close()
方法将其返回给 连接池。
public class JedisSentinelPoolTest {
private JedisSentinelPool jedisPool;
{
Set<String> sentinel = new HashSet<>();
sentinel.add("192.168.216.128:26380");
sentinel.add("192.168.216.128:26381");
sentinel.add("192.168.216.128:26382");
jedisPool = new JedisSentinelPool("mymaster", sentinel);
}
// value为String的情况
@Test
public void test01() {
try(Jedis jedis = jedisPool.getResource()) {
jedis.set("name", "张三");
jedis.mset("age", "23", "depart", "市场部");
System.out.println("name = " + jedis.get("name"));
System.out.println("age = " + jedis.get("age"));
System.out.println("depart = " + jedis.get("depart"));
}
}
}
7.1.7 连接分布式系统
对于 Redis 的分布式系统的连接,直接使用 JedisCluster 即可。其底层采用的也是 Jedis连接池技术。每次使用完毕后,无需显式关闭,其会自动关闭。
对于 JedisCluster 常用的 构造器 有两个。
- 一个是 只需一个集群节点 的 构造器,这个节点可以是集群中的任意节点,只要连接上了该节点,就连接上了整个集群。但该构造器存在一个风险:其指定的这个节点在连接之前恰好宕机,那么该客户端将无法连接上集群。
- 所以,推荐使用第二个 构造器,即将集群中所有节点全部罗列出来。这样就会避免这种风险了 。
public class JedisClusterTest{
private JedisCluster jedisCluster;
{
// 连接CLuster中的任意主机
// HostAndPort node = new HostAndPort("redis", 6380);
// jedisCluster = new JedisCluster(node);
//连接整个CLuster
HashSet<HostAndPort> nodes = new HashSet<>();
nodes.add(new HostAndPort("redis", 6380));
nodes.add(new HostAndPort("redis", 6381));
nodes.add(new HostAndPort("redis", 6382));
nodes.add(new HostAndPort("redis", 6383));
nodes.add(new HostAndPort("redis", 6384));
nodes.add(new HostAndPort("redis", 6385));
jedisCluster = new JedisCluster(nodes);
}
// value为String的情况
@Test
public void test01() {
jedisCluster,set("name", "张三");
// 会出现跨槽,报错
// jedisClustermset("age", "23", "depart","行政部");
System.out.println("name = " + jedisCluster.get("name"));
}
}
7.1.8 操作事务
对于 Redis 事务 的操作, Jedis 提供了 multi()
、 watch()
、 unwatch()
方法来对应 Redis 中的 multi
、 watch
、 unwatch
命令。 Jedis 的 multi()
方法返回一个 Transaction 对象,其 exec()
与 discard()
方法用于 执行和取消事务的执行。
⭐️(1) 抛出 Java 异常
public class JedisTxTest {
private JedisPool jedisPool = new JedisPool("192.168.216.128", 6379);
// value为String的情况
@Test
public void test01() {
try(Jedis jedis = jedisPool.getResource()) {
jedis.set("name", "张三");
jedis.mset("age", "23");
Transaction multi = jedis.multi();
try{
multi.set("name", "李四");
// 构造一个 Java 异常
int i = 3 / 0;
multi.set("age", "24");
multi.exec();
}catch (Exception e){
// 一旦发生异常,全部回滚
multi.discard();
}finally {
System.out.println("name = " + jedis.get("name"));
System.out.println("age = " + jedis.get("age"));
}
}
}
}
其输出结果为全部回滚的结果。
⭐️(2) 让 Redis 异常
@Test
public void test02() {
try(Jedis jedis = jedisPool.getResource()) {
jedis.set("name", "张三");
jedis.mset("age", "23");
Transaction tx = jedis.multi();
try {
tx.set("name", "李四");
// 构造出一个Redis异常
tx.incr("name");
tx.set("age", "24");
tx.exec();
} catch (Exception e) {
// 发生异常,全部回滚
tx.discard();
} finally {
System.out.println("name = " + jedis.get("name"));
System.out.println("age = " + jedis.get("age"));
}
}
}
其输出结果为修改过的值。说明 Redis 运行时 抛出的异常不会被 Java 代码 捕获到,其 不会影响 Java 代码的执行。
⭐️(3) watch()
@Test
public void test03() {
try(Jedis jedis = jedisPool.getResource()) {
jedis.mset("age", "23");
System.out.println("age增一前的值:" + jedis.get("age"));
jedis.watch("age");
Transaction tx = jedis.multi();
try {
tx.incr("age");
tx.exec();
} catch (Exception e) {
tx.discard();
} finally {
System.out.println("age增一后的值:" + jedis.get("age"));
}
}
}
7.2 金融产品交易平台
7.2.1 Spring Boot 整合 Redis
7.2.2 Redis 操作模板
Spring Boot 中可以直接使用 Jedis 实现对 Redis 的操作,但一般不这样用,而是使用 Redis 操作模板 RedisTemplate
类的实例来操作 Redis 。
RedisTemplate
类是一个对 Redis 进行操作的模板类。该模板类中具有很多方法,这些方法很多与 Redis 操作命令同名或类似。例如, delete()
、 keys()
、 scan()
,还有事务相关的 multi()
、exec()
、 discard()
、 watch()
等。当然还有获取对各种 Value
类型进行操作的操作实例的两类方法 boundXxxOps(k)
与 opsForXxx()
。
7.2.3 需求
下面通过一个例子来说明 Spring Boot 是如何与 Redis 进行 整合 的。
对于一个资深成熟的金融产品交易平台,其用户端首页一般会展示其最新金融产品列表同时还为用户提供了产品查询功能。另外,为了显示平台的实力与信誉,在平台首页非常显眼的位置还会展示平台已完成的总交易额与注册用户数量。对于管理端,管理员可通过管理页面修改产品、上架新产品、下架老产品。
为了方便了解 Redis 与 Spring Boot 的整合流程,这里对系统进行了简化:
- 用户端 首页仅提供根据金融产品名称的查询,显眼位置仅展示交易总额。
- 管理端 仅实现上架新产品功能。
7.2.4 创建 SSM 工程
⭐️(1) 创建工程
定义一个 Spring Boot 工程,并命名为 ssm
。
⭐️(2) 定义 pom 文件
在 pom
文件中需要导入 MySQL 驱动、 Druid 等大量依赖。 POM 文件中的重要内容如下:
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.12</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<directory>src/main/webapp</directory>
<targetPath>META-INF/resources</targetPath>
<includes>
<include>**/*.*</include>
</includes>
</resource>
</resources>
</build>
⭐️(3) 修改主配置文件
在主配置文件中配置 MyBatis
、 Spring
、日志
等配置:
⭐️(4) 实体类 Product
平台交易总额,即“产品募集结束日期”小于“当前查询日期”的“产品募集总额”之和。
⭐️(5) 创建数据库表
在 test
数据库中创建一个名称为 product
的表。创建的 sql 文件如下,直接运行该文件即可。
⭐️(6) 定义 index.jsp
在 src/main
下创建 webap 目录,用于存放 jsp
文件。这就是一个普通的目录,无需执行 Mark Directory As 。在 webapp 目录中创建一个 index.jsp
文件。
⭐️(7) 定义 manager.jsp
在 webapp 目录下再创建一个 jsp
子目录,在其中定义 manager.jsp
。
⭐️(8) 定义 result.jsp
在 webapp/jsp
中定义 result.jsp
。
⭐️(9) ProductController 类
A、 indexHandle()
B、 registerHandle()
C、 listHandle()
⭐️(10) ProductService 接口
⭐️(11) ProductServiceImpl 类
A、 saveProduct()
B、 三个 find 方法
⭐️(12) ProductDao 接口
⭐️(13) 映射文件
⭐️(14) Application 启动类
7.2.5 创建 SSRM 工程
7.3 高并发问题
Redis做缓存虽减轻了 DBMS 的压力,减小了 RT ,但在 高并发 情况下也是可能会出现各种问题的。
7.3.1 缓存穿透
当用户访问的 数据 既不在缓存 也不在数据库中 时,就会导致每个用户查询都会 “穿透” 缓存 “直抵” 数据库。这种情况就称为缓存穿透。当高并发的访问请求到达时,缓存穿透不仅增加了响应时间,而且还会引发对 DBMS 的高并发查询,这种 高并发查询 很可能会导致DBMS 的崩溃。
缓存穿透产生的主要原因有两个:一是在数据库中 没有 相应的查询结果,二是查询结果为 空 时,不对查询结果进行缓存。所以,针对以上两点,解决方案也有两个:
- 对 非法请求 进行 限制
- 对 结果为空 的查询给出 默认值
7.3.2 缓存击穿
对于 某一个缓存 ,在高并发情况下若其访问量特别巨大,当 该缓存的有效时限到达时,可能会出现大量的访问都要重建该缓存,即这些访问请求发现缓存中没有该数据,则立即到 DBMS 中进行查询,那么这就有可能会引发对 DBMS 的 高并发查询,从而接导致 DBMS 的崩溃。这种情况称为缓存击穿,而该缓存数据称为 热点数据。
对于缓存击穿 的解决方案,较典型的是使用 “双重检测锁 ” 机制。
7.3.3 缓存雪崩
对于缓存中的数据,很多都是有过期时间的。若 大量缓存 的 过期时间在同一很短的时间段内几乎同时到达,那么在高并发访问场景下就可能会引发对 DBMS 的高并发查询,而这将可能直接导致 DBMS 的崩溃。这种情况称为 缓存雪崩。
对于缓存雪崩没有很直接的解决方案,最好的解决方案就是 预防 ,即提前规划好缓存的过期时间。
- 要么就是让缓存 永久有效,当 DB 中数据发生变化时清除相应的缓存。
- 如果 DBMS 采用的是 分布式部署,则将热点数据 均匀分布在不同数据库节点中,将可能到来的访问负载均衡开来。
7.3.4 数据库缓存双写不一致
以上三种情况都是针对 高并发读 场景中可能会出现的问题,而数据库缓存双写不一致问题,则是在 高并发写 场景下可能会出现的问题。
对于数据库缓存 双写不一致 问题,以下两种场景下均有可能会发生:
⭐️(1) “修改 DB 更新缓存” 场景
对于具有缓存 warmup
功能的系统, DBMS 中常用数据的变更,都会引发缓存中相关数据的更新。在 高并发写 请求场景下,若多个请求要对 DBMS 中同一个数据进行修改,修改后还需要更新缓存中相关数据,那么就有可能会出现 缓存 与 数据库中 数据 不一致的情况。
⭐️(2) “修改 DB 删除缓存” 场景
在很多系统中是没有缓存 warmup
功能的,为了保持缓存与数据库数据的一致性,一般都是在对数据库执行了写操作后,就会 删除 相应缓存。
在 高并发读写 请求场景下,若这些请求对 DBMS 中同一个数据的操作既包含写也包含读,且修改后还要删除缓存中相关数据,那么就有可能会出现缓存与数据库中数据不一致的情况。
⭐️(3) 解决方案:延迟双删
延迟双删 方案是专门针对于“ 修改DB 删除缓存 ” 场景的解决方案。但该方案并不能彻底解决数据不一致的状况,其只可能降低发生数据不一致的概率。
延迟双删方案是指,在写操作完毕后会立即执行一次缓存的删除操作,然后再停上一段时间(一般为几秒)后再进行一次删除。而两次删除中间的间隔时长,要大于一次 缓存写操作 的 时长。
⭐️(4) 解决方案:队列
以上两种场景中,只所以会出现 数据库 与 缓存 中 数据不一致,主要是因为 对请求的处理出现了并行。只要将请求写入到一个统一的队列,只有处理完一个请求后才可处理下一个请求,即使系统对用户请求的处理 串行化,就可以完全解决数据不一致的问题。
⭐️(5) 解决方案:分布式锁
使用队列的串行化虽然可以解决数据库与缓存中数据不一致,但系统 失去了并发性,降低了性能。使用 分布式锁 可以在不影响并发性的前提下,协调各处理线程间的关系,使数据
库与缓存中的数据达成一致性。
只需要对数据库中的这个共享数据的访问通过 分布式锁 来协调对其的操作访问即可。
注:仅供学习参考,如有不足,欢迎指正!