Redis

安装

  • 下载redis文件上传到linux虚拟机中

  • 安装C语言的编译环境GCC
yum install gcc
  • 解压redis安装包
tar -zxvf redis-6.2.7.tar.gz
  • 进入redis目录,编译redis文件
make
  • 安装
make install
  • 安装成功

  • 安装的目录 /usr/local/bin

  • 默认安装目录
    • redis-benchmark:性能测试工具
    • redis-check-aof:修复有问题的AOF文件
    • redis-check-dump:修复有问题的dump.rdb文件
    • redis-sentinel:Redis集群使用
    • redis-server:Redis服务启动命令
    • redis-cli:客户端

  • redis前台启动
    -(执行redis-server命令),关闭终端窗口会停止

  • redis后台启动
  • 将redis下面的redis.conf文件复制到etc目录下(cp redis.conf /etc/redis.conf)
  • 将redis.conf文件里面的daemonize no 改为 daemonize ye
  • 启动redis redis-server /etc/redis.conf实现后台启动
  • 启动成功,使用redis-cli连接redis

常用命令

  • keys*: 查看当前库所有的key
  • exists key:判断某个key是否存在
  • type key:查看你的key是什么类型
  • del key:删除指定的key数据
  • unlink key:根据value选择非阻塞删除,仅将keys从keyspace元数据中删除,真正的删除会在后续异步操作。
  • expire key 10:为给定的key设置过期时间s
  • ttl key:查看还有多少秒过期,-1表示永不过期,-2表示已过期
  • select:命令切换数据库
  • dbsize:查看当前数据库的key的数量
  • flushdb:清空当前库
  • flushall:通杀全部库

String

  • String是Redis最基本的类型,一个key对应一个value
  • String类型是二进制安全的。意味着Redis的String可以包含任何数据。比如jpg图片或者序列化的对象
  • String类型是最基本的数据类型,一个Redis中字符串value最多可以是512M
  • String的数据结构为简单动态字符串。是可以修改的字符串,内部结构类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配
  • 当字符串长度小于1M时,扩容都是加倍现有的空间,如果超过1M,一次只会扩容1M的空间

# 设置key value
set name cml
# 通过key获取value
get name
# value追加值
append name youmo
# 查看value长度
strlen name
# 不存在则添加,存在则不添加
setnx name cml
# 数字+1
set age 1
incu age
# 数字减1
decr age
# 数字加上步长
incrby age 5
# 数字减去步长
decrby age 5
# 批量新增key
mset name cml age 24
# 批量获取value
mget name age
# 获取指定位置区间字符
getrange name 0 3
# 替换指定位置区间字符
setrange name 0 test
# 设置key过期时间
setex name 20 cml
# 查看key过期时间
ttl name
# 查看再替换
getset name youmo

List

  • Redis列表时简单的字符串列表,按照插入顺序排序,可以添加一个元素到列表的头部或者尾部
  • 底层是双向链表,对两端的操作性能很高,通过索引下标的操作中间的节点性能会较差
# 从左边插入元素
lpush list 1 2 3
# 从右边插入元素
rpush list 4 5 6
# 获取集合
lrange list 0 -1 
# 取出集合的元素
lpop list 1(个数)
rpop list 1
# 取出一个集合右边的元素添加到一个集合的左边
rpoplpush list1 list2
# 获取集合中指定索引的值
lindex list 1
# 查看集合长度
llen list
# 在指定元素前面加入元素(在1的前面添加0)
linsert list before 1 0
# 在指定元素后面加入元素(在1的前面添加0)
linsert list after 1 0
# 删除指定元素前面指定个数的元素(删除1前面的两个元素)
lrem list 2 1
# 替换指定下标的值(将下标为0的元素替换为10)
lset list 0 10

Set

  • Redis的Set是String类型的无需列表。他底层是一个value为null的hash表,所有添加,删除,查找的复杂度都是O(1)
# 将一个或多个元素添加到集合
sismember set value1
# 查看集合中的所有元素
smembers set
# 判断某个元素是否存在 存在返回1 不存在返回0
sismember set value1
# 查看集合中元素个数
scard set
# 删除集合中的某个元素
srem set value1
# 随机从集合中吐出一个元素
spop set
# 随机从一个集中取出n个值。不会从集合中删除
srandmember set 2
# 将一个集合中的一个元素移动到另一个集合
smove set1 set2 value1
# 返回两个集合的交集元素
sinter set1 set2
# 返回两个集合的并集元素
sunion set1 set2
# 返回两个集合的差集元素
sdiff set1 set2

Hash

  • Redis Hash是一个键值对集合
  • Redis Hash是一个String类型的field和value的映射表,hash特别适用于存储对象。类型Java里面的Map<String,Object>
# 给key集合中的field键赋值value
mset user:1001 name cml age 24
# 从key中取出field的value
mget user:1001 name
# 判断key中field是否存在
hexists user:1001 name
# 列出hash集合中的所有field
hkeys user:1001
# 列出该hash集合中的所有value
hvals user:1001
# 为hash集合中field的值加上增量
hincrby user:1001 age 10
# 将hash表中field的值设置为value,当filed不存在时
hsetnx user:1001 hobby code

Zset

  • Redis有序集合zset与普通集合set非常相似,是一个没有重复元素的字符串集合
  • 不同之处是有序集合的每个成员都关联一个评分(score),这个评分被用来按照从最低到最高的方式排序集合中的成员。集合的成员是唯一的,但是评分可以是重复了。
  • 因为元素是有序的,所以可以很快的根据评分或者次序来获取一个范围的元素
# 将一个或多个 member 元素及其 score 值加入到有序集 key 当中
zadd  <key><score1><value1><score2><value2>
# 返回有序集 key 中,下标在<start><stop>之间的元素,带WITHSCORES,可以让分数一起和值返回到结果集
zrange <key><start><stop>  [WITHSCORES]  
# 返回有序集 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员。有序集成员按 score 值递增(从小到大)次序排列
zrangebyscore key minmax [withscores] [limit offset count]
# 同上,改为从大到小排列。
zrevrangebyscore key maxmin [withscores] [limit offset count] 
# 为元素的score加上增量
zincrby <key><increment><value>
# 删除该集合下,指定值的元素
zrem  <key><value>
# 统计该集合,分数区间内的元素个数 
zcount <key><min><max>
# 返回该值在集合中的排名,从0开始
zrank <key><value>

配置文件

  • bind
    • 默认情况下bind=127.0.0.1只能接收本机的访问请求
    • 不写的情况下,无限制接收任何ip地址的访问
  • protected-mode
    • 访问保护模式
    • 如果开启了访问保护模式,那么在没有设定bind ip且没有设密码的情况下,Redis只允许接收本机的响应
  • Port
    • 端口,默认6379
  • tcp-backlog
    • 设置tcp的backlog,backlog其实是一个连接队列,backlog队列总和=未完成三次握手的队列+已完成三次握手队列
  • timeout
    • 一个空闲的客户端维持多少秒会关闭,0表示关闭该功能。即永不关闭
  • tcp-keepalive
    • 对访问客户端的一种心跳检测,每个n秒检测一次
    • 单位为秒,如果设置为0,则不会进行keepalive检测,建议设置成60
  • daemonize
    • 是否为后台进程,设置为yes
  • pidfile
    • 存放pid文件的位置,每个实例会产生一个不同的pid文件
  • loglevel
    • 指定日志记录级别,Redis共支持四个级别:debug、verbose、notice、warning,默认为notice
  • logfile
    • 日志文件名称
  • databases
    • 设定库的数量 默认16,默认数据库为0
  • requirepass
    • 密码,默认不设置密码
  • maxclients
    • 设置redis同时可以与多个客户端进行连接,默认情况下10000个客户端
    • 如果达到了此限制,redis则会拒绝新的连接请求,并且向这些连接请求方发出"max number of clients reached"作为回应
  • maxmemory
    • 设置redis可以使用的内存量

发布和订阅

  • 打开一个客户端订阅channel1
subscribe channel1
  • 打开另一个客户端,给channel1发布消息
publish channel1 hello

Jedis

  • maven依赖
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>3.2.0</version>

  • 连接到Redis
    public static void main(String[] args) {
        /**
         * 创建Jedis对象
         * 需要注释掉redis.conf中的bind参数
         * protected-mode模式改为no
         */
        Jedis jedis = new Jedis("192.168.56.128",6379);
        System.out.println(jedis.ping());

    }
  • 连接成功

  • 操作redis
    @Test
    public void demo01(){
        Jedis jedis = new Jedis("192.168.56.128",6379);
        jedis.set("name","cml");
        jedis.set("age","24");
        System.out.println(jedis.mget("name","age"));
        jedis.close();
    }

  • 实例,手机验证功能
    • 收入手机号,点击发送后随机生成6位数字码,一天内有效
    • 输入验证码,点击验证,返回验证成功或验证失败
    • 每个手机号每天只能输入3次
public class PhoneCode {
    public static void main(String[] args) {
        //验证码发送
        verifyCode("1111");
        //验证
        getRedisCode("1111","080363");
    }

    //1、生成6位数字验证码
    public static String getCode(){
        Random random = new Random();
        String code = "";
        for (int i = 0;i<6;i++){
            int rand = random.nextInt(10);
            code += rand;
        }
        return code;
    }

    //2 每个手机每天只能发送三次,验证码放到redis中,设置过期时间
    public static void verifyCode(String phone){
        //初始化jedis
        Jedis jedis = new Jedis("192.168.56.128",6379);

        //需要创建两个key 手机发送次数key 验证码key
        //手机发送次数key
        String countKey = "verifyCode"+phone+":count";
        //验证码key
        String codeKey = "verifyCode"+phone+":code";

        //每个手机只能发送三次
        String count = jedis.get(countKey);
        if (count == null){
            //每个发送次数的key则表示第一次发送,设置value = 1
            jedis.setex(countKey,24 * 60 * 60,"1");
        }else if (Integer.valueOf(count)<=2){
            //发送次数 +1
            jedis.incr(countKey);
        }else {
            //发送次数已经发送三次,不能在发送了
            System.out.println("今天的发送次数已经发送三次了,不能再发送了");
            jedis.close();
            return;
        }
        //将发送的验证码存放到redis中
        jedis.set(codeKey,getCode());
        //关闭连接
        jedis.close();

    }

    //3 验证码校验
    public static void getRedisCode(String phone ,String code){
        //初始化jedis
        Jedis jedis = new Jedis("192.168.56.128",6379);
        //从redis获取验证码
        String codeKey = "verifyCode"+phone+":code";
        String redisCode = jedis.get(codeKey);
        if (redisCode != null && redisCode.equals(code)){
            System.out.println("验证成功");
        }else{
            System.out.println("验证失败");
        }

    }
}

SpringBoot整合Redis

  • maven依赖
        <!-- redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!-- spring2.X集成redis所需common-pool2-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
            <version>2.6.0</version>
        </dependency>
  • application.properties
#Redis服务器地址
spring.redis.host=192.168.56.128
#Redis服务器连接端口
spring.redis.port=6379
#Redis数据库索引(默认为0)
spring.redis.database= 0
#连接超时时间(毫秒)
spring.redis.timeout=1800000
#连接池最大连接数(使用负值表示没有限制)
spring.redis.lettuce.pool.max-active=20
#最大阻塞等待时间(负数表示没限制)
spring.redis.lettuce.pool.max-wait=-1
#连接池中的最大空闲连接
spring.redis.lettuce.pool.max-idle=5
#连接池中的最小空闲连接
spring.redis.lettuce.pool.min-idle=0
  • 配置类RedisConfig.java
@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        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);
        template.setConnectionFactory(factory);
//key序列化方式
        template.setKeySerializer(redisSerializer);
//value序列化
        template.setValueSerializer(jackson2JsonRedisSerializer);
//value hashmap序列化
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        return template;
    }

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        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);
// 配置序列化(解决乱码的问题),过期时间600秒
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(600))
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
                .disableCachingNullValues();
        RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .build();
        return cacheManager;
    }
}
  • 测试
@SpringBootTest
@RunWith(SpringRunner.class)
public class RedisTest {

    @Autowired
    private RedisTemplate redisTemplate;

    @Test
    public void testRedis(){
        //使用redisTemplate向redis设置值
        redisTemplate.opsForValue().set("name","cml");
        System.out.println(redisTemplate.opsForValue().get("name"));
    }

}

事务和锁机制

  • Redis事务

    • Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断
    • Redis事务的主要作用就是串联多个命令防止别的命令插队
  • Mylti、Exec、discard

    • 从输入Muti命令开始,输入的命令都会一次进入命令队列中,但不会执行,直到输入Exec后,Redis会将之前的命令队列中的命令依次执行。
    • 组队过程中可以通过discard来放弃组队
  • 组队成功,提交成功

  • 组队失败(命令出现错误,执行时整个的所有队列都会被取消)

  • 组队失败(执行阶段某个命令执行错误,则只有报错的命令不会被执行,其他的命令都会执行)


  • Redis乐观锁
    • watch:在执行multi之前,先执行watch监视某个key,如果在事务执行过程中这个key被改动,那么这个事务就会被打断
    • unwatch:取消watch命令对key的监视
    • 如果在执行watch命令之后,exec命令或discard命令被执行了的话,那么就不需要再执行unwatch了

  • 在exce执行之前,使用另一个会话改变age的值,则第一个会话执行exce失败

  • Redis事务特性

    • 单独的隔离操作
      • 事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令所打断
    • 每个隔离级别概念
      • 队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何执行都不会被实际执行
    • 不保证原子性
      • 十五中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚

秒杀案例

    /**
     * 秒杀过程
     * @param uid  uuid
     * @param prodId 商品id
     * @return
     */
    public boolean doSecKill(String uid,String prodId){
        //1 uuid和productId判空
        if (uid == null || prodId == null){
            return false;
        }
        // 2 拼接key
        //库存key
        String kcKey = "sk:"+prodId+":qt";
        //秒杀成功用户key
        String userKey = "sk:"+prodId+"user";
        //3 获取库存,如果库存为空则表示秒杀还没有开始
        String kc = redisTemplate.opsForValue().get(kcKey).toString();
        if (kc == null){
            System.out.println("秒杀还没有开始");
            return false;
        }
        //4 判断用户是否已经秒杀过了
        Boolean isKill = redisTemplate.opsForSet().isMember(userKey, uid);
        if (isKill){
            System.out.println("已经秒杀过了,不能重复秒杀");
            return false;
        }

        //5 判断库存是否大于0
        if (Integer.valueOf(kc) <= 0){
            System.out.println("秒杀已经结束");
            return false;
        }

        //6 秒杀
        //库存-1
        redisTemplate.opsForValue().decrement(kcKey);
        //把秒杀成功的用户添加到清单里面
        redisTemplate.opsForSet().add(userKey,uid);
        System.out.println("秒杀成功");
        return true;
    }
  • 测试类
    @Test
    public void secKill(){
        for (int i = 0;i<10;i++){
            redisService.doSecKill("user00"+i,"1001");
        }
    }
  • redis添加库存
set sk:1001:qt 10
  • 运行测试类(单线程)

  • 10份库存,秒杀成功,剩余0


  • 安装多线程测试工具(httpd-tools)
yum install httpd-tools
  • 通过浏览器测试
    • ab -n 请求次数-c 并发次数 -p 参数文件 -T ContentType 请求地址
  • 新建postfile文件
    • 内容为prodid=1001&
    @PostMapping("doSecKill")
    public void doSecKill(String prodid){
        String id = UUID.randomUUID().toString();
        redisService.doSecKill(id+"",prodid);
    }
  • ab -n 1000 -c 100 -p ~/postfile -T application/x-www-form-urlencoded http://192.168.73.1:8080/doSecKill
    • (使用100个线程秒杀库存为10的商品)
  • 秒杀过后库存为-91

  • 在多线程情况下存在问题
    • 不加锁则会出现超卖的情况
    • 并发很高的时候redis可能会出现连接超时的情况
      • 配置连接池解决

  • 超买问题
    • 使用Redis乐观锁
//监视库存
redisTemplate.watch(kcKey);

//******************************

//秒杀的时候开启事务
redisTemplate.setEnableTransactionSupport(true);
redisTemplate.multi();
//******************************
redisTemplate.exec();

  • 使用10线程进行1000次请求,200库存的商品还剩68

  • 乐观锁造成的库存余留问题
    • 当一个线程在秒杀的过程中,其他线程改变了库存则这个线程会秒杀失败
    • redis默认不能直接使用悲观锁,只能使用乐观锁

  • LUA脚本在Redis中使用
    • 将复杂的或者多步的redis操作,写成一个脚本,一次提交给redis执行
    • LUA脚本是类似于redis事务,有一定的原子性,不会被其他命令插队,可以完成一些redis事务性的操作
    • 在Redis2.6yi以上版本才能使用

    @Autowired
    private RedisTemplate redisTemplate;

    String secKillScript = "local userid=KEYS[1];\r\n" +
            "local prodid=KEYS[2];\r\n" +
            "local qtkey='sk:'..prodid..\":qt\";\r\n" +
            "local usersKey='sk:'..prodid..\":usr\";\r\n" +
            "local userExists=redis.call(\"sismember\",usersKey,userid);\r\n" +
            "if tonumber(userExists)==1 then \r\n" +
            "   return 2;\r\n" +
            "end\r\n" +
            "local num= redis.call(\"get\" ,qtkey);\r\n" +
            "if tonumber(num)<=0 then \r\n" +
            "   return 0;\r\n" +
            "else \r\n" +
            "   redis.call(\"decr\",qtkey);\r\n" +
            "   redis.call(\"sadd\",usersKey,userid);\r\n" +
            "end\r\n" +
            "return 1";

    public boolean doSecKillByLua(String uid,String prodId){
        if (uid == null || prodId == null){
            return false;
        }

        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setResultType(Long.class);
        redisScript.setScriptText(secKillScript);
        Object execute = redisTemplate.execute(redisScript, Arrays.asList(uid, prodId),"mypar");

        String reString = String.valueOf(execute);
        if ("0".equals(reString)) {
            System.err.println("已抢空!!");
            return false;
        } else if ("1".equals(reString)) {
            System.out.println("抢购成功!!!!");
            return true;
        } else if ("2".equals(reString)) {
            System.err.println("该用户已抢过!!");
            return false;
        } else {
            System.err.println("抢购异常!!");
            return false;
        }
    }
  • controller
    @Autowired
    private RedisService redisService;

    @PostMapping("doSecKill")
    public void doSecKill(String prodid){
        String id = UUID.randomUUID().toString();
        redisService.doSecKillByLua(id+"",prodid);
    }
  • 使用10线程进行1000次请求,200库存的商品还剩0(成功)

Redis持久化

RDB

  • 在指定的时间间隔内将内存中的数据集写入磁盘
  • 执行过程
    • Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能如果需要进行大规模数据恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。
  • Fork
    • Fork的作用是赋值一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器)数据都和原进程一致,但是是一个全新的进程,并作为原进程的子进程
    • 在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,处于效率考虑,Linux中引入了"写时复制技术"
    • 一般情况父进程和子进程会共用同一段物理内存,只有进程空间和各段的内容要发生变化时,才会将父进程的内容复制一份给子进程

  • 配置文件redis.conf

    • dbfilename dump.rdb : rdb文件名
    • dir :rdb文件存储位置
    • save : 触发策略
      • save 30 10 : 30秒内有10个key发生变化则触发rdb
    • stop-writes-on-bgsave-error yes:当Redis无法写入磁盘的话,直接关掉Reids的写操作
    • rdbcompression yes:是否进行压缩存储
    • rdbchecksum yes:进行数据校验
  • Redis恢复

    • 启动redis时会自动加载指定文件的dump.rdb文件

  • 优势
    • 适合大规模的数据恢复
    • 对数据完整性和一致性要求不高更适合使用
    • 节省磁盘空间
    • 恢复速度快
  • 劣势
    • Fork的时候,内存中的数据被克隆了一份,大致2倍的膨胀需要考虑
    • 虽然Redis在fork时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能
    • 在备份周期在一定间隔时间做一次备份,如果Redis意外down掉的话,就会丢失最后一次快照后的所有修改

AOF

  • AOF(Append Only File)
    • 以日志的形式来记录每个写操作(增量保存),将Reids执行过的所有写指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据恢复工作
  • AOF持久化流程
    • 客户端的请求写命令会被append追加到AOF缓冲区内
    • AOF缓冲区根据AOF持久化策略[always,everysec,no]将操作sync同步到磁盘的AOF文件中
    • AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量
    • Redis服务重启时,会重新load加载AOF文件中的写操作达到数据恢复的目的
  • AOF默认不开启

  • 优势
    • 备份机制更加稳健,丢失数据概率更低
  • 劣势
    • 比起RDB占用更多的磁盘空间
    • 恢复备份速度要慢
    • 每次读写都同步的话,有一定的性能压力
    • 存在个别bug,造成不能恢复

主从复制

  • 主机数据更新后根据配置和策略,自动同步到备机的master/slaver机制,master以写为主,slave以读为主


  • 复制redis.conf到指定文件在
  • 在文件夹中创建redis_6379.conf、redsi_6380.conf、redsi_6381.conf三个配置文件,每个配置文件都引入redis.conf
# redis_6379.conf
include /myredis/redis.conf
pidfile /var/run/redis_6379.pid
port 6379
dbfilename dump6379.rdb

# redis_6380.conf
include /myredis/redis.conf
pidfile /var/run/redis_6380.pid
port 6380
dbfilename dump6380.rdb

# redis_6381.conf
include /myredis/redis.conf
pidfile /var/run/redis_6381.pid
port 6381
dbfilename dump6381.rdb
  • 启动三个redis服务

  • 成功启动

  • 使用三个客户端连上三个不同端口的redis服务
  • info replication 查看主从复制的信息
[root@192 myredis]# redis-cli -p 6379
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:0
master_failover_state:no-failover
master_replid:b28861d6399d81adb7ab0efb25b64d49fd39a22e
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
  • 给从机配置主机
    • slaveof ip port (成为某个实例的从服务器)

  • 配置6380、6381端口的服务为6379服务的从机
# redis_6380
127.0.0.1:6380> slaveof 127.0.0.1 6379
OK

# redis_6381
127.0.0.1:6381> slaveof 127.0.0.1 6379
OK
  • redis_6379服务为主机且有两个从机
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:2
slave0:ip=127.0.0.1,port=6380,state=online,offset=182,lag=1
slave1:ip=127.0.0.1,port=6381,state=online,offset=182,lag=1
master_failover_state:no-failover
master_replid:e865e0544a7337dfe611426f0875e7bf430bd72f
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:182
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:182
  • 如果从服务器挂掉了,重启后需要重新执行slaveof命令配置主服务器
  • 如果主服务器挂掉了重启就行

  • 复制原理
    • Slave启动成功连接到master后会发送一个sync命令
    • Master接到命令启动后台的存盘过程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后,master将传送整个数据文件到slave,以完成一次完全同步
    • 全量复制:slave服务在接收到数据库文件数据后,将其存盘并加载到内存中
    • 增量复制:master继续将新的所有收集到额修改命令一次传给slave完成同步

- slaveof no one - 当一个master宕机后,后面的slave可以立刻升为master
  • 哨兵模式
    • 能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库
  • 使用步骤
    • 自定义的/myredis目录下新建sentinel.conf文件
    • 配置哨兵 sentinel monitor mymaster 127.0.0.1 6379 1
      • mymaster为监控对象起的服务器名称,1 为至少有多个哨兵同一迁移的数量
    • 启动哨兵
      • redis-sentinel /myredis/sentinel.conf
  • 选取策略
    • 在redis.conf中默认:replica-priority 100,值越小优先级越高
    • 选择偏移量最大的
      • 获得原主机数据最全的
    • 选择runid最小的从服务器
      • 每个redis实例启动后都会随机生成一个40位的runid

  • 连接master Redis
private static JedisSentinelPool jedisSentinelPool=null;

public static  Jedis getJedisFromSentinel(){
if(jedisSentinelPool==null){
            Set<String> sentinelSet=new HashSet<>();
            sentinelSet.add("192.168.11.103:26379");

            JedisPoolConfig jedisPoolConfig =new JedisPoolConfig();
            jedisPoolConfig.setMaxTotal(10); //最大可用连接数
jedisPoolConfig.setMaxIdle(5); //最大闲置连接数
jedisPoolConfig.setMinIdle(5); //最小闲置连接数
jedisPoolConfig.setBlockWhenExhausted(true); //连接耗尽是否等待
jedisPoolConfig.setMaxWaitMillis(2000); //等待时间
jedisPoolConfig.setTestOnBorrow(true); //取连接的时候进行一下测试 ping pong

jedisSentinelPool=new JedisSentinelPool("mymaster",sentinelSet,jedisPoolConfig);
return jedisSentinelPool.getResource();
        }else{
return jedisSentinelPool.getResource();
        }
}

应用问题

  • 缓存穿透
    • key对应的数据在数据源并不存在,每次针对此key的请求从缓存获取不到,请求都会压到数据源,从而可能压垮数据源。
    • 解决方案
      • 对空值缓存:如果一个查询返回的数据为空,仍然把这个空结果进行缓存,设置空结果的过期时间会很短,最长不超过5分钟
  • 缓存击穿
    • key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮
    • 解决方案
      • 预先设置热门数据:在redis高峰访问之前,把一些热门数据提前存入到redis里面,加大这个热门数据Key的时长
      • 实时调整:现场监控哪些数据热门,实时调整key的过期时长
  • 缓存雪崩
    • key对应的数据存在,但在redis中过期,此时若有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮
    • 缓存雪崩和缓存击穿的区别在于缓存雪崩针对很多key,缓存击穿针对一个key
    • 解决方案
      • 构建多级缓存架构:nginx缓存、redis缓存、其他缓存
      • 将缓存失效时间分散开

分布式锁(基于Redis)

  • setnx实现加锁功能
    • 设置成功(加锁),返回1
    • 设置失败(加锁失败,锁还没有释放),返回0
  • del key实现解锁功能(释放锁)

  • 问题:setnx刚好获取到锁,业务逻辑出现异常,导致锁无法释放
  • 解决:设置过期时间,自动释放锁
    • set key value nx ex 10 (加锁并设置过期时间)
    public void testLock(){
        //1获取锁过期时间为10s set lock 111 nx ex 10
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111",10, TimeUnit.SECONDS);
        //2获取锁成功、查询num的值
        if(lock){
            Object value = redisTemplate.opsForValue().get("num");
            //2.1判断num为空return
            if(StringUtils.isEmpty(value)){
                return;
            }
            //2.2有值就转成成int
            int num = Integer.parseInt(value+"");
            //2.3把redis的num加1
            redisTemplate.opsForValue().set("num", ++num);
            //2.4释放锁,del
            redisTemplate.delete("lock");

        }else{
            //3获取锁失败、每隔0.1秒再获取
            try {
                Thread.sleep(100);
                testLock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
  • 问题:可能会出现释放其他服务器的锁
    • 服务1获取到锁,执行时间为5秒,而锁的过期时间为3秒,当3秒后锁过期了,服务二拿到锁,5秒后服务以执行完毕,释放锁,此时释放的是服务2的锁
  • 解决:
    • setnx获取锁时,设置一个指定的唯一值(uuid),释放前获取这个值,判断是否是自己的锁
    public void testLock(){
        //1获取锁过期时间为10s set lock 111 nx ex 10
        String uuid = UUID.randomUUID().toString();
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,10, TimeUnit.SECONDS);
        //2获取锁成功、查询num的值
        if(lock){
            Object value = redisTemplate.opsForValue().get("num");
            //2.1判断num为空return
            if(StringUtils.isEmpty(value)){
                return;
            }
            //2.2有值就转成成int
            int num = Integer.parseInt(value+"");
            //2.3把redis的num加1
            redisTemplate.opsForValue().set("num", ++num);
            //2.4释放锁,del
            //判断是否是自己的锁,如果是则释放,如果不是(锁自动过期了)则不释放
            if (uuid.equals(redisTemplate.opsForValue().get("lock"))){
                redisTemplate.delete("lock");
            }

        }else{
            //3获取锁失败、每隔0.1秒再获取
            try {
                Thread.sleep(100);
                testLock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
  • 问题:删除操作缺乏原子性
    • 服务1执行删除是,查询到的lock值确实和uuid相等,但在执行删除前lock刚好到了过期时间,则接下来执行删除key的操作时,删除的是其他服务的锁
  • 解决:使用LUA脚本保证删除的原子性
posted @ 2022-09-28 21:58  youmo~  阅读(25)  评论(0编辑  收藏  举报