Redis基础

技术栈作用

NoSql作用一

  • 分布式服务中,session的存放问题。
  • 用户请求信息存放在了第一台服务器,第二次请求到了第二台服务器,如果保证session共享。


Nosql作用二

  • 需要频繁读取的数据,存放在缓存中,减少和数据库交互IO压力
  • 存储在内存中,不用进行IO交互。速度快。

Redis 6

1. 安装Linux系统Redis

根据Linux版本安装相应的Redis版本,如果安装失败,退回之前的版本再试。

tar -zxf redis-4.0.11.tar.gz
  • 进入解压后的src目录,执行安装

# 安装
make install
  • 如果安装失败,尝试回退版本。

便捷操作

  • 将客户端和服务端从src目录中移动出来。
  • 新建binetc文件夹。

# 先进入src目录,然后执行移动命令
mv mkreleasehdr.sh redis-benchmark redis-check-aof redis-check-rdb redis-cli redis-server /usr/local/redis/redis-4.0.11/bin
  • 将配置文件redis.conf移动到etc目录。
  • 修改配置文件
# 设置ip
bind 127.0.0.1
# 设置密码
requirepass 123456
# 设置后台启动
daemonize yes
  • 指定配置文件启动Redis服务
# 进入bin目录
redis-server /usr/local/redis/redis-4.0.11/etc/redis.conf

1.1 使用Docker安装Redis镜像

启动redis客户端

docker exec -it redis redis-cli

2. Redis基础知识

  • Redis默认有16个数据库,默认使用第0个数据库。可以使用select切换数据库。
[root@rm ~]# docker exec -it redis redis-cli
127.0.0.1:6379> select 3
(error) NOAUTH Authentication required.   # 需要输入redis密码进行认证
127.0.0.1:6379> auth 'bjzr123'
OK 
127.0.0.1:6379> select 3         #切换为第三个数据库
OK
127.0.0.1:6379[3]> 
  • 使用dbsize查看当前数据库数据数量
127.0.0.1:6379[3]> dbsize
(integer) 0
127.0.0.1:6379[3]> set k1 v1
OK
127.0.0.1:6379[3]> dbsize
(integer) 1
127.0.0.1:6379[3]> select 7
OK
127.0.0.1:6379[7]> dbsize
(integer) 0
127.0.0.1:6379[7]> 
  • 使用 keys * 查看所有的key
127.0.0.1:6379[7]> select 3
OK
127.0.0.1:6379[3]> keys *
1) "k1"
  • 使用flushdb清空当前数据库所有数据
127.0.0.1:6379[3]> flushdb
OK
127.0.0.1:6379[3]> dbsize
(integer) 0
127.0.0.1:6379[3]> keys *
(empty array)
  • 使用flushall清空所有数据库所有数据
127.0.0.1:6379[3]> set name v1
OK
127.0.0.1:6379[3]> select 0
OK
127.0.0.1:6379> flushall
OK
127.0.0.1:6379> select 3
OK
127.0.0.1:6379[3]> keys *
(empty array)

Redis是单线程的

Redis是很快的,官方表示,Redis是基于内存操作,CPU不是Redis性能瓶颈,Redis的瓶颈是根据机器的内存和网络带宽,既然可以使用单线程来实现,所有就使用了单线程了!

  • 从6.0开始支持多线程。
  • 多线程上下文切换同样会耗时。

3. Redis常用数据类型

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

3.1 Redis常用命令

exists

  • 查看指定key在当前数据库是否存在
127.0.0.1:6379> set name zhangsan
OK
127.0.0.1:6379> set name1 lisi
OK
127.0.0.1:6379> exists name
(integer) 1
127.0.0.1:6379> exists name3
(integer) 0

del

  • 从当前数据库移除指定的key
127.0.0.1:6379> keys *
1) "k3"
2) "k2"
3) "k1"
127.0.0.1:6379> del k2
(integer) 1
127.0.0.1:6379> keys *
1) "k3"
2) "k1"

move

  • 将指定数据从当前数据库移动到指定数据库。
  • 如果指定数据库已经存在该key,则移动失败。
127.0.0.1:6379> move k1 4
(integer) 1
127.0.0.1:6379> select 4
OK
127.0.0.1:6379[4]> keys *
1) "k1"

expire

  • 给指定数据设置过期时间,单位(秒).
  • ttl 查看指定key还有多长时间过期.
127.0.0.1:6379[3]> set k1 v1
OK
127.0.0.1:6379[3]> expire k1 10
(integer) 1
127.0.0.1:6379[3]> ttl k1
(integer) 7
127.0.0.1:6379[3]> ttl k1
(integer) 5
127.0.0.1:6379[3]> ttl k1
(integer) 4
127.0.0.1:6379[3]> ttl k1
(integer) 3
127.0.0.1:6379[3]> keys *
(empty array)

type

  • 查看当前key的类型
127.0.0.1:6379> set name zhangsan
OK
127.0.0.1:6379> type name
string

3.1 String类型

修改字符串内容

127.0.0.1:6379> set name zhangsan
OK
127.0.0.1:6379> type name
string
127.0.0.1:6379> keys *
1) "name"
127.0.0.1:6379> append name lisi   # 给value后面追加字符串,如果当前key不存在,相当于set命令
(integer) 12
127.0.0.1:6379> get name
"zhangsanlisi"
127.0.0.1:6379> strlen name        # 查看当前key的value长度
(integer) 12

自增和自减

127.0.0.1:6379> set views 0
OK
127.0.0.1:6379> incr views    #增加1
(integer) 1
127.0.0.1:6379> incr views
(integer) 2
127.0.0.1:6379> get views
"2"
127.0.0.1:6379> decr views    #减少1
(integer) 1
127.0.0.1:6379> get views
"1"

127.0.0.1:6379> incrby views 10  # 设置步长
(integer) 11
127.0.0.1:6379> decrby views 20
(integer) -9
127.0.0.1:6379> get views
"-9"

获取字符串一部分内容

127.0.0.1:6379> set k1 hello,world
OK
127.0.0.1:6379> getrange k1 0 4   
"hello"
127.0.0.1:6379> get k1    # 获取字符串一部分不会修改原始数据
"hello,world"

127.0.0.1:6379> set k1 hello,world
OK
127.0.0.1:6379> get k1
"hello,world"
127.0.0.1:6379> setrange k1 6 java!
(integer) 11
127.0.0.1:6379> get k1    # 设置字符串一部分会修改原始数据
"hello,java!"

设置过期时间

  • setex (set with expire)
  • setnx (set if not exist)
127.0.0.1:6379> setex k1 30 v1        # 添加key的同时就设置该数据过期时间
OK
127.0.0.1:6379> ttl k1
(integer) 26
127.0.0.1:6379> get k1
"v1"
127.0.0.1:6379> setnx name zhangsan   # 如果不存在该key,才设置value
(integer) 1
127.0.0.1:6379> keys *
1) "name"
127.0.0.1:6379> ttl k1
(integer) -2
127.0.0.1:6379> setnx name lisi       # 如果不存在该key,才设置value,已经存在key=name,所以设置value失败
(integer) 0
127.0.0.1:6379> get name
"zhangsan"

批量设置值

127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3
OK
127.0.0.1:6379> keys *
1) "k3"
2) "k2"
3) "k1"
127.0.0.1:6379> mget k1 k2 k3
1) "v1"
2) "v2"
3) "v3"
-----------------------------------------------
# 如果不存在则批量设置(原子性,要么都成功,要么都失败)
127.0.0.1:6379> msetnx k1 v9 k4 v4 k5 v5
(integer) 0
127.0.0.1:6379> get k4
(nil)

存储对象

  • 这里的key是一个巧妙的设计 user:{id}:{filed}。
127.0.0.1:6379> mset user:1:name zhangsan user:1:age 15 user:2:name lisi user:2:age 20
OK
127.0.0.1:6379> keys *
1) "user:2:age"
2) "user:2:name"
3) "user:1:name"
4) "user:1:age"

getset

  • 先get,再set。
127.0.0.1:6379> getset db redis
(nil)
127.0.0.1:6379> get db
"redis"
127.0.0.1:6379> getset db mysql
"redis"
127.0.0.1:6379> get db
"mysql"

3.1 List类型

4. Redis基本的事务操作

事务 ----ACID原则

  • 原子性。要么都成功,要么都失败。

Redis单条命令是保证原子性的,比如msetnx。但是Redis事务是不保证原子性的。

4.1 Redis事务本质

本质是一组命令的集合。

  • 一个事务中的所有命令都会被序列化,在事务执行过程中,按照顺序依次执行。
  • 顺序性(按照顺序执行)、一次性(只执行一次)、排他性(不允许其他命令干扰)。
  • Redis事务没有隔离级别的概念,不会出现幻读、脏读、不可重复读。
  • 所有命令在事务中没有被立刻执行,只有当发起执行指令时,才会依次执行。

4.2 Redis事务操作

  • 开启事务(multi)
  • 命令入队
  • 执行指令(exec)

事务执行完毕该事务就会消失,再次使用需要重新开启事务。

127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> get k2
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) OK
3) "v2"

放弃事务

127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k4 v4
QUEUED
127.0.0.1:6379(TX)> discard
OK
127.0.0.1:6379> get k4
(nil)

编译型异常(代码有问题,命令出错),事务中所有指令都不会执行

127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> sss k3 v3
(error) ERR unknown command `sss`, with args beginning with: `k3`, `v3`, 
127.0.0.1:6379(TX)> set k4 v4
QUEUED
127.0.0.1:6379(TX)> exec
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> keys *
(empty array)

运行时异常(1/0),事务中出现语法性错误,其他命令可以正常执行

  • 没有原子性。
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 zhangsan
QUEUED
127.0.0.1:6379(TX)> decr k1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) (error) ERR value is not an integer or out of range
3) OK
127.0.0.1:6379> get k1
"zhangsan"

4.3 Redis实现乐观锁

监控

悲观锁

  • 无论做什么都加锁,影响性能。synchronized默认是悲观锁。

乐观锁

  • 无论做什么事都不会加锁,在更新数据的时候,判断在此期间是否有人改动过该数据。
  • mysql加一个version字段。更新的时候比较version。

加监控正常执行

127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> set zhangsan 100
OK
127.0.0.1:6379> set lisi 0
OK
127.0.0.1:6379> watch zhangsan
OK
127.0.0.1:6379> multi                           # 开启事务
OK
127.0.0.1:6379(TX)> decrby zhangsan 10
QUEUED
127.0.0.1:6379(TX)> incrby lisi 10
QUEUED
127.0.0.1:6379(TX)> exec
1) (integer) 90
2) (integer) 10

多线程操作数据

线程1开启监控,开启事务,此时还没有执行事务。

  • 在事务执行期间,张三或者李四的金额无论一方发生变化,事务都会执行失败。
  • 使用watch可以作为Redis的乐观锁操作。
127.0.0.1:6379> get zhangsan
"90"
127.0.0.1:6379> get lisi
"10"
127.0.0.1:6379> watch zhangsan lisi       # 可以监视多个key
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> decrby zhangsan 20
QUEUED
127.0.0.1:6379(TX)> incrby lisi 20
QUEUED

线程2修改zhangsan金额

127.0.0.1:6379> get zhangsan
"90"
127.0.0.1:6379> get lisi
"10"
127.0.0.1:6379> set zhangsan 70
OK

线程1在线程2修改完金额后才执行事务。

  • 事务执行失败
127.0.0.1:6379> get zhangsan
"90"
127.0.0.1:6379> get lisi
"10"
127.0.0.1:6379> watch zhangsan lisi       # 可以监视多个key
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> decrby zhangsan 20
QUEUED
127.0.0.1:6379(TX)> incrby lisi 20
QUEUED
127.0.0.1:6379(TX)> exec
(nil)

127.0.0.1:6379> get zhangsan
"70"
127.0.0.1:6379> get lisi
"10"

事务执行失败后的操作

  • 重新监视key,再执行一遍事务流程。
  • 事务执行失败,此watch已经失效,需要重新watch。
127.0.0.1:6379> watch zhangsan lisi       # 重新监视多个key
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> decrby zhangsan 10
QUEUED
127.0.0.1:6379(TX)> incrby lisi 10
QUEUED

5. SpringBoot集成Redis

新建一个SpringBoot、Maven项目

选择SpringBoot版本及勾选所需依赖

查看依赖包关系

  • SpringBoot 2.x 之后,原来使用的jedis被替换成了lettuce。
    • jedis:采用的直连,多个线程操作的话,是不安全的,如果想要避免不安全的,使用jedis pool连接池,类似BIO模式(阻塞)。
    • lettuce:采用netty(dubbo采用的也是netty),实例可以在多个线程中进行共享,不存在线程不安全的情况!可以减少线程数量了,类似NIO模式。
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
--------------------------------------------------------------
<dependency>
  <groupId>org.springframework.data</groupId>
  <artifactId>spring-data-redis</artifactId>
  <version>2.6.2</version>
  <scope>compile</scope>
</dependency>
<dependency>
  <groupId>io.lettuce</groupId>
  <artifactId>lettuce-core</artifactId>
  <version>6.1.6.RELEASE</version>
  <scope>compile</scope>
</dependency>

查看Redis配置

  • SpringBoot所有配置类都会绑定一个自动配置类。
  • 自动配置类会绑定一个properties文件。

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)   //绑定配置类
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {

        //使用RedisTemplate来操作Redis,Redis对象都需要序列化
	@Bean
	@ConditionalOnMissingBean(name = "redisTemplate")    //如果该对象不存在才创建,可以自己创建覆盖默认加载类
	@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
	public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
		RedisTemplate<Object, Object> template = new RedisTemplate<>();
		template.setConnectionFactory(redisConnectionFactory);
		return template;
	}
      
        //由于String是Redis最常使用的类型,单独创建了一个Bean。
	@Bean
	@ConditionalOnMissingBean
	@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
	public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
		return new StringRedisTemplate(redisConnectionFactory);
	}

}

application.yml可以配置的相关Redis配置

RedisProperties.class

配置端口及密码

spring:
  redis:
    host: 8.141.52.45
    port: 6379
    password: xxxxxx

测试

@SpringBootTest
class RedisApplicationTests {

    @Autowired
    RedisTemplate<String, String> redisTemplate;

    @Test
    void contextLoads() {
        //opsForValue:操作String数据类型
        //opsForList:操作List数据类型
        redisTemplate.opsForValue().set("name","zhansgan");
        System.out.println(redisTemplate.opsForValue().get("name"));
    }
}

5.1 Redis序列化问题

Redis如果想要存储对象,对象必须序列化才能成功存储,否则会报错。

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Person {
    private String name;
    private int age;
}
-------------------------------------------------------------------
@Test
void contextLoads() {
    Person person = new Person("张三", 10);
    redisTemplate.opsForValue().set("name",person);
    System.out.println(redisTemplate.opsForValue().get("name"));
}

实现序列化接口后

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Person implements Serializable {
    private String name;
    private int age;
}

实际开发中大多情况下不会直接存入对象,而是转换为JSON字符串存储

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.73</version>
</dependency>
@Test
void contextLoads() {
    Person person = new Person("张三", 10);
    String jsonString = JSONObject.toJSONString(person);

    redisTemplate.opsForValue().set("name",jsonString);
    System.out.println(redisTemplate.opsForValue().get("name"));
}

5.2 自定义RedisTemplate

@Bean
@SuppressWarnings("all")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory){
    //为了开发方便,使用<String, Object>
    RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
    template.setConnectionFactory(factory);

    //Json序列化配置
    Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
    ObjectMapper om = new ObjectMapper();
    om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
    om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
    jackson2JsonRedisSerializer.setObjectMapper(om);

    //String序列化配置
    StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

    //key和hash的key都采用String的序列化配置
    template.setKeySerializer(stringRedisSerializer);
    template.setHashKeySerializer(stringRedisSerializer);

    //value和hash的value采用Json的序列化配置
    template.setValueSerializer(jackson2JsonRedisSerializer);
    template.setHashValueSerializer(jackson2JsonRedisSerializer);

    template.afterPropertiesSet();

    return template;
}

真实开发中,方便使用,一般自定义工具类

@Component
public final class RedisUtil {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    /**
     * 指定缓存失效时间
     *
     * @param key  键
     * @param time 时间(秒)
     */
    public boolean expire(String key, long time, TimeUnit timeUnit) {
        try {
            if (time > 0) {
                redisTemplate.expire(key, time, timeUnit);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 根据key 获取过期时间
     *
     * @param key 键 不能为null
     * @return 时间(秒) 返回0代表为永久有效
     */
    public long getExpire(String key) {
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }


    /**
     * 判断key是否存在
     *
     * @param key 键
     * @return true 存在 false不存在
     */
    public boolean hasKey(String key) {
        try {
            return redisTemplate.hasKey(key);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 删除缓存
     *
     * @param key 可以传一个值 或多个
     */
    @SuppressWarnings("unchecked")
    public void del(String... key) {
        if (key != null && key.length > 0) {
            if (key.length == 1) {
                redisTemplate.delete(key[0]);
            } else {
                redisTemplate.delete((Collection<String>) CollectionUtils.arrayToList(key));
            }
        }
    }


    // ============================String=============================

    /**
     * 普通缓存获取
     *
     * @param key 键
     * @return 值
     */
    public Object get(String key) {
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }

    /**
     * 普通缓存放入
     *
     * @param key   键
     * @param value 值
     * @return true成功 false失败
     */

    public boolean set(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 普通缓存放入并设置时间
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒) time要大于0 如果time小于等于0 将设置无限期
     * @return true成功 false 失败
     */

    public boolean set(String key, Object value, long time) {
        try {
            if (time > 0) {
                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
            } else {
                set(key, value);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 递增
     *
     * @param key   键
     * @param delta 要增加几(大于0)
     */
    public long incr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递增因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, delta);
    }


    /**
     * 递减
     *
     * @param key   键
     * @param delta 要减少几(小于0)
     */
    public long decr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递减因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, -delta);
    }


    // ================================Map=================================

    /**
     * HashGet
     *
     * @param key  键 不能为null
     * @param item 项 不能为null
     */
    public Object hget(String key, String item) {
        return redisTemplate.opsForHash().get(key, item);
    }

    /**
     * 获取hashKey对应的所有键值
     *
     * @param key 键
     * @return 对应的多个键值
     */
    public Map<Object, Object> hmget(String key) {
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * HashSet
     *
     * @param key 键
     * @param map 对应多个键值
     */
    public boolean hmset(String key, Map<String, Object> map) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * HashSet 并设置时间
     *
     * @param key  键
     * @param map  对应多个键值
     * @param time 时间(秒)
     * @return true成功 false失败
     */
    public boolean hmset(String key, Map<String, Object> map, long time, TimeUnit timeUnit) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            if (time > 0) {
                expire(key, time, timeUnit);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 向一张hash表中放入数据,如果不存在将创建
     *
     * @param key   键
     * @param item  项
     * @param value 值
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 向一张hash表中放入数据,如果不存在将创建
     *
     * @param key   键
     * @param item  项
     * @param value 值
     * @param time  时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value, long time, TimeUnit timeUnit) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            if (time > 0) {
                expire(key, time, timeUnit);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 删除hash表中的值
     *
     * @param key  键 不能为null
     * @param item 项 可以使多个 不能为null
     */
    public void hdel(String key, Object... item) {
        redisTemplate.opsForHash().delete(key, item);
    }


    /**
     * 判断hash表中是否有该项的值
     *
     * @param key  键 不能为null
     * @param item 项 不能为null
     * @return true 存在 false不存在
     */
    public boolean hHasKey(String key, String item) {
        return redisTemplate.opsForHash().hasKey(key, item);
    }


    /**
     * hash递增 如果不存在,就会创建一个 并把新增后的值返回
     *
     * @param key  键
     * @param item 项
     * @param by   要增加几(大于0)
     */
    public double hincr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, by);
    }


    /**
     * hash递减
     *
     * @param key  键
     * @param item 项
     * @param by   要减少记(小于0)
     */
    public double hdecr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, -by);
    }


    // ============================set=============================

    /**
     * 根据key获取Set中的所有值
     *
     * @param key 键
     */
    public Set<Object> sGet(String key) {
        try {
            return redisTemplate.opsForSet().members(key);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }


    /**
     * 根据value从一个set中查询,是否存在
     *
     * @param key   键
     * @param value 值
     * @return true 存在 false不存在
     */
    public boolean sHasKey(String key, Object value) {
        try {
            return redisTemplate.opsForSet().isMember(key, value);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 将数据放入set缓存
     *
     * @param key    键
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSet(String key, Object... values) {
        try {
            return redisTemplate.opsForSet().add(key, values);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }


    /**
     * 将set数据放入缓存
     *
     * @param key    键
     * @param time   时间(秒)
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSetAndTime(String key, long time, TimeUnit timeUnit, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().add(key, values);
            if (time > 0) {
                expire(key, time, timeUnit);
            }
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }


    /**
     * 获取set缓存的长度
     *
     * @param key 键
     */
    public long sGetSetSize(String key) {
        try {
            return redisTemplate.opsForSet().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }


    /**
     * 移除值为value的
     *
     * @param key    键
     * @param values 值 可以是多个
     * @return 移除的个数
     */

    public long setRemove(String key, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().remove(key, values);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }


    /**
     * 获取list缓存的内容
     *
     * @param key   键
     * @param start 开始
     * @param end   结束 0 到 -1代表所有值
     */
    public List<Object> lGet(String key, long start, long end) {
        try {
            return redisTemplate.opsForList().range(key, start, end);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 获取list缓存的长度
     *
     * @param key 键
     */
    public long lGetListSize(String key) {
        try {
            return redisTemplate.opsForList().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }


    /**
     * 通过索引 获取list中的值
     *
     * @param key   键
     * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
     */
    public Object lGetIndex(String key, long index) {
        try {
            return redisTemplate.opsForList().index(key, index);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }


    /**
     * 将list放入缓存
     */
    public boolean lSet(String key, Object value) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 将list放入缓存
     * @param time  时间(秒)
     */
    public boolean lSet(String key, Object value, long time,TimeUnit timeUnit) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            if (time > 0) {
                expire(key, time,timeUnit);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

    }


    /**
     * 将list放入缓存
     */
    public boolean lSet(String key, List<Object> value) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

    }


    /**
     * 将list放入缓存
     * @param time  时间(秒)
     */
    public boolean lSet(String key, List<Object> value, long time,TimeUnit timeUnit) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            if (time > 0) {
                expire(key, time,timeUnit);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 根据索引修改list中的某条数据
     *
     * @param key   键
     * @param index 索引
     * @param value 值
     * @return
     */
    public boolean lUpdateIndex(String key, long index, Object value) {
        try {
            redisTemplate.opsForList().set(key, index, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 移除N个值为value
     *
     * @param key   键
     * @param count 移除多少个
     * @param value 值
     * @return 移除的个数
     */
    public long lRemove(String key, long count, Object value) {
        try {
            Long remove = redisTemplate.opsForList().remove(key, count, value);
            return remove;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }

    }
    
    public long pfadd(String key, String value) {
        return redisTemplate.opsForHyperLogLog().add(key, value);
    }

    public long pfcount(String key) {
        return redisTemplate.opsForHyperLogLog().size(key);
    }

    public void pfremove(String key) {
        redisTemplate.opsForHyperLogLog().delete(key);
    }

    public void pfmerge(String key1, String key2) {
        redisTemplate.opsForHyperLogLog().union(key1, key2);
    }

}

使用封装后的工具类

 @Autowired
 RedisUtil redisUtil;

@Test
void contextLoads() {
    Person person = new Person("张三", 10);
    String jsonString = JSONObject.toJSONString(person);
    redisUtil.set("name",jsonString);
    String name = (String) redisUtil.get("name");
    System.out.println(name);
}

6. Redis.conf 配置详解

配置不区分大小写

网络

bind 127.0.0.1   #绑定的ip
protected-mode yes  #开启远程保护模式,只允许主机使用
port 6379      #端口

通用GENERAL

daemonize yes  # 以守护进程的方式运行(后台运行),默认是no
pidfile /var/run/redis.pid  # 如果以守护进程模式运行,需要指定一个pid文件

# Specify the server verbosity level.
# This can be one of:
# debug (a lot of information, useful for development/testing)
# verbose (many rarely useful info, but not a mess like the debug level)
# notice (moderately verbose, what you want in production probably)
# warning (only very important / critical messages are logged)
loglevel notice            # 日志级别
logfile ""                 # 日志的文件名
databases 16               # 默认16个数据库

快照 SNAPSHOTTING

和持久化有关,在规定的时间内,执行了多少次操作,则会持久化到文件 .rdb .aof

  • redis是内存数据库,如果没有持久化,断电即失。
# 900秒内,如果至少有1个key进行了修改,就进行持久化操作
save 900 1
# 300秒内,如果至少有10个key进行了修改,就进行持久化操作
save 300 10
# 60秒内,如果至少有10000个key进行了修改,就进行持久化操作
save 60 10000

stop-writes-on-bgsave-error yes     # 持久化出错,是否会继续工作。 默认yes
rdbcompression yes                  # 是否压缩rdb文件,需要消耗cpu资源。
rdbchecksum yes                     # 保存rdb文件的时候,是否对其进行错误校验检查
dir ./                              # rdb文件保存的目录

复制 REPLICATION

安全 SECURITY

requirepass foobared  # 设置密码

限制 LIMITS

maxclients 10000  # 客户端最大连接数量
maxmemory <bytes>  # 最大内存容量
maxmemory-policy noeviction  # redis内存达到上限之后的策略
  # 可选策略 
  # volatile-lru -> remove the key with an expire set using an LRU algorithm
  # allkeys-lru -> remove any key according to the LRU algorithm
  # volatile-random -> remove a random key with an expire set
  # allkeys-random -> remove a random key, any key
  # volatile-ttl -> remove the key with the nearest expire time (minor TTL)
  # noeviction -> don't expire at all, just return an error on write operations

AOF配置 APPEND ONLY MODE

appendonly no  # 默认不开启aof,默认使用rdb方式持久化。
appendfilename "appendonly.aof"  # 持久化的文件名字

# appendfsync always   # 每次修改都会同步
appendfsync everysec   # 每秒执行一次同步,可能会丢失这一秒的数据
# appendfsync no       # 让操作系统决定何时同步

7. Redis持久化

7.1 RDB (Redis DataBase)

  • 在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里。

  • Redis会单独创建 ( fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的。这就确保了极高的性能。如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。

  • RDB的缺点是最后一次持久化后的数据可能丢失。

  • 默认是RDB模式,大多情况下足够使用。

  • 生产环境一般会对dump.rdb文件进行备份。

  • RDB保存的文件dump.rdb

生成dump.rdb文件机制

  • save规则满足下,生成dump.rdb文件
  • 执行flushall命令,生成dump.rdb文件
  • 退出redis-server服务,生成dump.rdb文件

如何将rdb文件恢复进内存

  • 只需要将rdb文件放在redis启动目录,redis启动时会自动检查dump.rdb文件并加载进内存。

优点︰
1、适合大规模的数据恢复!
2、对数据的完整性要不高!
缺点:
1、需要一定的时间间隔进程操作!如果redis意外宕机了,这个最后一次修改数据就没有的了!
2、 fork进程的时候,会占用一定的内容空间!!

7.2 AOF (Append Only File)

将所有的命令都记录下来,恢复的时候再执行一遍所有命令。

  • 以日志的形式来记录每个写操作,将Redis执行过的所有指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据。
  • 换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。
  • AOF保存的文件appendonly.aof
appendonly no  # 默认不开启aof,默认使用rdb方式持久化。
  • 默认是不开启的,我们需要手动进行配置!我们只需要将appendonly 改为yes就开启了aof! 重启redis, 配置文件就生效了!
  • 如果这个aof文件有错误,这时候redis是启动不起来的。
  • 我们需要修复这个aof文件redis给我们提供了一个工具 redis-check-aof --fix

优点︰
1、每一次修改都同步,文件的完整会更加好!
2、每秒同步一次,可能会丢失一秒的数据.
缺点︰
1、相对于数据文件来说,aof远远大于rdb,修复的速度也比 rdb慢!
2、Aof运行效率也要比 rdb 慢,所以我们redis默认的配置就是rdb持久化!

8. Redis发布订阅

  • Redis发布订阅(pub/sub)是一种消息通信模式︰发送者(pub)发送消息,订阅者(sub)接收消息。微信、微博、关注系统!
  • Redis 客户端可以订阅任意数量的频道。

下图展示了频道 channel1 , 以及订阅这个频道的三个客户端 —— client2 、 client5 和 client1 之间的关系:

当有新消息通过 PUBLISH 命令发送给频道 channel1 时, 这个消息就会被发送给订阅它的三个客户端:

8.1 演示

开启两个客户端

订阅两个频道

127.0.0.1:6379> subscribe cctv1 cctv2
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "cctv1"
3) (integer) 1
1) "subscribe"
2) "cctv2"
3) (integer) 2

向频道中发送数据

127.0.0.1:6379> publish cctv1 chunwan
(integer) 1

订阅者就会收到消息

1) "message"
2) "cctv1"    # 订阅的频道
3) "chunwan"  # 频道发布的内容

使用场景:

  • 1、实时消息系统!
  • 2、实时聊天室!(频道当做聊天室,将信息回显给所有人即可!)
  • 3、订阅,关注系统都是可以的!

稍微复杂的发布订阅使用消息中间件。

127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:0
master_failover_state:no-failover
master_replid:2051d7751cd44069e9df2178a0bdfca86c5849ee
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0

9. Redis主从复制

概念

主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(masterleader),后者称为从节点(slave/follower);

  • 数据的复制是单向的,只能由主节点到从节点。Master以写为主,Slave以读为主。
  • 默认情况下,每台Redis服务器都是主节点;且一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。

主从复制的作用主要包括:
1、数据冗余︰主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
2、故障恢复∶当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。
3、负载均衡︰在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
4、高可用基石∶除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。

一般来说,要将Redis运用于工程项目中,只使用一台Redis是万万不能的,原因如下:
1、从结构上,单个Redis服务器会发生单点故障,并且一台服务器需要处理所有的请求负载,压力较大;
2、从容量上,单个Redis服务器内存容量有限,就算一台Redis服务器内存容量为256G,也不能将所有内存用作Redis存储内存,一般来说,单台Redis最大使用内存不应该超过20G。

电商网站上的商品,一般都是一次上传,无数次浏览的,说专业点也就是"多读少写",可以使用以下架构。

配置一个Redis集群,最少需要3台服务器,1主2从。之后的哨兵模式需要在从机中选举,只有一台从机是不够的。

9.1 配置主从复制环境

只配置从库即可,不用配置主库。因为Redis服务默认就是主库。

127.0.0.1:6379> info replication
# Replication
role:master                           # 角色:主机
connected_slaves:0                    # 连接从机数量:0
master_failover_state:no-failover
master_replid:2051d7751cd44069e9df2178a0bdfca86c5849ee
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0

复制3份配置文件,分别修改内容

主机使用Docker安装的Redis--6379端口,从机使用这三个配置文件启动的Redis-Server服务。

修改配置文件

pidfile /var/run/redis_6382.pid
port 6382
logfile "6382.log"
dbfilename dump82.rdb
masterauth bjzr123   #如果主节点配置了密码,从节点需要配置主节点密码

启动从机

从机启动成功

启动不同端口的客户端

redis-cli -p 6380
redis-cli -p 6381
redis-cli -p 6382

查看每个节点信息(默认都是主节点)



9.2 配置主从复制

一般情况下,只需要配置从机即可。(认老大)

slaveof 127.0.0.1 6379  #主机的ip和端口

# 也可以在从机配置文件中写死主机的ip和端口。永久配置
# slaveof <masterip> <masterport>

# 命令行配置的,在Redis重启后就失去主从关系。临时配置

此时查看主机信息

细节

主机能写,从机只能读

主机写的内容会复制到所有从机

127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6380> keys *
1) "k1"
127.0.0.1:6381> keys *
1) "k1"

主机宕机

  • 主机下线,没有配置哨兵模式的情况下,所有从机还是从机,只能读不能写。

  • 主机再次上线,配置不变的情况下。再次向主机写入值,所有从机还是能再次获取到值的。

从机宕机

  • 命令行认老大时:从机宕机,再次上线会自动变成主机。
  • 配置文件认老大:从机再次上线后依然能获取到主机的所有数据。

复制原理

Slave启动成功连接到master后会发送一个sync(同步)命令

Master接到命令,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后,master将传送整个数据文件到slave,并完成一次完全同步。

  • 全量复制︰slave服务在接收到数据库文件数据后,将其存盘并加载到内存中。
  • 增量复制:Master继续将新的所有收集到的修改命令依次传给slave,完成同步

但是只要是重新连接master,一次完全同步(全量复制)将被自动执行。

层层链路模型

80依旧是从机

slaveof no one  # 如果主机断了,从机可以使用该指令使自己变为主机

主机断开,1从机设置为主机,其余的从机可以手动归属于1从机(主机)的从机。此时主机重新上线,依然是主机,不过不再拥有从机。

10. 哨兵模式

概述

主从切换技术的方法是∶当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式。

  • Redis从2.8开始正式提供了Sentinel (哨兵)架构来解决这个问题。
  • 谋朝篡位的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库。
  • 哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。

哨兵的作用

  • 通过发送命令,让Redis服务器返回监控其运行状态,包括主服务器和从服务器。
  • 当哨兵监测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机。

为了防止哨兵宕机,通常将哨兵也配置为集群模式,各个哨兵之间还会进行监控

假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象称为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票由一个哨兵发起,进行falover[故障转移]操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从机切换为主机,这个过程称为客观下线。

10.1 哨兵模式操作

如果某台从机宕机,那么将该从机下线即可,如果主机宕机了呢?

思考:
在一主多从的模式下,如果主机宕机了,需要思考一下几个问题?

  1. 如果确认主机确定是宕机了,谁来确认?
  2. 需要将宕机的主机从这个集群中下线。
  3. 在其他从机中选择一个当做主机。
  4. 通知其余的从机来连接这个新的主机。
  5. 重新启动所有的主机和从机。
  6. 如果此时,主机回来了,该怎么办?

10.2 哨兵的作用

监控

不断的检查master和slave是否正常运行。master存活检测、masterslave运行情兄检测

通知(提醒)

当被监控的服务器出现问题时,向其他(哨兵间,客户端)发送通知

自动故障转移

断开master与slave连接,选取一个slave作为master,将其他slave连接到新的master,并告知客户端新的服务器地址

哨兵也是一台redis服务器,只是不提供数据服务通常哨兵配置数量为单数

10.2 哨兵的配置

新建一个sentinel.conf配置文件。

# mymaster :为每一个一主多从集群起一个名字
# 127.0.0.1 6379  :一主多从集群的主机
# 1               :哨兵的数量,如果只启动一个哨兵服务,但是配置5的话,主机宕机,需要半数以上哨兵确认宕机才认为是宕机。
                    但是如今只启动一个哨兵服务,所有不认为主机是宕机的。
sentinel monitor mymaster 127.0.0.1 6379 1

# 设置连接master和slave时的密码,注意的是sentinel不能分别为master和slave设置不同的密码,因此master和slave的密码应该设置相同。
sentinel auth-pass mymaster bjzr123   # 设置主机的密码

10.3 测试环境搭建

启动三个Redis-Server服务,6380端口作为主机,6381和6382作为从机,测试6380主机宕机后,哨兵是否会自动进行选举。

  • 主机和从机密码相同。

6380配置文件

pidfile /var/run/redis_6380.pid
port 6380
logfile "6380.log"
dbfilename dump80.rdb
masterauth bjzr123   #如果主机宕机后再次上线,需要连接新的主机,所以也配上主机密码。

6381配置文件

pidfile /var/run/redis_6381.pid
port 6381
logfile "6381.log"
dbfilename dump81.rdb
masterauth bjzr123   #配置主机密码

# Redis5.0之前使用slaveof   5.0之后使用replicaof
slaveof 127.0.0.1 6380   # 从机需要配置主机地址

6382配置文件

pidfile /var/run/redis_6382.pid
port 6382
logfile "6382.log"
dbfilename dump82.rdb
masterauth bjzr123   #配置主机密码

# Redis5.0之前使用slaveof   5.0之后使用replicaof
slaveof 127.0.0.1 6380   # 从机需要配置主机地址

哨兵配置

  • 设置连接master和slave时的密码,注意的是sentinel不能分别为master和slave设置不同的密码,因此master和slave的密码应该设置相同.
sentinel monitor mymaster 127.0.0.1 6380 1
sentinel auth-pass mymaster bjzr123   # 设置主机的密码

先启动一主多从

redis-server redis80.conf
redis-server redis81.conf
redis-server redis82.conf

检查主从关系

  • 主从关系没问题
[root@rm bin]# redis-cli -p 6380
127.0.0.1:6380> info replication
# Replication
role:master
connected_slaves:2
slave0:ip=127.0.0.1,port=6381,state=online,offset=98,lag=0
slave1:ip=127.0.0.1,port=6382,state=online,offset=98,lag=0
master_failover_state:no-failover
master_replid:0ff4ab70d71d4ee60002795edc32b0271ca2ddc9
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:98
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:98
127.0.0.1:6380> 

启动哨兵服务

[root@rm bin]# redis-sentinel sentinel.conf 
9896:X 03 Mar 2022 15:36:09.387 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
9896:X 03 Mar 2022 15:36:09.387 # Redis version=6.2.2, bits=64, commit=00000000, modified=0, pid=9896, just started
9896:X 03 Mar 2022 15:36:09.387 # Configuration loaded
9896:X 03 Mar 2022 15:36:09.387 * monotonic clock: POSIX clock_gettime
                _._                                                  
           _.-``__ ''-._                                             
      _.-``    `.  `_.  ''-._           Redis 6.2.2 (00000000/0) 64 bit
  .-`` .-```.  ```\/    _.,_ ''-._                                  
 (    '      ,       .-`  | `,    )     Running in sentinel mode
 |`-._`-...-` __...-.``-._|'` _.-'|     Port: 26379
 |    `-._   `._    /     _.-'    |     PID: 9896
  `-._    `-._  `-./  _.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |           https://redis.io       
  `-._    `-._`-.__.-'_.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |                                  
  `-._    `-._`-.__.-'_.-'    _.-'                                   
      `-._    `-.__.-'    _.-'                                       
          `-._        _.-'                                           
              `-.__.-'                                               

9896:X 03 Mar 2022 15:36:09.388 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
9896:X 03 Mar 2022 15:36:09.390 # Sentinel ID is a78631d851682c3b32c7bd1697a08a3bc2a842b4
9896:X 03 Mar 2022 15:36:09.391 # +monitor master mymaster 127.0.0.1 6380 quorum 1
9896:X 03 Mar 2022 15:36:09.391 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ mymaster 127.0.0.1 6380
9896:X 03 Mar 2022 15:36:09.393 * +slave slave 127.0.0.1:6382 127.0.0.1 6382 @ mymaster 127.0.0.1 6380

再次查看配置文件,发现配置文件多了些内容

sentinel monitor mymaster 127.0.0.1 6380 1
sentinel auth-pass mymaster bjzr123
# Generated by CONFIG REWRITE
protected-mode no
port 26379
user default on nopass ~* &* +@all
dir "/usr/local/redis/bin"
sentinel myid a78631d851682c3b32c7bd1697a08a3bc2a842b4
sentinel config-epoch mymaster 0
sentinel leader-epoch mymaster 0
sentinel current-epoch 0
sentinel known-replica mymaster 127.0.0.1 6382
sentinel known-replica mymaster 127.0.0.1 6381

80宕机

127.0.0.1:6380> shutdown

查看81的信息

  • 从机变为了主机。并且82从机也连接到了该主机。
127.0.0.1:6381> info replication
# Replication
role:master
connected_slaves:1
slave0:ip=127.0.0.1,port=6382,state=online,offset=5849,lag=1
master_failover_state:no-failover
master_replid:684e8a246189930551e05df122a1cfb363077feb
master_replid2:0ff4ab70d71d4ee60002795edc32b0271ca2ddc9
master_repl_offset:5982
second_repl_offset:4735
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:5982
127.0.0.1:6381> 

查看82配置文件,发现主机地址被修改为了81

此时再次上线80,发现80变成了从机

127.0.0.1:6380> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6381
master_link_status:up
master_last_io_seconds_ago:1
master_sync_in_progress:0
slave_repl_offset:22168
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:684e8a246189930551e05df122a1cfb363077feb
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:22168
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:21054
repl_backlog_histlen:1115
127.0.0.1:6380> 

查看80配置文件

查看哨兵配置

  • 将81作为了主机

11. SpringBoot操作Redis哨兵集群

由于一主多从集群中,任何一个从机都可能变成主机,所以在SpringBoot配置文件中就不应该再写固定的主机ip和端口了。

  • 哨兵知道动态的主机到底是谁,所以配置哨兵地址即可。
  • 先开放26379端口,如果报连接超时,可以先关闭防火墙。
查看防火墙
systemctl status firewalld
service iptables status
暂时关闭防火墙
systemctl stop firewalld
service iptables stop
永久关闭防火墙
systemctl disable firewalld
chkconfig iptables off
spring:
  redis:
    sentinel:
      # 哨兵配置文件中的代表某个一主多从服务
      master: mymaster
      # 如果是哨兵集群,每个哨兵之间以逗号隔开
      nodes: 8.141.52.45:26379
      # 主节点和从节点密码
      password: bjzr123

11.Redis缓存穿透和雪崩

11.1 缓存穿透

产生后的现象

  • 1.应用服务器压力增大。
  • 2.Redis命中率降低。在Redis缓存中查询不到数据。
    • 请求先到Redis缓存,查到返回,查不到去数据库查,然后再将结果缓存,再返回。
  • 3.所有请求都到数据库,数据库压力增大。

何时会产生

  • 1.Redis查询不到数据库,造成命中率降低。
  • 2.出现大量非正常url访问。(这些url访问缓存中不存在的数据,一般是遭受恶意攻击。)

解决方案

    1. 对空值进行缓存
    • 如果一个查询返回的结果是空(不管数据是否存在),将这个空结果进行缓存,设置空值的过期时间会很短,一般不超过5分钟。
    1. 设置请求可访问的名单(白名单)
    • 使用bitmaps类型定义一个可以访问的名单,名单id作为bitmaps的偏移量,每次访问和id进行比较,如果id不在bitmaps中,进行拦截,拒绝访问。
    1. 使用布隆过滤器
    • 布隆过滤器底层就是一个bitmaps,优点是空间效率和查询时间都远超一般算法,缺点是有一定的误识别率。
    1. 进行实时监控
    • 当发现Redis命中率开始急剧降低时,需要排查访问数据,可以设置黑名单限制服务。

11.2 缓存击穿

产生后的现象

  • 1.数据库压力增大。
  • 2.Redis里面没有出现大量key过期。
  • 3.Redis正常运行,数据库崩溃。

何时会产生

  • Redis中的某个key过期了,但是瞬间有大量的请求来访问这个key。

解决方案

  • 1.预先设置热门数据:在redis高峰访问之前,把一些热门数据提前存入到redis里面,加大这些热门数据key的过期时间。
  • 2.实时调整:现场监控哪些数据热门,实时调整key的过期时长
  • 3.使用锁:
    • 就是在缓存失效的时候(判断拿出来的值为空),不是立即去 load db。
    • 先使用缓存工具的某些带成功操作返回值的操作(比如Redis 的 SETNX )

11.2 缓存雪崩

产生后的现象

  • 数据库压力变大,应用服务器返回时间变长。
  • 应用访问变慢,Redis中造成大量访问等待。
  • 数据库崩溃,服务器崩溃,Redis崩溃。

何时会产生

  • 极少时间段,查询大量已过期key。

解决方案

    1. 构建多级缓存架构:nginx缓存+redis缓存+其他缓存( ehcache等 ) 。
    1. 使用锁或队列:
    • 用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。不适用高并发情况·
    1. 设置过期标志更新缓存∶
    • 记录缓存数据是否过期(设置提前量),如果过期会触发通知另外的线程在后台去更新实际key的缓存。
    1. 将缓存失效时间分散开:
    • 比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
posted @ 2022-02-08 18:07  初夏那片海  阅读(65)  评论(0)    收藏  举报