Redis(缓存)

缓存机制说明

缓存机制原理说明
缓存机制:
缓存中的数据,一定是数据库中的数据,使用缓存主要的目的就是降低用户访问物理设备的频次.
如果数据库记录更新,则缓存应该同步更新.

1.缓存的数据结构 K-V结构进行数据保存
2. 开发语言选择 C语言程序
3. 缓存数据的运行环境在内存中, 断电即擦除. 将内存数据进行持久化操作.
4. 如果不停的添加缓存的记录肯定会造成内存溢出. 定期优化缓存机制.
1.LRU算法 2.LFU算法 3.Random 4.TTL
5. 缓存也需要实现高可用机制.搭建缓存集群.

1 Redis介绍
1.1 官网介绍
URL地址: http://www.redis.cn
Redis 是一个开源(BSD许可)的 内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。

效率: 读: 11.2万次/秒 写: 8.6万次/秒 平均:10万次/秒
口诀: 1-5-8

1.2 知识扩展
1.数据库:
1.关系型数据库 mysql数据库/oracle数据 2维表 行/列
2.非关系型数据库 mongodb/hbase/Redis 1维表
2. 中间件:
50万次/秒 吞吐量


2 Redis命令
2.1服务器命令
启动redis redis-server redis.conf
进入客户端 redis-cli -p 6379 exit 退出|quit|ctrl+c
关闭redis redis-cli -p 6379 shutdown
2.2客户端命令
1String类型


2Hash类型
说明:可以用散列类型保存对象和属性值
例子:User对象{id:2,name:小明,age:19}

3List类型
说明:Redis中的List集合是双端循环列表,分别可以从左右两个方向插入数据.
List集合可以当做队列使用,也可以当做栈使用
队列:存入数据的方向和获取数据的方向相反
栈:存入数据的方向和获取数据的方向相同

4Redis事务命令
说明:redis中操作可以添加事务的支持.一项任务可以由多个redis命令完成,如果有一个命令失败导致入库失败时.需要实现事务回滚


3.Redis入门案例
3.1 导入jar包

         <!--spring整合redis -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-redis</artifactId>
        </dependency>

1
2
3
4
5
6
7
8
9
10
3.2 客户端操作String类型

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.SetParams;

//@SpringBootTest //目的:动态获取spring容器中的数据
public class TestRedis {

    /**
     * 主要目的测试程序远程操作Redis是否有效
     * 配置redis服务:
     *      1.redis需要关闭IP绑定模式
     *      2.redis关闭保护模式
     *      3.redis最好开启后端运行
     *
     * 完成redis客户端操作
     */
    @Test
    public void test01() throws InterruptedException {
        //1.测试链接
        Jedis jedis = new Jedis("192.168.126.129",6379);
        jedis.set("a", "动态获取redis中的数据");
        System.out.println(jedis.get("a"));

        //2.测试数据是否存在
        if(jedis.exists("a")){
            jedis.set("a", "修改数据");
        }else{
            jedis.set("a", "新增数据");
        }

        //3.删除redis
        jedis.del("a");

        //4.清空所有的数据
        jedis.flushDB();
        jedis.flushAll();

        //5.为数据添加超时时间
        jedis.set("b", "设定超时时间");
        jedis.expire("b", 10);
        Thread.sleep(2000);
        System.out.println(jedis.ttl("b"));
    }

    //原子性
    @Test
    public void test02(){
        Jedis jedis = new Jedis("192.168.126.129", 6379);
        jedis.set("c", "测试redis");
        //需求1: 如果数据不存在时,才会为数据赋值.
        jedis.setnx("d","测试setnx方法");
        System.out.println(jedis.get("d"));

        //需求2: 需要为数据添加超时时间,同时满足原子性的要求
                //jedis.set("s", "为数据添加超时时间");
                //有时程序中断了,下列的方法将不会执行.
                //jedis.expire("s", 20);
                //System.out.println(jedis.ttl("s"));
        //为数据添加超时时间
        jedis.setex("s", 20, "为数据添加超时111");
        System.out.println("获取超时时间:"+jedis.ttl("s"));
    }

    /**
     *  需求: 如果数据存在才修改,并且为数据添加超时时间,满足原子性要求
     *  SetParams:
     *          XX: 数据存在时赋值.
     *          NX: 数据不存在时赋值
     *          EX: 添加超时时间单位秒
     *          PX: 添加超时时间单位毫秒
     */
    @Test
    public void test03(){
        Jedis jedis = new Jedis("192.168.126.129", 6379);
        jedis.flushAll();
        SetParams setParams = new SetParams();
        setParams.xx().ex(20);
        jedis.set("a", "测试方法",setParams);
        System.out.println(jedis.get("a"));
    }
}


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
4 关于List集合说明
4.1 关于队列应用场景
秒杀场景: 马上过年了, 店铺周年店庆 1部苹果12proMax 12000 1元秒杀? 提前预付活动费 10块… 如果秒杀不成功 则7日内退还?

4.2 入门案例测试

@Test
    public void testList(){
        Jedis jedis = new Jedis("192.168.126.129",6379);
        jedis.lpush("list", "1","2","3");
        System.out.println(jedis.rpop("list")); //队列
    }

1
2
3
4
5
6
7
5关于事务控制
 //弱事务控制
    @Test
    public void testTx(){
        Jedis jedis = new Jedis("192.168.126.129",6379);
        Transaction transaction = jedis.multi();  //开启事务
        try {
            transaction.set("k", "k");
            transaction.set("c", "c");
            transaction.exec();
        }catch (Exception e){
            e.printStackTrace();
            transaction.discard();
        }
    }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
6 SpringBoot整合Redis
6.1编辑pro配置文件
说明:由于redis是公共的第三方,所以将配置放到common中即可


6.2 编辑配置类
说明: 需要在common中添加redis的配置类

package com.jt.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import redis.clients.jedis.Jedis;

@Configuration  //表示一个配置类  一般会与@Bean的注解联用
@PropertySource("classpath:/redis.properties") //导入配置文件
public class RedisConfig {

    @Value("${redis.host}")
    private String host;
    @Value("${redis.port}")
    private Integer port;

    @Bean   //将方法的返回值结果,交给spring容器进行管理.
    public Jedis jedis(){

        return new Jedis(host, port);
    }

}


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
6.3 测试redis案例
测试类的包路径:
![在这里插入图片描述](https://img-blog.csdnimg.cn/80f6c8eb6bbb4fd78a06c87e7b10

7 JSON转化工具API
7.1 入门案例测试

package com.jt.test;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jt.pojo.ItemDesc;
import org.junit.jupiter.api.Test;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

public class TestObjectMapper {

    @Test
    public void test01() throws JsonProcessingException {
        ObjectMapper objectMapper = new ObjectMapper();
        //将对象转化为JSON  调用的是对象的get方法获取属性/属性的值
        ItemDesc itemDesc = new ItemDesc();
        itemDesc.setItemId(1000L).setItemDesc("对象与json转化")
                .setCreated(new Date()).setUpdated(new Date());
        String json = objectMapper.writeValueAsString(itemDesc);
        System.out.println(json);

        //将JSON串转化为对象 调用的是对象的set方法为对象属性赋值
        ItemDesc itemDesc2 = objectMapper.readValue(json, ItemDesc.class);
        System.out.println(itemDesc2.getItemDesc());
    }

    @Test
    public void test02() throws JsonProcessingException {
        ObjectMapper objectMapper = new ObjectMapper();
        //将对象转化为JSON  调用的是对象的get方法获取属性/属性的值
        ItemDesc itemDesc = new ItemDesc();
        itemDesc.setItemId(1000L).setItemDesc("对象与json转化").setCreated(new Date()).setUpdated(new Date());
        ItemDesc itemDesc2 = new ItemDesc();
        itemDesc2.setItemId(2000L).setItemDesc("对象与json转化2").setCreated(new Date()).setUpdated(new Date());

        List<ItemDesc> list2 = new ArrayList<>();
        list2.add(itemDesc);
        list2.add(itemDesc2);

        String json = objectMapper.writeValueAsString(list2);
        System.out.println(json);

        //将JSON串转化为对象 调用的是对象的set方法为对象属性赋值
        List list3 = objectMapper.readValue(json,list2.getClass());
        System.out.println(list3);
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
7.2 封装工具API

package com.jt.util;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jt.pojo.Item;
import com.jt.pojo.ItemDesc;
import com.sun.corba.se.spi.ior.IORTemplate;

/**
 * 该工具类,主要的功能实现对象与JSON串的互相转化.
 * 1.对象转化为JSON
 * 2.JSON转化为对象
 */
public class ObjectMapperUtil {

    private static final ObjectMapper MAPPER = new ObjectMapper();

    //1.对象转化为JSON
    public static String toJSON(Object object){
        try {
            return MAPPER.writeValueAsString(object);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

    //2.JSON转化为对象 要求用户传递什么类型就返回什么对象??
    public static <T> T toObj(String json,Class<T> target){

        try {
            return MAPPER.readValue(json, target);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

}


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
8 利用缓存实现商品分类查询
8.1 业务说明
说明: 商品分类信息每次展开封闭的节点,都需要查询数据库.这样的效率并不高. 可以使用redis缓存来提升效率.
流程:
1.用户第一次查询先查询缓存
2.缓存中没有数据(这就是第一次查询),查询数据库. 将数据库记录保存到缓存中即可.
3.缓存中有记录. 直接通过缓存获取数据之后返回即可.

8.2 编辑ItemCatController

 /**
     * 业务: 实现商品分类的查询
     * URL地址: http://localhost:8091/itemCat/list?id=xxx
     * 请求参数: 传递节点的ID
     * 返回值:  List<EasyUITree>对象   页面JS-VO~~~~POJO--DB
     */
    @RequestMapping("/list")
    public List<EasyUITree> findItemCatList(Long id){
        //1.查询一级商品分类信息
        Long parentId = (id==null?0L:id);
        //return itemCatService.findItemCatList(parentId);
        //利用redis缓存查询数据
        return itemCatService.findItemCatCache(parentId);
    }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
8.3 编辑ItemCatService

/**
     * 原理说明:
     *      1.定义存取redis中的key  业务名称+标识符  ITEMCAT_PARENTID::0
     *      2.通过key获取redis中的记录
     *      3.空:    查询数据库 将返回值结果保存到缓存中即可
     *      4.非空    直接将缓存数据获取之后,返回给用户即可.
     * @param parentId
     * @return
     */
    @Override
    public List<EasyUITree> findItemCatCache(Long parentId) {
        long startTime = System.currentTimeMillis();
        String key = "ITEMCAT_PARENTID::" + parentId;
        List treeList = new ArrayList();
        if(jedis.exists(key)){
            //如果存在则直接返回
            String json = jedis.get(key);
            treeList = ObjectMapperUtil.toObj(json, treeList.getClass());
            System.out.println("查询Redis缓存!!!");
            long endTime = System.currentTimeMillis();
            System.out.println("耗时:"+(endTime - startTime)+"毫秒");
        }else{
            //如果不存在 则查询数据库.
            treeList = findItemCatList(parentId);
            //将数据保存到缓存中
            String json = ObjectMapperUtil.toJSON(treeList);
            jedis.set(key,json);
            System.out.println("查询数据库!!!");
            long endTime = System.currentTimeMillis();
            System.out.println("耗时:"+(endTime - startTime)+"毫秒");
        }
        return treeList;
    }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
8.4 速度差


9 利用AOP实现商品分类缓存
9.1 为什么使用AOP
问题1: 如果将业务代码直接写死,那么该代码不具有通用性.
问题2: 代码冗余 代码的耦合性高.
AOP: 面向切面编程.
AOP作用: 在不修改原有方法的条件下.对原有的方法进行扩展.

9.2 关于AOP复习
公式: AOP = 切入点表达式 + 通知方法

9. 2.1 通知方法
before 目标方法执行之前执行
afterThrowing 目标方法执行之后 抛出异常时执行
afterReturning 目标方法执行之后 返回结果时执行
after 目标方法执行之后执行(finally)
around 环绕通知功能最为强大 可以控制目标方法的执行 在目标方法执行前后都要执行

9.2.2 切入点表达式
1.bean(bean的Id) 按照bean匹配!! Spring容器管理的对象称之为bean 粗粒度
2.within(包名.类名) 按照包路径匹配 其中可以使用通配符代替
within("com.jt.service. ") 位于com.jt.service中的包的所有的类都会匹配. 粗粒度
3.execution(返回值类型 包名.类名.方法名(参数列表)) 匹配的是方法参数级别 细粒度
execution(* com.jt.service…(…)) 解释:返回值类型任意 在com.jt.service的包路径中的任意类的任意方法的任意参数…
execution(* com.jt.service.userService.add*(int,String))
4.@annotation(包名.注解名称) 按照注解匹配.
注解: @Find
@annotation(com.jt.anno.Find)

9.2.3 关于AOP案例

package com.jt.aop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;

import java.util.Arrays;

/*@Service
@Controller
@Repository*/
@Component  //组件 将类交给spring容器管理
@Aspect     //表示我是一个切面
public class RedisAOP {

    //公式 aop = 切入点表达式   +   通知方法
    //@Pointcut("bean(itemCatServiceImpl)")
    //@Pointcut("within(com.jt.service.*)")
    //@Pointcut("execution(* com.jt.service.*.*(..))")   //.* 当前包的一级子目录
    @Pointcut("execution(* com.jt.service..*.*(..))")  //..* 当前包的所有的子目录
    public void pointCut(){

    }

    //如何获取目标对象的相关参数?
    //ProceedingJoinPoint is only supported for around advice
    @Before("pointCut()")
    public void before(JoinPoint joinPoint){    //连接点
        Object target = joinPoint.getTarget();
        Object[] args = joinPoint.getArgs();
        String className = joinPoint.getSignature().getDeclaringTypeName();
        String methodName = joinPoint.getSignature().getName();
        System.out.println("目标对象:"+target);
        System.out.println("方法参数:"+Arrays.toString(args));
        System.out.println("类名称:"+className);
        System.out.println("方法名称:"+methodName);
    }

}


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
9.3 AOP实现缓存业务
9.3.1 业务需求
1). 自定义注解 @CacheFind(key=“xxx”,second=-1)
2). 使用自定义注解 标识业务方法 将方法的返回值保存到缓存中.
3). 利用AOP 拦截注解 利用环绕通知方法实现业务

9.3.2 自定义注解@CacheFind

9.3.3 注解标识

9.3.4 编辑AOP

package com.jt.aop;

import com.jt.anno.CacheFind;
import com.jt.util.ObjectMapperUtil;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import redis.clients.jedis.Jedis;

import java.lang.reflect.Method;
import java.util.Arrays;

/*@Service
@Controller
@Repository*/
@Component  //组件 将类交给spring容器管理
@Aspect     //表示我是一个切面
public class RedisAOP {

    @Autowired
    private Jedis jedis;

    /*
    * 实现AOP业务调用
    * 1.拦截指定的注解
    * 2.利用环绕通知实现
    * 实现步骤:
    *       1.获取KEY  必须先获取注解 从注解中获取key?
    *       2.校验redis中是否有值
    *     *
    * 3.知识点补充:
    *   指定参数名称进行传值,运行期绑定参数类型完成注解的拦截
    *   joinPoint必须位于参数的第一位.
    */
    @Around("@annotation(cacheFind)")
    public Object around(ProceedingJoinPoint joinPoint,CacheFind cacheFind){
        Object result = null;
        //key=业务名称::参数
        String key = cacheFind.key();
        String args = Arrays.toString(joinPoint.getArgs());
        key = key + "::" + args;

        //2.校验是否有值
        if(jedis.exists(key)){
            String json = jedis.get(key);
            MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
            Class returnType = methodSignature.getReturnType();

            result = ObjectMapperUtil.toObj(json,returnType);
            System.out.println("AOP查询redis缓存");

        }else{
            //redis中没有数据,所以需要查询数据库,将数据保存到缓存中
            try {
                result = joinPoint.proceed();
                String json = ObjectMapperUtil.toJSON(result);
                //是否设定超时时间
                if(cacheFind.seconds()>0){
                    jedis.setex(key, cacheFind.seconds(), json);
                }else{
                    jedis.set(key,json);
                }
                System.out.println("AOP查询数据库");
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            }
        }

        return result;
    }


    /**
     *   //1.获取key 注解  方法对象  类 方法名称  参数
     *         Class targetClass = joinPoint.getTarget().getClass();
     *         //2.获取方法对象
     *         String methodName = joinPoint.getSignature().getName();
     *         Object[] args = joinPoint.getArgs();
     *         Class[] classArgs = new Class[args.length];
     *         for(int i=0;i<args.length;i++){
     *             classArgs[i] = args[i].getClass();
     *         }
     *         try {
     *             //反射实例化对象
     *             Method method = targetClass.getMethod(methodName,classArgs);
     *             CacheFind cacheFind = method.getAnnotation(CacheFind.class);
     *             String key = cacheFind.key();
     *             System.out.println(key);
     *         } catch (NoSuchMethodException e) {
     *             e.printStackTrace();
     *         }
     */


    //公式 aop = 切入点表达式   +   通知方法
    //@Pointcut("bean(itemCatServiceImpl)")
    //@Pointcut("within(com.jt.service.*)")
    //@Pointcut("execution(* com.jt.service.*.*(..))")   //.* 当前包的一级子目录
   /* @Pointcut("execution(* com.jt.service..*.*(..))")  //..* 当前包的所有的子目录
    public void pointCut(){

    }*/

    //如何获取目标对象的相关参数?
    //ProceedingJoinPoint is only supported for around advice
   /* @Before("pointCut()")
    public void before(JoinPoint joinPoint){    //连接点
        Object target = joinPoint.getTarget();
        Object[] args = joinPoint.getArgs();
        String className = joinPoint.getSignature().getDeclaringTypeName();
        String methodName = joinPoint.getSignature().getName();
        System.out.println("目标对象:"+target);
        System.out.println("方法参数:"+Arrays.toString(args));
        System.out.println("类名称:"+className);
        System.out.println("方法名称:"+methodName);
    }*/
}


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
10 关于Redis常规属性
10.1 Redis中持久化策略-RDB
10.1.1 需求说明
说明: Redis中将数据都保存到了内存中,但是内存的特点断电及擦除. 为了保证redis中的缓存数据不丢失,则需要将内存数据定期进行持久化操作.
持久化: 将内存数据,写到磁盘中.
10.1.2 RDB模式
特点:
1.RDB模式是Redis默认的持久化规则.
2.RDB模式记录的是Redis内存数据快照(只保留最新数据)
3.RDB模式定期持久化(时间可调) 可能会导致数据丢失.
4.RDB模式备份效率是最高的.
5.RDB模式备份阻塞式的 在备份时不允许其他用户操作. 保证数据安全性. save
命令:
1.主动备份 save 会阻塞用户操作
2.后台备份 bgsave 异步的方式进行持久化操作 不会阻塞.

10.1.3 关于持久化配置
1.save 900 1 900秒内,用户执行了一次更新操作时,那么就持久化一次
2.save 300 10 300秒内,用户执行了10次更新操作. 那么就持久化一次
3.save 60 10000 60秒内,用户执行了10000次的更新操作,则持久化一次.
4.save 1 1 1秒内 1次更新 持久化一次!! 性能特别低.

10.1.4 关于持久化文件名称设定
默认的条件下,持久化文件名称 dump.rdb

10.1.5 文件存储目录
./ 代表当前文件目录. 意义使用绝对路径的写法.

10.2 Redis中持久化策略-AOF
10.2.1 AOF特点
1).AOF模式默认的条件下是关闭状态.需要手动开启.
2).AOF模式记录的是用户的操作过程. 可以实现实时持久化.保证数据不丢失.
3).AOF模式维护的持久化文件占用的空间较大.所以持久化效率不高. 并且需要定期的维护持久化文件.
4).AOF模式一旦开启,则redis以AOF模式为主 读取的是AOF文件.

10.2.2 AOF配置
1).开启AOF模式

2).持久化策略
always: 用户更新一次,则持久化一次.
everysec: 每秒持久化一次 效率更高
no: 不主动持久化. 操作系统有关. 几乎不用

关于Redis面试题
一、关于flushAll操作
业务场景:
小丽是一个特别漂亮的实习生.你是他的项目主管. 由于小丽业务不熟,在生产环境中无意执行了flushAll操作. 问如何补救??
场景1: redis中的服务只开启了默认的持久策略 RDB模式.
解决方案:
1.关闭现有的redis服务器.
2.检查RDB文件是否被覆盖. 如果文件没有覆盖.则重启redis即可.(希望渺茫)
3.如果flushAll命令,同时执行了save操作,则RDB模式无效.
场景2: redis中的服务开启了AOF模式.
解决方案:
1.关闭redis服务器.
2.编辑redis 持久化文件 将flushAll命令删除.
3.重启redis服务器
一般条件下: RDB模式和AOF模式都会开启. 通过save命令执行rdb持久化方式.

二、单线程Redis为什么快
1).redis运行环境在内存中,纯内存操作.
2).单线程操作 避免频繁的上下文切换. 避免了开关链接的开销.
3).采用了非阻塞I/O(BIO|NIO) 多路复用的机制(动态感知).

4). Redis最新版本 6.0版本 6.0以前的版本都是单线程操作方式. 6.0以后支持多线程操作方式. (执行时依旧是单线程操作).

10.3 关于Redis内存优化策略
10.3.1 业务场景
如果频繁使用redis,不停的向其中保存数据,并且不做删除操作,则内存必然溢出. 能否优化内存策略.
能否自动的删除不用的数据,让redis中保留热点数据!!!.
10.3.2 LRU算法
LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面(数据)予以淘汰。该算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 t,当须淘汰一个页面时,选择现有页面中其 t 值最大的,即最近最少使用的页面予以淘汰。
计算维度: 自上一次以来所经历的时间T.

说明:LRU算法是内存优化中最好用的算法.

10.3.3 LFU算法
LFU(least frequently used (LFU) page-replacement algorithm)。即最不经常使用页置换算法,要求在页置换时置换引用计数最小的页,因为经常使用的页应该有一个较大的引用次数。但是有些页在开始时使用次数很多,但以后就不再使用,这类页将会长时间留在内存中,因此可以将引用计数寄存器定时右移一位,形成指数衰减的平均使用次数。
维度: 引用次数
常识: 计算机左移 扩大倍数
计算机右移 缩小倍数

10.3.4 随机算法
随机删除数据.

10.3.5 TTL算法
说明:将剩余存活时间排序,将马上要被删除的数据,提前删除.

10.3.6 Redis默认的内存优化策略
说明1: Redis中采用的策略定期删除+惰性删除策略
说明2:
1.定期删除: redis默认每隔100ms 检查是否有过期的key, 检查时随机的方式进行检查.(不是检查所有的数据,因为效率太低.)
问题: 由于数据众多,可能抽取时没有被选中.可能出现 该数据已经到了超时时间,但是redis并没有马上删除数据.
2. 惰性策略: 当用户获取key的时候,首先检查数据是否已经过了超时时间. 如果已经超时,则删除数据.
问题: 由于数据众多, 用户不可能将所有的内存数据都get一遍.必然会出现 需要删除的数据一直保留在内存中的现象.占用内存资源.
3.可以采用上述的内存优化手段,主动的删除.

内存优化算法说明:
1.volatile-lru 在设定超时时间的数据 采用LRU算法进行优化
2.allkeys-lru 在所有的数据采用LRU算法进行优化
3.volatile-lfu 在设定了超时时间的数据中采用LFU算法优化
4.allkeys-lfu 在所有的数据中采用LFU算法进行优化
5.volatile-random 在设定了超时时间的数据 采用随机算法
6.allkeys-random 所有数据采用 随机算法
7.volatile-ttl 设定超时时间的TTl算法
8.noeviction 不主动删除数据,如果内存溢出则报错返回.


11. Redis分片机制
11.1 业务需求
说明: 单台redis存储的数据容量有限的. 如果需要存储海量的缓存数据,则使用单台redis肯定不能满足要求.为了满足数据扩容的需求.则可以采用分片的机制实现.

11.2 Redis分片机制实现
11.2.1搭建策略
分别准备3台redis 6379/6380/6381
11.2.2 准备文件目录

11.2.3 复制配置文件
说明: 将redis的配置文件放到shards目录中.

修改配置文件端口号 依次修改6380/6381

启动3台redis:
redis-server 6379.conf
redis-server 6380.conf
redis-server 6381.conf

校验服务器:

11.2.4 Redis分片入门案例

package com.jt.test;

import org.junit.jupiter.api.Test;
import redis.clients.jedis.JedisShardInfo;
import redis.clients.jedis.ShardedJedis;

import java.util.ArrayList;
import java.util.List;

public class TestRedisShards {

    @Test
    public void testShards(){
        List<JedisShardInfo> shards = new ArrayList<>();
        shards.add(new JedisShardInfo("192.168.126.129",6379));
        shards.add(new JedisShardInfo("192.168.126.129",6380));
        shards.add(new JedisShardInfo("192.168.126.129",6381));
        ShardedJedis shardedJedis = new ShardedJedis(shards);
        //3台redis当做1台使用  内存容量扩大3倍.  79/80/81???
        shardedJedis.set("shards", "redis分片测试");
        System.out.println(shardedJedis.get("shards"));
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
11.3 一致性hash算法
11.3.1 算法介绍
一致性哈希算法在1997年由麻省理工学院提出,是一种特殊的哈希算法,目的是解决分布式缓存的问题。 [1] 在移除或者添加一个服务器时,能够尽可能小地改变已存在的服务请求与处理请求服务器之间的映射关系。一致性哈希解决了简单哈希算法在分布式哈希表( Distributed Hash Table,DHT) 中存在的动态伸缩等问题 [2] 。
作用: 解决缓存数据,在哪存储的问题…
11.3.2 算法说明
常识:

如果数据相同,则hash结果必然相同.
常见hash值 由8位16进制数组成. 共用多少种可能性? 2^32

11.3.3 平衡性
①平衡性是指hash的结果应该平均分配到各个节点,这样从算法上解决了负载均衡问题 [4] 。
说明:通过虚拟节点实现数据的平衡

11.3.4 单调性
②单调性是指在新增或者删减节点时,不影响系统正常运行 [4] 。
原则: 如果节点新增/减少 应该尽可能保证原始数据尽可能不变.
11.3.5 分散性
③分散性是指数据应该分散地存放在分布式集群中的各个节点(节点自己可以有备份),不必每个节点都存储所有的数据 [4]
将数据分散存储,即使将来服务器宕机,则影响只是一部分,.而不是全部.
谚语: 鸡蛋不要放到一个篮子里.

11.4 SpringBoot整合Redis分片机制
11.4.1 编辑pro配置文件

11.4.2 编辑配置类


11.4.3 修改AOP中的配置


12 Redis哨兵机制
12.1 Redis分片存在问题
说明:Redis分片机制,虽然可以实现Redis Redis内存扩容,但是redis 节点并没有实现高可用.如果节点宕机,则整合redis分片将不可使用.

12.2 Redis主从结构搭建
定: 6379主机 /6380/6381 从机
12.2.1 复制文件目录
12.2.2 删除持久化文件

12.2.3 启动3台Redis服务器
1.redis-server 6379.conf
2.redis-server 6380.conf
3.redis-server 6381.conf


12.2.4 实现redis主从挂载
命令1.: slaveof host port
命令说明: 在从机中执行上述命令 挂载的是主机的地址.

命令2: info replication

主从结构关系:

12.3 Redis哨兵工作原理
12.3.1 工作流程图

原理说明:
1.哨兵监控主机的运行的状态. 通过心跳检测机制(PING-PONG)如果连续3次节点没有响应,则断定主机宕机,哨兵开始进行选举.
2.哨兵通过链接主机,获取主机的相关配置信息(包含主从结构),挑选链接当前主机的从机.根据随机算法挑选出新的主机. 并且将其他的节点设置为新主机的从.

12.3.2 编辑哨兵配置文件
1).复制哨兵的配置文件

2).关闭保护模式

3).开启后端运行

4).设定哨兵的投票数

5).修改选举的超时时间

6).修改哨兵的状态


12.3.3 哨兵测试
哨兵命令: redis-sentinel sentinel.conf
检查redis服务:

redis高可用测试:
1.关闭redis主机6379
2.等待10秒 检查6380/6381到底谁是主机.
3.重启6379服务器,检查是否充当了新主机的从

12.3.4 哨兵的入门案例

@Test
    public void test01(){
        //定义哨兵的集合信息
        Set<String> sentinels = new HashSet<>();
        sentinels.add("192.168.126.129:26379");
        //定义链接池信息
        JedisPoolConfig poolConfig = new JedisPoolConfig();
        poolConfig.setMaxTotal(200);    //链接池 最多200个链接
        poolConfig.setMaxIdle(20);      //最大空闲链接数20
        poolConfig.setMinIdle(10);      //最小空闲链接数10
        JedisSentinelPool pool = new JedisSentinelPool("mymaster",sentinels,poolConfig);
        //动态获取jedis链接
        Jedis jedis = pool.getResource();
        jedis.set("abc", "redis赋值操作");
        System.out.println(jedis.get("abc"));
        jedis.close();
    }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
12.4 关于分片/哨兵总结
1.分片机制: 可以实现内存数据的扩容. 但是本身没有实现高可用的效果.
2.哨兵机制: 哨兵可以实现redis节点的高可用.但是哨兵本身没有实现高可用的效果.
需求: 1.不依赖第三方实现高可用
2.实现内存数据的扩容
3.各个节点可以高可用.
3. Redis集群搭建 功能包含上述的3种机制. 一般公司中都会采用集群的方式部署redis.

13.Redis集群搭建
13.1 关于redis集群搭建问题说明
注意事项:
1.启动6个redis节点
2.保证redis节点中的数据都是null的
3.根据报错提示 排查问题. 检查IP地址 检查防火墙…
1). 关闭所有的Redis服务器 sh stop.sh
2).检查redis配置文件

3).删除多余文件

13.2 关于Redis集群高可用测试

1).关闭7000节点
redis-cli -p 7000 shutdown

2).检查主从状态

3).重启7000 检查状态

13.3 Redis集群宕机条件
宕机条件: Redis中的主机缺失时,并且没有从机替补,Redis内存数据丢失.这时Redis集群崩溃了.

问题1: 6台redis 3主3从(1主1从分为3组). 至少Redis宕机几台集群崩溃. 至少2台 集群崩溃.
问题2: 9台redis 3主6从(1主2从分为3组). 至少宕机几台Redis集群崩溃. 至少5台 集群崩溃.

集群宕机的条件: 当主机的数量不能保证时集群崩溃.
特点:集群中如果主机宕机,那么从机可以继续提供服务,
当主机中没有从机时,则向其它主机借用多余的从机.继续提供服务.如果主机宕机时没有从机可用,则集群崩溃.
答案:9个redis节点,节点宕机5-7次时集群才崩溃.

13.4 Redis分区算法
13.4.1 集群测试入门案例

13.4.2 hash槽算法
Hash槽算法 分区算法.
说明: RedisCluster采用此分区,所有的键根据哈希函数(CRC16[key]%16384)映射到0-16383槽内,共16384个槽位,每个节点维护部分槽及槽所映射的键值数据.根据主节点的个数,均衡划分区间.
算法:哈希函数: Hash()=CRC16[key]%16384

当向redis集群中插入数据时,首先将key进行计算.之后将计算结果匹配到具体的某一个槽的区间内,之后再将数据set到管理该槽的节点中.


14 Redis面试题
1 关于集群/分片算法说明
问题:一个数据很大.一个槽位不够存怎么办??? 错误?? A 逻辑错误 B. 有道理
解答:
1.一致性hash算法 hash(key) 43亿 按照顺时针方向找到最近的节点 进行set操作.
2.Hash槽算法 crc16(key)%16384 (0-16383) 计算的结果归哪个节点管理,则将数据保存到节点中.
核心知识: 一致性hash算法/hash槽算法 都是用来确定 数据归谁管理的问题. 最终的数据都会存储到node节点中.

2 面试题1
问: Redis集群中一共可以存储16384个数据? A 对 B 错 为什么???
小明猜想: 由于redis中共有16384个槽位,所以每个槽位存储一个key.那么不就是16384个key吗??
答案: 错误
原因: Redis集群中确实有16384个槽位.但是这些槽位是用来划分数据归谁管理的.不是用来存储数据的. 并且根据hash计算的规则肯能出现碰撞的问题.比如

hash(key1)%16384=3000
hash(key2)%16384=3000
                        说明key1和key2归同一个node管理.
                        node.set(key1,value1);
                        node.set(key2,value2);
由于槽位只是用来区分数据,数据到底能存储多少个完成由redis内存决定.
1
2
3
4
5
6
3 面试题2
问题: 为Redis集群中最多有多少台主机?? 16384台主机

4 缓存相关面试题
4.1 缓存穿透
说明: 用户高并发环境下,频繁访问数据库中不存在的数据.导致用户请求直接访问数据库.严重时导致数据库服务器宕机.

解决方案:

IP限流操作 API网关中设置 设定用户访问的上限 规定每个IP单位时间内只能发送N次请求. 3-5治标不治本 (IP代理服务器: 1分钟变化一个IP)
4.1.1 布隆过滤器
布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。

核心知识点:
用法: 由二进制向量,hash函数组合.
作用: 判断一个元素是否存在于集合中.
优点: 占用空间更小/效率更高
缺点: 有一定的误判率(hash碰撞), 删除困难.
4.1.2 优点说明
问题: 如果有1000万的热点数据需要保存到redis缓存中. 问:是否可行?
计算: 1000万的数据如果需要存储 大约需要20G左右的空间…
知识铺垫:
1 Byte = 8 bit (二进制)。
1KB (Kilobyte 千)=1024B,
  1MB (Megabyte 兆)=1024KB,
  1GB (Gigabyte 吉)=1024MB,
  1TB (Trillionbyte 太)=1024GB,

转变: 假设可以通过 0/1的方式,判断数据是否存在.同时占用的空间较小.那么这个问题就解决了.
计算2: 假设1个数据占用1个bit 问占用空间多大? 1.19M

4.1.3 布隆过滤器应用场景
说明:当用户查询服务器时,首先查询布隆过滤器,如果查询存在该数据,则执行后续的流程,
如果查询没有该数据,则直接返回.无需执行后续流程.

4.1.4 布隆过滤器算法介绍

4.1.5 关于布隆过滤器优化说明
1.根据hash原则 数据存在hash碰撞的概率. 则使用布隆过滤器容器造成误判. 如何解决?

4.1.6 优化hash碰撞概率-增加二进制向量
4.1.7 优化hash碰撞概率-增加hash函数个数

4.1.8 关于布隆在项目中使用过程

4.2 缓存击穿
说明: 在高并发环境下 某个热点数据由于删除/超时导致该数据在缓存中失效. 这时有大量的请求直接访问数据库.
导致数据库宕机.

如何优化:
1.定期更新热点数据的超时时间.
2.增加多级缓存机制.

4.3 缓存雪崩
概念: 由于Redis中大量的内存数据失效.导致用户访问缓存的命中率太低.大量的请求直接访问数据库.导致数据库宕机.

命令: flushDB/flushAll这样的命令慎用…
解决方案:
1.设定不同的超时时间/动态更新超时时间
2.设定多级缓存.


15 SpringBoot整合Redis集群
15.1 编辑pro配置文件

# 准备redis节点信息
#redis.host=192.168.126.129
#redis.port=6379

# 准备3台redis
#redis.nodes=192.168.126.129:6379,192.168.126.129:6380,192.168.126.129:6381

#准备6个redis节点
redis.nodes=192.168.126.129:7000,192.168.126.129:7001,192.168.126.129:7002,192.168.126.129:7003,192.168.126.129:7004,192.168.126.129:7005


1
2
3
4
5
6
7
8
9
10
11
15.2 编辑配置类

15.3 编辑CacheAOP

————————————————
版权声明:本文为CSDN博主「北木桥溪」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_54685622/article/details/124034238

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值