Redis

  • 数据库访问压力:为了降低对数据库的访问压力,当多个用户请求相同的数据时,我们可以将第一次从数据库查询到数据进行缓存(存储在内存中),以减少对数据库的访问次数。
  • 首页数据的加载效率:将大量的且不经常改变的数据缓存在内容中,可以大幅度提高访问速度

一、简介

1.1支持的数据类型

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

1.2 特点

  • 基于内存存储,数据读写效率很高
  • 支持持久化
  • 支持集群、主从模式

二、Redis安装及配置

2.1Redis安装

基于Linux环境安装redis

2.1.1下载Redis

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

2.1.2安装redis

  • 安装gcc

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

    tar -zxvf redis-5.0.5.tar.gz
    
  • 进入redis-5.0.5目录

    cd redis-5.0.5
    
  • 编译

    make MALLOC=libc
    
  • 安装

    make install
    
  • 启动redis

    ## 完成安装后,即可执行redis相关指令
    redis-server ## 启动redis服务
    redis-server & ## 后台启动redis服务
    
  • 打开客户端

    redis-cli ## 启动redis操作客户端(命令行客户端)
    
  • 退出

    "Ctrl+C"
    

2.2Redis配置

  • 使用redis-server指令启动redis服务的时候,可以在指令后添加redis配置文件的路径,以指定redis以何种配置启动
redis-server redis-6380.conf & ## 以redis-6380.conf文件中的配置参数来启动服务
  • 如果不指定配置文件,则按照redis的默认配置启动(默认配置与是否存在redis.conf无关)

  • 通过在redis根目录下创建多个不同端口的配置文件,启动多个服务

redis-server redis-6380.conf &
redis-server redis-6381.conf &

常用redis配置

## 设置redis实例(服务)为守护模式,默认值为no,可以设置为yes
daemonize no
## 设置当前redis实例启动之后保存进程id的文件路径
pidfile /var/run/redis_6379.pid
## 设置redis实例的启动端口(默认6379)
port 6380
## 设置当前redis实例是否开启保护模式
protected-mode yes
## 设置允许访问当前redis实例的ip地址列表
bind 127.0.0.1
## 设置连接密码
requirepass 123456
## 设置redis实例中的数据库个数(默认16个,编号0-15)
databasses 16
## 设置最大并发数量
maxclients

三、Redis基本使用

redis常用数据结构有五种

3.1Redis存储的数据结构

3.2 String常用指令

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

3.3 hash常⽤指令

## 向key对应的hash中添加键值对
hset key field value
## 从key对应的hash获取field对应的值
hget key field
## 向key对应的hash结构中批量添加键值对
hmset key f1 v1 [f2 v2 ...]
## 从key对应的hash中批量获取值
hmget key f1 [f2 f3 ...]
## 在key对应的hash中的field对应value上加v
hincrby key field v
## 获取key对应的hash中所有的键值对
hgetall key
## 获取key对应的hash中所有的field
hkeys key
## 获取key对应的hash中所有的value
hvals key
## 检查key对应的hash中是否有指定的field
hexists key field
## 获取key对应的hash中键值对的个数
hlen key
## 向key对应的hash结构中添加f-v,如果field在hash中已经存在,则添加失败
hsetnx key field value

3.4 list常⽤指令

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

3.5 set常⽤指令

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

3.6 zset常⽤指令

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

3.7 key相关指令

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

3.8 db常⽤指令

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

四、Redis的持久化

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

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

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

4.1RDB

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

  • 原理:

    RDB是redis默认的持久化策略,当redis中的写操作达到指定的次数、同时距离上⼀次持 久化达到指定的时间就会将redis内存中的数据⽣成数据快照,保存在指定的rdb⽂件 中。

  • 默认触发持久化条件:

    • 900s 1次:当操作次数达到1次,900s就会进⾏持久化
    • 300s 10次:当操作次数达到10次,300s就会进⾏持久化
    • 60s 10000次:当操作次数达到10000次,60s就会就⾏持久化
  • 我们可以通过修改redis.conf⽂件,来设置RDB策略的触发条件:

    ## rdb持久化开关
    rdbcompression yes
    ## 配置redis的持久化策略
    save 900 1
    save 300 10
    save 60 10000
    ## 指定rdb数据存储的⽂件
    dbfilename dump.rdb
    
  • RED持久化细节分析:

    缺点

    • 如果redis出现故障,存在数据丢失的⻛险,丢失上⼀次持久化之后的操作数据;

    • RDB采⽤的是数据快照形式进⾏持久化,不适合实时性持久化;

    • 如果数据量巨⼤,在RDB持久化过程中⽣成数据快照的⼦进程执⾏时间过⻓,会导致 redis卡顿,因此save时间周期设置不宜过短;

    优点

    • 但是在数据量较⼩的情况下,执⾏速度⽐较快;

    • 由于RDB是以数据快照的形式进⾏保存的,我们可以通过拷⻉rdb⽂件轻松实现redis 数据移植

#### 4.2AOF

Apeend Only File,当达到设定触发条件时,将redis执⾏的写操作指令存储在aof⽂件 中,Redis默认未开启aof持久化

  • 原理: Redis将每⼀个成功的写操作写⼊到aof⽂件中,当redis重启的时候就执⾏aof⽂件中的指令以恢复数据
  • 配置:
## 开启AOF
appendonly yes
## 设置触发条件(三选⼀)
appendfsync always ## 只要进⾏成功的写操作,就执⾏aof
appendfsync everysec ## 每秒进⾏⼀次aof
appendfsync no ## 让redis执⾏决定aof
## 设置aof⽂件路径
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
## 关闭保护模式
protected-mode no
## 将bind注释掉(如果不注释,默认为 bind 127.0.0.1 只能本机访问)
# bind 127.0.0.1
## 密码可以设置(也可以不设置)
# requirepass 123456
  • 重启redis
 redis-server redis-6379.conf
  • 阿⾥云安全组设置放⾏6379端⼝

5.2 在普通Maven⼯程连接Redis

使用jedis客户端连接

5.1.1 添加Jedis依赖

<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
 <groupId>redis.clients</groupId>
 <artifactId>jedis</artifactId>
 <version>2.9.0</version>
</dependency>

5.1.2 使⽤案例

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

5.2 在SpringBoot⼯程连接Redis

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

  • RedisTemplate
  • StringRedisTemplate

5.2.1创建SpringBoot应用

5.2.2 配置redis

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

spring:
    redis:
        host: localhost/公网IP
        port: 6379
        database: 0
        password: 

5.2.3 使用Java操作redis

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

StringRedisTemplate继承RedisTemplate,两者的数据是不共通的。

  • StringRedisTemplate采用的是String的序列化策略

当Redis中存储的是字符串数据或者要存取的数据就是字符串类型数据的时候,那么使用StringRedisTemplate。

  • RedisTemplate 采用的是JDK的序列化策略

当数据是复杂的对象类型,而取出的时候又不想做任何的数据转换,直接从Redis里面取出一个对象,那么使用RedisTemplate是更好的选择。

RedisTemplate使用的序列类在在操作数据的时候,存入数据会将数据先序列化成字节数组然后在存入Redis数据库,数据不是以可读的形式展现的,而是以字节数组显示。
当然从Redis获取数据的时候也会默认将数据当做字节数组转化,这样就会导致一个问题,当需要获取的数据不是以字节数组存在redis当中而是正常的可读的字符串的时候,RedisTemplate就无法获取导数据,这个时候获取到的值就是NULL。那么此时便需要使用StringRedisTemplate来取得数据。

在使用StringRedisTemplate存储数据时,需要搭配一些序列化和反序列化json的框架如Jackson。
在存入数据时将对象、集合转成String类型;在取出数据时将String类型数据转换成需要的数据类型,如下代码

//将List集合转换成String类型对象
ArrayList<categoryVO> categoryVOS;
String categoriesRedisTemplate = objectMapper.writeValueAsString(categoryVOS);
//存入Redis
stringRedisTemplate.boundValueOps("categories").set(categoriesRedisTemplate);

//从Redis中取出String类型数据
String categoryRedisTemplate = stringRedisTemplate.boundValueOps("categories").get();
//将Redis中String类型数据转换成List集合
JavaType javaType = 
    objectMapper.getTypeFactory().constructParametricType(ArrayList.class, CategoryVO.class);
List<CategoryVO> categoryVOS = objectMapper.readValue(categoryRedisTemplate, javaType);

5.3 Spring Data Redis

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

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

5.3.2 string类型的操作⽅法

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

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

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

六、什么数据适合用redis做缓存

因为缓存中的数据需要进行数据一致性的维护(当数据库数据发生变化,要同步更新缓存中数据)故

  • 对于数据的写操作较少,但会频繁查询的数据适合使用缓存。
  • 对于可能发生修改,但对数据一致性要求不高的数据也适合使用缓存。

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

7.1 redis作为缓存的使⽤流程

7.2 在使用redis缓存商品详情

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

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

7.2.2 在application.yml配置redis数据源

spring:
  redis:
    port: 6379
    host: 101.42.168.179
    database: 0
    password: *******

7.2.3 修改productServiceImpl业务代码

@Transactional(propagation = Propagation.SUPPORTS)
public ResultVO getProductBasicInfo(String productId) {
    try {
        //封装查询结果的Map
        HashMap<String,Object> basicInfo = new HashMap<>();
        //------------------------使用Redis作为缓存---------------------------------
        //一.根据商品id查询Redis
        String productTemplateInfo = (String) stringRedisTemplate.boundHashOps("products").get(productId);

        if(productTemplateInfo != null){
            //redis中有商品信息,那么证明就会有sku和img,直接查询
            String productImgsRedisTemplate = (String) stringRedisTemplate.boundHashOps("productImgs").get(productId);
            String productSkusRedisTemplate = (String) stringRedisTemplate.boundHashOps("productSkus").get(productId);

            //将查询到的结果从String转换得到List集合
            JavaType javaType = objectMapper.getTypeFactory().constructParametricType(ArrayList.class, ProductImg.class);
            List<ProductImg> productImgList = objectMapper.readValue(productImgsRedisTemplate, javaType);
            javaType = objectMapper.getTypeFactory().constructParametricType(ArrayList.class,ProductSku.class);
            List<ProductSku> productSkuList = objectMapper.readValue(productSkusRedisTemplate, javaType);

            //将查询到的结果加入到返回结果的Map
            basicInfo.put("products",objectMapper.readValue(productTemplateInfo, Product.class));
            basicInfo.put("productImgs",productImgList);
            basicInfo.put("productSkus",productSkuList);
            return new ResultVO(ResStatus.OK.getCode(), "success", basicInfo);

        }else {
            //二、redis中没有商品信息,在mysql中查
            //(*).商品基本信息
            Example example = new Example(Product.class);
            Example.Criteria criteria = example.createCriteria();
            criteria.andEqualTo("productId", productId);
            criteria.andEqualTo("productStatus", 1);//状态为1表示上架商品

            //mysql中查询到商品
            List<Product> products = productMapper.selectByExample(example);
            if(products.size()>0){
                // 三、将mysql中查询到的存入redis
                String productAsString = objectMapper.writeValueAsString(products.get(0));
                stringRedisTemplate.boundHashOps("products").put("productId", productAsString);

                //(*).商品图片
                Example example1 = new Example(ProductImg.class);
                Example.Criteria criteria1 = example1.createCriteria();
                criteria1.andEqualTo("itemId", productId);
                List<ProductImg> productImgs = productImgMapper.selectByExample(example1);
                //添加到redis中
                stringRedisTemplate.boundHashOps("productImgs").put(productId,objectMapper.writeValueAsString(productImgs));

                //(*).商品套餐
                Example example2 = new Example(ProductSku.class);
                Example.Criteria criteria2 = example2.createCriteria();
                criteria2.andEqualTo("productId", productId);
                criteria2.andEqualTo("status",1);
                List<ProductSku> productSkus = productSkuMapper.selectByExample(example2);
                //添加到redis中
                stringRedisTemplate.boundHashOps("productSkus").put(productId, objectMapper.writeValueAsString(productSkus));

                //将查询到的结果加入到返回结果的Map
                basicInfo.put("products",products);
                basicInfo.put("productImgs", productImgs);
                basicInfo.put("productSkus", productSkus);

                return new ResultVO(ResStatus.OK.getCode(), "success", basicInfo);

            }else {
                //商品不存在
                return new ResultVO(ResStatus.NO.getCode(), "查询的商品不存在", null);

            }
        }
    } catch (JsonProcessingException e) {
        e.printStackTrace();
    }
    return null;
}

说明:除了使用redis来缓存商品详情以外,我们还可以使用页面静态化技术达到此目的。

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

八、使用Redis做缓存使用存在的问题

当用户并发请求Tomcat时,Tomcat为每个请求都单独创建一个线程。每个线程都要与数据库建立连接,以访问数据库。当用户发送高并发请求时,服务器就会创建更多的线程,同时需要更多的数据库连接,数据库连接资源是有限的,我们要尽量减少不必要的连接创建,因此使用redis作为缓存在高并发场景下有可能出现缓存击穿缓存穿透缓存雪崩等问题

8.1 缓存击穿

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

使用双重检测锁解决缓存击穿问题

public ResultVO listCategories() {
    try {
        List<CategoryVO> categoryVOS;
        //首先查询Redis
        String categoryRedisTemplate = stringRedisTemplate.boundValueOps("categories").get();
        if(categoryRedisTemplate != null){
            //redis中有数据
            JavaType javaType = objectMapper.getTypeFactory().constructParametricType(ArrayList.class, CategoryVO.class);
            categoryVOS = objectMapper.readValue(categoryRedisTemplate, javaType);
        }else {
            //-----------------------双重检测锁,将请求串行---------------------------------------
            synchronized (this){
                String categoryRedisTemplate2 = stringRedisTemplate.boundValueOps("categories").get();
                if(categoryRedisTemplate2 == null){
                    //只有第一个请求会再次为null
                    //redis中没数据,向mysql中查询
                    categoryVOS = categoryMapper.selectAllCategories();
                    //-------------------------解决缓存穿透---------------------------------
                    if(categoryVOS == null){
                        //数据库中没有要查询的数据
                        //向Redis中存入一个临时数据,并设置过期时间
                        List<CategoryVO> list = new ArrayList<>();
                        stringRedisTemplate.boundValueOps("categories").set(objectMapper.writeValueAsString(list),100,TimeUnit.SECONDS);
                    }else {
                        //数据库中查到数据
                        //存入redis
                        String categoriesRedisTemplate = objectMapper.writeValueAsString(categoryVOS);
                        stringRedisTemplate.boundValueOps("categories").set(categoriesRedisTemplate,1, TimeUnit.DAYS);
                    }

                }else {
                    //redis中有数据
                    JavaType javaType = objectMapper.getTypeFactory().constructParametricType(ArrayList.class, CategoryVO.class);
                    categoryVOS = objectMapper.readValue(categoryRedisTemplate2, javaType);
                }
            }
        }
        ResultVO resultVO = new ResultVO(ResStatus.OK.getCode(), "success", categoryVOS);
        return resultVO;
    } catch (JsonProcessingException e) {
        e.printStackTrace();
    }
    return null;
}

8.2缓存穿透

大量的请求访问一个数据库(MySQL)中不存在的数据,首先在Redis中无法命中,最终所有的请求都会请求数据库,同样导致数据库承受大量访问压力

解决方式:当数据库查询为null时,向Redis中写入一个非空的数据,并设置过期时间。

 //-------------------------解决缓存穿透---------------------------------
if(categoryVOS == null){
    //数据库中没有要查询的数据
    //向Redis中存入一个临时数据,并设置过期时间
    List<CategoryVO> list = new ArrayList<>();
    stringRedisTemplate.boundValueOps("categories").set
        (objectMapper.writeValueAsString(list),100,TimeUnit.SECONDS);
}else {
    //数据库中查到数据
    //存入redis
    String categoriesRedisTemplate = objectMapper.writeValueAsString(categoryVOS);
    stringRedisTemplate.boundValueOps("categories").set
        (categoriesRedisTemplate,1, TimeUnit.DAYS);
}

8.3缓存雪崩

缓存中大量的数据集中过期,导致请求这些数据的大量并发请求同时访问数据库,使数据库承受大量访问压力

解决方案:

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

8.4 Jmeter测试

TODO

九、Redis高级应用

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

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

9.1 主从配置(高可用/高并发)

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

  • 通过主从配置可以实现Redis数据的备份(主备配置,slave就是master的备份),保证数据的安全性。——高可用
  • 通过主从配置可以实现Redis的读写分离(向master中写入,从slave中读取)。——高并发

主从配置示例:

  • 启动三个redis实例

9.1.1 新建文件夹存放主从节点的配置文件

在redis-5.0.5目录下创建 msconf 文件夹

[root@VM-8-17-centos redis-5.0.5]# mkdir msconf

9.1.2 新建主从节点的配置文件

  • 向msconf目录拷贝redis.conf 分别作为master的配置文件
[root@VM-8-17-centos redis-5.0.5]# cat redis.conf |grep -v "#" | grep -v "^$" > msconf/redis-master.conf
  • 修改配置文件
[root@VM-8-17-centos redis-5.0.5]# vim redis-master.conf
  • 拷贝redis-master.conf作为slave1和slave2的配置文件
[root@VM-8-17-centos redis-5.0.5]# sed 's/6380/6381/g' redis-master.conf > redis-slave1.conf
[root@VM-8-17-centos redis-5.0.5]# sed 's/6380/6382/g' redis-master.conf > redis-slave2.conf

9.1.3 设置slave节点跟从

修改redis-slave1.conf、redis-slave2.conf 设置跟从

[root@VM-8-17-centos redis-5.0.5]# vim redis-slave1.conf
[root@VM-8-17-centos redis-5.0.5]# vim redis-slave2.conf
#### 在配置文件中加入(如果不在同一服务器上则要添加主结点服务器的IP地址)
slaveof 127.0.0.1 6380
---------------此时三个节点配置文件都已经配置完成-------------------

9.1.4 启动Redis服务

----先主后从

[root@VM-8-17-centos redis-5.0.5]redis-server redis-master.conf &
[root@VM-8-17-centos redis-5.0.5]redis-server redis-slave1.conf &
[root@VM-8-17-centos redis-5.0.5]redis-server redis-slave2.conf &

9.2 哨兵模式高可用

哨兵模式:用于监听主库,当确认主库宕机之后,从备库中选举一个转为主库,更改主从配置

(哨兵的数量要为奇数个,最少为三个)

哨兵模式配置

  • 首先实现三个redis实例之间的主从配置

  • 创建并启动三个哨兵

sentinel.conf为哨兵的模板配置文件,默认端口号26379。

9.2.1 新建哨兵配置文件

##1.在redis-5.0.5目录下创建sentinelconf文件夹存放哨兵配置文件
[root@VM-8-17-centos redis-5.0.5]# mkdir sentinelconf
##2.拷贝sentinel.conf文件到sentinelconf文件夹:sentinel-26380.conf、sentinel-26381.conf、sentinel-26382.conf
[root@VM-8-17-centos redis-5.0.5]# cat sentinel.conf | grep -v "#" | grep -v "^$" > sentinelconf/sentinel-26380.conf

修改sentinel-26380.conf内容

##拷贝其他两个哨兵配置文件
[root@VM-8-17-centos sentinelconf]# sed 's/26380/26381/g' sentinel-26380.conf > sentinel-26381.conf
[root@VM-8-17-centos sentinelconf]# sed 's/26380/26382/g' sentinel-26380.conf > sentinel-26382.conf

9.2.2 启动服务

  • xshell上新建三个连接分别启动主从节点服务
[root@VM-8-17-centos ~]# redis-server /usr/local/redis-5.0.5/msconf/redis-master.conf

[root@VM-8-17-centos ~]# redis-server /usr/local/redis-5.0.5/msconf/redis-slave1.conf

[root@VM-8-17-centos ~]# redis-server /usr/local/redis-5.0.5/msconf/redis-slave2.conf
  • xshell上新建三个连接分别启动三个哨兵
[root@VM-8-17-centos ~]# redis-sentinel /usr/local/redis-5.0.5/sentinelconf/sentinel-26380.conf 
[root@VM-8-17-centos ~]# redis-sentinel /usr/local/redis-5.0.5/sentinelconf/sentinel-26381.conf 
[root@VM-8-17-centos ~]# redis-sentinel /usr/local/redis-5.0.5/sentinelconf/sentinel-26382.conf 

9.3 集群配置高并发

Redis要求配置集群时,每个节点都要有最少一个备用节点(哨兵模式)

  • 每个节点都是对等的,没有主从之分。
  • 数据按照slots分布式存储在不同的redis节点上,节点中的数据可共享,可以动态调整数据分布
  • 可扩展性强,可动态增删节点,最多可扩展至1000+节点。

集群模式配置

9.3.1 开放端口

在云服务器防火墙/安全组中开放对应端口

  • 7001-7006 Redis集群节点端口
  • 17001-17003 Redis集群总线端口(为集群master节点端口号+10000)

9.3.2 新建配置文件

##在redis文件夹中新建cluster-conf文件夹存放集群配置文件
[root@VM-8-17-centos redis-5.0.5]# mkdir cluster-conf
##拷贝模板配置文件
[root@VM-8-17-centos redis-5.0.5]# cat redis.conf | grep -v "#"|grep -v "^$" > cluster-conf/redis-7001.conf
  • 编辑配置文件
[root@VM-8-17-centos redis-5.0.5]# cd cluster-conf/
[root@VM-8-17-centos cluster-conf]# vim redis-7001.conf 
  • 拷贝其他五个配置文件
[root@VM-8-17-centos cluster-conf]# sed 's/7001/7002/g' redis-7001.conf > redis-7002.conf
[root@VM-8-17-centos cluster-conf]# sed 's/7001/7003/g' redis-7001.conf > redis-7003.conf
[root@VM-8-17-centos cluster-conf]# sed 's/7001/7004/g' redis-7001.conf > redis-7004.conf
[root@VM-8-17-centos cluster-conf]# sed 's/7001/7005/g' redis-7001.conf > redis-7005.conf
[root@VM-8-17-centos cluster-conf]# sed 's/7001/7006/g' redis-7001.conf > redis-7006.conf

9.3.3 启动服务

分别后台启动六个redis服务

[root@VM-8-17-centos cluster-conf]# redis-server redis-7001.conf &
[root@VM-8-17-centos cluster-conf]# redis-server redis-7002.conf &
[root@VM-8-17-centos cluster-conf]# redis-server redis-7003.conf &
[root@VM-8-17-centos cluster-conf]# redis-server redis-7004.conf &
[root@VM-8-17-centos cluster-conf]# redis-server redis-7005.conf &
[root@VM-8-17-centos cluster-conf]# redis-server redis-7006.conf &

##查看服务是否启动成功
[root@VM-8-17-centos cluster-conf]# ps -ef | grep redis
-----------------------------------------------------------------------------
root      2169     1  0 19:43 ?        00:00:00 redis-server *:7001 [cluster]
root      2351     1  0 19:44 ?        00:00:00 redis-server *:7002 [cluster]
root      2371     1  0 19:44 ?        00:00:00 redis-server *:7003 [cluster]
root      2410     1  0 19:44 ?        00:00:00 redis-server *:7004 [cluster]
root      2443     1  0 19:44 ?        00:00:00 redis-server *:7005 [cluster]
root      2451     1  0 19:44 ?        00:00:00 redis-server *:7006 [cluster]

9.3.4 创建集群

Redis会自动设置主从关系

[root@VM-8-17-centos cluster-conf]# redis-cli --cluster create 101.42.168.179:7001 101.42.168.179:7002 101.42.168.179:7003 101.42.168.179:7004 101.42.168.179:7005 101.42.168.179:7006 --cluster-replicas 1
------------------------------------------------------------------
>>> Performing hash slots allocation on 6 nodes...
Master[0] -> Slots 0 - 5460
Master[1] -> Slots 5461 - 10922
Master[2] -> Slots 10923 - 16383
Adding replica 101.42.168.179:7005 to 101.42.168.179:7001
Adding replica 101.42.168.179:7006 to 101.42.168.179:7002
Adding replica 101.42.168.179:7004 to 101.42.168.179:7003
>>> Trying to optimize slaves allocation for anti-affinity
[WARNING] Some slaves are in the same host as their master
M: 60d9fd2c1abcb7d26827c2f0cd840466a9d7ff41 101.42.168.179:7001
   slots:[0-5460] (5461 slots) master
M: 20200df89b1955b9bcbc03ea2287bbb6af0272f1 101.42.168.179:7002
   slots:[5461-10922] (5462 slots) master
M: cf222dcb07ba38f471213727b96115a7d7c2647e 101.42.168.179:7003
   slots:[10923-16383] (5461 slots) master
S: 879f82aebc8bb8f13ddd23802d97fb567bcbeae8 101.42.168.179:7004
   replicates 20200df89b1955b9bcbc03ea2287bbb6af0272f1
S: 8231c93242bcb1e5cf777b72e12c9e0018b61562 101.42.168.179:7005
   replicates cf222dcb07ba38f471213727b96115a7d7c2647e
S: bc880f6f0b3da2719fd2491cd2c0874ee8cf03c2 101.42.168.179:7006
   replicates 60d9fd2c1abcb7d26827c2f0cd840466a9d7ff41
Can I set the above configuration? (type 'yes' to accept): 
##输入yes,即可启动节点
-----------------------------------------------------------------------
>>> Nodes configuration updated
>>> Assign a different config epoch to each node
>>> Sending CLUSTER MEET messages to join the cluster
Waiting for the cluster to join
.......
>>> Performing Cluster Check (using node 101.42.168.179:7001)
M: 60d9fd2c1abcb7d26827c2f0cd840466a9d7ff41 101.42.168.179:7001
   slots:[0-5460] (5461 slots) master
   1 additional replica(s)
S: 8231c93242bcb1e5cf777b72e12c9e0018b61562 101.42.168.179:7005
   slots: (0 slots) slave
   replicates 60d9fd2c1abcb7d26827c2f0cd840466a9d7ff41
S: 879f82aebc8bb8f13ddd23802d97fb567bcbeae8 101.42.168.179:7004
   slots: (0 slots) slave
   replicates cf222dcb07ba38f471213727b96115a7d7c2647e
M: 20200df89b1955b9bcbc03ea2287bbb6af0272f1 101.42.168.179:7002
   slots:[5461-10922] (5462 slots) master
   1 additional replica(s)
S: bc880f6f0b3da2719fd2491cd2c0874ee8cf03c2 101.42.168.179:7006
   slots: (0 slots) slave
   replicates 20200df89b1955b9bcbc03ea2287bbb6af0272f1
M: cf222dcb07ba38f471213727b96115a7d7c2647e 101.42.168.179:7003
   slots:[10923-16383] (5461 slots) master
   1 additional replica(s)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.

9.3.5 进入集群中的redis客户端

需要在命令后加-p 端口号 -c

[root@VM-8-17-centos cluster-conf]# redis-cli -p 7001 -c
127.0.0.1:7001> set k1 v1
-> Redirected to slot [12706] located at 101.42.168.179:7003
OK
##可见在向7001端口节点储存数据,保存到了7003节点。

/集群管理/

  • 创建集群:

    [root@VM-8-17-centos cluster-conf]# redis-cli --cluster create 101.42.168.179:7001 101.42.168.179:7002 101.42.168.179:7003 101.42.168.179:7004 101.42.168.179:7005 101.42.168.179:7006 --cluster-replicas 1
    
  • 查看集群状态:

    [root@VM-8-17-centos /]# redis-cli --cluster info 101.42.168.179:7001
    101.42.168.179:7001 (60d9fd2c...) -> 0 keys | 5461 slots | 0 slaves.
    101.42.168.179:7002 (20200df8...) -> 1 keys | 5462 slots | 0 slaves.
    101.42.168.179:7003 (cf222dcb...) -> 1 keys | 5461 slots | 0 slaves.
    [OK] 2 keys in 3 masters.
    0.00 keys per slot on average.
    
  • 平衡节点数据槽数量

    [root@VM-8-17-centos /]# redis-cli --cluster rebalance 101.42.168.179:7001
    >>> Performing Cluster Check (using node 101.42.168.179:7001)
    [OK] All nodes agree about slots configuration.
    >>> Check for open slots...
    >>> Check slots coverage...
    [OK] All 16384 slots covered.
    *** No rebalancing needed! All nodes are within the 2.00% threshold.
    
  • 关闭集群

    逐个关闭节点进程即可

    [root@VM-8-17-centos /]# ps -ef | grep redis
    [root@VM-8-17-centos /]# kill -9 pid
    ##也可执行以下命令来关闭redis进程
    [root@VM-8-17-centos /]# pkill -9 redis
    

9.3.6 SpringBoot应用连接集群

  • 添加依赖

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

    spring:
      redis:
          cluster:
              nodes: IP:端口号1,IP端口号2....(只添加master节点即可)
              max-redirects: 3 (重新连接数)
    
  • 操作集群

    使用stringRedisTemplate、redisTemplate操作集群即可。

十、Redis淘汰策略

Redis是基于内存结构进行数据缓存的,当内存资源消耗完毕,想要有新的数据缓存,必须将之前的一些数据释放。

  • volatile-lru,针对设置了过期时间的key,使用lru算法进行淘汰。
  • allkeys-lru,针对所有key使用lru算法进行淘汰。
  • volatile-lfu,针对设置了过期时间的key,使用lfu算法进行淘汰。
  • allkeys-lfu,针对所有key使用lfu算法进行淘汰。
  • volatile-random,从所有设置了过期时间的key中使用随机淘汰的方式进行淘汰。
  • allkeys-random,针对所有的key使用随机淘汰机制进行淘汰。
  • volatile-ttl,淘汰过期时间最短的一个键。
  • noeviction(默认),不删除键,内存不够时抛出异常。

LRU:最久最近未使用(时间)
LFU:最近最少使用(频率)

十一、Redis高频面试题

在项目中Redis的使用场景?

  • 用于缓存首页数据
  • ...

Redis的持久化策略?

Redis支持的数据类型?

如何保证Redis的高可用?

  • 同时开启rdb和aof
  • 主从配置,实现主备配置
  • 集群配置,默认要求每个节点都要有从节点

脑裂问题?

脑裂的主要原因其实就是哨兵集群认为主节点已经出现故障了,重新选举其它从节点作为主节点,而原主节点其实是假故障,从而导致短暂的出现两个主节点,那么在主从切换期间客户端一旦给原主节点发送命令,就会造成数据丢失。

所以应对脑裂的解决办法应该是去限制原主库接收请求,Redis提供了两个配置项。

  • min-slaves-to-write:最小从节点数,与主节点通信的从节点数量必须大于等于该值
  • min-slaves-max-lag:最大数据同步延迟,主节点与从节点通信的ACK消息延迟必须小于该值

这两个配置项必须同时满足,不然主节点拒绝写入。

在假故障期间满足min-slaves-to-write和min-slaves-max-lag的要求,那么主节点就会被禁止写入,脑裂造成的数据丢失情况自然也就解决了。

十二、Redis实现分布式会话

将用户信息以token为key存放到Redis中

12.1流程分析

12.2具体实现

12.2.1 动静分离

public ResultVO checkLogin(String name, String pwd) {
    //根据用户名进行查询
    Example example = new Example(Users.class);
    Example.Criteria criteria = example.createCriteria();
    criteria.andEqualTo("username", name);
    Users user = usersMapper.selectOneByExample(example);

    //比对用户密码是否一致
    if(user == null) {
        //用户不存在
        return new ResultVO(ResStatus.NO.getCode(), "用户不存在", null);
    }else if (!Objects.equals(MD5Utils.md5(pwd), user.getPassword())) {
        //密码不一致
        return new ResultVO(ResStatus.NO.getCode(), "密码错误", null);
    } else {
        //密码一致
        //使用jwt规则生成token字符串
        JwtBuilder builder = Jwts.builder();

        HashMap<String, Object> hashMap = new HashMap<>();
        String token = builder.setSubject(name)     //主题,就是token中携带的数据
                .setIssuedAt(new Date())            //设置token的生成时间
                .setId(user.getUserId() + "")       //设置用户id为token id
                .setClaims(hashMap)                 //map中可以存放用户的角色权限信息
                .setExpiration(new Date(System.currentTimeMillis() + 24 * 60 * 60 * 1000))
                                                    //设置token过期时间
                .signWith(SignatureAlgorithm.HS256, "jyh9961")
                .compact();                         //设置加密方式和加密密码

        
        try {
    //------实现分布式会话:1.用户登录成功,将用户信息以token为key储存进Redis------
    stringRedisTemplate.boundValueOps(token).set(objectMapper.writeValueAsString(user),30,TimeUnit.MINUTES);//设置过期时间30分钟
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }

        return new ResultVO(ResStatus.OK.getCode(), token, user);
    }

}

12.2.2 拦截器重置session过期时间

@Component
public class ResetTimeInterceptor implements HandlerInterceptor {

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        String token = request.getHeader("token");
        if(token!=null){
            stringRedisTemplate.boundValueOps(token).expire(30, TimeUnit.MINUTES);
        }
        return true;
    }
}
@Component
public class CheckTokenInterceptor implements HandlerInterceptor {

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        //如果是预检请求,则直接放行
        String method = request.getMethod();
        if("OPTIONS".equalsIgnoreCase(method)){
            return true;
        }

        String token = request.getHeader("token");

        if(token == null){
            ResultVO resultVO = new ResultVO(ResStatus.LOGIN_FAIL_NOT.getCode(), "请先登录", null);
            //提示先登录
            doResponse(response, resultVO);
        }else {
           
            String userRedisTemplate = stringRedisTemplate.boundValueOps(token).get();
            if(userRedisTemplate==null){
                ResultVO resultVO = new ResultVO(ResStatus.NO.getCode(), "请登录", null);
                doResponse(response, resultVO);
            }else {
                stringRedisTemplate.boundValueOps(token).expire(30, TimeUnit.MINUTES);
                return true;
            }

        }
        return false;
    }

    private void doResponse(HttpServletResponse response,ResultVO resultVO) throws IOException {
        response.setContentType("application/json");
        response.setCharacterEncoding("utf-8");
        PrintWriter out = response.getWriter();
        String s = new ObjectMapper().writeValueAsString(resultVO);
        out.print(s);
        out.flush();
        out.close();
    }
}
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

    @Autowired
    private CheckTokenInterceptor checkTokenInterceptor;
    @Autowired
    private ResetTimeInterceptor resetTimeInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //创建拦截url集合
        ArrayList<String> PathPatternsList = new ArrayList<String>(){{
            add("/shopcart/**");
            add("/orders/**");
            add("/useraddr/**");
        }};


        registry.addInterceptor(checkTokenInterceptor)
                //拦截的url
                .addPathPatterns(PathPatternsList)
                //放行的url
                .excludePathPatterns("/user/**");

        registry.addInterceptor(resetTimeInterceptor).addPathPatterns("/**").
                excludePathPatterns("/user/**");
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容