Redis

39

一、介绍Redis

1.1 现存问题

在单体架构搭建集群之后,会因为Tomcat之间的JVM缓存中的数据无法同步,导致一些问题……

由于单体架构搭建集群后,传统的缓存方案(JVM缓存-Map)失效,暂时没有一个比较好的替换方式……

由于多个JVM的原因,传统的锁失效了……

MySQL对于一 些非结构化数据,没有太好的存储方案,MySQL本身会将数据存储到本地磁盘,读写性能一般…

1.2 NoSQL&Redis介绍

NoSQL(Not Only SQL),对非关系型数据库的一种泛指。

一般市面上,给NoSQL大致分为了4类:

  • key-value:Redis,Memcache……
  • 文档型:Elasticsearch,MongoDB,Solr……
  • 面向列:Hbase……
  • 图形化:Neo4j……

Java中用的比较多的Key-Value存储的Redis

  • Redis时完全脱离Tomcat之外的一个单独的中间件。
  • Redis基于内存存储数据的,查询速度特别快。
  • Redis还提供了高性能的读写能力,并且并发能力极强,读的速度是110000次/s,写的速度是81000次/s 。
  • Redis还提供了丰富的存储数据的格式,可以满足大多数的非结构化存储。
  • Redis还提供了实现锁的机制,甚至由第三方框架Redisson提供的分布式锁的实现方案。
  • Redis的出生就时因为作者任务MySQL性能一般,自己写了Redis。

二、安装Redis

docker-compose.yml

version: '3.1'
services:
  redis:
    image: daocloud.io/library/redis:5.0.9
    container_name: redis
    ports:
      - 6379:6379

连接Redis服务:

  • 官方提供的客户端:
    • 进入到redis的容器内部:docker exec -it redis bash
    • 采用配置好环境变量的redis-cli连接:redis-cli [-h 127.0.0.1 -p 6379]
  • 图形化界面:
    • Redis的软件,直接傻瓜式安装redis-desktop-manager连接Redis服务方式

三、Redis存储数据的结构

Redis提供了八种存储数据的结构,只需要掌握五种即可:

  • string:value正常存储一个字符串/byte[]
  • hash/map:value可以存储map集合
  • list:value可以存储一个列表(允许重复,存取有序,有下标)
  • set:value可以存储一个集合(不允许重复,存取无序,没有下标)
  • zset(sorted-set):value可以存储一个有序的集合(不允许重复,存取会根据score排序,有下标)
  • HyperLogLog:去重计数,空间复杂度极低,采用12kb,可以存储2^64数据。时间复杂度是O(n),虽然空间复杂度比较低,但是计算存在误差。
  • GEO:存储地理位置的,可以存储经纬度,并且计算两个经纬度之间的距离等等操作……
  • 位图bitmap:存储二级制,只存储0,1两个数据。

[wppay]

四、Redis的基础操作命令

常用的几个小命令:

  • 存储数据:set key value
  • 获取数据:get key
  • 设置key的生存时间:expire key second
  • 存储数据并设置生存时间:setex key second value
  • 删除key:del key

五、 Spring操作Redis

Java操作Redis服务的客户端大概有两种

  • Jedis:Redis的命令是什么,Java种操作的方法就是什么!
  • Lettuce:将Redis的命令进行了一些封装,操作原理不变,提供了很多更方便的方式。

5.1 Jedis操作Redis

创建项目:…………

导入依赖:

​<dependencies>
 <dependency>
     <groupId>redis.clients</groupId>
     <artifactId>jedis</artifactId>
     <version>2.9.0</version>
 </dependency>
 <dependency>
     <groupId>junit</groupId>
     <artifactId>junit</artifactId>
     <version>4.12</version>
 </dependency>
</dependencies>

测试:

@Test
public void test(){
 //1. 连接Redis
 Jedis jedis = new Jedis("192.168.41.41",6379);
​
 //---------------------------------------------
 //2. 操作
 jedis.set("birthday","2011-11-11");
​
 //---------------------------------------------
 //3. 释放资源
 jedis.close();
}

5.2 存储对象

5.2.1 存储JSON

导入依赖:

<dependency>
 <groupId>com.fasterxml.jackson.core</groupId>
 <artifactId>jackson-databind</artifactId>
 <version>2.9.8</version>
</dependency>

创建POJO类:

public class User {
​
 private Integer id;
​
 private String name;
​
 private Date birthday;
​
    // set .... get ....
}

测试存储

/**
 * 推荐采用这种方式.
 * @throws JsonProcessingException
 */
@Test
public void setJSON() throws JsonProcessingException {
    //1. 创建连接
    Jedis jedis = new Jedis("192.168.41.41",6379);
    //-----------------------------------
    //2. 操作
    User user = new User(1,"王大拿",new Date());
    ObjectMapper mapper = new ObjectMapper();
    String value = mapper.writeValueAsString(user);
​
    jedis.set("user",value);
    //-----------------------------------
    //3. 释放资源
    jedis.close();
}
5.2.2 存储byte[]

导入依赖

<dependency>
 <groupId>org.springframework</groupId>
 <artifactId>spring-context</artifactId>
 <version>5.2.10.RELEASE</version>
</dependency>

测试存储

/**
 * 不推荐使用
 */
@Test
public void setByteArray(){
    //1. 创建连接
    Jedis jedis = new Jedis("192.168.41.41",6379);
    //-----------------------------------
    //2. 操作
    User user = new User(1,"王大拿",new Date());
    byte[] value = SerializationUtils.serialize(user);
    byte[] key = SerializationUtils.serialize("user");
​
    jedis.set(key,value); 
    //-----------------------------------
    //3. 释放资源
    jedis.close();
}

5.3 连接池

频繁的创建和销毁Jedis对象,很消耗性能。准备一个大池子,将创建好的Jedis对象存放进去,用的时候获取一个,用完再换回来。

/**
 * 一定要将jedis.close()放到finally代码块中
 */
@Test
public void pool(){
    //1. 创建连接池
    JedisPool pool = new JedisPool("192.168.41.41",6379);
    Jedis jedis = null;
​
    try {
        //2. 获取连接对象
        jedis = pool.getResource();
​
        //3. 操作
        String json = jedis.get("user");
        System.out.println(json);
    } finally {
        //4. 还连接对象
        jedis.close();
    }
}

5.4 管道(批量操作)

如果需要同步MySQL中的数据到Redis服务中,并且数据条数比较多时,可能会在多次请求和响应的过程中耗时过长。希望可以采用批量操作的效果。

@Test
public void test(){
 //1. 创建连接池
 JedisPool pool = new JedisPool("192.168.41.41",6379);
 Jedis jedis = null;
​
 try {
     //2. 获取连接对象
     jedis = pool.getResource();
​
     System.out.println(System.currentTimeMillis());
     /**
         * 3. 不用管道,同步10万条数据操作,49秒
        for (int i = 0; i < 100000; i++) {
            jedis.set("user" + i, UUID.randomUUID().toString());
        }
        */
​
        /**
         * 3. 用管道,同步10万条数据操作 ,1秒不到
​
        Pipeline pipelined = jedis.pipelined();
        for (int i = 0; i < 100000000; i++) {
            pipelined.set("age" + i,UUID.randomUUID().toString());
        }
        pipelined.sync();
         */
        System.out.println(System.currentTimeMillis());
    } finally {
        //4. 还连接对象
        jedis.close();
    }
}

任务:

用Redis解决多台Tomcat之间Session数据无法共享的问题:

业务要求

  • 在用户登录成功之后,将用户的信息存储到Redis中。
    • 仿照Session,让Redis的数据,存活30分钟。
    • 仿照Session和Cookie中的JSessionID的绑定方式,在Cookie中存储Redis中的key,让用户每次请求可以找到Redis中的value。
  • 修改拦截器的代码,让拦截器去Redis中查看客户信息,来判断客户端是否登录过。
    • Session活跃时,每次操作会重置Session的超时时间。仿照这个操作,重置Redis中key的生存时间。

代码要求

  • 创建JedisPool的操作,交给Spring工厂。
  • 因为要求Jedis.close必须放到finally代码块中,将set,get,expire,setex等命令封装一下。
  • 设置key的时候,推荐添加一个统一的前缀。(user:)

完成任务:

导入Jedis依赖

<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>

编写配置文件,构建JedisPool

<!--    创建Jedis连接池-->
<bean id="jedisPool" class="redis.clients.jedis.JedisPool">
<constructor-arg name="host" value="${redis.host}"/>
<constructor-arg name="port" value="${redis.port}"/>
</bean>

编写Jedis的工具类,封装常用命令

package com.qf.air.util;
​
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
​
/** 
 * 封装和Redis的基本操作
 */
@Component
public class JedisUtil {
​
    @Autowired
    private JedisPool pool;
​
​
    // set
    public void set(String key,String value){
        Jedis jedis = null;
        try {
            jedis = pool.getResource();
            jedis.set(key,value);
        } finally {
            jedis.close();
        }
    }
​
    // get
    public String get(String key){
        Jedis jedis = null;
        try {
            jedis = pool.getResource();
            String value = jedis.get(key);
            return value;
        } finally {
            jedis.close();
        }
    }
​
    // expire
    public void expire(String key,int second){
        Jedis jedis = null;
        try {
            jedis = pool.getResource();
            jedis.expire(key,second);
        } finally {
            jedis.close();
        }
    }
​
    // setex
    public void setex(String key,int second,String value){
        Jedis jedis = null;
        try {
            jedis = pool.getResource();
            jedis.setex(key,second,value);
        } finally {
            jedis.close();
        }
    }
​
    // del
    public void del(String... key){
        Jedis jedis = null;
        try {
            jedis = pool.getResource();
            jedis.del(key);
        } finally {
            jedis.close();
        }
    }
}

修改登录功能代码

/**
* 3. 将登录成功的用户信息存储到Session中
  session.setAttribute("user",user);
*/
​
//3. 将登录成功的用户信息存储到Redis中
String keySuffix = UUID.randomUUID().toString();
String key = AirConstants.USER_REDIS_PREFIX + keySuffix;
​
ObjectMapper objectMapper = new ObjectMapper();
String value = objectMapper.writeValueAsString(user);
​
jedisUtil.setex(key,AirConstants.USER_TIMEOUT,value);
​
// 为了让客户端下次请求时,可以找到存储在Redis中的value信息
// 需要将keySuffix响应给客户端(响应体,响应头,Cookie)
// 以Cookie的形式,将keySuffix写回
Cookie cookie = new Cookie(AirConstants.USER_COOKIE,keySuffix);
cookie.setMaxAge(999999999);
cookie.setPath("/");
resp.addCookie(cookie);

修改拦截器的前处理,让从Redis中获取用户信息

//前处理
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    /**
     * 1. 从session中查询用户信息
     *  User user = (User) request.getSession().getAttribute("user");
     */
​
    //1. 从Redis中查询用户信息
    //1.1 获取Cookie中的userKey信息
    String keySuffix = null;
    String value = null;
    Cookie[] cookies = request.getCookies();
    if (cookies != null) {
        for (Cookie cookie : cookies) {
            if (AirConstants.USER_COOKIE.equals(cookie.getName())) {
                // 找到了匹配要求的cookie
                keySuffix = cookie.getValue();
                break;
            }
        }
    }
​
    //1.2 不为null,查询Redis
    if (!StringUtils.isEmpty(keySuffix)) {
        value = jedisUtil.get(AirConstants.USER_REDIS_PREFIX + keySuffix);
    }
​
    //2.如果登录了,return true,如果没登陆 return false
    if (value != null) {
        jedisUtil.expire(AirConstants.USER_REDIS_PREFIX + keySuffix,AirConstants.USER_TIMEOUT);
        return true;
    } else {
        response.sendRedirect(request.getContextPath() + "/login.html");
        return false;
    }
}

重新部署到Linux中

五、SpringBoot操作Redis

(2条消息) RedisTemplate操作Redis,这一篇文章就够了(一)lydms的博客-CSDN博客redistemplate

六、Redis的操作命令(整理)

简单阐述string,hash,list,set,zset,key,db的操作命令

6.1 string

set key value:设置key和value到string结构

get key:基于key获取value

setex key second value:设置key和value的同时,设置生存时间

setnx key value:set if not exists,如果key不存在,正常的set数据,如果key存在,什么事都不做。

set key value [EX second | PX millionSecond] [NX|XX]:实现setex功能的同时,还能够实现setnx的功能

incr|decr key:对key的value进行自增/自减1,要求value必须是数值,或者key不存在也成。

incrby key increment:对key的value进行自增increment大小,要求value必须是数值,或者key不存在也成。

6.2 hash

hash结构大多是用来存储对象,做缓存操作。

hset key field value:存储数据到hash结构。

hmset key field value [field value ...]: 批量存储数据到hash结构中

hget key field:查询hash结构中某一个field对应的value

hgetall key:根据key直接查询整个hash结构数据

hincrby key field increment:针对hash结构中的某一个field对应的value自增

6.3 list

list结构更多的是用来实现各种数据的结构的,栈,队列。

lpush/rpush key value [value ...]:从左侧/右侧插入多个数据到list结构中。

lpop/rpop key:从左侧/右侧弹出一个数据

lrange key start stop:根据索引范围检索出list中的具体数据,start代表起始索引位置,stop结束索引位置,如果查询全部,stop设置-1,代表最后一条。从0开始

6.4 set

set结构一般用来存储用户的token,set结构不允许重复,做筛选时和比较合适。同时还提供了交集,并集,差集操作,无序。

sadd key member [member ...]: 存储数据到set结构中

sismember key member: 判断你给的member是否存在set结构中

sinter key [key ……]:交集操作

sunion key [key ......]: 并集操作

sdiff key [key ...]:差集操作

6.5 zset(sortedSet)

zset结构是有序的set,在存储数据的同时,要额外设置一个score,根据score进行排序。

点击量,排行榜,基于score的特点来实现一些业务

zadd key score member [score member ...]:存储数据到zset结构

zincrby key increment member:修改指定member的分数

zrange key start stop [withscores]:根据下标检索数据,添加withscores,会同时返回分数

zrevrange key start stop [withscores]:基于score从大到小排序,并根据下标加索数据

zrangebyscore key min max [withscores] [limit offset count]:基于score分数进行范围检索,查询出指定分数范围的数据,并且可以通过limit指定检索的数据的起始位置和条数

zrevrangebyscore key max min [withscores] [limit offset count]: 基于分数从最大分数检索到最小分数

byscore的检索,如果想设置分数的最大值和最小值,要通过 +inf-inf来设置

byscore的检索,默认分数是带有等于效果的,如果不想带等于效果,需要 (score

6.6 key

key的命令只针对key,对于value没有任何关系

expire key second:设置key的生存时间,单位是秒

pexpire key millionSecond:设置key的生存时间,单位是毫秒

expireat key timestamp:设置key能活到什么时候,时间戳的单位是秒

pexpireat key timestamp:设置key能活到什么时候,时间戳的单位是毫秒

ttl | pttl key:查看key剩余生存时间,单位为秒 | 毫秒,如果返回-1代表没有设置生存时间,返回-2key不存在

del key [key...]:删除key

keys pattern:查看当前库中有多少个key,并且打印,pattern支持*通配符

6.7 db(了解)

单机版的Redis默认有16个库

select 0~15:切换库

move key db:移动当前库的key到其他库

flushdb:清空当前库的全部数据

flushall:清空Redis服务的全部数据

lastsave:查看到最后一次写操作时间

monitor:监控操作Redis服务的线程

七、Redis的AUTH

现在连接Redis只需要ip和port,导致Redis服务很不安全。

提升Redis服务的安全性,设置Redis服务的密码连接。

基于Redis的配置文件设置Redis服务的密码

采用Docker的数据卷将redis.conf文件映射到容器内部,并且让容器启动时,加载redis.conf文件。

7.1 Redis服务设置密码

修改Redis的docker-compose.yml文件

version: '3.1'
services:
  redis:
    image: daocloud.io/library/redis:5.0.9
    container_name: redis
    ports:
      - 6379:6379
    volumes:
      - ./conf/redis.conf:/data/redis.conf
    command: ['redis-server','redis.conf']

准备yml文件所在目录下的conf中的redis.conf文件

requirepass 密码

先down掉Redis容器,再重新up -d

7.2 客户端AUTH连接Redis

7.2.1 redis-cli

需要再连接之后,先输入auth 密码通过校验后,才可以执行读写等其他操作命令

7.2.2 Jedis

需要给JedisPool设置Redis服务的密码,保证每一个取出的Jedis对象都可以直接和Redis服务交互

通过有参构造设置

air项目中Redis配置的修改

7.2.3 springboot

配置文件中指定redis的密码

八、Redis的持久化(重要)

Redis基于内存进行读写数据,Redis的效率特别高。

内存存储数据不安全,如果Redis宕机,数据会全部丢失。

Redis官方提供了两种持久化机制。

RDB(Redis Data Base):

  • RDB以二进制的文件形式,将内存中的数据持久化到本地。
  • RDB的执行时机:在多少秒之内,写了多少key就执行一次RDB操作,执行时机无法保证数据的绝对安全
  • 特点:虽然无法保证安全,但是占用空间小,恢复数据快

AOF(Append Only File):

  • AOF以日志文件的形式,将执行过的写操作持久化到本地。
  • AOF的执行时机:
    • 每执行一次写操作,立即将写操作记录的日志中
    • 每秒执行一次AOF持久化,将写操作记录的到日志中
    • 根据操作系统情况,1~100秒左右,将写操作记录的到日志中
  • 特点:虽然尽可能的保证了数据安全,但是占用空间大,恢复数据慢

在正常的生产环境中,Redis官方推荐同时开启RDB和AOF,做到互补的效果

同时开启RDB和AOF时,注意几点:

  • AOF恢复数据时,优先级高于RDB
  • 在RDB执行持久化时,会根据AOF持久化文件中的日志,进行持久化。

AOF持久化的是日志文件,可能会由于写操作过多,导致日志文件特别大。官方提供了AOF文件重写机制

  • 自动重写:会在AOF文件为初始大小的一倍并且大于等于64m时,执行一次AOF重写,将日志文件重写成二进制文件。
  • 手动重写:执行一个命令来重写,BGREWRITEAOF

在配置文件中配置:

8.1 开启RDB持久化

RDB默认开启,不需要配置开关

RDB持久化文件的位置,名称。并且重写配置RDB的持久化时机

# 持久化文件位置
dir ./
# 持久化文件名称
dbfilename redis.rdb
# 持久化时机
save 10 1

8.2 开启AOF持久化

AOF默认关闭,需要手动开启

配置AOF文件持久化位置和名称,以及持久化时机

# 开启AOF持久化
appendonly yes
# AOF文件名称
appendfilename "redis.aof"
# 持久化时机,每秒执行一次
appendfsync everysec

九、Redis的事务(分布式锁Redisson)

命令行操作Redis的事务

传统的关系型数据库的事务:一次事务操作要么都成功,要么都失败。

Redis的事务:将一系列的命令放到一个队列中统一执行,该成功的成功,该失败的失败,Redis不保证原子性。

开启事务:multi,开启事务后,所有执行的命令会被放到一个队列中。

提交事务:exec ,提交事务后,会将队列中的命令依次执行。

取消事务:discard ,取消事务,会将队列中的命令丢弃。

一般情况Redis的事务要配合watch机制使用

可以在开启Redis事务之前,先通过watch命令监听一个或多个key,在开启事务后,如果监听的key发生改变,直接执行取消事务操作。

监听key:watch key [key ...]

取消监听:unwatch ,如果提交/取消事务,自动执行unwatch

十、Redis的消息队列(了解)

消息队列,应用场景很多,帮助我们实现服务与服务之间的异步通讯……………

10.1 通过list结构实现

因为list结构提供了lpushrpush以及lpoprpop

需要让生产者通过lpush推送消息到list结构中

让消费者通过rpop弹出list结构中的数据

问题:list结构只能实现点对点的方式,如果涉及到一对多或者是广播的形式,list结构实现麻烦。

10.2 Redis提供的消息队列

命令行

为了可以实现一对多或者是广播的形式,可以直接使用Redis提供的发布与订阅模式实现

publish channel message,发布message到指定的channel(频道)

subscribe channel [channel ...],订阅一个或多个channel中的message

  • 好处:
  • 坏处:无法保证消息的可靠性,并不是专业做消息队列的。

(后面会学习更专业的MQ,Kafka,RocketMQ,RabbitMQ)

十一、Redis的主从架构(了解)

Redis读写性能高,特别是读操作,所以Redis更多的是用来做缓存。

Redis做缓存提供读操作时,也时有性能瓶颈。

如何提升Redis读性能的瓶颈。

通过Redis的主从架构提升单体Redis服务的读性能瓶颈

搭建主从架构:docker-compose.yml

version: "3.1"
services:
  redis1:
    image: daocloud.io/library/redis:5.0.9
    container_name: redis1
    ports:
      - 7001:6379
    volumes:
      - ./conf/redis1.conf:/usr/local/redis/redis.conf
    command: ["redis-server","/usr/local/redis/redis.conf"]
  redis2:
    image: daocloud.io/library/redis:5.0.9
    container_name: redis2
    ports:
      - 7002:6379
    volumes:
      - ./conf/redis2.conf:/usr/local/redis/redis.conf
    links:
      - redis1:master
    command: ["redis-server","/usr/local/redis/redis.conf"]
  redis3:
    image: daocloud.io/library/redis:5.0.9
    container_name: redis3
    ports:
      - 7003:6379
    volumes:
      - ./conf/redis3.conf:/usr/local/redis/redis.conf
    links:
      - redis1:master
    command: ["redis-server","/usr/local/redis/redis.conf"]

数据卷映射的配置文件

只需要在redis2.conf和redis3.conf配置即可

# 从节点在对应的conf文件中配置master节点所在的ip和port
replicaof master 6379

十二、Redis的哨兵(了解)

哨兵是为了解决主从架构中存在的单点故障问题

单点故障:当Master节点挂掉了,整个集群就挂掉了

Master节点提供读与写操作,而Slave节点只提供读操作

搭建主从+哨兵:docker-compose.yml

version: "3.1"
services:
  redis1:
    image: daocloud.io/library/redis:5.0.9
    container_name: redis1
    ports:
      - 7001:6379
    volumes:
      - ./conf/redis1.conf:/usr/local/redis/redis.conf
      - ./conf/sentinel1.conf:/data/sentinel.conf        # 添加的内容
    command: ["redis-server","/usr/local/redis/redis.conf"]
  redis2:
    image: daocloud.io/library/redis:5.0.9
    container_name: redis2
    ports:
      - 7002:6379
    volumes:
      - ./conf/redis2.conf:/usr/local/redis/redis.conf
      - ./conf/sentinel2.conf:/data/sentinel.conf        # 添加的内容
    links:
      - redis1:master
    command: ["redis-server","/usr/local/redis/redis.conf"]
  redis3:
    image: daocloud.io/library/redis:5.0.9
    container_name: redis3
    ports:
      - 7003:6379
    volumes:
      - ./conf/redis3.conf:/usr/local/redis/redis.conf
      - ./conf/sentinel3.conf:/data/sentinel.conf        # 添加的内容 
    links:
      - redis1:master
    command: ["redis-server","/usr/local/redis/redis.conf"]

redis.conf跟之前一样,需要添加sentinel.conf文件

# sentinel monitor <master-name> <ip> <redis-port> <quorum>
# 哨兵指定Master节点的名称和ip:port,并且设置哨兵投票至少几票成
sentinel monitor 随便写 master 6379 2
​
# sentinel auth-pass <master-name> <password>
# 如果Master节点有密码,设置密码
​
# Default is 30 seconds.
sentinel down-after-milliseconds mymaster 30000

Redis容器启动之后,进入到每一台Redis容器中,启动哨兵

当Master宕机后,通过日志很清晰的查看到投票选举的过程

哨兵在确定新的Master后,会直接修改Redis的配置文件,实现Master选举!

十三、Redis的集群

Redis集群的出现,是为了解决主从 + 哨兵无法提升Redis存储数据的上限问题,以及写操作瓶颈问题。

13.1 Redis集群架构(重要)

13.2 Redis集群搭建

docker-compose.yml文件

# docker-compose.yml
version: "3.1"
services:
  redis1:
    image: daocloud.io/library/redis:5.0.9
    container_name: redis1
    ports:
      - 7001:7001
      - 17001:17001
    volumes:
      - ./conf/redis1.conf:/usr/local/redis/redis.conf
    command: ["redis-server","/usr/local/redis/redis.conf"]
  redis2:
    image: daocloud.io/library/redis:5.0.9
    container_name: redis2
    ports:
      - 7002:7002
      - 17002:17002
    volumes:
      - ./conf/redis2.conf:/usr/local/redis/redis.conf
    command: ["redis-server","/usr/local/redis/redis.conf"]  
  redis3:
    image: daocloud.io/library/redis:5.0.9
    container_name: redis3
    ports:
      - 7003:7003
      - 17003:17003
    volumes:
      - ./conf/redis3.conf:/usr/local/redis/redis.conf
    command: ["redis-server","/usr/local/redis/redis.conf"]  
  redis4:
    image: daocloud.io/library/redis:5.0.9
    container_name: redis4
    ports:
      - 7004:7004
      - 17004:17004
    volumes:
      - ./conf/redis4.conf:/usr/local/redis/redis.conf
    command: ["redis-server","/usr/local/redis/redis.conf"]  
  redis5:
    image: daocloud.io/library/redis:5.0.9
    container_name: redis5
    ports:
      - 7005:7005
      - 17005:17005
    volumes:
      - ./conf/redis5.conf:/usr/local/redis/redis.conf
    command: ["redis-server","/usr/local/redis/redis.conf"]  
  redis6:
    image: daocloud.io/library/redis:5.0.9
    container_name: redis6
    ports:
      - 7006:7006
      - 17006:17006
    volumes:
      - ./conf/redis6.conf:/usr/local/redis/redis.conf
    command: ["redis-server","/usr/local/redis/redis.conf"]  

Redis的配置文件

# 端口号
port 7001
# 开启Redis集群配置
cluster-enabled yes
# Redis服务在集群中的信息配置
cluster-config-file nodes-7001.conf
# Docker搭建Redis集群时,需要的配置
# 宿主机的ip地址
cluster-announce-ip 192.168.41.41
# 宿主机的port
cluster-announce-port 7001
# 消息总线的端口
cluster-announce-bus-port 17001

启动6个Redis服务之后,需要通过Redis容器内部的redis-cli命令开启集群

redis-cli --cluster create 192.168.41.41:7001 192.168.41.41:7002 192.168.41.41:7003 192.168.41.41:7004 192.168.41.41:7005 192.168.41.41:7006 --cluster-replicas 1

输入后会展示集群节点分配的详细信息,输入yes之后,会在控制台出现......,如果出现了一整行还没启动成功,说明配置文件有问题,修改一下。

13.3 Jedis连接Redis集群

Jedis提供了JedisCluster对象和Redis集群交互

public static void main(String[] args) {
    Set<HostAndPort> nodes = new HashSet<>();
    nodes.add(new HostAndPort("192.168.41.41",7001));
    nodes.add(new HostAndPort("192.168.41.41",7002));
    nodes.add(new HostAndPort("192.168.41.41",7003));
    nodes.add(new HostAndPort("192.168.41.41",7004));
    nodes.add(new HostAndPort("192.168.41.41",7005));
    nodes.add(new HostAndPort("192.168.41.41",7006));
    JedisCluster jedisCluster = new JedisCluster(nodes);
    jedisCluster.set("key","value");
}

十四、Redis的删除策略(重要)

Redis最佳实践:推荐将所有存储到Redis中的key,设置过期时间。

如果key的生存时间到了,Redis服务会根据删除策略删除已经过期的key:

  • 定时删除:Redis服务会每100ms从Redis服务中随机获取3个设置了生存时间的key,查看生存时间是否到期,如果到期直接删除。
  • 惰性删除:当有客户端读取设置了生存时间的key时,Redis服务会先查看剩余的生存时间,如果生存时间已经到期,删除当前key,并返回null。

十五、Redis的淘汰策略(重要)

当向Redis服务申请内存存储key-value数据时,如果空间不足,执行淘汰策略:

  • volatile-lru:在设置了生存时间的key中,删除最久没有使用的key
  • volatile-lfu:在设置了生存时间的key中,删除最少频次使用的key
  • volatile-ttl:在设置了生存时间的key中,删除剩余生存时间最少的key
  • volatile-random:在设置了生存时间的key中,随机删除一个key
  • allkeys-lru:在所有的key中,删除最久没有使用的key
  • allkeys-lfu:在所有的key中,删除最少频次使用的key
  • allkeys-ramdom:在所有的key中,随机删除一个key
  • noeviction(默认):抛出异常……
volatile-lru -> Evict using approximated LRU among the keys with an expire set.
allkeys-lru -> Evict any key using approximated LRU.
volatile-lfu -> Evict using approximated LFU among the keys with an expire set.
allkeys-lfu -> Evict any key using approximated LFU.
volatile-random -> Remove a random key among the ones 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 evict anything, just return an error on write operations.

十六、常见的缓存问题 (重要)

缓存改变频次不高,经常被查询的数据。

缓存数据大多数是为了避免数据库压力过大,从数据库查询,并存储到缓存中。

16.1 缓存穿透

缓存中没有数据,数据库中也没有。大量的请求导致数据库宕机

16.2 缓存击穿

热点数据生存时间到期,导致大量请求访问数据库,最终数据库宕机

16.3 缓存雪崩

大量的缓存生存时间同时到期,导致大量请求访问数据库,数据库宕机

16.4 缓存倾斜

超级热点数据导致缓存服务宕机

  • 限流
  • 缓存
  • 从节点

16.5 双写一致性

在修改数据库中的数据后,如何保证数据库和缓存中的数据一致

我要修改数据,避免你读到脏数据所以删除了缓存,你再去读缓存的时候为空,从数据库读,读的时候我可能还没有修改,你就读到了脏数据,我这时候修改了数据库的数据,你因为查缓存的时候为空,会去同步你之前查的脏数据,所以我需要延迟一段时间再删除一次缓存。

[/wppay]