0%

Redis学习笔记

前言

  NoSQL(NoSQL = Not Only SQL ),意即“不仅仅是SQL”,泛指非关系型的数据库。NoSQL 不依赖业务逻辑方式存储,而以简单的key-value模式存储。因此大大的增加了数据库的扩展能力。常用的有Memcache,Redis,MongoDB

NoSQL数据库

简介

NoSQL(NoSQL = Not Only SQL ),意即“不仅仅是SQL”,泛指非关系型的数据库
NoSQL 不依赖业务逻辑方式存储,而以简单的key-value模式存储。因此大大的增加了数据库的扩展能力。常用的有Memcache,Redis,MongoDB

  • 不遵循SQL标准。

  • 不支持ACID。

  • 远超于SQL的性能。

    适用场景

  • 对数据高并发的读写

  • 海量数据的读写

  • 对数据高可扩展性的

    不适用场景

  • 需要事务支持

  • 基于sql的结构化查询存储,处理复杂的关系,需要即席查询。

  • (用不着sql的和用了sql也不行的情况,请考虑用NoSql)

Redis概述安装

  1. 安装步骤
    • 需要先安装gcc:yum install gcc(gcc –version命令判断有无安装)
    • 到下载文件目录执行以下命令
    • image.png
    • make install安装结束
  2. 安装默认文件目录及对应组件作用

image.png

  1. 启动redis服务
    1. 前台启动(不推荐)

image.png
不推荐使用,离开这个页面就会自动关闭(CTRL+c手动关闭)

  1. 后台启动
    • 默认不支持后台启动,设置如下
    • 复制redis.conf文件到其他目录 cp redis.conf /etc/redis.conf
    • 修改复制到新目录的redis.conf vi redis.conf后台启动设置daemonize no改成yes
    • 重新回到usr/local/bin目录下启动 redis-server /etc/redis.conf
    • 客户端访问命令redis-cli(redis-cli -p6379)
    • 查看redis相关端口ps -ef|grep redis
  2. 关闭服务
    1. 单实例redis-cli shutdown
    2. 多实例关闭,指定端口关闭:redis-cli -p 6379 shutdown
    3. 进入终端直接shutdown
    4. 查看服务然后,kill pid
  3. 相关知识

image.png

常用数据类型

key相关命令操作

  • keys *查看当前库所有key (匹配:keys *1)
  • exists key判断某个key是否存在
  • type key 查看你的key是什么类型
  • del key 删除指定的key数据
  • unlink key 根据value选择非阻塞删除
  • 仅将keys从keyspace元数据中删除,真正的删除会在后续异步操作。
  • expire key 10 10秒钟:为给定的key设置过期时间
  • ttl key 查看还有多少秒过期,-1表示永不过期,-2表示已过期

其他命令

  • select命令切换数据库
  • dbsize查看当前数据库的key的数量
  • flushdb清空当前库
  • flushall通杀全部库

image.png

string

  1. String是Redis最基本的类型,一个key对应一个value。

String类型是二进制安全的。Redis的string可以包含任何数据。比如jpg图片或者序列化的对象。
String类型是Redis最基本的数据类型,一个Redis中字符串value最多可以是512M

  1. 命令
  • set 添加键值对
  • get 查询对应键值
  • append 将给定的 追加到原值的末尾
  • strlen 获得值的长度
  • setnx 只有在 key 不存在时 设置 key 的值
  • incr
  • 将key 中储存的数字值增1
  • 只能对数字值操作,如果为空,新增值为1
  • decr 将key 中储存的数字值减1;只能对数字值操作,如果为空,新增值为-1
  • incrby / decrby <步长>将key 中储存的数字值增减。自定义步长。
  • image.png
  • mset ….. 同时设置一个或多个key-value对
  • mget …..同时获取一个或多个value
  • msetnx ….. 同时设置一个或多个key-value 对,当且仅当所有给定 key 都不存在。原子性,有一个失败则都失败
  • getrange <起始位置><结束位置>获得值的范围,类似java中的substring,前包,后包
  • setrange <起始位置> 覆写所储存的字符串值,从<起始位置>开始**(索引从0开始)。**
  • setex <过期时间>设置键值的同时,设置过期时间,单位秒。
  • getset 以新换旧,设置了新值同时获得旧值。
  • image.png

image.png
set时候后面可选参数

  • *NX:当数据库中key不存在时,可以将key-value添加数据库
  • *XX:当数据库中key存在时,可以将key-value添加数据库,与NX参数互斥
  • *EX:key的超时秒数
  • *PX:key的超时毫秒数,与EX互斥

注意:redis中的操作均是原子操作,因为redis本身就是单线程的;Java中不是原子操作
有意思的示例:
image.png

  1. 数据结构

类似于Java中的arraylist;
image.png
如图中所示,内部为当前字符串实际分配的空间capacity一般要高于实际字符串长度len。当字符串长度小于1M时,扩容都是加倍现有的空间,如果超过1M,扩容时一次只会多扩1M的空间。需要注意的是字符串最大长度为512M。

list

  1. 简介

单键多值,底层为一个双向列表,可从两端插入数据,根据插入数据排序。

  1. 命令
  • lpush/rpush …. 从左边/右边插入一个或多个值。
  • lpop/rpop 从左边/右边吐出一个值。值在键在,值光键亡。
  • rpoplpush 列表右边吐出一个值,插到列表左边。

改成lpoprpush不行

  • lrange 按照索引下标获得元素(从左到右)(0-1表示获取所有
  • lindex 按照索引下标获得元素(从左到右)
  • llen 获得列表长度
  • linsert before 的后面插入插入值
  • lrem 从左边删除n个value(从左到右)
  • lset将列表key下标为index的值替换成value
  • image.png
  1. 数据结构

List的数据结构为快速链表quickList
首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是ziplist,也即是压缩列表。
它将所有的元素紧挨着一起存储,分配的是一块连续的内存。
当数据量比较多的时候才会改成quicklist。
因为普通的链表需要的附加指针空间太大,会比较浪费空间。比如这个列表里存的只是int类型的数据,结构上还需要两个额外的指针prev和next。
image.png
Redis将链表和ziplist结合起来组成了quicklist。也就是将多个ziplist使用双向指针串起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。

set

  1. 简介

可以看作没有重复值的list;并且set提供判断一个值是否存在于key中

  1. 命令
  • sadd ….. 将一个或多个member 元素加入到集合 key 中,已经存在的member 元素将被忽略
  • smembers 查看该集合的所有值。
  • sismember 判断集合是否为含有该值,有1,没有0
  • scard返回该集合的元素个数。
  • srem …. 删除集合中的指定元素。
  • spop 随机从该集合中吐出一个值,并将其删除
  • srandmember 随机从该集合中取出n个值。不会从集合中删除
  • image.png
  • smove value把集合中一个值从一个集合移动到另一个集合
  • sinter 返回两个集合的交集元素。
  • sunion 返回两个集合的并集元素。
  • sdiff 返回两个集合的差集元素(key1中的,不包含key2中的)
  • image.png
  1. 数据结构

Set数据结构是dict字典,字典是用哈希表实现的
Java中HashSet的内部实现使用的是HashMap,只不过所有的value都指向同一个对象。Redis的set结构也是一样,它的内部也使用hash结构,所有的value都指向同一个内部值。

hash

  1. 简介

hash就相当于键值对的集合,适合存储对象,key为字符串,value为键值对集合;类似于Java中的map,如下图所示
image.png

  1. 命令
  • hset 集合中的 键赋值
  • hget 集合取出value
  • hmset 批量设置hash的值
  • hexists查看哈希表 key 中,给定域 field 是否存在。
  • hkeys 列出该hash集合的所有field
  • hvals 列出该hash集合的所有value
  • hincrby 为哈希表 key 中的域 field 的值加上增量1 -1
  • hsetnx 将哈希表 key 中的域 field 的值设置为value ,当且仅当域field 不存在.
  • image.png
  1. 数据结构

Hash类型对应的数据结构是两种:ziplist(压缩列表),hashtable(哈希表)。当field-value长度较短且个数较少时,使用ziplist,否则使用hashtable。

zset

  1. 简介

zset就是一个有序的set,实现可以理解为hash,不过添加了一个score,来根据这个score的大小进行排序。

  1. 命令
  • zadd …将一个或多个member 元素及其 score 值加入到有序集key 当中。
  • zrange [WITHSCORES] 返回有序集key 中,下标在之间的元素(0,-1全部返回)带WITHSCORES,可以让分数一起和值返回到结果集。
  • zrangebyscore key [withscores] [limit offset count]返回有序集key 中,所有 score 值介于min 和max 之间(包括等于min 或max )的成员。有序集成员按score 值递增**(从小到大**)次序排列。
  • zrevrangebyscore key [withscores] [limit offset count] 同上,改为从大到小排列。
  • zincrby 为元素的score加上增量
  • zrem 删除该集合下,指定值的元素
  • zcount 统计该集合,分数区间内的元素个数
  • zrank 返回该值在集合中的排名,从0开始。
  • image.png
  1. 数据结构

SortedSet(zset)是Redis提供的一个非常特别的数据结构,一方面它等价于Java的数据结构 Map<String, Double>,可以给每一个元素value赋予一个权重score,另一方面它又类似于TreeSet,内部的元素会按照权重score进行排序,可以得到每个元素的名次,还可以通过score的范围来获取元素的列表。
zset底层使用了两个数据结构
(1)hash,hash的作用就是关联元素value和权重score,保障元素value的唯一性,可以通过元素value找到相应的score值。
(2)跳跃表,跳跃表的目的在于给元素value排序,根据score的范围获取元素列表。

配置文件

  1. 网络相关配置

image.png
默认只接受本机访问,不写则不限则;
image.png
生产环境要写应用服务器的地址;服务器是需要远程访问的,所以需要将bind注释掉,并且protected-mode no(本机保护模式,这个代表只接受本机访问)
image.png\

  • 设置tcp的backlog,backlog其实是一个连接队列,backlog队列总和=未完成三次握手队列+ 已经完成三次握手队列。
  • timeout 一个空闲的客户端维持多少秒会关闭,0表示关闭该功能。即永不关闭。
  • tcp keepalive对访问客户端的一种心跳检测,每个n秒检测一次。单位为秒,如果设置为0,则不会进行Keepalive检测,建议设置成60
  1. 通用设置

image.png
后台启动
image.png
存放pid文件的位置·,每个实例不同pid文件
image.png
日志级别设置:指定日志记录级别,Redis总共支持四个级别:debug、verbose、notice、warning,默认为notice;四个级别根据使用阶段来选择,生产环境选择notice 或者warning
image.png
日志文件存放位置
image.png
数据库数目,默认16个

  1. 密码设置

image.png
设置密码
在命令中设置密码,只是临时的。重启redis服务器,密码就还原了。
永久设置,需要再配置文件中进行设置。
image.png

Redis中的发布和订阅

  1. 概念

Redis 发布订阅 (pub/sub) 是一种消息通信模式:发送者 (pub) 发送消息,订阅者 (sub) 接收消息。
Redis 客户端可以订阅任意数量的频道。
image.png

  1. 命令实现
  • 打开一个客户端订阅channel1 SUBSCRIBE channel1

image.png

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

image.png
返回的1是订阅者数量

  • 打开第一个客户端可以看到发送的消息

image.png
注:发布的消息没有持久化,如果在订阅的客户端收不到hello,只能收到订阅后发布的消息

新的数据类型

Bitmaps

  1. 简介

本质上就是字符串,不过是在位上操作;大量用户访问可以减少内存占用

  1. 命令
  • setbit设置Bitmaps中某个偏移量的值(0或1)*offset:偏移量从0开始
  • getbit获取Bitmaps中某个偏移量的值
  • bitcount[start end] 统计字符串从start字节到end字节比特值为1的数量,统计字符串被设置为1的bit数
  • image.png
  • bitop and(or/not/xor) [key…] bitop是一个复合操作, 它可以做多个Bitmaps的and(交集) 、 or(并集) 、 not(非) 、 xor(异或) 操作并将结果保存在destkey中。
  • image.png

    HyperLogLog

  1. 简介

主要适用于解决基数问题(可以粗略理解为大量数据的去重,用很小的精度来解决大量的去重)

  1. 命令
  • pfadd < element> [element …] 添加指定元素到HyperLogLog 中
  • pfcount [key …] 计算HLL的近似基数,可以计算多个HLL,比如用HLL存储每天的UV,计算一周的UV可以使用7天的UV合并计算即可
  • pfmerge [sourcekey …] 将一个或多个HLL合并后的结果存储在另一个HLL中,比如每月活跃用户可以使用每天的活跃用户来合并计算可得
  • image.png

    Geospatial

  1. 简介

主要是对地理信息相关的操作

  1. 命令
  • geoadd< longitude> [longitude latitude member…] 添加地理位置(经度,纬度,名称)
  • geopos [member…] 获得指定地区的坐标值
  • geodist [m|km|ft|mi ] 获取两个位置之间的直线距离
  • georadius< longitude>radius m|km|ft|mi 以给定的经纬度为中心,找出某一半径内的元素
  • image.png

Jedis

  1. 简介

主要用于Java中操作redis

  1. 使用准备

    1. 导入依赖

      1
      2
      3
      4
      5
      <dependency>
      <groupId>redis.clients</groupId>
      <artifactId>jedis</artifactId>
      <version>3.2.0</version>
      </dependency>
    2. linux系统中修改配置文件,关闭防火墙

systemctl status firewalld(查看防火墙状态)
**systemctl stop/disable firewalld.service **
redis.conf中注释掉bind 127.0.0.1 ,然后protected-mode no

  1. 测试连接成功与否

    1
    2
    3
    4
    5
    6
    public static void main(String[] args) {
    Jedis jedis = new Jedis("192.168.241.128",6379);
    System.out.println(jedis.ping());
    }

    //成功则结果为 pong
  2. 操作

操作与命令方式一样,就相当于用jedis操作这个命令

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {
Jedis jedis = new Jedis("192.168.241.128",6379);
System.out.println(jedis.ping());

jedis.mset("k1","v1","k2","v2","k3","v3"); //添加string
System.out.println(jedis.get("k2"));
System.out.println(jedis.keys("*"));
System.out.println(jedis.exists("k1"));
System.out.println(jedis.ttl("k1"));
System.out.println(jedis.get("k1"));


}
  1. 验证码实例

    1. 实现思路image.png
    2. 代码如下
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      public class verifyCode {
      public static void main(String[] args) {
      // System.out.println(getCode());
      codeRedis("11212121234",getCode());
      // getResult("11212121234","967191");
      }

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

      //2 redis存入数据,并设置过期时间
      public static void codeRedis(String phone,String code){
      Jedis jedis = new Jedis("192.168.241.128",6379);

      String countKey="code"+phone+"count:";
      String codeKey="code"+phone+":";

      //判断次数,并存入redis
      String count=jedis.get(countKey);
      if(count==null){
      jedis.setex(countKey,24*60*60,"1");
      }else if (Integer.parseInt(count)<=2){
      jedis.incr(countKey);
      }else{
      System.out.println("已达上限三次");
      jedis.close();
      return;
      }

      //将对应验证码加入redis
      jedis.setex(codeKey,120,code);
      jedis.close();
      }

      //3 判断
      public static void getResult(String phone,String code){
      Jedis jedis = new Jedis("192.168.241.128",6379);

      String codeKey="code"+phone+":";
      if(code.equals(jedis.get(codeKey))){
      System.out.println("success");
      }else{
      System.out.println("failed");
      }
      }
      }

      整合SpringBoot

  2. 导入依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <!-- 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>
  3. 编写配置文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    #Redis服务器地址
    spring.redis.host=192.168.241.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
  4. 编写配置类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    @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;
    }
    }
  5. 控制层编写

主要对redis的操作使用RedisTemplate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
@RequestMapping("/redisTest")
public class RedisController {
@Autowired
private RedisTemplate redisTemplate;

@GetMapping
public String testRedis() {
//设置值到redis
redisTemplate.opsForValue().set("name","lucy");
//从redis获取值
String name = (String)redisTemplate.opsForValue().get("name");
return name;
}
}

事务

  1. 简介

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

  1. 命令
  • multi 组队
  • exec执行
  • discard 中断组队 image.png
  • image.png
  1. 疑问:为什么redis是单线程的还需要事务呢?

对于redis服务器本身而言是没有竞态的,将活跃的客户端一个一个取出,将客户端中的请求一条一条执行,所有的处理都是one by one的。
但是,对于客户端而言是存在静态的,一个redis服务器会有多个客户端进行连接,他们之间可能会出现竞态的。

  1. redis解决事务冲突的方法

乐观锁。乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的。

  • 乐观锁执行命令:watch key1 [key2]

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

  • **unwatch **取消WATCH 命令对所有key 的监视。

如果在执行WATCH 命令之后,EXEC 命令或DISCARD 命令先被执行了的话,那么就不需要再执行UNWATCH 了。

  1. redis事务的特性
  • 单独隔离操作
    • 事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
  • 无隔离级别概念
    • 队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行
  • 不保证原子性
    • 事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚
  1. 事务–秒杀实例
    1. 使用工具ab,来模拟秒杀请求的发送,多个请求以及并发

安装方式:yum install httpd-tools
使用命令:ab -n 2000 -c 200 -k -p ~/postfile -T application/x-www-form-urlencoded http://192.168.2.115:8081/Seckill/doseckill(模拟2000个用户,200个并发)
image.png

  1. 出现连接超时的问题

使用连接池来解决,节省每次连接redis服务带来的消耗,把连接好的实例反复利用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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, "192.168.44.168", 6379, 60000 );
}
}
}
return jedisPool;
}

public static void release(JedisPool jedisPool, Jedis jedis) {
if (null != jedis) {
jedisPool.returnResource(jedis);
}
}

}
  1. 出现超卖问题,产品库存出现负数

解决,使用乐观锁来解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
//秒杀过程
public static boolean doSecKill(String uid,String prodid) throws IOException {
//1 uid和prodid非空判断
if(uid == null || prodid == null) {
return false;
}

//2 连接redis
//Jedis jedis = new Jedis("192.168.44.168",6379);
//通过连接池得到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);

System.out.println("秒杀成功了..");
jedis.close();
return true;
}
  1. 库存遗留问题

指抢购结束,有人没抢到商品,库存却还有。原因:乐观锁导致很多请求失败了,先点的失败,后点却成功了。通过LUA脚本来解决这个问题:
将复杂的或者多步的redis操作,写为一个脚本,一次提交给redis执行,减少反复连接redis的次数。提升性能。LUA脚本是类似redis事务,有一定的原子性,不会被其他命令插队,可以完成一些redis事务性的操作。但是注意redis的lua脚本功能,只有在Redis 2.6以上的版本才可以使用。
利用lua脚本淘汰用户,解决超卖问题。
redis 2.6版本以后,通过lua脚本解决争抢问题,实际上是redis 利用其单线程的特性,用任务队列的方式解决多任务并发问题。

redis持久化

redis信息存在内存之中,持久化就是将其存到硬盘之中

RDB

  1. 简介

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

  1. 执行过程

Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能 如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失
image.png
image.png
为什么要用临时文件,而不是直接持久化到硬盘之中呢?
防止持久化的数据缺失,不完整;比如,我数据复制到一半的时候,服务器挂了,那硬盘之中的数据就是异常的,显然不行。复制到临时文件中就不用考虑,因为是否备份的权力还是在我自己。

  1. 配置文件中相关的信息

image.png
image.png
image.png

  1. 测试备份使用方法

先关闭redis,复制dump.rdb到其他地方,即使删除原来的dump.rdb,将复制的文件复制回来,redis依然可以在重启的时候加载出数据。
停止保存策略:redis-cli config set save ""#save后给空值,表示禁用保存策略

AOF

  1. 简介

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

  1. 执行流程
    1. 客户端的请求写命令会被append追加到AOF缓冲区内;
    2. AOF缓冲区根据AOF持久化策略[always,everysec,no]将操作sync同步到磁盘的AOF文件中;
    3. AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量;
      • 压缩是指将某些产生相同结果的命令缩减,比如多个set,压缩成mset。可以使用命令bgrewriteaof
      • AOF文件持续增长而过大时,会fork出一条新进程来将文件重写(也是先写临时文件最后再rename),redis4.0版本后的重写,是指上就是把rdb 的快照,以二级制的形式附在新的aof头部,作为已有的历史数据,替换掉原来的流水账操作。
      • 触发时刻:Redis会记录上次重写时的AOF大小,默认配置是当AOF文件大小是上次rewrite后大小的一倍且文件大于64M时触发
      • 重写虽然可以节约大量磁盘空间,减少恢复时间。但是每次重写还是有一定的负担的,因此设定Redis要满足一定条件才会进行重写。
    4. Redis服务重启时,会重新load加载AOF文件中的写操作达到数据恢复的目的;

image.png

  1. 配置信息相关

image.png
image.png
appendfsync always
始终同步,每次Redis的写入都会立刻记入日志;性能较差但数据完整性比较好
appendfsync everysec
每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失。
appendfsync no
redis不主动进行同步,把同步时机交给操作系统。

  1. 备份使用方式

同rdb;
异常恢复使用:
image.png

优劣对比

  1. 优先级?

rdb默认开启,aof默认关闭;官方推荐都开启,优先aof,因为能保证数据的完整性

  1. rdb

    • 适合大规模的数据恢复

    • 对数据完整性和一致性要求不高更适合使用

    • 节省磁盘空间

    • 恢复速度快

    • Fork的时候,内存中的数据被克隆了一份,大致2倍的膨胀性需要考虑

    • 虽然Redis在fork时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能。

    • 在备份周期在一定间隔时间做一次备份,所以如果Redis意外down掉的话,就会丢失最后一次快照后的所有修改。

  2. aof

    • 备份机制更稳健,丢失数据概率更低。

    • 可读的日志文本,通过操作AOF稳健,可以处理误操作

    • 比起RDB占用更多的磁盘空间。

    • 恢复备份速度要慢。

    • 每次读写都同步的话,有一定的性能压力。

    • 存在个别Bug,造成恢复不能。

主从复制

  1. 简介

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

  • 读写分离,性能扩展
  • 容灾快速恢复

image.png
一般是一主多从,多个主可以通过集群来解决

  1. 简单的实现(一主两从)

image.png
配置文件信息如下:
image.png
启动
image.png
进入客户端之后,查看主从复制相关信息
image.png
可以发现,此时全部都是主机,设置从机的命令:slaveof <ip><port>
image.png
image.png
简单查询数据,可以发现从机已经可以获取到主机新增的数据了(主机连接之前的获取不到),且从机无法进行写操作,只有读操作。
image.png
主从复制支持套娃,从机也可以当其他从机的主机(当然还是不能写,小弟就是小弟)
image.png

  1. 从机挂掉分析
    • 挂掉之后再重启,默认是一个新的主机
    • 重新连接主机之后,会把自己错过的重新复制一遍数据(之前连接时候的数据还在,因为持久化),比如主机1,2,3;复制就是3,2,1(主机连接之前的获取不到
  2. 主机挂掉分析
    • 挂掉再连接还是之前的主机
    • 从机在主机挂掉期间,不会自动变成主机,还是从机,相关信息可以看到主机挂了
  3. 复制原理

03-主从复制原理.png

  1. 哨兵模式
    1. 反客为主

当主机挂掉,可以使用命令slaveof no one来将从机升级成为主机,每次都要手动执行,不方便

  1. 哨兵模式(上述升级版)
    1. 实现

设置好一主两从,新增文件sentinel.conf
image.png
image.png
将主机挂掉之后,6380自动升级成为主机
image.png
image.png

  2. 分析

image.png
偏移量大小指获得主机数据全面,越全面越大
runid,每个redis实例都会生成40位

  3. 问题

由于所有的写操作都是先在Master上操作,然后同步更新到Slave上,所以从Master同步到Slave机器有一定的延迟,当系统很繁忙的时候,延迟问题会更加严重,Slave机器数量的增加也会使这个问题更加严重。

  1. jedis代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    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();
    }
    }

    集群

  2. 简介

Redis 集群实现了对Redis的水平扩容,即启动N个redis节点,将整个数据库分布存储在这N个节点中,每个节点存储总数据的1/N无论哪个主机写数据,其他主机都能读到数据。
Redis 集群通过分区(partition)来提供一定程度的可用性(availability):即使集群中有一部分节点失效或者无法进行通讯,集群也可以继续处理命令请求。实现redis的容量水平扩容,和并发写操作的分摊

  1. 简单实现

首先删除之前生成的rdb,aof文件,重新写六个配置文件(:%s/6379/6380快速替换)
image.png
将六个redis实例启动,到redis.6.2.6/src目录下然后合体
redis-cli --cluster create --cluster-replicas 1 192.168.11.101:6379 192.168.11.101:6380 192.168.11.101:6381 192.168.11.101:6389 192.168.11.101:6390 192.168.11.101:6391一个集群至少要有三个主节点。选项 –cluster-replicas 1 表示我们希望为集群中的每个主节点创建一个从节点。分配原则尽量保证每个主数据库运行在不同的IP地址,每个从库和主库不在一个IP地址上。
image.png
查看集群相关信息
image.png
插槽的概念slots,一共16384,每个节点平均分,插入的值会自动转到对应节点下
image.png
image.png
image.png
image.png

  1. 异常处理
  • 主节点挂掉,从节点自动升级成为主节点
  • 原来的主节点恢复也只是从节点
  • 要是一段插槽的主从节点全部挂掉,怎么处理?
    • 取决与redis.conf中的cluster-require-full-coverage
    • 若为yes则整个都挂掉,反之为no只有该段插槽全都无法使用

      常见问题

      缓存穿透

  1. 问题描述

一直请求不存在的数据,导致缓存几乎失效,一直向缓存和数据库中查询不存在的数据,返回空的数据,redis又不会存储空结果,服务器压力过大
05-缓存穿透.png

  1. 解决措施

image.png

缓存击穿

  1. 问题描述

redis正常运行,但刚好一个key过期的时候,大量使用这个key的请求涌入,可能压垮服务器
06-缓存击穿.png

  1. 解决方式

image.png

缓存雪崩

  1. 问题描述

某个极端时间内,大量key过期,造成服务器压力瞬间过大
07-缓存雪崩.png

  1. 解决方案

image.png

分布式锁

  1. 简介

分布式系统多线程,多进程分布在不同不同机器上,单纯的Java api无法提供分布式锁。可使用redis来实现

  1. 简单使用

set user 11 nx ex 10(原子操作,防止执行一个命令执行就挂掉的极端情况)
nx上锁,ex设置过期时间

  1. 可能出现的问题—>误删锁?

09-分布式锁(UUID防误删).png
大概描述就是a由于卡顿或者其他因素锁自动释放了,b拿到锁之后,此时a认为该释放锁了,将b的锁又手动释放了。
解决:添加一个uuid,只有执行释放锁操作的uuid和获取该锁的uuid一样时才执行该操作

上述操作存在一个问题,就是可能a卡了一下自动释放了,然后刚好判断uuid一样a把b刚拿到的锁释放了,原因是因为缺乏原子性;使用lua脚本可解决
10-分布式锁(原子操作).png

新功能

  1. ACL
  • 就是对操作权限进行更细化的控制
  • acl list查看用户权限列表
  • acl cat命令查看添加权限指令类别(加参数类型名可以查看类型下具体命令)
  • acl whoami命令查看当前用户
  • aclsetuser命令创建和编辑用户ACL
  1. 多线程
  • 并不是单线程变成多线程的意思
  • IO多线程其实指客户端交互部分网络IO交互处理模块多线程,而非执行命令多线程
  1. 工具支持cluster
  • 无需再安装ruby环境
  1. RESP3新的 Redis 通信协议:优化服务端与客户端之间通信
  2. Client side caching客户端缓存:基于RESP3 协议实现的客户端缓存功能。为了进一步提升缓存的性能,将客户端经常访问的数据cache到客户端。减少TCP网络交互。
  3. Proxy集群代理模式:Proxy 功能,让 Cluster 拥有像单实例一样的接入方式,降低大家使用cluster的门槛。不过需要注意的是代理不改变 Cluster 的功能限制,不支持的命令还是不会支持,比如跨 slot 的多Key操作。
  4. Modules API

Redis 6中模块API开发进展非常大,因为Redis Labs为了开发复杂的功能,从一开始就用上Redis模块。Redis可以变成一个框架,利用Modules来构建不同系统,而不需要从头开始写然后还要BSD许可。Redis一开始就是一个向编写各种系统开放的平台。

感谢

感谢尚硅谷的视频:https://www.bilibili.com/video/BV1Rv41177Af

-------------本文结束感谢您的阅读-------------
您的支持将成为我创作的动力!