Redis简述

目录

基础常用命令

常见数据类型

String数据类型

Hash数据类型

List数据类型

Set数据类型

Zset数据类型

Java中操作redis

Jedis的基本应用

连接池JedisPool连接池

RedisTemplate基本应用(lettuceAPI)

基于业务定制RedisTemplate对象

SpringBoot工程中Redis与Aop技术的整合

Redis 数据持久化

 配置准备工作

Rdb方式持久化

 Aof方式数据持久化

Redis事务

Redis主从复制(读多写少 不支持高可用)

Redis哨兵模式(观察者模式,高可用)


Redis(Remote Dictionary Server )即远程字典服务,是 C 语言开发的一个开源的key/value存储系统(官网:http://redis.io),是一个分布式缓存数据库。

官方只提供Linux版本,windows公司提供了Windows版的(Redis的次版本号(第一个小数点后的数字为偶数的版本是稳定版本,奇数为非稳定版本)

Redis作为一种key/value结构的数据存储系统,为了便于对数据进行进行管理,提供了多种数据类型,Reids中基础数据结构包含字符串、散列,列表,集合,有序集合

优势: 并发处理能力非常好 , 更好的保证数据的可靠性, 有丰富的数据类型

Bootnb 相关:https://www.runoob.com/redis/redis-tutorial.html
Redis 官网:https://redis.io/
源码地址:https://github.com/redis/redis
Redis 在线测试:http://try.redis.io/
Redis 命令参考:http://doc.redisfans.com/

Redis数据存于磁盘中,采用TCP协议进行通讯

双写: 数据同时存入分布式缓存和数据库中

下载安装于linux操作系统中的docker容器中

基础常用命令

启动及关闭redis服务

docker ps #查看进程
docker stop 618 #关闭进程
docker start redis #启动redis进程
ps -ef | grep redis # 查看正在运行的进程过滤redis进程
docker exec -it redis01 bash #redis01 为容器名 进入redis容器
redis-cli #登陆本地redis服务
或者
redis-cli -p 6379
或者
redis-cli -p 6379 -a  password #-a后面为password,此操作需要开启redis.conf文件中的 requirepass选项
redis-cli  -h ip  -p 6379  -a  password #登录远端redis服务
info #查看当前redis节点的详细配置信息
shutdown #保护模式关闭redis服务
exit #关闭redis服务
select 1 #单机模式Redis默认支持16个数据库,下标是0-15
flushdb #清除当前数据库数据
flushall #清除所有数据库数据

set key1 100 #存入数据
type key1 #查看数据类型

①set key2 100 EX 5 #设置数据存储的有效时长 EX秒 PX毫秒
②set key3 100 
 expire key3 5 #数据保留5秒
ttl key3 #查看数据有效时长

keys * #查看当前数据库的所有key
get key1 #查询key1的数据

常见数据类型

String数据类型

存储的值可以是字符串,其最大字符串长度支持到512M
于此类型,可以实现博客的字数统计将日志不断追加到指定key,实现一个分布式自增id,实现一个博客的的点赞操作

set num 1 #如果值存在则覆盖
setnx num 9 #判断是否存在num,存在就不存入数据,不存在则存入数据

incr num #2 数据在内存中递增,数据库自增是在数据库程序中 订单分布式id
incr num #3 如果num不存在,则自动会创建,如果存在自动+1
incrby num 2 #每次递增2
decr num #递减
decrby num 3 #每次递减3

del key1 key2 #删除

append test "abc" #向尾部追加值。如果键不存在则创建该键

strlen test #字符串长度,返回数据的长度,如果键不存在则返回0。注意,如果键值为空串,返回也是0

mset a 1 b 2 c 3 #同时设置/获取多个键值
mget a b c

exists a b #判断某个值是否存在,0为不存在

Hash数据类型

Redis散列类型相当于Java中的HashMap,实现原理跟HashMap一致,一般用于存储对象信息,存储了字段(field)和字段值的映射,K V中V又是KV结构

hset user username chenchen
hset user password 123
hset user username chenchen password 123
hsetnx user password 456

hget user username
hgetall user 
hmget user username password
hvals user #chenchen 123
hkeys user #username password

hincrby article total -1		#没有hdecrby自减命令

hexists user username 

hdel user username # 操作的是小key
del user #操作的是大key

List数据类型

Redis的list类型相当于java中的LinkedList,其原理就就是一个双向链表。支持正向、反向查找和遍历等操作,插入删除速度比较快。经常用于实现热销榜,最新评论等的设计K V 中的V为很多个有序可重复的V

lpush list1 a b c c d
rpop list1 5 #a b c c d
llen list1 #0

lpush list1 a b c c d
lpop list1 5 #d c c b a

lpush list1 a b c #最先进去的是最后的
linsert list1 before a  f
rpop list1 4 #a f b c

lpush list1 a b c #c的下标是0
lset list1 0 g
rpop list1 3 #a b g

lpush num1 a b c
lrem num1 1 a
lrange num1 0 -1 #c b
ltrim num1 0 0
lindex  num 0

lpush g1 a b
lpush g2 g f
rpoplpush g2 g1 
rpop g1 3 #a b g

lpush num1 a b c
brpop num1 20 #a 当元素移除完后,线程会指定阻塞时间20秒
brpop num1 20 #b
brpop num1 20 #c

Set数据类型

Redis的Set类似Java中的HashSet,是string类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。Redis中Set集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。K V 中的V为很多个无序不可重复的V

sadd set1 a a b b c c c d d 
smembers set1 #d b a c
spop set1 1 #c
spop set1 2 #a b

sadd set1 a a b b c c c d d 
scard set1 #4
sadd set2 7 8 9
smove set1 set2 c
spop set2 #7 c 8 9
sunion set1 set2

Zset数据类型

zet是Redis提供的一个非常特别的数据结构,常用作排行榜等功能,以用户d为value,关注时间或者分数作为score进行排序。K score1 user1 score2 user2  

Java中操作redis

Redis 是一种C/S 架构的分布式缓存数据库,它有自带的命令行客户端,也有对应的Java或其它语言客户端,可以在这些客户端中通过一些API对redis进行读写操作。

创建父工程,pom文件添加编译配置

<build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>8</source>
                    <target>8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

Jedis的基本应用

Jedis是Java中操作redis的一个客户端,类似通过jdbc访问mysql数据库.

创建redis-jedis 工程添加如下依赖

<!--这组API是redis官方推荐的一组用于在java中操作
       redis数据库的API-->
       <dependency>
           <groupId>redis.clients</groupId>
           <artifactId>jedis</artifactId>
           <version>3.5.2</version>
       </dependency>
       <dependency>
           <groupId>junit</groupId>
           <artifactId>junit</artifactId>
           <version>4.12</version>
           <scope>test</scope>
       </dependency>
       <!--这是Google公司推出的用于实现Java对象和JSON字符串之间
       相互转换的一组API-->
       <dependency>
           <groupId>com.google.code.gson</groupId>
           <artifactId>gson</artifactId>
           <version>2.8.6</version>
       </dependency>

在Jedis工程中的src/test/java目录创建单元测类

public class JedisTests {
    /**
     * 测试hash类型数据
     */
    @Test
    public void testHashOper01(){
        //1.建立连接(TCP)
        Jedis jedis=new Jedis(
                "192.168.126.128", 6379);
        //2.添加数据
        Map<String,String> map1=new HashMap<>();
        map1.put("id", "201");
        map1.put("title", "redis6.2.5");
        jedis.hset("blog:201", map1);//不存在则直接添加
        jedis.hset("blog:201", "title", "redis6.2.6");
        //3.读取数据
        map1=jedis.hgetAll("blog:201");
        System.out.println(map1);
        //4.释放资源
        jedis.close();
    }
    /**
     * 测试字符串操作
     */
    @Test
    public void testStringOper02() {
        //1.建立连接
        Jedis jedis=new Jedis(
                "192.168.126.128", 6379);
        //2.将一个map对象转换为json字符串,然后写入到redis并给定有效期
        Map<String,String> map1=new HashMap<>();
        map1.put("id", "101");
        map1.put("title", "redis6.2.5");
        Gson gson=new Gson();
        String jsonStr1=gson.toJson(map1);
        System.out.println(jsonStr1);
        jedis.set("blog:101", jsonStr1);
        jedis.expire("blog:101", 20);
        //3.从redis将json字符串读出,并转换为map然后输出.
        String jsonStr2 = jedis.get("blog:101");
        Map<String,String> map2=gson.fromJson(jsonStr2,Map.class);
        System.out.println(map2);
        //4.释放资源
        jedis.close();
    }
    /**
     * 测试字符串操作
     */
    @Test
    public void testStringOper01(){
      //1.建立连接
        Jedis jedis=new Jedis(
                "192.168.126.128", 6379);
      //2.添加数据(Create)
        jedis.set("id", "100");
        Long setnx = jedis.setnx("lock", "AAA");
        jedis.set("logs", "abc");
        //3.获取数据(Retrieve)
        String id = jedis.get("id");
        System.out.println("update.before.id="+id);
      //4.修改数据(Update)
        jedis.incr("id");
        id = jedis.get("id");
        System.out.println("update.after.id="+id);

        jedis.expire("lock", 10);
        jedis.append("logs","EF");

      //5.删除数据(Delete)
        jedis.del("id");
        id = jedis.get("id");
        System.out.println("delete.after.id="+id);
      //6.释放资源
        jedis.close();

    }

    @Test
    public void testGetConnection(){
        //1.建立连接
        //假如无法建立连接,要从如下几个方面进行分析
        //1)ip
        //2)port
        //3)防火墙
        //4)redis服务是否启动ok
        Jedis jedis=new Jedis(
                "192.168.126.128", 6379);
        //jedis.auth("123456");
        //2.执行ping操作
        String result = jedis.ping();
        System.out.println(result);
    }
}

连接池JedisPool连接池

public class JedisPoolTests {
    /**
     * Jedis连接池应用测试
     * 注意:所有连接池都有享元模式的应用,以实现对象重用.
     */
    @Test
    public void testJedisPool(){
        //1.创建池对象
        //GenericObjectPoolConfig poolConfig=
                //new GenericObjectPoolConfig();
        JedisPoolConfig poolConfig=//这个类型是上面类型的子类类型.
                new JedisPoolConfig();
        poolConfig.setMaxTotal(128);//最大连接数
        poolConfig.setMaxIdle(8);//最大空闲连接数
        JedisPool jedisPool=
                //new JedisPool("192.168.126.128", 6379);
                new JedisPool(poolConfig,
                        "192.168.126.128", 6379);
        //2.获取连接
        Jedis resource = jedisPool.getResource();
        //3.读写数据
        resource.set("id", "100");
        String id = resource.get("id");
        System.out.println(id);
        //4.释放资源
        resource.close();
        jedisPool.close();//以后这个池是关服务的时候关.
    }
}

提取连接池配置到配置类中,需要时直接通过方法获取即可
可以参考HikariDataSource类中连接池的创建,以上代码还是存在线程不停切换的问题,需要将成员变量赋值给局部变量,再进行判断,可以减少线程之间切换的问题; 还可以进一步优化,将连接池的配置参数提取到pom配置文件中

public class JedisDataSource {


   //初始化池对象并获取连接 (何时需要何时创建池对象~懒加载)
    private static volatile JedisPool jedisPool;
    /**
     * volatile 关键字特点:
     * 1)修饰属性
     * 2)禁止指令重排序(JVM底层执行指令时,可能考虑到一定优化,会对指令进行重排序)
     * 3)保证线程可见性(一个线程修改了这个值,其它线程立刻可见.)
     */
    public static  Jedis getConnection(){ //JedisDataSource.class
        if(jedisPool==null) {
            synchronized (JedisDataSource.class) {
                if (jedisPool == null) {
                    JedisPoolConfig poolConfig = new JedisPoolConfig();
                    poolConfig.setMaxTotal(128);//最大连接数
                    poolConfig.setMaxIdle(8);//最大空闲连接数
                    jedisPool = new JedisPool(poolConfig, "192.168.126.128", 6379);
                    //1.对象创建的过程?
                    //1)分配内存
                    //2)初始化属性
                    //3)执行构造方法
                    //4)将JedisPool对象赋值给jedisPool变量
                }
            }
        }
        return jedisPool.getResource();
    }

    public static void close(){
        jedisPool.close();
    }
}

RedisTemplate基本应用(lettuceAPI)

创建子工程redis-template,添加添加依赖

<dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.3.2.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>


        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.2</version>
        </dependency>

        <!--假如项目中配置lettuce池对象,需要添加如下依赖-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

    </dependencies>
spring:
  redis:
    host: 192.168.126.129  #写自己的ip
    port: 6379

创建启动类测试连接是否成功
RedisTemplate 对象在存取数据时默认会按照如下方式执行:
1)key和value在存储时,会基于JDK方式进行序列化
2)基于key取值时,默认会基于JDK做反序列化.

基于RedisTemplate对象的incrment方法实现key值的递增时,注意值的结构类型

@SpringBootTest
public class RedisTemplateTests {
    /**
     * RedisTemplate 对象在存取数据时默认会按照如下方式执行:
     * 1)key和value在存储时,会基于JDK方式进行序列化
     * 2)基于key取值时,默认会基于JDK做反序列化.
     */
    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 应用redisTemplate对象的另一种方式
     */
    @Test
    void testFlushdb(){
        redisTemplate.execute(new RedisCallback() {
            @Override
            public Object doInRedis(
                    RedisConnection redisConnection)
                    throws DataAccessException {
                //这里的RedisConnection表示连接
                //通过这里的连接对象,你可以执行一些其它数据库操作
                redisConnection.flushAll();
                return "ok";
            }
        });
    }

    @Test
    void testHashOper02() throws InvocationTargetException, IllegalAccessException {
        //1.创建Blog对象
        Blog blog1=new Blog();
        blog1.setId(100L);
        blog1.setTitle("Title-A");
        blog1.setContent("Content-AAA");
        //2.将Blog对象写入到redis
        //2.1序列化存取方案1:
        ValueOperations valueOperations =
                redisTemplate.opsForValue();
        valueOperations.set("blog:100", blog1,
                Duration.ofSeconds(60));
        Blog blog2=(Blog)valueOperations.get("blog:100");
        System.out.println(blog2);
        //2.2序列化存取方案2:
        HashOperations hashOperations =
                redisTemplate.opsForHash();
      
        //Map map=hashOperations.entries("blog:200");
        //System.out.println(map);
        //反射存储(序列化)
        Class<?> clazz=blog1.getClass();//字节码对象(反射起点)
        Method[] methods=clazz.getDeclaredMethods();//获取类中所有方法
        for(Method m:methods){
           if(m.getName().startsWith("get")){//getId,getTitle
               String temp=m.getName().substring(3);//去掉get单词,Id,Title,
               hashOperations.put("blog:200",
                       temp.substring(0,1).toLowerCase()//去掉get后的第一个字母小写
                               +temp.substring(1),
                       m.invoke(blog1));// m.invoke(blog1)表示执行blog1的get方法
           }
        }
        Map entries = hashOperations.entries("blog:200");
        System.out.println(entries);

    }

    @Test
    void testHashOper01(){
        //1.获取Hash类型操作对象
        //也可以自己定义序列化方式
        //redisTemplate.setKeySerializer(RedisSerializer.string());
        //redisTemplate.setHashKeySerializer(RedisSerializer.string());
        //redisTemplate.setHashValueSerializer(RedisSerializer.string());
        HashOperations hashOperations =
                redisTemplate.opsForHash();
        //2.读写数据
        hashOperations.put("blog:1","id","1");
        hashOperations.put("blog:1", "title", "hello redis");
        Map entries = hashOperations.entries("blog:1");
        System.out.println(entries);
    }

    @Test
    void testStringOper02(){
        //1.修改key/value的序列化方式
        redisTemplate.setKeySerializer(RedisSerializer.string());
        redisTemplate.setValueSerializer(RedisSerializer.string());
        //2.获取redis操作对象
        ValueOperations valueOperations =
                redisTemplate.opsForValue();
        valueOperations.set("code:11",
                "ABCD",Duration.ofSeconds(60));
        Object o = valueOperations.get("code:11");
        System.out.println(o);
    }

    @Test
    void testStringOper01(){
        //1.获取redis操作对象
        ValueOperations valueOperations =
                redisTemplate.opsForValue();
        //2.读写数据
        valueOperations.set("x", 100);//采用jdk默认序列化规则对key/value进行序列化
        //valueOperations.increment("x");不可以
        Object x = valueOperations.get("x");
        System.out.println(x);
        valueOperations.increment("y");//第一次执行这个操作时,要确保redis中没有y
        valueOperations.increment("y");
        Long y = valueOperations.increment("y");

        System.out.println(y);//3,6,9,....

        //存储数据时,给定一个有效期.
        valueOperations.set("z",
                200, Duration.ofSeconds(10));
    }

    @Test
    void testGetConnection(){
        RedisConnection connection =
                redisTemplate.getConnectionFactory()
                        .getConnection();
        String result = connection.ping();
        System.out.println(result);
    }
}

StringRedisTemplate 是一个特殊的RedisTemplate对象,默认基于字符串序列化方式存取数据

@SpringBootTest
public class StringRedisTemplateTests {
    /**
     * StringRedisTemplate 继承RedisTemplate对象,
     * 此对象默认采用string类型进行序列化方式的数据操作
     */
      @Autowired
      private StringRedisTemplate stringRedisTemplate;
      @Test
      void testStringOper() throws JsonProcessingException {
          ValueOperations<String, String> valueOperations =
                  stringRedisTemplate.opsForValue();
          valueOperations.set("x", "100");
          valueOperations.increment("x");
          String x = valueOperations.get("x");
          System.out.println(x);
          //存储一个对象
          Blog blog1=new Blog();
          blog1.setId(100L);
          blog1.setTitle("Title-AA");
          blog1.setContent("Content-AAA");
          valueOperations.set("blog:100",
                  //将对象转换为json格式字符串进行存储
                  new ObjectMapper().writeValueAsString(blog1));
          //读取对象内容
          String blogJsonStr=valueOperations.get("blog:100");
          System.out.println(blogJsonStr);
          //将json字符串转换为Blog对象(ObjectMapper为jack中的api)
          Blog blog2 =
                  new ObjectMapper().readValue(blogJsonStr,
                  Blog.class);
          System.out.println(blog2);
      }
}

基于业务定制RedisTemplate对象

参考renren.io,若依

简单定制

@Configuration
public class RedisConfig {
  
    /**
     * 基于业务定制RedisTemplate对象
     * @param connectionFactory
     * @return
     */
    @Bean
    public RedisTemplate<Object, Object> simpleRedisTemplate(
            RedisConnectionFactory connectionFactory){
       RedisTemplate<Object, Object> redisTemplate=
               new RedisTemplate<>();
       redisTemplate.setConnectionFactory(connectionFactory);
       //设置序列化方式
       redisTemplate.setKeySerializer(RedisSerializer.string());
       redisTemplate.setHashKeySerializer(RedisSerializer.string());
       redisTemplate.setHashValueSerializer(RedisSerializer.string());
       //redisTemplate.setValueSerializer(RedisSerializer.java());
       redisTemplate.setValueSerializer(RedisSerializer.json());
       return redisTemplate;
    }
}

高度定制 

@Configuration
public class RedisConfig {

    public RedisSerializer<?> redisSerializer(){
      //1.定义序列化对象,并指定可以对哪些类型对象进行序列化
       Jackson2JsonRedisSerializer serializer=
                new Jackson2JsonRedisSerializer(Object.class);
      //2.构建对象映射对象(将对象转换为json或者将json转换对象)
       ObjectMapper objectMapper=new ObjectMapper();
       //2.1设置序列化时的可见方法
       objectMapper.setVisibility(PropertyAccessor.GETTER,//序列化时只调用get方法
               JsonAutoDetect.Visibility.ANY);//get方法的访问修饰符可以任意
       //2.2序列化时值的规则定义(例如值为null还是否要序列化)
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
       //2.3激活类型存储(这个一般在反序列化时有用,否则反序列化时数据默认存储在LinkedHashMap中)
       objectMapper.activateDefaultTyping(
               objectMapper.getPolymorphicTypeValidator(),//开启多态校验
               ObjectMapper.DefaultTyping.NON_FINAL,//序列化的来不能使用final修饰
               JsonTypeInfo.As.PROPERTY);//将类型以属性形式存储到json串中
       //2.4设置对象映射对象
       serializer.setObjectMapper(objectMapper);

       return serializer;
    }

    @Bean
    public RedisTemplate<Object, Object> redisTemplate(
            RedisConnectionFactory connectionFactory) {
        RedisTemplate<Object, Object> redisTemplate =
                new RedisTemplate<>();
        redisTemplate.setConnectionFactory(connectionFactory);
        //设置序列化方式
        redisTemplate.setKeySerializer(RedisSerializer.string());
        redisTemplate.setHashKeySerializer(RedisSerializer.string());
        redisTemplate.setHashValueSerializer(redisSerializer());
        redisTemplate.setValueSerializer(redisSerializer());
        //建议更新了序列化方式后,执行一下如下方法
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
    
}
/**
 * redis配置
 * 
 * @author ruoyi
 */
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport
{
    @Bean
    @SuppressWarnings(value = { "unchecked", "rawtypes" })
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
    {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

        FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);

        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        serializer.setObjectMapper(mapper);

        // 使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);

        // Hash的key也采用StringRedisSerializer的序列化方式
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);

        template.afterPropertiesSet();
        return template;
    }
}

SpringBoot工程中Redis与Aop技术的整合

 @Cacheable
从缓存中查询指定的key,如果有,从缓存中取,不再执行方法.如果没有,则执行方法,并且将方法的返回值和指定的key关联起来,放入到缓存中。
@Cacheput
和 @Cacheable 类似,执行方法,将方法的返回值覆盖到过去的缓存数据(修改缓存中指定key 的数据),主要用于数据新增和修改方法
@CacheEvict                                
方法执行成功后会从缓存中移除相应数据
@CacheConfig
所有的@Cacheable()里面都有一个value=“xxx”的属性,这显然如果方法多了,写起来也是挺累的,如果可以一次性声明完 那就省事了,
所以,有了@CacheConfig这个配置,@CacheConfig is a class-level annotation that allows to share the cache names,如果你在你的方法写别的名字,那么依然以方法的名字为准。

@CacheConfig("books")
public class BookRepositoryImpl implements BookRepository {
 
    @Cacheable
    public Book findBook(ISBN isbn) {...}
}
参数解释example
value缓存的名称,在 spring 配置文件中定义,必须指定至少一个@CacheEvict(value=”my cache”)
key缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合@CacheEvict(value=”testcache”,key=”#userName”)
condition缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存@CacheEvict(value=”testcache”,condition=”#userName.length()>2”)
allEntries是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存@CachEvict(value=”testcache”,allEntries=true)
beforeInvocation是否在方法执行前就清空,缺省为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存

@CachEvict(value=”testcache”,beforeInvocation=true)

//@Cacheable将在执行方法之前( #result还拿不到返回值)判断condition,如果返回true,则查缓存; 
@Cacheable(value = "user", key = "#id", condition = "#id lt 10")
public User conditionFindById(final Long id)  
 
//@CachePut将在执行完方法后(#result就能拿到返回值了)判断condition,如果返回true,则放入缓存; 
@CachePut(value = "user", key = "#id", condition = "#result.username ne 'zhang'")  
public User conditionSave(final User user)   
 
//@CachePut将在执行完方法后(#result就能拿到返回值了)判断unless,如果返回false,则放入缓存;(即跟condition相反)
@CachePut(value = "user", key = "#user.id", unless = "#result.username eq 'zhang'")
public User conditionSave2(final User user)   
 
//@CacheEvict, beforeInvocation=false表示在方法执行之后调用(#result能拿到返回值了);且判断condition,如果返回true,则移除缓存;
@CacheEvict(value = "user", key = "#user.id", beforeInvocation = false, condition = "#result.username ne 'zhang'")  
public User conditionDelete(final User user)   

@Caching
有时候我们可能组合多个Cache注解使用;比如用户新增成功后,我们要添加id–>user;username—>user;email—>user的缓存;此时就需要@Caching组合多个注解标签了。

@Caching(put = {
@CachePut(value = "user", key = "#user.id"),
@CachePut(value = "user", key = "#user.username"),
@CachePut(value = "user", key = "#user.email")
})
public User save(User user) {

自定义缓存注解

比如之前的那个@Caching组合,会让方法上的注解显得整个代码比较乱,此时可以使用自定义注解把这些注解组合到一个注解中,如:

@Caching(put = {
@CachePut(value = "user", key = "#user.id"),
@CachePut(value = "user", key = "#user.username"),
@CachePut(value = "user", key = "#user.email")
})
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface UserSaveCache {
}

这样我们在方法上使用如下代码即可,整个代码显得比较干净。

@UserSaveCache
public User save(User user)

依赖及配置文件如下:

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.3.2.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.2</version>
        </dependency>

        <!--假如项目中配置lettuce池对象,需要添加如下依赖-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

    </dependencies>
spring:
  redis:
    host: 192.168.126.128
    port: 6379
#  redis:
#    cluster: #redis集群配置
#      nodes: 192.168.126.128:8010,192.168.126.128:8011,192.168.126.128:8012,192.168.126.128:8013,192.168.126.128:8014,192.168.126.128:8015
#      max-redirects: 3
#    lettuce: #假如配置这个池,还需要额外添加依赖(commons-pool2)
#      pool:
#        max-active: 16 #最大活动连接数
#        max-idle: 8 #最大空闲连接数

  datasource:
    url: jdbc:mysql://localhost:3306/jt-sso?serverTimezone=Asia/Shanghai&characterEncoding=utf8
    username: root
    password: root
@Service
public class AopMenuServiceImpl implements MenuService {

    @Autowired
    private MenuMapper menuMapper;
    private static final Logger log= LoggerFactory.getLogger(AopMenuServiceImpl.class);

    /**
     * @Cacheable 注解描述的方法为缓存切入点方法,
     * 表示在执行这个方法时,先从缓存去取数据,缓存有则直接返回,
     * 缓存没有则执行数据库的查询操作
     * 默认使用的是JDK的序列化方式
     * 1)value属性的值一般会作为key前缀(建议写模块名)
     * 2)key属性的值一般会作为key的后缀(能够具备唯一性)
     * @param id
     * @return
     */
    @Cacheable(value="menuCache",key = "#id")//menuCache::1
    @Override
    public Menu selectById(Long id) {
        log.info("Get Data from MySql");
        return menuMapper.selectById(id);
    }
    /**
     * @CachePut 注解描述的方法为缓存切入点方法,
     * 当执行完它描述的方法后,会将方法的返回值存储到cache
     * ,key由@CachePut注解指定,值为方法的返回值.
     * @param menu
     * @return
     */
    @CachePut(value="menuCache",key = "#menu.id")
    @Override
    public Menu insertMenu(Menu menu) {
        menuMapper.insert(menu);
        return menu;
    }


    @CachePut(value="menuCache",key = "#menu.id")
    @Override
    public Menu updateMenu(Menu menu) {
        menuMapper.updateById(menu);
        return menu;
    }
}

 @EnableCaching 注解用于描述配置类或启动,告诉底层开启aop方式的缓存应用.

/**
 * @EnableCaching 注解用于描述配置类或启动,
 * 告诉底层开启aop方式的缓存应用.
 */
@EnableCaching
@SpringBootApplication
public class RedisApplication {
    public static void main(String[] args) {
        SpringApplication.run(RedisApplication.class, args);
    }
}

默认Key的序列化方式是字符串的,value的序列化方式是JDK的,不满足业务需求时,可以对K和V的序列化方式进行配置

/**
 * 定义缓存配置类,在此配置类中
 * 修改默认的缓存配置
 */
@Configuration
public class CacheManagerConfig {
    /**
     * 定制缓存管理器对象.
     * @param connectionFactory
     * @return
     */
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory connectionFactory){
       RedisCacheConfiguration redisCacheConfiguration=
               RedisCacheConfiguration.defaultCacheConfig()
                //指定key序列化方式
               .serializeKeysWith(RedisSerializationContext
                       .SerializationPair
                       .fromSerializer(RedisSerializer.string()))
                //指定value的序列化方式
               .serializeValuesWith(RedisSerializationContext
                       .SerializationPair
                       .fromSerializer(RedisSerializer.json()));

       return RedisCacheManager.builder(connectionFactory)
               .cacheDefaults(redisCacheConfiguration).build();
    }
}

Redis 数据持久化

Redis中为了保证在系统宕机(类似进程被杀死)情况下,能更快的进行故障恢复,设计了两种数据持久化方案,分别为rdb和aof方式。

 配置准备工作

https://redis.io/topics/config/该网址下载以下文件,替换原配置文件并进行修改,基于vim打开redis.conf文件,然后注释 bind 127.0.0.1这一行,并修改protected-mode的值修改为no.(java连接redis需要改这两项目)

Rdb方式持久化

Rdb方式是通过快照方式保存redis中key/value的一种机制,默认开启。

 RDB方式的持久化是默认开启的,也可按规则自己配置,打开redis.conf文件进行配置

# 这里表示每隔60s,如果有超过1000个key发生了变更,那么就生成一个新的dump.rdb文件,就是当前redis内存中完整的数据快照,这个操作也被称之为snapshotting(快照)。
save 60 1000

# 持久化 rdb文件遇到问题时,主进程是否接受写入,yes 表示停止写入,如果是no 表示redis继续提供服务。
stop-writes-on-bgsave-error yes
    
# 在进行快照镜像时,是否进行压缩。yes:压缩,但是需要一些cpu的消耗。no:不压缩,需要更多的磁盘空间。
rdbcompression yes
# 一个CRC64的校验就被放在了文件末尾,当存储或者加载rbd文件的时候会有一个10%左右的性能下降,为了达到性能的最大化,你可以关掉这个配置项。
rdbchecksum yes

# 快照的文件名
dbfilename dump.rdb

# 存放快照的目录
dir /var/lib/redis

触发redis中rdb方式: 
1)基于配置文件中的save规则周期性的执行持久化。
2)手动执行了shutdown操作会自动执行rdb方式的持久化。
3)手动调用了save或bgsave指令执行数据持久化。
4)在Master/Slave架构下,当有新的Slave连接Master时,Master会对数据进行rdb方式持久化.

RDB持久化机制有哪些优点(占用空间很小, 性能较好)
RDB文件是经过压缩的二进制文件,占用空间很小,它保存了 Redis 某个时间点的数据集,很适合做冷备.
RDB 非常适用于灾难恢复,它只有一个文件,并且内容都非常紧凑,可以(在加密后)将它传送到别的数据中心。
RDB 方式持久化性能较好,执行持久化时可以fork 一个子进程,由子进程处理保存工作,父进程无须执行任何磁盘 I/O 操作。

RDB持久化机制有哪些缺点(容易造成数据的丢失)
1.RDB方式在服务器故障时容易造成数据的丢失。实际项目中,我们可通过配置来控制持久化的频率。但是,如果频率太频繁,可能会对 Redis 性能产生影响。所以通常可能设置至少5分钟才保存一次快照,这时如果 Redis 出现宕机等情况,则意味着最多可能丢失5分钟数据。
2.RDB 方式使用 fork 子进程进行数据的持久化,子进程的内存是在fork操作时父进程中数据快照的大小,如果数据快照比较大的话,fork 时开辟内存会比较耗时,同时这个fork是同步操作,所以,这个过程会导致父进程无法对外提供服务。

3)RDB持久化过程中的fork操作,可能会导致内存占用加倍,Linux系统fork 子进程采用的是 copy-on-write 的方式(写时复制,修改前先复制),在 Redis 执行 RDB 持久化期间,如果 client 写入数据很频繁,那么将增加 Redis 占用的内存,最坏情况下,内存的占用将达到原先的2倍。

 Aof方式数据持久化

Aof方式是通过记录操作日志的方式,实现redis数据持久化的一种机制,这个机制默认是关闭的,采用的是写后日志(MySQL采用写前日志,可以执行事务回滚)。
如果Rdb和Aof都打开,先采用Rdb进行恢复,再采用Aof进行增量恢复

打开redis.conf文件进行配置

# 是否开启AOF,默认关闭
appendonly yes
# 指定 AOF 文件名
appendfilename appendonly.aof
# Redis支持三种刷写模式:
# appendfsync always #每次收到写命令就立即强制写入磁盘,类似MySQL的sync_binlog=1,是最安全的。但该模式下速度也是最慢的,一般不推荐使用。
appendfsync everysec #每秒钟强制写入磁盘一次,在性能和持久化方面做平衡,推荐该方式。
# appendfsync no     #完全依赖OS的写入,一般为30秒左右一次,性能最好但是持久化最没有保证,不推荐。
    
#在日志重写时,不进行命令追加操作,而只是将其放在缓冲区里,避免与命令的追加造成DISK IO上的冲突。
#设置为yes表示rewrite期间对新写操作不fsync,暂时存在内存中,等rewrite完成后再写入,默认为no,建议yes
no-appendfsync-on-rewrite yes
#当前AOF文件大小是上次日志重写得到AOF文件大小的二倍时,自动启动新的日志重写过程。
auto-aof-rewrite-percentage 100
#当前AOF文件启动新的日志重写过程的最小值,避免刚刚启动Reids时由于文件尺寸较小导致频繁的重写。
auto-aof-rewrite-min-size 64mb

 AOF持久化机制有哪些优点?(数据更加可靠, 易修复,)
AOF 比 RDB更加可靠。你可以设置不同的 fsync 策略(no、everysec 和 always)。默认是 everysec,在这种配置下,redis 仍然可以保持良好的性能,并且就算发生故障停机,也最多只会丢失一秒钟的数据。
AOF文件是一个基于纯追加模式的日志文件。即使日志因为某些原因而包含了未写入完整的命令(比如写入时磁盘已满,写入中途停机等等), 我们也可以使用 redis-check-aof 工具也可以轻易地修复这种问题。
当 AOF文件太大时,Redis 会自动在后台进行重写。重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。整个重写是绝对安全,因为重写是在一个新的文件上进行,同时 Redis 会继续往旧的文件追加数据。当新文件重写完毕,Redis 会把新旧文件进行切换,然后开始把数据写到新文件上。
AOF 文件有序地保存了对数据库执行的所有写入操作,以 Redis 协议的格式保存, 因此 AOF 文件的内容非常容易被人读懂, 对文件进行分析也很轻松。如果你不小心执行了 FLUSHALL 命令把所有数据刷掉了,但只要 AOF 文件没有被重写,那么只要停止服务器, 移除 AOF 文件末尾的 FLUSHALL 命令, 并重启 Redis , 就可以将数据集恢复到 FLUSHALL 执行之前的状态。

AOF持久化机制有哪些缺点(性能相对比较差)
对于相同的数据集,AOF 文件的大小一般会比 RDB 文件大。根据所使用的 fsync 策略(同步刷盘策略),AOF 的速度可能会比 RDB 慢。通常 fsync 设置为每秒一次就能获得比较高的性能,而关闭 fsync 可以让 AOF 的速度和 RDB 一样快。AOF 可能会因为个别命令的原因,导致 AOF 文件在重新载入时,无法将数据集恢复成保存时的原样。

Redis事务

MySQL采用的是悲观锁,Redis为了性能,采用了乐观锁方式进行事务控制,减少阻塞,减少对CPU的切换.

事务的四大特性:ACID

原子性: 没有回滚机制
一致性:从一个一致性状态切换到另一个一致性状态。
持久性
隔离性:事务之间必须相互隔离的,互不影响。

1、事务开始

multi命令的执行,multi命令会将客户端状态的 flags 属性中打开redis_multi 标识来完成的。

2、命令入队

如果客 户端发送的命令为MULTIEXECWATCHDISCARD中的一个,立即执行这个命令,否则将命令放入一 个事务队列里面,然后向客户端返回 QUEUED 回复

①如果客户端发送的命令为 exec、DISCARD、WATCHMULTI 四个命令的其中一个,那么服务器 立即执行这个命令。

②如果客户端发送的是四个命令以外的其他命令,那么服务器并不立即执行这个命令。

首先检查此命令的格式是否正确,如果不正确,服务器会在客户端状态 flags 属 性关闭 REDIS_MULTI 标识,并且返回错误信息给客户端。

如果正确,将这个命令放入一个事务队列里面,然后向客户端返回 QUEUED 回复事务队列是按照FIFO的方式保存入队的命令

3、事务执行

客户端发送 EXEC 命令,服务器执行 EXEC 命令逻辑。

如果客户端状态的 flags 属性不包含 REDIS_MULTI 标识,或者包含 REDIS_DIRTY_CAS 或者 REDIS_DIRTY_EXEC 标识,那么就直接取消事务的执行。

否则客户端处于事务状态(flags REDIS_MULTI 标识),服务器会遍历客户端的事务队列,然 后执行事务队列中的所有命令,最后将返回结果全部返回给客户端;

redis 不支持事务回滚机制,但是它会检查每一个事务中的命令是否错误。Redis 事务不支持检查那些程序员自己逻辑错误。例如对 String 类型的数据库键执行对 HashMap 类型

的操作!

  • multi 开启事务
  • exec 提交事务
  • discard 取消事务
  • watch 监控,如果监控的值发生变化,则提交事务时会失败
  • unwatch 去掉监控

Redis主从复制(读多写少 不支持高可用)

在这里插入图片描述

 设计一主从架构,一个Master,两个Slave,其中Master负责Redis读写操作,并将数据同步到Slave,Slave只负责读.
启动Master再启动Slave,Master会将rdb文件同步给对应的Slave,当Master进行写操作时,会将对应的Aof文件同步给对应的Slave

Redis哨兵模式(观察者模式,高可用)

在这里插入图片描述

 哨兵Sentinel是Redis主从架构模式下,实现高可用性的一种机制。
由一个或多个Sentinel实例组成的Sentinel系统可以监视任意多个主服务器,以及这些主服务器属下的所有从服务器,并在被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器,然后由新的主服务器代替已下线的主服务器继续处理命令请求。

Cluter模式:Cluster模式是用得比较多的模式,它支持多主多从,这种模式会按照key进行糟位的分配,可以使得不同的key分散到不同的主节点上,利用这种模式可以使得整个集群支持更大的数据容量,同时每个主节点可以拥有自己的多个从节点,如果该主节点宕机,会从它的从节点中选举一个新的主节点
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值