⼀、 Redis介绍

1.1 锋迷商城项目问题

  • 数据库访问压力:为了降低对数据库的访问压⼒,当多个用户请求相同的数据时,我们可以将第⼀次从数据库查询到数据进行缓存(存储在内存中),以减少对数据库的访问次数
  • 首页数据的加载效率:将大量的且不经常改变的数据缓存在内容中,可以大幅度提高访问速度
  • 集群部署下的商品超卖:分布式事务
  • ⽤户登录:分布式会话

1.2 Redis介绍

1.2.1 Redis的产生背景

2008年 萨尔瓦多——开发⼀个进行网站实时统计软件项目(LLOOGG) ,项目的实时统计功能需要频繁的进行数
据库的读写(对数据库的读写要求很高——数千次/s), MySQL满足不了项目的需求,萨尔瓦多就使用C语言自定义
了⼀个数据存储系统——Redis。后来萨尔瓦多不满足仅仅在LLOOGG这个项目中使⽤redis,就对redis进行产品化并进行开源,以便让更多的⼈能够使用。

1.2.2 Redis使用

Redis就是⼀个用C语言开发的、基于内存结构进行键值对数据存储的、高性能的、非关系型NoSQL数据库

1.2.3 Redis支持的数据类型

redis是基于键值对进行数据存储的,但是value可以是多种数据类型:

  • string 字符串
  • hash 映射
  • list 列表(队列)
  • set 集合
  • zset 无序集合

1.2.4 Redis特点

  • 基于内存存储,数据读写效率很⾼
  • Redis本身支持持久化
  • Reids虽然基于key-value存储,但是⽀持多种数据类型
  • Redis支持集群、支持主从模式

1.3 Redis应用场景

  • 缓存:在绝大多数的互联网项目中,为了提供数据的访问速度、降低数据库的访问压力,我们可以使用redis作为缓存来实现
  • 点赞、排行榜、计数器等功能:对数据实时读写要求比较高,但是对数据库的⼀致性要求并不是太高的功能场景
  • 分布式锁:基于redis的操作特性可以实现分布式锁功能
  • 分布式会话:在分布式系统中可以使用redis实现 session (共享缓存)
  • 消息中间件:可以使用redis实现应用之间的通信

1.4 Redis的优缺点

1.4.1 优点

  • redis是基于内存结构,性能极高(读 110000次/秒,写 81000次/秒)
  • redis基于键值对存储,但是支持多种数据类型
  • redis的所有操作都是原子性,可以通过lua脚本将多个操作合并为⼀个原子操作(Redis的事务)
  • reids是基于单线程操作,但是其多路复用实现了高性能读写

1.4.2 缺点

  • 缓存数据与数据库数据必须通过两次写操作才能保持数据的⼀致性
  • 使⽤缓存会存在缓存穿透、缓存击穿及缓存雪崩等问题,需要处理
  • redis可以作为数据库使用进行数据的持久存储,存在丢失数据的风险

⼆、 Redis安装及配置

2.1 Redis安装

基于linux环境安装redis

2.1.1 下载Redis

  1. wget http://download.redis.io/releases/redis-5.0.5.tar.gz

2.1.2 安装redis

  • 安装gcc

    1. yum -y install gcc
  • 解压redis安装包

    1. tar -zxvf redis-5.0.5.tar.gz
  • 解压之后进⼈到redis-5.0.5⽬录

    1. cd redis-5.0.5
  • 编译

    1. make MALLOC=libc
  • 安装

    1. make install
  • 启动redis

    1. ##当我们完成redis安装之后,就可以执⾏redis相关的指令
    2. redis-server ## 启动redis服务
    3. redis-server &
  • 打开客户端
    ```shell redis-cli ## 启动redis操作客户端(命令⾏客户端)

若有多个redis实例可以通过端口号指定,也可以使用ip地址

redis-cli -p 6380 redis-cli -p 6380 -h ip

  1. <a name="3b5518b6"></a>
  2. ### 2.2 Redis配置
  3. >
  4. > - 使⽤ redis-server 指令启动redis服务的时候,可以在指令后添加redis配置文件的路径,以设置redis是以何种配置进行启动
  5. > - 如果不指定配置文件的名字,则按照redis的默认配置启动(默认配置≠redis.conf)
  6. > - 我们可以通过创建redis根⽬录下 redis.conf 来创建多个配置⽂件,启动多个redis服务常⽤redis配置
  7. >
  8. ```shell
  9. redis-server redis-6380.conf & ## redis以redis-6380.conf⽂件中的配置来启动
  1. redis-server redis-6380.conf &
  2. redis-server redis-6381.conf &

常用redis配置

  1. ## 设置redis实例(服务)为守护模式,默认值为no,可以设置为yes
  2. daemonize no
  3. ## 设置当前redis实例启动之后保存进程id的⽂件路径
  4. pidfile /var/run/redis_6379.pid
  5. ## 设置redis实例的启动端⼝(默认6379)
  6. port 6380
  7. ## 设置当前redis实例是否开启保护模式
  8. protected-mode yes
  9. ## 设置允许访问当前redis实例的ip地址列表
  10. bind 127.0.0.1
  11. ## 设置连接密码
  12. requirepass 123456
  13. ## 设置redis实例中数据库的个数(默认16个,编号0-15)
  14. databases 16
  15. ## 设置最⼤并发数量
  16. maxclients
  17. ## 设置客户端和redis建⽴连接的最⼤空闲时间,设置为0表示不限制
  18. timeout 0

三、 Redis基本使用

3.1 Redis存储的数据结构

Redis是以键值对形式进行数据存储的,但是value支持多种数据类型

12、Redis - 图1

3.2 string常用指令

  1. ## 设置值/修改值 如果key存在则进⾏修改
  2. set key value
  3. ## 取值
  4. get key
  5. ## 批量添加
  6. mset k1 v1 [k2 v2 k3 v3 ...]
  7. ## 批量取值
  8. mget k1 [k2 k3...]
  9. ## ⾃增和⾃减
  10. incr key ## 在key对应的value上⾃增 +1
  11. decr key ## 在key对应的value上⾃减 -1
  12. incrby key v ## 在key对应的value上+v
  13. decrby key v ## 在key对应的value上-v
  14. ## 添加键值对,并设置过期时间(TTL)
  15. setex key time(seconds) value
  16. ## 设置值,如果key不存在则成功添加,如果key存在则添加失败(不做修改操作)
  17. setnx key value
  18. ## 在指定的key对应value拼接字符串
  19. append key value
  20. ## 获取key对应的字符串的⻓度
  21. strlen key

3.3 hash常用指令

  1. ## 向key对应的hash中添加键值对
  2. hset key field value
  3. ## 从key对应的hash获取field对应的值
  4. hget key field
  5. ## 向key对应的hash结构中批量添加键值对
  6. hmset key f1 v1 [f2 v2 ...]
  7. ## 从key对应的hash中批量获取值
  8. hmget key f1 [f2 f3 ...]
  9. ## 在key对应的hash中的field对应value上加v
  10. hincrby key field v
  11. ## 获取key对应的hash中所有的键值对
  12. hgetall key
  13. ## 获取key对应的hash中所有的field
  14. hkeys key
  15. ## 获取key对应的hash中所有的value
  16. hvals key
  17. ## 检查key对应的hash中是否有指定的field
  18. hexists key field
  19. ## 获取key对应的hash中键值对的个数
  20. hlen key
  21. ## 向key对应的hash结构中添加f-v,如果field在hash中已经存在,则添加失败
  22. hsetnx key field value

3.4 list常用指令

12、Redis - 图2

  1. ## 存储数据
  2. lpush key value # 在key对应的列表的左侧添加数据value
  3. rpuhs key value # 在key对应的列表的右侧添加数据value
  4. ## 获取数据
  5. lpop key # 从key对应的列表的左侧取⼀个值
  6. rpop key # 从key对应的列表的右侧取⼀个值
  7. ## 修改数据
  8. lset key index value #修改key对应的列表的索引位置的数据(索引从左往右,从0开始)
  9. ## 查看key对应的列表中索引从start开始到stop结束的所有值
  10. lrange key start stop
  11. ## 查看key对应的列表中index索引对应的值
  12. lindex key index
  13. ## 获取key对应的列表中的元素个数
  14. llen key
  15. ## 从key对应的列表中截取key在[start,stop]范围的值,不在此范围的数据⼀律被清除掉
  16. ltrim key start stop
  17. ## 从k1右侧取出⼀个数据存放到k2的左侧
  18. rpoplpush k1 k2

3.5 set常用指令

  1. ## 存储元素 :在key对应的集合中添加元素,可以添加1个,也可以同时添加多个元素
  2. sadd key v1 [v2 v3 v4...]
  3. ## 遍历key对应的集合中的所有元素
  4. smembers key
  5. ## 随机从key对于听的集合中获取⼀个值(出栈)
  6. spop key
  7. ## 交集
  8. sinter key1 key2
  9. ## 并集
  10. sunion key1 key2
  11. ## 差集
  12. sdiff key1 key2
  13. ## 从key对应的集合中移出指定的value
  14. srem key value
  15. ## 检查key对应的集合中是否有指定的value
  16. sismember key value

3.6 zset常用指令

zset 有序不可重复集合

  1. ## 存储数据(score存储位置必须是数值,可以是float类型的任意数字; member元素不允许重复)
  2. zadd key score member [score member...]
  3. ## 查看key对应的有序集合中索引[start,stop]数据——按照score值由⼩到⼤(start 和 stop指的不是score,⽽是元素在有序集合中的索引)
  4. zrange key start top
  5. ##查看member元素在key对应的有序集合中的索引
  6. zscore key member
  7. ## 获取key对应的zset中的元素个数
  8. zcard key
  9. ## 获取key对应的zset中, score在[min,max]范围内的member个数
  10. zcount key min max
  11. ## 从key对应的zset中移除指定的member
  12. zrem key6 member
  13. ## 查看key对应的有序集合中索引[start,stop]数据——按照score值由⼤到⼩
  14. zrevrange key start stop

3.7 key相关指令

  1. ## 查看redis中满⾜pattern规则的所有的key(keys *)
  2. keys pattern
  3. ## 查看指定的key谁否存在
  4. exists key
  5. ## 删除指定的key-value对
  6. del key
  7. ## 获取当前key的存活时间(如果没有设置过期返回-1,设置过期并且已经过期返回-2)
  8. ttl key
  9. ## 设置键值对过期时间
  10. expire key seconds
  11. pexpire key milliseconds
  12. ## 取消键值对过期时间
  13. persist key

3.8 db常用指令

redis的键值对是存储在数据库中的——db

redis中默认有16个db,编号 0-15

  1. ## 切换数据库
  2. select index
  3. ## 将键值对从当前db移动到⽬标db
  4. move key index
  5. ## 清空当前数据库数据
  6. flushdb
  7. ## 清所有数据库的k-v
  8. flushall
  9. ## 查看当前db中k-v个数
  10. dbsize
  11. ## 获取最后⼀次持久化操作时间
  12. lastsave

四、Redis的持久化 [重点]

Redis是基于内存操作,但作为⼀个数据库也具备数据的持久化能⼒;但是为了实现高效的读写操作,并不会
即时进行数据的持久化,而是按照⼀定的规则进行持久化操作的——持久化策略

Redis提供了2中持久化策略:

  • RDB (Redis DataBase)
  • AOF(Append Only File)

4.1 RDB

在满足特定的redis操作条件时,将内存中的数据以数据快照的形式存储到rdb⽂件中

12、Redis - 图3

  • 原理:
    RDB是redis默认的持久化策略,当redis中的写操作达到指定的次数、同时距离上⼀次持久化达到指定的时间就会将redis内存中的数据⽣成数据快照,保存在指定的rdb⽂件中。
  • 默认触发持久化条件:
    • 900s 1次:当操作次数达到1次, 900s就会进⾏持久化
    • 300s 10次:当操作次数达到10次, 300s就会进⾏持久化
    • 60s 10000次:当操作次数达到10000次, 60s就会就⾏持久化
  • 我们可以通过修改redis.conf⽂件,来设置RDB策略的触发条件:
    ```shell

    rdb持久化开关

    rdbcompression yes

配置redis的持久化策略

save 900 1 save 300 10 save 60 10000

指定rdb数据存储的⽂件

dbfilename dump.rdb

  1. - RED持久化细节分析:<br />缺点:
  2. - 如果redis出现故障,存在数据丢失的风险,丢失上⼀次持久化之后的操作数据;
  3. - RDB采用的是数据快照形式进⾏持久化,不适合实时性持久化;
  4. - 如果数据量巨大,在RDB持久化过程中生成数据快照的子进程执行时间过长,会导致redis卡顿,因此<br />save时间周期设置不宜过短;
  5. 优点:
  6. - 但是在数据量较小的情况下,执行速度比较快;
  7. - 由于RDB是以数据快照的形式进行保存的,我们可以通过拷贝rdb文件轻松实现redis数据移植
  8. <a name="c5e29c65"></a>
  9. ### 4.2 AOF
  10. > Apeend Only File,当达到设定触发条件时,将redis执行的写操作指令存储在aof文件中, Redis默认未开启aof<br />持久化
  11. ![](https://s2.loli.net/2021/12/29/SgioAM3QxZ2rVCL.png#crop=0&crop=0&crop=1&crop=1&id=hrJtW&originHeight=295&originWidth=1294&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
  12. - 原理:<br />Redis将每⼀个成功的写操作写⼊到aof⽂件中,当redis重启的时候就执⾏aof⽂件中的指令以恢复数据
  13. - 配置:
  14. ```shell
  15. ## 开启AOF
  16. appendonly yes
  17. ## 设置触发条件(三选⼀)
  18. appendfsync always ## 只要进⾏成功的写操作,就执⾏aof
  19. appendfsync everysec ## 每秒进⾏⼀次aof
  20. appendfsync no ## 让redis执⾏决定aof
  21. ## 设置aof⽂件路径
  22. appendfilename "appendonly.aof"
  • AOF细节分析:
    • 也可以通过拷贝aof文件进行redis数据移植
    • aof存储的指令,而且会对指令进⾏整理;而RDB直接生成数据快照,在数据量不大时RDB比较快
    • aof是对指令文件进行增量更新,更适合实时性持久化
    • redis官方建议同时开启两中持久化策略,如果同时存在aof文件和rdb文件的情况下aof优先

五、 Java应用连接Redis

5.1 设置redis允许远程连接

Java应用连接Redis,首先要将我们的Redis设置允许远程连接

  • 修改redis-6379.conf
    ```shell

    关闭保护模式

    protected-mode no

将bind注释掉(如果不注释,默认为 bind 127.0.0.1 只能本机访问)

bind 127.0.0.1

密码可以设置(也可以不设置)

requirepass 123456

  1. - 重启redis
  2. ```shell
  3. redis-server redis-6379.conf &
  • 阿里云安全组设置放行6379端⼝
    12、Redis - 图4

5.2 在普通Maven工程连接Redis

使用jedis客户端连接

5.2.1 添加Jedis依赖

  1. <!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
  2. <dependency>
  3. <groupId>redis.clients</groupId>
  4. <artifactId>jedis</artifactId>
  5. <version>2.9.0</version>
  6. </dependency>

5.2.2 使用案例

  1. public static void main(String[] args) {
  2. Product product = new Product("101", "娃哈哈AD钙奶", 2.5);
  3. //1.连接redis
  4. Jedis jedis = new Jedis("47.96.11.185", 6379);
  5. //2.操作
  6. String s = jedis.set(product.getProductId(), new Gson().toJson(product));
  7. System.out.println(s);
  8. //3.关闭连接
  9. jedis.close();
  10. }

5.2.3 redis远程可视化客户端

Redis desktop manager

5.3 在SpringBoot⼯程连接Redis

Spring Data Redis, part of the larger Spring Data family, provides easy configuration and access to Redis from
Spring applications. It offers both low-level and high-level abstractions for interacting with the store, freeing the
user from infrastructural concerns.

Spring Data Redis依赖中,提供了用于连接redis的客户端:

  • RedisTemplate
  • StringRedisTemplate

5.3.1 创建springBoot应用

12、Redis - 图5

5.3.2 配置redis

application.yml⽂件配置redis的连接信息

  1. spring:
  2. redis:
  3. host: 47.96.11.185
  4. port: 6379
  5. database: 0
  6. password: 123456

5.3.3 使用redis客户端连接redis

直接在service中注入 RedisTemplate 或者 StringRedisTemplate ,就可以使用此对象完成redis操作

5.4 Spring Data Redis

5.4.1 不同数据结构的添加操作

  1. //1.string
  2. //添加数据 set key value
  3. stringRedisTemplate.boundValueOps(product.getProductId()).set(jsonstr);
  4. //2.hash
  5. stringRedisTemplate.boundHashOps("products").put(product.getProductId(),jsonstr);
  6. //3.list
  7. stringRedisTemplate.boundListOps("list").leftPush("ccc");
  8. //4.set
  9. stringRedisTemplate.boundSetOps("s1").add("v2");
  10. //5.zset
  11. stringRedisTemplate.boundZSetOps("z1").add("v1",1.2);

5.4.2 string类型的操作方法

  1. //添加数据 set key value
  2. stringRedisTemplate.boundValueOps(product.getProductId()).set( jsonstr);
  3. //添加数据时指定过期时间 setex key 300 value
  4. stringRedisTemplate.boundValueOps("103").set(jsonstr,300);
  5. //设置指定key的过期时间 expire key 20
  6. stringRedisTemplate.boundValueOps("103").expire(20, TimeUnit.SECONDS);
  7. //添加数据 setnx key value
  8. Boolean absent = stringRedisTemplate.boundValueOps("103").setIfAbsent(jsonstr);

5.4.3 不同数据类型的取值操作

  1. //string
  2. String o = stringRedisTemplate.boundValueOps("103").get();
  3. //hash
  4. Object v = stringRedisTemplate.boundHashOps("products").get("101");
  5. //list
  6. String s1 = stringRedisTemplate.boundListOps("list").leftPop();
  7. String s2 = stringRedisTemplate.boundListOps("list").rightPop();
  8. String s3 = stringRedisTemplate.boundListOps("list").index(1);
  9. //set
  10. Set<String> vs = stringRedisTemplate.boundSetOps("s1").members();
  11. //zset
  12. Set<String> vs2 = stringRedisTemplate.boundZSetOps("z1").range(0, 5);

六、使用redis缓存数据库数据

6.1 redis作为缓存的使用流程

12、Redis - 图6

6.2 在使用redis缓存商品详情(锋迷商城)

6.2.1 在service子工程添加Spring data redis依赖

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-data-redis</artifactId>
  4. </dependency>

6.2.2 在application.yml配置redis数据源

  1. spring:
  2. druid:
  3. driver-class-name: com.mysql.jdbc.Driver
  4. ## 如果后端项⽬服务器和数据库服务器不在同⼀台主机,则需要修改localhost为数据库服务器ip地址
  5. url: jdbc:mysql://localhost:3306/fmmall2?characterEncoding=utf-8
  6. username: root
  7. password: admin123
  8. redis:
  9. port: 6379
  10. host: 47.96.11.185
  11. database: 0
  12. password: 123456

6.3.3 在ProductServiceImpl中修改业务代码

12、Redis - 图7

12、Redis - 图8

12、Redis - 图9

12、Redis - 图10

6.3、案例

需要先定义一个返回值

涉及查询存放,以及格式转换

设定过期时间

  1. @Service
  2. public class CategoryServiceImpl implements CategoryService {
  3. @Autowired
  4. private CategoryMapper categoryMapper;
  5. @Autowired
  6. private StringRedisTemplate stringRedisTemplate;
  7. private ObjectMapper objectMapper=new ObjectMapper();
  8. //查询分类列表(包含三个级别的分类)
  9. @Override
  10. public ResultVO listCategories() {
  11. List<CategoryVO> categoryVOS=null;
  12. try {
  13. //1、查询redis
  14. String cateories = stringRedisTemplate.boundValueOps("cateories").get();
  15. //2、判断是否为空
  16. if(cateories!=null){
  17. JavaType javaType=objectMapper.getTypeFactory().constructParametricType(ArrayList.class, CategoryVO.class);
  18. categoryVOS = objectMapper.readValue(cateories, javaType);
  19. }else {
  20. //如果redis中没有类型数据,需要查询数据库
  21. categoryVOS = categoryMapper.selectAllCategories();
  22. stringRedisTemplate.boundValueOps("cateories").set(objectMapper.writeValueAsString(categoryVOS),1, TimeUnit.DAYS);
  23. }
  24. } catch (JsonProcessingException e) {
  25. e.printStackTrace();
  26. }
  27. ResultVO resultVO = new ResultVO(ResStatus.OK, "success", categoryVOS);
  28. return resultVO;
  29. }
  30. //查询所有一级分类,同时查询当前一级分类下销量最高的6个商品
  31. @Override
  32. public ResultVO listFirstLevelCategory() {
  33. List<CategoryVO> categoryVOS = categoryMapper.selectFirstLevelCategories();
  34. ResultVO resultVO = new ResultVO(ResStatus.OK, "success", categoryVOS);
  35. return resultVO;
  36. }
  37. }

6.5、页面静态化

页面静态化:将数据库中的每条数据结合模板生成单独的HTML文件进行存储(一条数据对应一个独立的HTML文件),当用户访问数据时,直接访问不同的静态文件即可

七、使用Redis做缓存使用存在的问题 [重点]

使用redis做为缓存在高并发场景下有可能出现缓存击穿、缓存穿透、缓存雪崩等问题

7.1 缓存击穿

缓存击穿:大量的并发请求同时访问同⼀个在redis中不存在的数据,就会导致大量的请求绕过redis同时并发
访问数据库,对数据库造成了高并发访问压力。

  • 使用 双重检测锁 解决 缓存击穿 问题
  1. @Service
  2. public class IndexImgServiceImpl implements IndexImgService {
  3. @Autowired
  4. private IndexImgMapper indexImgMapper;
  5. @Autowired
  6. private StringRedisTemplate stringRedisTemplate;
  7. private ObjectMapper objectMapper = new ObjectMapper();
  8. public ResultVO listIndexImgs() {
  9. List<IndexImg> indexImgs = null;
  10. try {
  11. //1000个并发请求,请求轮播图
  12. String imgsStr = stringRedisTemplate.boundValueOps("indexImgs").get();
  13. //1000个请求查询到redis中的数据都是null
  14. if (imgsStr != null) {
  15. // 从redis中获取到了轮播图信息
  16. JavaType javaType = objectMapper.getTypeFactory().constructParametricType(ArrayList.class, IndexImg.class);
  17. indexImgs = objectMapper.readValue(imgsStr, javaType);
  18. } else {
  19. // 1000个请求都会进⼊else
  20. // (service类在spring容器中是单例的,
  21. // 1000个并发会启动1000个线程来处理,但是共⽤⼀个service实例)
  22. synchronized (this){
  23. // 第⼆次查询redis
  24. String s = stringRedisTemplate.boundValueOps("indexImgs").get();
  25. if(s == null){
  26. // 这1000个请求中,只有第⼀个请求再次查询redis时依然为null
  27. indexImgs = indexImgMapper.listIndexImgs();
  28. System.out.println("----------------查询数据库"); stringRedisTemplate.boundValueOps("indexImgs").set(objectMapper.writeValueAsString(indexImgs));
  29. stringRedisTemplate.boundValueOps("indexImgs").expire(1, TimeUnit.DAYS);
  30. }else{
  31. JavaType javaType = objectMapper.getTypeFactory().constructParametricType(ArrayList.class, IndexImg.class);
  32. indexImgs = objectMapper.readValue(s, javaType);
  33. }
  34. }
  35. }
  36. } catch (JsonProcessingException e) {
  37. e.printStackTrace();
  38. }
  39. //返回数据
  40. if(indexImgs != null){
  41. return new ResultVO(ResStatus.OK,"success",indexImgs);
  42. }else{
  43. return new ResultVO(ResStatus.NO,"fail",null);
  44. }
  45. }
  46. }

7.2 缓存穿透

缓存穿透:大量的请求⼀个数据库中不存在的数据,首先在redis中无法命中,最终所有的请求都会访问数据
库,同样会导致数据库承受巨大的访问压力

  • 解决方案:当从数据库查询到⼀个null时,写⼀个非空的数据到redis,并设置过期时间

12、Redis - 图11

  1. indexImgs = indexImgMapper.listIndexImgs();
  2. if(indexImgs != null) {
  3. String s = objectMapper.writeValueAsString(indexImgs);
  4. stringRedisTemplate.boundValueOps("indexImgs").set(s);
  5. stringRedisTemplate.boundValueOps("indexImgs").expire(1, TimeUnit.DAYS);
  6. }else{
  7. //当从数据库查询数据为null时,保存⼀个⾮空数据到redis,并设置过期时间
  8. stringRedisTemplate.boundValueOps("indexImgs").set("[]");
  9. stringRedisTemplate.boundValueOps("indexImgs").expire(10, TimeUnit.SECONDS);
  10. }

7.3 缓存雪崩

缓存雪崩:缓存大量的数据集中过期,导致请求这些数据的⼤量的并发请求会同时访问数据库

解决方案:

  • 将缓存中的数据设置成不同的过期时间
  • 在访问洪峰到达前缓存热点数据,过期时间设置到流量最低的时段

7.4 Jmeter测试

Jmeter是基于Java开发的一个测试工具,因此需要先安装JDK

7.4.1创建测试计划

12、Redis - 图12

7.4.2 创建线程组

12、Redis - 图13

7.4.3 设置HTTP请求

12、Redis - 图14

7.4.4 设置监听器

12、Redis - 图15

12、Redis - 图16

然后保存点击绿色三角运行

⼋、 Redis高级应用

使用redis作为缓存数据库使用目的是为了提升数据加载速度、降低对数据库的访问压力,我们需要保证redis
的可用性。

  • 主从配置
  • 哨兵模式
  • 集群配置

8.1 主从配置

主从配置:在多个redis实例建立起主从关系,当主redis中的数据发生变化, 从redis中的数据也会同步变化。

  • 通过主从配置可以实现redis数据的备份( 从redis 就是对 主redis 的备份),保证数据的安全性;
  • 通过主从配置可以实现redis的读写分离
    12、Redis - 图17

主从配置示例

  • 启动三个redis实例
    12、Redis - 图18
    12、Redis - 图19
    ```shell

    在redis-5.0.5⽬录下创建 msconf ⽂件夹

    [root@theo redis-5.0.5]# mkdir msconf

拷⻉redis.conf⽂件 到 msconf⽂件夹 —-> redis-master.conf,在这个过程中去掉注释与空行

[root@theo redis-5.0.5]# cat redis.conf |grep -v “#” | grep -v “^$” > msconf/redismaster.conf

修改 redis-master.conf 端⼝及远程访问设置

[root@theo msconf]# vim redis-master.conf

  1. ```shell
  2. ## 将 redis-master.conf 拷⻉两份分别为: redis-slave1.conf redis-slave2.conf
  3. [root@theo msconf]# sed 's/6380/6381/g' redis-master.conf > redis-slave1.conf
  4. [root@theo msconf]# sed 's/6380/6382/g' redis-master.conf > redis-slave2.conf
  5. ## 修改redis-slave1.conf redis-slave2.conf,设置“跟从”---127.0.0.1 6380
  6. [root@theo msconf]# vim redis-slave1.conf
  7. [root@theo msconf]# vim redis-slave2.conf
  1. ## 启动三个redis实例
  2. [root@theo msconf]# redis-server redis-master.conf &
  3. [root@theo msconf]# redis-server redis-slave1.conf &
  4. [root@theo msconf]# redis-server redis-slave2.conf &

8.2 哨兵模式

哨兵模式:用于监听主库,当确认主库宕机之后,从备库(从库)中选举⼀个转备为主

12、Redis - 图20

  • 哨兵模式配置
  1. ##⾸先实现三个redis实例之间的主从配置(如上)
  2. ## 创建并启动三个哨兵
  3. ## 拷⻉sentinel.conf⽂件三份: sentinel-26380.conf sentinel-26381.conf sentinel-26382.conf
  4. ## 创建sentinelconf⽬录
  5. [root@theo redis-5.0.5]# mkdir sentinelconf
  6. ## 拷⻉sentinel.conf⽂件到 sentinelconf⽬录: sentinel-26380.conf
  7. [root@theo redis-5.0.5]# cat sentinel.conf | grep -v "#" | grep -v "^$" >
  8. sentinelconf/sentinel-26380.conf
  9. [root@theo redis-5.0.5]# cd sentinelconf/
  10. [root@theo sentinelconf]# ll
  11. total 4
  12. -rw-r--r-- 1 root root 326 May 19 17:09 sentinel-26380.conf
  13. ## 编辑 sentinelconf/sentinel-26380.conf⽂件
  14. [root@theo sentinelconf]# vim sentinel-26380.conf
  1. port 26380
  2. daemonize no
  3. pidfile "/var/run/redis-sentinel-26380.pid"
  4. logfile ""
  5. dir "/tmp"
  6. sentinel deny-scripts-reconfig yes
  7. # 此处配置默认的主库的ip 和端⼝ 最后的数字是哨兵数量的⼀半多⼀个
  8. sentinel monitor mymaster 127.0.0.1 6380 2
  9. sentinel down-after-milliseconds mymaster 30000
  10. sentinel config-epoch mymaster 0
  11. sentinel leader-epoch mymaster 0
  12. protected-mode no
  13. sentinel current-epoch 0
  14. [root@theo sentinelconf]# sed 's/26380/26381/g' sentinel-26380.conf > sentinel-26381.conf
  15. [root@theo sentinelconf]# sed 's/26380/26382/g' sentinel-26380.conf > sentinel-26382.conf

测试:

  1. 启动 redis
  2. 启动 1redis
  3. 启动 2redis
  4. 再依次启动三个哨兵:
  5. [root@theo sentinelconf]# redis-sentinel sentinel-26380.conf

8.3 集群配置

高可用:保证redis⼀直处于可用状态,即时出现了故障也有备用方案保证可用性

高并发:⼀个redis实例已经可以支持多达11w并发读操作或者8.1w并发写操作;但是如果对于有更高并发需求的应用来说,我们可以通过读写分离 、 集群配置来解决高并发问题 tdc

12、Redis - 图21

Redis集群

  • Redis集群中每个节点是对等的,无中心结构
  • 数据按照slots分布式存储在不同的redis节点上,节点中的数据可共享,可以动态调整数据的分布
  • 可扩展性强,可以动态增删节点,最多可扩展至1000+节点
  • 集群每个节点通过主备(哨兵模式)可以保证其高可用性

8.3.1 集群搭建

  1. [root@theo ~]# cd /usr/local/redis-5.0.5
  2. [root@theo redis-5.0.5]# mkdir cluster-conf
  3. [root@theo redis-5.0.5]# cat redis.conf | grep -v "#"|grep -v "^$" > cluster-conf/redis-
  4. 7001.conf
  5. [root@theo redis-5.0.5]# cd cluster-conf/
  6. [root@theo cluster-conf]# ls
  7. redis-7001.conf
  8. [root@theo cluster-conf]# vim redis-7001.conf

12、Redis - 图22

  • 拷贝6个文件,端口分别为 7001-7006

    1. [root@theo cluster-conf]# sed 's/7001/7002/g' redis-7001.conf > redis-7002.conf
    2. [root@theo cluster-conf]# sed 's/7001/7003/g' redis-7001.conf > redis-7003.conf
    3. [root@theo cluster-conf]# sed 's/7001/7004/g' redis-7001.conf > redis-7004.conf
    4. [root@theo cluster-conf]# sed 's/7001/7005/g' redis-7001.conf > redis-7005.conf
    5. [root@theo cluster-conf]# sed 's/7001/7006/g' redis-7001.conf > redis-7006.conf
  • 启动6个redis实例

    1. [root@theo cluster-conf]# redis-server redis-7001.conf &
    2. [root@theo cluster-conf]# redis-server redis-7002.conf &
    3. [root@theo cluster-conf]# redis-server redis-7003.conf &
    4. [root@theo cluster-conf]# redis-server redis-7004.conf &
    5. [root@theo cluster-conf]# redis-server redis-7005.conf &
    6. [root@theo cluster-conf]# redis-server redis-7006.conf &
  • 查看6个实例是否启动

    1. [root@theo cluster-conf]# ps -ef|grep redis
    2. root 4789 1 0 10:20 ? 00:00:00 redis-server *:7001 [cluster]
    3. root 4794 1 0 10:20 ? 00:00:00 redis-server *:7002 [cluster]
    4. root 4799 1 0 10:20 ? 00:00:00 redis-server *:7003 [cluster]
    5. root 4806 1 0 10:21 ? 00:00:00 redis-server *:7004 [cluster]
    6. root 4811 1 0 10:21 ? 00:00:00 redis-server *:7005 [cluster]
    7. root 4816 1 0 10:21 ? 00:00:00 redis-server *:7006 [cluster]
  • 启动集群
    12、Redis - 图23

    1. [root@theo cluster-conf]# redis-cli --cluster create 47.96.11.185:7001 47.96.11.185:7002 47.96.11.185:7003 47.96.11.185:7004 47.96.11.185:7005 47.96.11.185:7006 --cluster-replicas 1
  • 连接集群:

    1. [root@theo cluster-conf]# redis-cli -p 7001 -c

8.3.2 集群管理

  • 如果集群启动失败:等待节点加入
    • 服务器检查安全组是否放行redis实例端口,以及+10000的端口
    • Linux防火墙是否放行redis服务(关闭防火墙)
    • Linux状态(top) —— 更换云主机操作系统
    • redis配置文件错误
  • 创建集群:

    1. [root@theo cluster-conf]# redis-cli --cluster create 47.96.11.185:7001 47.96.11.185:7002 47.96.11.185:7003 47.96.11.185:7004 47.96.11.185:7005 47.96.11.185:7006 --cluster-replicas 1
  • 查看集群状态

    1. [root@theo cluster-conf]# redis-cli --cluster info 47.96.11.185:7001
    2. 47.96.11.185:7001 (4678478a...) -> 2 keys | 5461 slots | 1 slaves.
    3. 47.96.11.185:7002 (e26eaf2a...) -> 0 keys | 5462 slots | 1 slaves.
    4. 47.96.11.185:7003 (5752eb20...) -> 1 keys | 5461 slots | 1 slaves.
    5. [OK] 3 keys in 3 masters.
    6. 0.00 keys per slot on average.
  • 平衡节点的数据槽数

    1. [root@theo cluster-conf]# redis-cli --cluster rebalance 47.96.11.185:7001
    2. >>> Performing Cluster Check (using node 47.96.11.185:7001)
    3. [OK] All nodes agree about slots configuration.
    4. >>> Check for open slots...
    5. >>> Check slots coverage...
    6. [OK] All 16384 slots covered.
    7. *** No rebalancing needed! All nodes are within the 2.00% threshold.
  • 迁移节点槽
    12、Redis - 图24

  • 删除节点

    1. [root@theo cluster-conf]# redis-cli --cluster del-node 47.96.11.185:7001 4678478aa66b6d37b23944cf7db0ac07298538a4
    2. >>> Removing node 4678478aa66b6d37b23944cf7db0ac07298538a4 from cluster 47.96.11.185:7001
    3. >>> Sending CLUSTER FORGET messages to the cluster...
    4. >>> SHUTDOWN the node.
    5. [root@theo cluster-conf]# redis-cli --cluster info 47.96.11.185:7002
    6. 47.96.11.185:7002 (e26eaf2a...) -> 1 keys | 8192 slots | 2 slaves.
    7. 47.96.11.185:7003 (5752eb20...) -> 2 keys | 8192 slots | 1 slaves.
    8. [OK] 3 keys in 2 masters.
    9. 0.00 keys per slot on average.
  • 添加节点

    1. [root@theo cluster-conf]# redis-cli --cluster add-node 47.96.11.185:7007 47.96.11.185:7002

8.3.3 SpringBoot应用连接集群

  • 添加依赖

    1. <dependency>
    2. <groupId>org.springframework.boot</groupId>
    3. <artifactId>spring-boot-starter-data-redis</artifactId>
    4. </dependency>
  • 配置集群节点

    1. spring:
    2. redis:
    3. cluster:
    4. nodes: 47.96.11.185:7001,47.96.11.185:7002,47.96.11.185:7003
    5. max-redirects: 3
  • 操作集群

    1. @RunWith(SpringRunner.class)
    2. @SpringBootTest(classes = RedisDemo3Application.class)
    3. class RedisDemo3ApplicationTests {
    4. @Autowired
    5. private StringRedisTemplate stringRedisTemplate;
    6. @Test
    7. void contextLoads() {
    8. //stringRedisTemplate.boundValueOps("key1").set("value1");
    9. String s = stringRedisTemplate.boundValueOps("key1").get();
    10. System.out.println(s);
    11. }
    12. }

九、 Redis淘汰策略

Redis是基于内存结构进行数据缓存的,当内存资源消耗完毕,想要有新的数据缓存进来,必然要从Redis的
内存结构中释放⼀些数据。如何进⾏数据的释放呢? ——Redis的淘汰策略

Redis提供的8中淘汰策略

  1. # volatile-lru -> 从设置了过期时间的数据中淘汰最久未使⽤的数据.
  2. # allkeys-lru -> 从所有数据中淘汰最久未使⽤的数据.
  3. # volatile-lfu -> 从设置了过期时间的数据中淘汰使⽤最少的数据.
  4. # allkeys-lfu -> 从所有数据中淘汰使⽤最少的数据.
  5. # volatile-random -> 从设置了过期时间的数据中随机淘汰⼀批数据.
  6. # allkeys-random -> 从所有数据中随机淘汰⼀批数据.
  7. # volatile-ttl -> 淘汰过期时间最短的数据.
  8. # noeviction -> 不淘汰任何数据,当内存不够时直接抛出异常.
  • 理解两个算法名词:
    LRU 最久最近未使用
    LFU 最近最少使用

十、Redis高频面试题

  1. 在项目中redis的使用场景
    • 用于缓存首页数据
  2. Redis的持久化策略
  3. Redis支持的数据类型
  4. 如何保证redis的高可用
    • Redis支持持久化,同时开启rdb 和 aof ,以保证数据的安全性(还是存在数据丢失风险的)
    • Redis支持主从配置,我们可通配置哨兵,实现主备配置,保证可用性
    • Redis也支持集群,通过集群配置可以保证redis的高并发
  5. 你刚才提到了redis集群,请问如何解决redis集群的脑裂问题?
  6. redis中的数据可以设置过期时间,当数据过期之后有些key并没有及时清除,请问如何处理?

⼗⼀、使⽤Redis实现分布式会话

11.1 流程分析

12、Redis - 图25

11.2 在锋迷商城中使用redis实现分布式会话

11.2.1 修改登录接口

  • 当登录成功以token为key将用户信息保存到redis
  1. @Service
  2. public class UserServiceImpl implements UserService {
  3. @Autowired
  4. private UsersMapper usersMapper;
  5. @Autowired
  6. private StringRedisTemplate stringRedisTemplate;
  7. private ObjectMapper objectMapper = new ObjectMapper();
  8. @Transactional
  9. public ResultVO userResgit(String name, String pwd) {
  10. //...
  11. }
  12. @Override
  13. public ResultVO checkLogin(String name, String pwd) {
  14. Example example = new Example(Users.class);
  15. Example.Criteria criteria = example.createCriteria();
  16. criteria.andEqualTo("username", name);
  17. List<Users> users = usersMapper.selectByExample(example);
  18. if(users.size() == 0){
  19. return new ResultVO(ResStatus.NO,"登录失败,⽤户名不存在! ",null);
  20. }else{
  21. String md5Pwd = MD5Utils.md5(pwd);
  22. if(md5Pwd.equals(users.get(0).getPassword())){
  23. //如果登录验证成功,则需要⽣成令牌token(token就是按照特定规则⽣成的字符串)
  24. //使⽤jwt规则⽣成token字符串
  25. JwtBuilder builder = Jwts.builder();
  26. HashMap<String,Object> map = new HashMap<>();
  27. map.put("key1","value1");
  28. map.put("key2","value2");
  29. String token = builder.setSubject(name)
  30. .setIssuedAt(new Date())
  31. .setId(users.get(0).getUserId() + "")
  32. .setClaims(map)
  33. .setExpiration(new Date(System.currentTimeMillis() + 24*60*60*1000))
  34. .signWith(SignatureAlgorithm.HS256, "QIANfeng6666")
  35. .compact();
  36. //当⽤户登录成功之后,以token为key 将⽤户信息保存到reids
  37. try {
  38. String userInfo = objectMapper.writeValueAsString(users.get(0));
  39. stringRedisTemplate.boundValueOps(token).set(userInfo,30, TimeUnit.MINUTES);
  40. } catch (JsonProcessingException e) {
  41. e.printStackTrace();
  42. }
  43. return new ResultVO(ResStatus.OK,token,users.get(0));
  44. }else{
  45. return new ResultVO(ResStatus.NO,"登录失败,密码错误! ",null);
  46. }
  47. }
  48. }
  49. }

11.2.2 在需要只用用户信息的位置,直接根据token从redis查询

  1. @PostMapping("/add")
  2. public ResultVO addShoppingCart(@RequestBody ShoppingCart cart,@RequestHeader("token")String
  3. token) throws JsonProcessingException {
  4. ResultVO resultVO = shoppingCartService.addShoppingCart(cart);
  5. String s = stringRedisTemplate.boundValueOps(token).get();
  6. Users users = objectMapper.readValue(s, Users.class);
  7. System.out.println(users);
  8. return resultVO;
  9. }

11.2.3 修改受限资源拦截器

  1. @Component
  2. public class CheckTokenInterceptor implements HandlerInterceptor {
  3. @Autowired
  4. private StringRedisTemplate stringRedisTemplate;
  5. @Override
  6. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object
  7. handler) throws Exception {
  8. String method = request.getMethod();
  9. if("OPTIONS".equalsIgnoreCase(method)){
  10. return true;
  11. }
  12. String token = request.getHeader("token");
  13. if(token == null){
  14. ResultVO resultVO = new ResultVO(ResStatus.LOGIN_FAIL_NOT, "请先登录! ", null);
  15. doResponse(response,resultVO);
  16. }else{
  17. //根据token从redis中获取⽤户信息
  18. String s = stringRedisTemplate.boundValueOps(token).get();
  19. if(s == null){
  20. //如果⽤户信息为空,表示⽤户未登录或者距离上⼀次访问超过30分钟
  21. ResultVO resultVO = new ResultVO(ResStatus.LOGIN_FAIL_NOT, "请先登录! ", null);
  22. doResponse(response,resultVO);
  23. }else{
  24. // 如果不为空,表示⽤户登录成功,续命
  25. stringRedisTemplate.boundValueOps(token).expire(30, TimeUnit.MINUTES);
  26. return true;
  27. }
  28. }
  29. return false;
  30. }
  31. private void doResponse(HttpServletResponse response,ResultVO resultVO) throws IOException{
  32. response.setContentType("application/json");
  33. response.setCharacterEncoding("utf-8");
  34. PrintWriter out = response.getWriter();
  35. String s = new ObjectMapper().writeValueAsString(resultVO);
  36. out.print(s);
  37. out.flush();
  38. out.close();
  39. }
  40. }

11.2.4 创建非受限资源拦截器

即使访问的是非受限资源,但是如果已经登录,只要与服务器有交互也要续命

  1. @Component
  2. public class SetTimeInterceptor implements HandlerInterceptor {
  3. @Autowired
  4. private StringRedisTemplate stringRedisTemplate;
  5. @Override
  6. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
  7. String token = request.getHeader("token");
  8. if(token != null){
  9. String s = stringRedisTemplate.boundValueOps(token).get();
  10. if(s != null){
  11. stringRedisTemplate.boundValueOps(token).expire(30, TimeUnit.MINUTES);
  12. }
  13. }
  14. return true;
  15. }
  16. }
  • 配置拦截器:
  1. @Configuration
  2. public class InterceptorConfig implements WebMvcConfigurer {
  3. @Autowired
  4. private CheckTokenInterceptor checkTokenInterceptor;
  5. @Autowired
  6. private SetTimeInterceptor setTimeInterceptor;
  7. @Override
  8. public void addInterceptors(InterceptorRegistry registry) {
  9. registry.addInterceptor(checkTokenInterceptor)
  10. .addPathPatterns("/shopcart/**")
  11. .addPathPatterns("/orders/**")
  12. .addPathPatterns("/useraddr/**")
  13. .addPathPatterns("/user/check");
  14. registry.addInterceptor(setTimeInterceptor).addPathPatterns("/**");
  15. }
  16. }

前端访问注意事项: 只要前端有token,对接⼝的访问就要携带token