Redis6(二)

5.Redis 的发布和订阅

Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息

Redis 客户端可以订阅任意数量的频道

在这里插入图片描述

5.1 发布订阅命令行实现

1. 打开一个客户端订阅 channel1

SUBSCRIBE channel1

在这里插入图片描述

2.打开另一位客户端,给 channel1 发布消息 hello

publish channel1 hello

在这里插入图片描述

在这里插入图片描述

6.Redis 新数据类型

6.1 Bitmaps

现代计算机用二进制(位)作为信息的基础单位,1个字节等于 8位,例如 “abc” 字符串是由 3个字节组成, 但实际在计算机存储时将其用二进制表示,“abc”分别对应的 ASCII 码分别是 97、98、99,对应的二进制分别是 0110001、01100010 和 01100011,如图下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0ffzA4jZ-1622288403415)(C:\Users\heng\AppData\Roaming\Typora\typora-user-images\image-20210518210940551.png)]

合理地使用操作位能够有效地提高内存使用率和开发效率。

Redis 提供了 Bitmaps 这个 “数据类型” 可以实现对位的操作:

  • Bitmaps 本身不是一种数据类型,实际上它的字符串(key-value),但是它可以对字符串的位进行操作
  • Bitmaps 单独提供了一套命令,所以在 Redis 中使用 Bitmaps 和使用字符串的方法不太相同。把 Bitmaps 想象成一个以位为单位的数组,数组的每个单元只能存储 0 和 1, 数组的下标在 Bitmaps 中叫偏移量

在这里插入图片描述

常用命令

**setbit< key>< offset >< value> **设置 Bitmaps 中某个偏移量的值(0 或 1)

在这里插入图片描述

offset:偏移量从 0 开始

每个独立用户是否访问过网站存放在 Bitmaps 中, 将访问的用户记做 1, 没有访问的用户记做 0, 用偏移量作为用户的 id

设置键的第 offset 个位的值(从 0 算起),假设现在有 20 个用户, userid = 1,6,11,15,19 的用户对网站进行了访问,那么当前 Bitmaps 初始化结果如图

在这里插入图片描述

getbit< key>< offset> 获取 Bitmaps 中某个偏移量

在这里插入图片描述

获取键的第 offset 位的值(从 0 开始算)

在这里插入图片描述

bitcount

统计字符串被设置为 1 的 bit 数。 一般情况下,给定的整个字符串都会被进行计数,通过指定额外的 start 或 end 参数,可以让计数只在特定的位上进行。 start 和 end 参数,可以让计数只在特定的位上进行。start 和 end 参数的设置,都可以使用负数值:比如 -1 表示最后一个位,而 -2 表示倒数第二个位,start、 end 是指bit 组的字节的下标数,二者都包含

bitcount< key>[ start end] 统计字符串从 start 字节到 end 字节比特值为 1 的数量

在这里插入图片描述

bitop and(or/not/xor) < destkey> [key…]

在这里插入图片描述

bitop 是一个复合操作,它可以做多个 Bitmaps 的 and(交集)、or(并集)、not(非)、xor(异或)操作并将结果保存在 destkey 中

实例

setbit unique:users:20210518 1 1

setbit unique:users:20210518 2 1

setbit unique:users:20210518 5 1

setbit unique:users:20210518 9 1

setbit unique:users:20210517 0 1

setbit unique:users:20210517 4 1

setbit unique:users:20210517 9 1

bitop and unique:users:and:20210518_03 unique:users:20210517 unique:users:20210518

在这里插入图片描述

Bitmaps 与 set 对比

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

6.2 HyperLogLog

简介

在这里插入图片描述

在这里插入图片描述

常用命令

  • pfadd < key>< element> [element…] 添加指定元素到 HyperLogLog 中

    • 将所有元素添加到指定 HyperLogLog 数据结构做。如果执行命令后 HLL 估计的近似基数发生变化,则返回1,否则返回 0。
  • pfcount< key> [key …] `计算 HLL 的近似基数,可以计算多个 HLL,比如用 HLL 存储每天的 UV,计算一周的 UV 可以使用 7 天的 UV 合并计算即可

在这里插入图片描述

  • pfmerge< destkey>< sourcekey> [sourcekey…] 将一个或多个 HLL 合并后的结果存储在另一个 HLL 中,比如每月活跃用户可以使用每天的活跃用户来合并计算可得

在这里插入图片描述

6.3 Geospatial

Redis 3.2 中增加了对 GEO 类型的支持。 GEO ,Geographic ,地理信息的缩写。该类型,就是元素的 2 维坐标,在地图上就是经纬度。redis 基于该类型,提供了经纬度设置,查询 ,范围查询,距离查询,经纬度 Hash 等常见操作。

  • geoadd< key>< longitude>< latitude>< member> [longitude latitude member…]添加地理位置(经度,纬度,名称)

两极无法直接添加,一般会下载城市数据,直接通过 Java 程序一次性导入

有效的经度从 -180度 到 180度。 有效的纬度 从 -85.05112878度 到 85.05112878度

当坐标位置超出指定范围时,该命令将会返回一个错误

已经添加的数据, 是无法再次往里面添加的

  • geoadd < key>< member> [member…] 获得指定地区的坐标

在这里插入图片描述

  • geodist< key>< member1>< member2> [m|km|ft|mi] 获取两个位置之间的直线距离

在这里插入图片描述

  • georadius< key>< longitude>< latiude> radius m|km|ft|mi 以给定的经纬度为中心,找出某一半经内的元素

在这里插入图片描述

7.Redis_Jedis_测试

Jedis Maven

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

测试连接

public class JedisDemo {

    public static void main(String[] args) {
        //创建Jedis对象
        Jedis jedis = new Jedis("8.135.114.163",6379);

        //测试
        String value = jedis.ping();
        
    }
}

记得开放服务器 redis

  • vi /etc/redis.conf
  • bind=127.0.0.1 注掉
  • protected-mode yes 改为 protected-mode no 允许接受外面服务器

然后重启redis

在这里插入图片描述

记得防火墙或安全组开放6379

在这里插入图片描述

@Test
    public void demo01(){
        //创建Jedis对象
        Jedis jedis = new Jedis("8.135.114.xxx",6379);

        //jedis点方法 就是之前redis指令
        
        // .var 回车补全
        // 添加数据
        jedis.set("name","heng");
        //获取
        String name = jedis.get("name");
        
        //设置多个key-value
        jedis.mset("k2","v1","k3","v2");
        jedis.mget("k1","k2","k3");
        
        System.out.println(name);
        Set<String> keys = jedis.keys("*");
        for(String key : keys){
            System.out.println(key);
        }
    }

验证码栗子

package dream.jedis;

import redis.clients.jedis.Jedis;

import java.util.Random;

public class PhoneCode {

    public static void main(String[] args) {
        //模拟验证码发送
        verifyCode("13553692104");
        //getRedisCode("13553692104","482429");
    }

    //验证码校验
    public static void getRedisCode(String phone,String code){
        //从redis获取验证码

        // 连接redis
        Jedis jedis = new Jedis("x.x.x.x",6379);
        //验证码key
        String codeKey = "VerifyCode"+phone+":code";
        String redisCode = jedis.get(codeKey);
        //判断
        if(redisCode.equals(code)){
            System.out.println("成功");
        } else {
            System.out.println("失败");
        }
        jedis.close();
    }

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

        //拼接key
        //手机发送次数key
        String countKey = "VerifyCode"+phone+":count";
        //验证码key
        String codeKey = "VerifyCode"+phone+":code";

        //每个手机每天只能发送三次
        String count = jedis.get(countKey);
        if(count == null){
            //没有发送次数,第一次发送
            //设置发送次数是1
            jedis.setex(countKey,24*60*60,"1");
        }else if(Integer.parseInt(count)<=2){
            // 发送次数+1
            jedis.incr(countKey);
        } else if(Integer.parseInt(count)>2){
            //发送三次,不能在发送
            System.out.println("今天发送次数已经超过三次");
            jedis.close();
        }

       //发送验证码放到redis里面
        String vcode = getCode();
        jedis.setex(codeKey,120,vcode);
        jedis.close();
    }

    //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;
    }
}

8.SpringBoot 整合 Redis

依懒:

<!--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.yml文件

spring:
  redis:
    #Redis 服务器地址
    host: x.x.x.x
    #Redis 服务器连接端口
    port: 6379
    #Redis 数据库索引(默认为0)
    database: 0
    #连接超时时间(毫秒)
    timeout: 1800000
    #连接池最大连接数(使用负值表示没有限制)
    lettuce:
      pool:
        max-active: 20
        #最大阻塞等待时间(负数表示没有限制)
        max-wait: 1
        #连接池中的最大空闲连接
        max-idle: 5
        #连接池中的最小空闲连接
        min-idle: 0

Redis 配置类

@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.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, 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.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, 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;
    }
}

测试类

@Controller
public class TestController {

    @Autowired
    private RedisTemplate redisTemplate;


    @GetMapping("/testRedis")
    public void testRedis(){
        redisTemplate.opsForValue().set("name","heng");
        System.out.println(redisTemplate.opsForValue().get("name"));
    }
}

结果:

在这里插入图片描述

9.Redis-事务-锁机制-秒杀

9.1 Redis 的事务定义

Redis 事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。

Redis 事服的主要作用就是串联多个命令防止别的命令插队。

9.2 Multi、Exec、discard

  • Multi 开启事务 执行(set、get等指令进入队列)
  • discard 取消事务(退出前执行取消前面队列没有成功的指令)
  • Exce 退出执行

在这里插入图片描述

栗子:

在这里插入图片描述

事务执行时 有1 或 N指令出错:

在这里插入图片描述

在这里插入图片描述

总结:

  • 组队时 指令没问题 执行时 那个有错误 就报那个错
  • 组队时 指令有问题就全部都报错

9.3事务冲突

栗子:

在这里插入图片描述

悲观锁(Pessimistic Lock)

每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会 block 直到他拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁表锁等,读锁写锁等,都是在操作之前先上

乐观锁(Optimistic Lock)

每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。

乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种 check-and-set 机制实现事务的。

场景:抢票

在这里插入图片描述

WATCH key [key …]

在执行 multi 之前,先执行 watch key1 [key2],可以监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断`。

乐观锁使用

开始版本号 1.0

在 会话1 加watch exec 完成指令会更改版本号 1.1

在这里插入图片描述

在 会话2 加watch 所以会报版本号错误 nil 1.0 判断 1.1

在这里插入图片描述

10.Redis 事务三特性

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

11. Redis _ 事务 _ 秒杀案例

11.1 解决计数器和人员记录的事务操作

在这里插入图片描述

栗子:

@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.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, 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.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, 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;
    }
}

结果:设置了10个产品

在这里插入图片描述

11.2 Redis事务–秒杀并发模拟

我个人使用jmeter 使用jmeter 测试跳 测试开始

使用

使用工具 ab 模拟传输

CentOS6 需要手动安装

联网:yum install httpd-tools

无网:

测试下是否安装成功:ab --help

  • -n 请求次数
  • -c 并发次数
  • -t POST/PUT 参数类型 需要设置 ‘application/x-www-form-urlencoded’
  • -p POST 请求时 参数

测试开始

ab:

ab -n 1000 -c 100 -p ~/postfile http://x.x.x.x:8060/testRedis

ab -n 1000 -c 100 -p ~/postfile -T application/x-www-form-urlencoded http://x.x.x.x:8060/testRedis

jmeter:

按链接栗子测试 就好

设置库存

x.x.x.x:6379> del sk:0101:user(integer) 1x.x.x.x:6379> set sk:0101:qt 10OKx.x.x.x:6379> get sk:0101:qt"10"

结果:

查看:

x.x.x.x:6379> keys *1) "sk:0101:user"2) "sk:0101:qt"

get sk:0101:qt

x.x.x.x:6379> keys *1) "sk:0101:user"2) "sk:0101:qt"x.x.x.x:6379> get sk:0101:qt"-25"

出现-25 我是 200次数 100并发

x.x.x.x:6379> del sk:0101:user(integer) 1x.x.x.x:6379> set sk:0101:qt 10OKx.x.x.x:6379> get sk:0101:qt"10"x.x.x.x:6379> keys *1) "sk:0101:user"2) "sk:0101:qt"x.x.x.x:6379> get sk:0101:qt"-2"

出现-2 我是 1000次数 100并发

在删除用户 del sk:0101:user

set sk:0101:qt 10

get sk:0101:qt

clear

执行

ab -n 2000 -c 200 -p ~/postfile http://localhost:8060/testRedis

ab -n 2000 -c 200 -p ~/postfile -T application/x-www-form-urlencoded http://localhost:8060/testRedis

x.x.x.x:6379> del sk:0101:user(integer) 1x.x.x.x:6379> set sk:0101:qt 10OKx.x.x.x:6379> get sk:0101:qt"10"x.x.x.x:6379> keys(error) ERR wrong number of arguments for 'keys' commandx.x.x.x:6379> keys *1) "sk:0101:user"2) "sk:0101:qt"x.x.x.x:6379> get sk:0101:qt"-3"

出现-2 我是 2000次数 200并发

在 del sk:0101:user

set sk:0101:qt 10

get sk:0101:qt

ab -n 2000 -c 300 -p ~/postfile http://localhost:8060/testRedis

ab -n 2000 -c 300 -p ~/postfile -T application/x-www-form-urlencoded http://localhost:8060/testRedis

x.x.x.x:6379> del sk:0101:user(integer) 1x.x.x.x:6379> set sk:0101:qt 10OKx.x.x.x:6379> get sk:0101:qt"10"x.x.x.x:6379> get sk:0101:qt"-5"

出现-5 我是 1800次数 300并发

11.3 超卖问题

在这里插入图片描述

栗子2:

127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> set sk:0101:qt 10
OK
127.0.0.1:6379> keys *
1) "sk:0101:qt"
127.0.0.1:6379> get sk:0101:qt
"10"
127.0.0.1:6379> get sk:0101:qt
"0"
127.0.0.1:6379> 

出现0 1000次数 100并发

思路

  • 建立连接池
package com.blog.config;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

public class JedisPoolUtil {
    private static volatile JedisPool jedisPool = null;

    private JedisPoolUtil() {
    }

    public static JedisPool getJedisPoolInstance() {
        if (null == jedisPool) {
            synchronized (JedisPoolUtil.class) {
                if (null == jedisPool) {
                    JedisPoolConfig poolConfig = new JedisPoolConfig();
                    poolConfig.setMaxTotal(200);
                    poolConfig.setMaxIdle(32);
                    poolConfig.setMaxWaitMillis(100*1000);
                    poolConfig.setBlockWhenExhausted(true);
                    poolConfig.setTestOnBorrow(true);  // ping  PONG

                    jedisPool = new JedisPool(poolConfig, "8.135.114.163", 7321, 60000 );
                }
            }
        }
        return jedisPool;
    }

    public static void release(JedisPool jedisPool, Jedis jedis) {
        if (null != jedis) {
            jedisPool.close();
        }
    }
}
  • 乐观锁 watch
 public String testRedis()  {
//        redisTemplate.opsForValue().set("name","heng");
//        System.out.println(redisTemplate.opsForValue().get("name"));
        String Idcode = getCode();
        boolean b = doSecKill(Idcode, "0101");
        if(b){
            System.out.println("秒杀成功了!哇嘎嘎");
            return "正确";
        } else {
            return "抢完了!哇嘎嘎";
        }
    }

    public static boolean doSecKill(String uid,String prodid) {
        //1 uid和prodid非空判断
        if(uid == null || prodid == null){
            return false;
        }

        //2 连接redis
//        Jedis jedis = new Jedis("8.135.114.163",7321);
        //通过连接池得到jedis对象
        JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
        Jedis jedis = jedisPoolInstance.getResource();

        //3 拼接key
        //3.1库存key
        String kckey = "sk:"+prodid+":qt";
        //3.2 秒杀成功用户key
        String userKey = "sk:"+prodid+":user";
        //监控库存
        jedis.watch(kckey);
        //4 获取库存,如果库存null,秒杀还没有开始
        String kc = jedis.get(kckey);
        if(kc == null){
            System.out.println("秒杀还没有开始,清等待");
            jedis.close();
            return false;
        }

        //5 判断用户是否重复秒杀操作
        if(jedis.sismember(userKey, uid)){
            System.out.println("已经秒杀成功了,不能重复秒杀");
            jedis.close();
            return false;
        }
        //6 判断如果商品数量,库存数量小于1,秒杀结束
        if(Integer.parseInt(kc)<=0){
            System.out.println("秒杀已经结束了");
            jedis.close();
            return false;
        }

        //7秒杀过程
        //使用事务
        Transaction multi = jedis.multi();
        //组队操作
        multi.decr(kckey);
        multi.sadd(userKey,uid);
        //执行
        List<Object> results = multi.exec();

        if(results == null || results.size() == 0){
            System.out.println("秒杀失败了....");
            jedis.close();
            return false;
        }
        //7.1 库存-1
        //jedis.decr(kckey);
        //7.2 把秒杀成功用户添加清单里面
        //jedis.sadd(userKey,uid);

        return true;
    }

11.4 解决库存遗留的问题

乐观锁造成的库存遗留的问题

N个人抢 版本号 来不及

在这里插入图片描述

在这里插入图片描述

栗子3:

在这里插入图片描述

@GetMapping("/testRedis")
    public String testRedis()  {
//        redisTemplate.opsForValue().set("name","heng");
//        System.out.println(redisTemplate.opsForValue().get("name"));
        String Idcode = getCode();
        boolean b = doSecKill2(Idcode, "0101");
        if(b){
            System.out.println("秒杀成功了!哇嘎嘎");
            return "正确";
        } else {
            return "抢完了!哇嘎嘎";
        }
    }

//lua
static 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" ;

    static String secKillScript2 =
            "local userExists=redis.call(\"sismember\",\"{sk}:0101:usr\",userid);\r\n" +
                    " return 1";


    public static boolean doSecKill2(String uid,String prodid)  {

        JedisPool jedispool =  JedisPoolUtil.getJedisPoolInstance();
        Jedis jedis=jedispool.getResource();

        //String sha1=  .secKillScript;
        String sha1=  jedis.scriptLoad(secKillScript);
        Object result= jedis.evalsha(sha1, 2, uid,prodid);

        String reString=String.valueOf(result);
        if ("0".equals( reString )  ) {
            System.err.println("已抢空!!");
        }else if("1".equals( reString )  )  {
            System.out.println("抢购成功!!!!");
        }else if("2".equals( reString )  )  {
            System.err.println("该用户已抢过!!");
        }else{
            System.err.println("抢购异常!!");
        }
        jedis.close();
        return true;
    }

结果:

127.0.0.1:6379> del sk:0101:user
(integer) 1
127.0.0.1:6379> set sk:0101:qt 500
OK
127.0.0.1:6379> get sk:0101:qt
"500"
127.0.0.1:6379> get sk:0101:qt
"500"
127.0.0.1:6379> get sk:0101:qt
"478"
// 解决遗留问题
127.0.0.1:6379> del sk:0101:user
(integer) 1
127.0.0.1:6379> set sk:0101:qt 500
OK
127.0.0.1:6379> keys *
1) "sk:0101:qt"
127.0.0.1:6379> get sk:0101:qt
"500"
127.0.0.1:6379> get sk:0101:qt
"0"

出现0 1800次数 300并发

总结:
  • 第一版 出现超卖问题,查看库存会出现负数

  • 第二版 使用乐观锁 版本号问题 watch 事务 解决秒杀

    • 出现连接池超时问题
    • 遗留库存
  • 连接池超时问题 使用工具类

  • 解决遗留问题使用 lua脚步解决

12. Redis 持久化

  • RDB(Redis DataBase)
  • AOF(Append Of File)

12.1 RDB(Redis DataBase)

简介

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

12.1.1 备份是如何执行的

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

12.1.2 Fork

  • Fork 的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器)数值都和原进程一致,但是 是一个全新的进程,作为原进程的子进程
  • 在 Linux 程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会 exec 系统调用,出于效率考虑,Linux 中引入了 “写时复制技术”
  • 一般情况父进程和子进程会共用一段物理内存,只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程

12.1.3 dump.rdb 文件

在 redis.conf 中配置文件名称,默认为 dump.rdb

在这里插入图片描述

配置快照的 时间间隔

在这里插入图片描述

解开

save 三个值

设置 第二个值 30 10 代表(30个key 和 10改变就持久化操作)

在这里插入图片描述

查看

在这里插入图片描述

持久化计算

在这里插入图片描述

先满一车 在计下一车

12.1.4 命令 save VS bgsave

  • save:save 时只管保存,其它不管,全部阻塞。手动保存。不建议
  • bgsave:Redis 会在后台异步进行快照操作,快照同时还可以响应客户端请求

手动操作(不推荐)
在这里插入图片描述

12.1.5 flushall 命令

执行 flushall 命令,也会产生 dump.rdb 文件,但里面是空的,无意义

12.1.5 Save

格式:save 秒钟 写操作次数

RDB 是整个内存的压缩过的 snapshot ,RDB 的数据结构,可以配置复合的快照触发条件

默认是一分钟改了 1W次,或 5 分钟内改了 10 次,或 15 分钟内改了 1 次

禁用

不设置 save 指令,或者给 save 传入空字符串

12.1.6 stop-writes-on-bgsave-error

在这里插入图片描述

当 Redis 无法写入磁盘的话,直接关掉Redis 的写操作, 推荐 yes

12.1.6 rdbcompression 压缩文件

在这里插入图片描述

对于存储到磁盘中的快照,可以设置是否进行压缩存储,如果是的话,redis 会采用 LZF算法进行压缩

如果你不想消耗 cpu 来进行压缩的话,可以设置为关闭此功能。推荐 yes

12.1.6 rdbchecksum 检查完整性

在这里插入图片描述

在存储快照后,还可以让 redis 使用 CRC64 算法来进行数据校验

但是这样做会增加大约 10% 的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能 推荐 yes

12.1.7 rdb 的备份

先通过 config get dir 查询 rdb 文件的目录

将*.rdb 的文件拷贝到别的地方

rdb 的恢复

  • 关闭 redis
  • 先备份的文件拷贝到工作目录下 cp dump2.rdb dump.rdb
  • 启动 redis,备份数据直接加载

12.2 AOF(Append Only File)

简介:

以日志的形式来记录每个写操作(增量保存),将 Redis 执行过的所有写指令记录下来(读操作不记录只许追加文件但不可以改写文件,redis 启动之初会读取该文件重新构建数据,Redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作

12.2.1 AOF持久化流程

  • (1)客户端的请求写命令会被 append 追加到 AOF 缓冲区内;

  • (2)AOF 缓冲区根据 AOF 持久化策略[always,everysec,on]将操作 sync 同步到磁盘的 AOF 文件中;

  • (3)AOF 文件大小超过重写策略或手动重写时,会对 AOF 文件 rewite 重写 ,压缩 AOF 文件容量;

  • (4)Redis 服务重启时,会重新 load 加载 AOF 文件中的写操作达到数据恢复的目的;

12.2.2 AOF 默认不开启

可以在 redis.conf 中配置文件名称,默认为appendonly.aof

AOF 文件的保存路径,同 RDB 的路径一致

在这里插入图片描述

位置在你启动默认当前文件夹下

在这里插入图片描述

12.2.3 AOF 和 RDB 同时开启, reids 听谁的?

AOF 和 RDB 同时开启,系统默认取 AOF 的数据(数据不会存在丢失)

12.2.4 AOF启动/修复/恢复

  • AOF的备份机制和性能虽然和 RDB 不同,但是备份和恢复的操作同 RDB 一样,都是拷贝备份文件,需要恢复时再拷贝到 Redis 工作目录下,启动系统即加载
  • 正常恢复
    • 修改默认的 appendonly no,改为 yes
    • 将有数据的 aof 文件复制一份保存到对应目录(查看目录:config get dir)
    • 恢复:重启 redis 然后重新加载

恢复栗子:

初始

在这里插入图片描述

添加数据

127.0.0.1:3366> keys *
(empty array)
127.0.0.1:3366> set k11 v11
OK
127.0.0.1:3366> set k12 v12
OK
127.0.0.1:3366> set k13 v13
OK
127.0.0.1:3366> set k14 v14
OK

结果:

[root@lehua bin]# ll
total 18876
-rw-r--r-- 1 root root     147 May 27 17:35 appendonly.aof
-rwxr-xr-x 1 root root 4829568 May 27 17:01 redis-benchmark
lrwxrwxrwx 1 root root      12 May 27 17:01 redis-check-aof -> redis-server
lrwxrwxrwx 1 root root      12 May 27 17:01 redis-check-rdb -> redis-server
-rwxr-xr-x 1 root root 5002976 May 27 17:01 redis-cli
lrwxrwxrwx 1 root root      12 May 27 17:01 redis-sentinel -> redis-server
-rwxr-xr-x 1 root root 9484312 May 27 17:01 redis-server

备份栗子:

[root@lehua bin]# cp appendonly.aof appendonly.aof.bak			//备份appendonly.aof.bak
[root@lehua bin]# rm -rf appendonly.aof							//删除原文件
[root@lehua bin]# ll
total 18880
-rw-r--r-- 1 root root     147 May 27 17:37 appendonly.aof.bak
-rw-r--r-- 1 root root     133 May 27 17:38 dump.rdb
-rwxr-xr-x 1 root root 4829568 May 27 17:01 redis-benchmark
lrwxrwxrwx 1 root root      12 May 27 17:01 redis-check-aof -> redis-server
lrwxrwxrwx 1 root root      12 May 27 17:01 redis-check-rdb -> redis-server
-rwxr-xr-x 1 root root 5002976 May 27 17:01 redis-cli
lrwxrwxrwx 1 root root      12 May 27 17:01 redis-sentinel -> redis-server
-rwxr-xr-x 1 root root 9484312 May 27 17:01 redis-server

然后改备份文件为原文件名称

[root@lehua bin]# mv appendonly.aof.bak appendonly.aof
[root@lehua bin]# ll
total 18880
-rw-r--r-- 1 root root     147 May 27 17:37 appendonly.aof
-rw-r--r-- 1 root root     133 May 27 17:38 dump.rdb
-rwxr-xr-x 1 root root 4829568 May 27 17:01 redis-benchmark
lrwxrwxrwx 1 root root      12 May 27 17:01 redis-check-aof -> redis-server
lrwxrwxrwx 1 root root      12 May 27 17:01 redis-check-rdb -> redis-server
-rwxr-xr-x 1 root root 5002976 May 27 17:01 redis-cli
lrwxrwxrwx 1 root root      12 May 27 17:01 redis-sentinel -> redis-server
-rwxr-xr-x 1 root root 9484312 May 27 17:01 redis-server

然后退出 redis-server 重启服务 会读取 AOF的内容

127.0.0.1:3366> shutdown
not connected> exit
[root@lehua bin]# redis-server /etc/redis.conf
[root@lehua bin]# redis-cli -p 3366 -a ^^7750731hua
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
127.0.0.1:3366> keys *
1) "k13"
2) "k14"
3) "k12"
4) "k11"

异常恢复:

  • 修改默认的 appendonly on 改为 yes
  • 如遇到 AOF 文件损坏,通过/usr/local/bin/redis-check-aof-fix appendonly进行恢复
  • 备份被写坏的 AOF 文件
  • 恢复:重启 reids,然后重新加载

异常栗子:

[root@lehua bin]# vi appendonly.aof

加数据 Hello

在这里插入图片描述

[root@lehua bin]# redis-server /etc/redis.conf
[root@lehua bin]# redis-cli -p 3366 -a ^^7750731hua
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
Could not connect to Redis at 127.0.0.1:3366: Connection refused
not connected> 

使用 reids-check-aof --fix

[root@lehua bin]# redis-check-aof --fix appendonly.aof
0x              93: Expected prefix '*', got: 'H'
AOF analyzed: size=154, ok_up_to=147, ok_up_to_line=34, diff=7
This will shrink the AOF from 154 bytes, with 7 bytes, to 147 bytes
Continue? [y/N]: y
Successfully truncated AOF

修复成功

在查看 vi appendonly.aof

[root@lehua bin]# redis-server /etc/redis.conf
[root@lehua bin]# redis-cli -p 3366 -a ^^7750731hua
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
127.0.0.1:3366> keys *
1) "k13"
2) "k11"
3) "k14"
4) "k12"

12.2.5 AOF 同步频率设置

  • appendfsync always

  • 始终同步,每次 Redis 的写入都会立刻记入日志;性能较差但数据完整性比较好

  • appendfsync everysec

  • 每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失

  • appendfsync on

  • redis 不主动进行同步,把同步时机交给操作系统

12.2.6 Rewrite 压缩

简介

是什么:

AOF 采用文件追加方式,文件会越来越大为避免出现此种情况,新增了重写机制,当AOF文件的大小超过所设定的

阈值时, Redis 就会启动 AOF 文件的内容压缩,只保留可以恢复数据的最小指令集,可以使用命令 bgrewriteaof

重写原理,如何实现重写

AOF 文件持续增长而过大时,会 fork 出一条新进程来将文件重写(也是先写临时文件最后再 rename),redis 4.0 版本后重写,是指上就是把 rdb 的快照,以二级制的形式附在新的 aof 头部,作为已有的历史数据,替换掉原来的流水账操作

no-appendfsync-on-rewrite:

如果 no-appendfsync-on-rewrite=yes ,不写入 aof 文件只写入缓存,用户请求不会,阻塞,但是在这段数据如果宕机会丢失这段时间的缓存数据。(降低数据安全性,提高性能)

如果 no-appendfsync-on-rewrite=on,还是会把数据往磁盘里刷,但是遇到重写操作,可能会发生阻塞。(数据安全,但是性能降低)

触发机制,何时重写

Redis 会记录上次重写时的 AOF 大小,默认配置是当 AOF 文件大小是上次 rewrite 后大小的一倍且文件大于 64M 时触发

重写虽然可以节约大量磁盘空间,减少恢复时间。但是每次重写还是有一定的负担的,因此设定 Redis 要满足一定条件才会进行重写

auto-aof-rewrite-percentage:设置重写的基准值,文件达到 100% 时开始重写(文件是原来重写文件的 2 倍时触发)

auto-aof-rewrite-min-size:设置重写的基准值,最小文件 64 MB。 达到这个值开始重写

在这里插入图片描述

重写流程
  • bgrewriteaof 触发重写,判断是否当前有 bgsave 或 bgrewriteaof 在运行,如果,则等待该命令结束后再继续执行

  • 主进程 fork 出子进程执行重写操作,保证主进程不会阻塞

  • 子进程遍历 redis 内存中数据到临时文件,客户端的写请求同时写入 aof_buf 缓冲区和 aof_rewrite_buf 重写缓冲区保证原 AOF 文件完整以及新 AOF 文件生成期间的新的数据修改动作不会丢失。

  • 1).子进程写完新的 AOF 文件后,向主进程发信号,父进程更新统计信息。

    • 2).主进程把 aof_rewrite_buf 中的数据写入到新的 AOF 文件
  • 使用新的 AOF 文件覆盖旧的 AOF 文件,完成 AOF 重写

12.2.7 优势

在这里插入图片描述

  • 备份机制更稳健,丢失数据概率更低
  • 可读的日志文本,通过操作 AOF 稳健,可以处理误操作

12.2.8 劣势

  • 比起 RDB 占用更多的磁盘空间
  • 恢复备份速度要慢
  • 每次读写都同步的话,有一定的性能压力
  • 存在个别 Bug,造成恢复不能

12.3 总结

在这里插入图片描述

用哪个好

官方推荐两个都启用

如果对数据不敏感,可以选单独用 RDB

不建议单独用 AOF,因为可能会出现 Bug

如果只能做纯内存缓存,可以都不用

12.3.1 官方建议

  • RDB 持久化方式能够在指定的时间间隔能对你的数据进行快照存储
  • AOF 持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF 命令以 redis 协议追加保存每次写的操作到文件末尾
  • Redis 还能对 AOF文件进行后台重写,使得 AOF 文件的体积不至于过大
  • 只中缓存:如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化方式
  • 同时开启两种持久化方式
  • 在这种情况下,当 redis 重启的时候会优先载入 AOF 文件来恢复原始的数据,因为在通常情况下 AOF 文件保存的数据集要比 RDB 文件保存的数据集要完整
  • RDB 的数据不实时,同时使用两者时服务器重启也只会找 AOF 文件。 那要不要只使用 AOF 呢?
  • 建议不要,因为 RDB 更合适用于备份数据库(AOF 在不断变化不好备份),快速重启,而且不会有 AOF 可能潜在的 Bug,留着作为一个万一的手段
  • 性能建议
    • 因为 RDB 文件只用作为后备用途,建议只在 Slave 上持久化 RDB 文件 ,只要15分钟备份一次就够了,只保留 save 900 1这条规则
    • 如果使用 AOF ,好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单只 load 自己的 AOF 文件就可以了
    • 代价
      • 带来了持续的 IO
      • AOF rewrite 的最后将 rewrite 过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的
    • 只要硬盘许可,应该尽量减少 AOF rewrite 的频率,AOF 重写的基础大小默认值 64M太小了,可以设到5G以上
    • 默认超过原大小 100% 大小时重写可以改到适当的数值
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值