Redis

端口号:6379

NoSQL

全称为Not Only SQL,即为不仅仅是SQL的含义,指非关系型数据库

用来解决性能问题的,Redis是一种NoSQL

可以作为一个缓存数据库,例如服务端缓存session中的token

使用key-value的方式进行存储的

适用于:

  • 对数据的高并发读写
  • 海量数据的读写

并不适用于:

  • 事务
  • 结构化查询

Redis:

  • 数据存储在内存中
  • 支持持久化
  • 支持多种数据类型

image-20220330115244739

使用

安装后,可以使用redis-cli运行客户端

登录redis-cli -a 密码

Redis默认有16个数据库,下标从0开始,默认用的第0个,所有库的密码相同

使用的是单线程+多路IO复用

切换数据库

select 下标

dbsize可以查看当前数据库的大小

flushdb代表清空当前数据库

flushall代表清空所有数据库

常用五大数据类型

字符串String

列表List

集合Set

哈希Hash

有序集合Zset

key操作

keys *查看当前库中的所有的键

exists key名可以查看是否存在一个key

  • 如果返回1,代表存在
  • 如果返回0,代表不存在

type key名可以查看这个key的数据类型

del key名删除一个key

  • 返回1代表成功
  • 0代表失败

unlink也可以删除,选择非阻塞删除,是异步删除

expire key名 秒数可以设置一个key的过期时间:

  • expire中文为失效,读音为ɪkˈspaɪər

ttl key名可以查看这个key还有多少秒过期

  • -1代表永不过期
  • -2代表已过期

String 字符串

是一个最基本的数据类型,是二进制安全的,可以存储任何数据,例如图片、Java中的可序列化对象,一个Redis字符串中的value最大是512MB

常用命令

设置值:

set key名 value具体的值

get key名可以取出值,如果不存在则返回(nil)

append key名 值,会将值添加到已存在的key名值的后边:

  • 并返回这个key的长度
  • 如果这个key不存在,将会新建一个key,值为追加的部分的值

strlen key名,可以获取这个keyvalue的长度

setnx 键 值,当这个不存在时才设置相应的

  • 设置成功返回1
  • 设置失败返回0

针对于整型数值,当值为数字时才能使用:

  • incr 键会使相应键的值自增1
  • decr 键会使相应的值减1
  • incrby 键 步长会使相关键的值增加相应的步长
  • decrby 键 步长会使相关键的值减少相应的步长

Java中的i++的操作过程:不断地 取值、加1,如果有两个线程对i++(100次)进行操作,那么这个时候i的值的范围是2-200

mset key1 value1 key2 value2....keyn valuen可以批量的设置值

mget key1 .... keyn批量获取值

  • 当有一个失败时,所有的都失败

msetnx key1 value1 key2 value2....keyn valuen可以批量设置如果一个值不存在就插入

  • 如果有一个键存在,那么此时所有的数据都不会被插入

  • 127.0.0.1:6379> keys *
    1) "key"
    2) "kk"
    3) "key2"
    4) "key1"
    5) "key3"
    # 执行
    msetnx kk vv k1 v1
    # 返回结果:
    (integer) 0
    
    # 查询所有的键
    127.0.0.1:6379> keys *
    1) "key"
    2) "kk"
    3) "key2"
    4) "key1"
    5) "key3"
    
  • 因为此时的kk键存在,导致k1没有插入进去

getrange key名 开始位置 结束位置,相当于subString(开始, 结束),从下标0开始

setrange key名 位置 值会将key相应位置的值设置为,如果值的长度大于1,则会往后覆盖

setex key名 秒 值设置键值的同时设置过期时间,秒数必须大于0

getset key名 新值返回旧的值,设置为新的值

String底层数据结构

采用的动态字符串,类似于Java中的ArrayList

List 列表

单键多值

列表是一个字符串列表,按照插入的顺序排序,可以将一个元素插入到头部或者尾部,底层采用的是双向循环链表,随机访问的效率比较差

lpush key名 值1 值2 .... 值n,在key名侧插入一系列值,并将值1...值n按照值n...值1的顺序插入

  • 127.0.0.1:6379> lpush key 11 22 33 44 55
    (integer) 5
    127.0.0.1:6379> lrange key 0 1000
    1) "55"
    2) "44"
    3) "33"
    4) "22"
    5) "11"
    127.0.0.1:6379> lrange key 0 -1
    1) "55"
    2) "44"
    3) "33"
    4) "22"
    5) "11"
    127.0.0.1:6379> lpush key 888 999
    (integer) 7
    127.0.0.1:6379> lrange key 0 -1
    1) "999"
    2) "888"
    3) "55"
    4) "44"
    5) "33"
    6) "22"
    7) "11"
    

rpush key名 值1 值2 .... 值n,在key名的侧插入一系列值,并将值1...值n按照值1...值n的顺序插入

  • 127.0.0.1:6379> lrange key 0 -1
    1) "999"
    2) "888"
    3) "55"
    4) "44"
    5) "33"
    6) "22"
    7) "11"
    127.0.0.1:6379> rpush key 00 -11 -22 -33 -44
    (integer) 12
    127.0.0.1:6379> lrange key 0 -1
     1) "999"
     2) "888"
     3) "55"
     4) "44"
     5) "33"
     6) "22"
     7) "11"
     8) "00"
     9) "-11"
    10) "-22"
    11) "-33"
    12) "-44"
    

lpop/rpop key在左侧或者右侧弹出一个值,如果一个列表中没有值,那么这个键将会被删除

rpoplpush key1 key2key1的右侧弹出一个值并且插入到key2的左侧

lrange key 开始位置 结束位置,从左到右按照下标获取值

  • lrange key 0 -1代表取出所有的值

lindex key 索引,获取索引处的值

llen key获取列表的大小

linsert key名 before/after 旧值 新值在旧值之前/之后插入新值

  • 如果有多个相同的旧值,将会从0-n以第一个旧值为准
  • 如果不存在旧值,将不会插入

lrem key名 n 值从左边删除n个值,无论这几个值是否连续

  • 127.0.0.1:6379> lrange key 0 -1
     1) "00"
     2) "11"
     3) "22"
     4) "33"
     5) "44"
     6) "44"
     7) "55"
     8) "66"
     9) "44"
    10) "77"
    11) "88"
    12) "99"
    13) "44"
    127.0.0.1:6379> lrem key 4 44
    (integer) 4
    127.0.0.1:6379> lrange key 0 -1
    1) "00"
    2) "11"
    3) "22"
    4) "33"
    5) "55"
    6) "66"
    7) "77"
    8) "88"
    9) "99"
    

lset key 下标 值

List底层数据结构

如果元素比较少,将会使用一块连续的内存存储,如果数据比较多时将不会使用连续内存方式存储,会将多个连续的内容作为链表串起来

Set集合

功能和list差不多,但会自动去重,是一个底层时String类型的无需集合,并且是一个value为空的hash表,添加、删除、查找的时间都是***O(1)***

sadd key 值1 ... 值n将一个或者多个元素放入到集合中

smembers key取出集合中所有的值

sismember key 值判断集合中是否有相应的值

  • 如果有,返回1
  • 如果没有,返回0

scard key返回集合的大小

srem key 值1 ... 值n删除相应的值

spop key将会随机的在集合中弹出一个值

srandmember key n随机在集合中取出n个值,不会从集合中删除

smove key1名 key2名 值key1集合中的移动到key2集合

sinter key1 key2返回两个集合的交集

sunion key1 key2返回两个集合的交集

sdiff key1 key2返回差集,相当于key1 - key2

  • 127.0.0.1:6379> smembers key
    1) "44"
    2) "66"
    3) "88"
    4) "99"
    127.0.0.1:6379> smembers key2
    1) "11"
    2) "22"
    3) "33"
    4) "44"
    5) "55"
    6) "66"
    127.0.0.1:6379> sdiff key key2
    1) "88"
    2) "99"
    127.0.0.1:6379> sdiff key2 key
    1) "11"
    2) "22"
    3) "33"
    4) "55"
    127.0.0.1:6379> sdiff key2 key
    1) "11"
    2) "22"
    3) "33"
    4) "55"
    

hash 哈希

是一个键值对的集合,是一个String类型的映射表,但值中可以存储一个对象类似于Map<String, Object>

hset key 属性名 值给key中的属性值设置为值

hget key 属性名从key中获取属性值

hmset key 属性1 值1 ... 属性n 值n,批量设置值

hexists key 属性查看属性是否存在

hkeys key查看该key所有的属性

hvals key查看该hash集合的所有值

hincrby key 属性名 值将属性名中的值增加或者减少相应的值

  • 值大于0,代表增加相应的值
  • 值小于0,代表减少相应的值

hsetnx key 属性 值当相关的属性不存在时才给其设置值

hash数据结构

如果属性中field-value比较少时,使用列表

如果比较多时,使用hashtable

Zset 有序集合

也是没有重复元素的集合,每个元素都被关联了一个评分/权值,这个评分/权值将作为排序的依据,按照这个评分/权值从高到低进行排序,评分/权值可以是重复的

zadd key 权值 值 ... 权值 值可以将一个或者多个元素放入到key中

zrange key 开始位置 结束位置输出开始位置到结束位置之间的元素

  • zrange key 0 -1代表输出所有的元素

  • 默认是从小到大进行排名

  • 还有一个可选参数withscores代表把权重一并输出

  • 127.0.0.1:6379> zrange key 0 -1 withscores
    1) "bb"
    2) "0"
    3) "cc"
    4) "2"
    5) "a"
    6) "11"
    7) "dd"
    8) "99"
    

zrangebyscore key 开始权值 结束权值表示输出开始权值 - 结束权值之间的元素,按照权值由小到大排序

  • withscores也是一个可选参数,有这个参数代表把权重一并输出
  • 需要保证开始权值要比结束权值小

zrevrangebyscore key 开始权值 结束权值表示输出开始权值 - 结束权值之间的元素,按照权值由大到小排序

  • withscores也是一个可选参数,有这个参数代表把权重一并输出
  • 需要保证开始权值要比结束权值大

zincrby key 值 元素表示把元素的权值增加/减少相应的值

  • 值为正数表示增加
  • 值为负数表示减少

zrem key 元素1 ... 元素n表示删除这个集合中的相应的元素

zcount key 开始权值 结束权值统计区间中的值的个数(开始权值 - 结束权值

zrank key 值返回这个值在该集合中的排名,从0开始,rank中文为等级

image-20220331193459571

Redis配置文件

开始位置定义了一些度量单位,只支持bytes,大小写不敏感

# 1k => 1000 bytes
# 1kb => 1024 bytes
# 1m => 1000000 bytes
# 1mb => 1024*1024 bytes
# 1g => 1000000000 bytes
# 1gb => 1024*1024*1024 bytes

下方是INCLUDES部分,表示这个文件中可以包含其他的子文件

NETWORK部分

  • 会有类似于bind 127.0.0.1的配置项,表示只能本地连接
  • protected-mode yes代表只能本机访问,改为no之后其他设备都能访问
  • port 6379用来指定端口
  • tcp-backlog 511指定tcp的握手队列的大小
  • timeout 0,设置超时时间,如果一定时间连接不上就不再连接了,如果为0表示不限制
  • tcp-keepalive 300心跳时间,如果超过相应秒数没有操作,就会断开连接

General部分

  • loglevel xxx用来指定日志等级
  • logfile "路径"用来指定日志的路径
  • databases 16用来设置数据库的个数

SECURITY部分:

  • requirepass 密码用来设置密码

CLIENTS部分:

  • maxclients 10000用来设置最大的客户端连接数

MEMORY MANAGEMENT部分

  • maxmemory <bytes>设置最大的内存

新数据类型

Bitmap

未标题-1

专门用于位操作的字符串,位时以字符串的形式进行表示

偏移量offset0开始

setbit key 偏移量 值key中的某一位设置值,值只能取0/1,上图可以表示为:

setbit key 5 1
setbit key 11 1

getbit key 偏移量可以获取这个位上的值:

127.0.0.1:6379> getbit key 5
(integer) 1
127.0.0.1:6379> getbit key 11
(integer) 1

如果第一次初始化bitmap时,指定的偏移量非常大时,整个初始化过程会非常缓慢,会造成redis的阻塞

bitcount key可以获取设置位1的位的数量

  • 有两个可选参数:
  • bitcount key 开始字节 结束字节表示查看这个字节范围内1的个数,也是从0开始
  • 上图中,查看0-7位的1的个数可以表示为bitcount key 0 0,同理8-17位可以表示为bitcount key 1 1

bitop 操作 结果key key1 ... keyn位操作,全称:bitoperator,可以做多个key的按位与、按位或、按位取反、按位异或

  • 操作:and按位与、or按位或、not按位取反、xor按位异或

  • not按位取反是按照最大字节进行取反的

    • 例如

    • setbit key 4 1
      # 再执行
      bitop not result key
      # 此时的result中的值
             1 1 1 1 0 1 1 1
      offset 0 1 2 3 4 5 6 7
      
    • setbit key 12 1
      # 再执行
      bitop not result key
      # 此时的result中的值
             1  1  1  1  1  1  1  1  1  1  1  1  0  1  1  1 
      offset 0  1  2  3  4  5  6  7  8  9  10 11 12 13 14 15 
      
    • setbit key10 32 0
      # 再执行
      bitop not result key10
      # 此时的result中的值
      0-39位都是1
      
    • 百万级别用户的访问记录的存储空间:

      image-20220403130233558

HyperLogLog

可以记录网站访问量,也可以用来去重

pfadd key 值1 .... 值n添加值

pfcount key1 ... keyn获取个数

  • 如果有多个key,那么将会合并起来,看这些所有key中的不重复的元素的个数

pfmerge 新key名 key1 ... keyn将多个key合并到一个新键中

这个类型主要适用于去重,放入其中的值不能够取出

Geospatial

地理中的基本经纬度

Jedis 操作 Redis

使用Java操作Redis

引入依赖

<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>4.2.1</version>
</dependency>
Jedis jedis = new Jedis("ip", 端口);
// 密码,如果有
jedis.auth("密码");

验证码的一个例子

  • 生成6位数验证码
  • 70秒内有效
  • 24小时内最多验证3次
//检查手机号是否正确
public boolean checkPhoneNumber(String number){
    return number.matches("^((13[0-9])|(14[5,7,9])|(15([0-3]|[5-9]))|(166)|(17[0,1,3,5,6,7,8])|(18[0-9])|(19[8|9]))\\d{8}$");
}

@RequestMapping("/sms")
public Map<String, String> getSMS(@RequestParam String phone) {
    HashMap<String, String> map = new HashMap<>();
    if (!checkPhoneNumber(phone)) {
        map.put("code", "手机号不正确");
        return map;
    }
    map.put("phone", phone);
    Jedis jedis = new Jedis("localhost", 6379);

    // 表2为验证次数
    jedis.select(2);
    boolean isUpperLimit = "3".equals(jedis.get(phone));
    // 表1为当前的验证码
    jedis.select(1);
    boolean existsCode = jedis.exists(phone);
    if (!isUpperLimit && !existsCode) {
        Random random = new Random();
        String code = "";
        for (int i = 0; i < 6; i++) {
            code += random.nextInt(10);
        }
        jedis.setex(phone, 70, code);
        map.put("code", code);
        jedis.select(2);
        if (jedis.exists(phone)) {
            jedis.incr(phone);
        } else {
            jedis.setex(phone, 24 * 3600, "1");
        }
    } else if (isUpperLimit) {
        jedis.select(2);
        map.put("code", "超过每天的验证码发送次数限制,请等待" + jedis.ttl(phone) + "秒后再试");
    } else {
        map.put("code", "您已经发送过验证码了,最后的验证码是:" + jedis.get(phone) + ",还剩余:" + jedis.ttl(phone) + "秒");
    }
    return map;
}

@RequestMapping("/checkcode")
public Map<String, String> verificationCodeCheck(@RequestParam String phone, @RequestParam String code) {
    HashMap<String, String> map = new HashMap<>();
    if (!checkPhoneNumber(phone)) {
        map.put("info", "手机号格式不正确");
    } else if (code.length() != 6){
        map.put("info", "验证码不符合要求");
    } else {
        Jedis jedis = new Jedis("localhost", 6379);
        jedis.select(1);
        String verificationCode = jedis.get(phone);
        if (verificationCode != null && verificationCode.equals(code)) {
            map.put("info", "验证成功");
            jedis.del(phone);
        } else {
            map.put("info", "验证失败");
        }
    }
    return map;
}

Spring Boot整合Redis

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>2.6.6</version>
</dependency>

application.xml中进行配置

spring:
  redis:
    host: ip
    port: 端口

默认的底层操作客户端使用的是Lettuce,也是可以切换成Jredis的:

  • 方式1:移除掉Lettuce

  • 方式2:配置文件指定

    • redis:
        client-type: jedis
      

切换数据库:

以下代码缺一不可

    @Autowired
    StringRedisTemplate redisTemplate;


LettuceConnectionFactory connectionFactory = (LettuceConnectionFactory) redisTemplate.getConnectionFactory();
connectionFactory.setDatabase(3);
redisTemplate.setConnectionFactory(connectionFactory);
connectionFactory.afterPropertiesSet();
connectionFactory.resetConnection();

事务

单独的隔离操作,使命令按照顺序执行

  • multi表示开启事务
    • 可以理解为将命令放到队列中,等待执行
  • exec
    • 执行阶段,提交事务
  • discard
    • 放弃执行,中文为丢弃
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set key1 v1
QUEUED
127.0.0.1:6379> set key2 v2
QUEUED
127.0.0.1:6379> set key3 v3
QUEUED
127.0.0.1:6379> set key4 v4
QUEUED
127.0.0.1:6379> exec
1) OK
2) OK
3) OK
4) OK

队列中,只要有一条失败,在执行exec时队列中所有的都会失败

127.0.0.1:6379> multi
OK
127.0.0.1:6379> set key1 v1
QUEUED
127.0.0.1:6379> set key2 v2
QUEUED
127.0.0.1:6379> set key3 v3
QUEUED
127.0.0.1:6379> set kk
(error) ERR wrong number of arguments for 'set' command
127.0.0.1:6379> set key66666 k
QUEUED
127.0.0.1:6379> set key7777 kkkk
QUEUED
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.

情况2:组队时成功,执行时失败

127.0.0.1:6379> multi
OK
127.0.0.1:6379> set key1 0
QUEUED
127.0.0.1:6379> incr key1
QUEUED
127.0.0.1:6379> incr key1
QUEUED
127.0.0.1:6379> set key2 vvv
QUEUED
127.0.0.1:6379> incr key2
QUEUED
127.0.0.1:6379> incr key2
QUEUED
127.0.0.1:6379> exec
1) OK
2) (integer) 1
3) (integer) 2
4) OK
5) (error) ERR value is not an integer or out of range
6) (error) ERR value is not an integer or out of range

事务冲突

悲观锁:认为每次的操作都不安全,即每次操作都加锁

乐观锁:带有版本号的操作,认为每次操作都是安全的,不直接上锁

乐观锁在Redis中的应用

watch key1 ... keyn命令,在执行multi开启事务之前可以执行watch用来监视一个或者多个值

如果在事务执行之前这几个key发生了任何改变,都会导致整个事务执行失败

在执行exec或者discard命令后,将会自动取消本次对所有key的监视

也可以使用unwatch命令手动的取消监视所有的key,但必须是在执行multi之前

应用场景:商品秒杀

将商品的库存作为监视的key,开启事务,在事务中写上库存-1等一系列的操作语句

最后执行事务

Redis事务特性

  • 单独的隔离操作
  • 没有事务的隔离级别的概念
  • 不能保证原子性,即有一条命令执行失败,其余命令继续执行,将不会回滚

持久化操作

提供了两种方式RDBAOF

  • RDB
    • redis database
  • AOF
    • append of file

RDB

在指定的时间间隔内,将内存的数据快照写入到磁盘

过程:

  • 单独新建一个子进程作为持久化的进行
  • 会有一个临时文件,会先将数据写入到临时文件中
    • 是为了数据写入时安全的考虑
  • 如果在临时文件中写完了,将会在此写入到磁盘中,覆盖之前的持久化文件

优点:适合大规模的数据恢复,对数据完整性和一致性不高的时候适合使用,节省磁盘空间,恢复速度快

缺点:最后一次持久化后的数据可能会丢失

默认的持久化文件为dump.rdb

可以在配置文件中设置,例如指定的秒数内有nkey发生变化就会自动触发持久化的操作

备份和恢复

dump.rdb复制即可完成备份

恢复:

  • 关闭redisredis-cli shutdown
  • dump.rdb移到指定的目录下
  • 启动redis

AOF

以日志的形式记录每一个写操作,包括增删改,但不包括查询,每次重启都会根据日志的内容逐行执行完成数据的恢复

AOF默认不开启,需要手动开启,文件保存的路径和RDB的路径一致,如果两者同时开启,那么将以AOF为准

默认的文件名为appendonly.aof

备份和恢复的过程和RDB相同

可以修复持久化文件redis-check-rdb/aof命令

redis-check-aof --fix 文件名.aof即可快速修复有问题的文件

image-20220405183549061

AOF支持压缩操作:

  • 将两个指令合并到一块,例如

    • set a a1
      set b b1
      
      # 压缩合并为
      set a a1 b b1
      

压缩重新也是采用的子进程

持久化过程:

  • 所有的写的命令会被追加到缓冲区中
  • 缓冲区根据持久化策略将操作同步到AOF文件中
  • 如果文件带线啊哦超过重写策略或者手动重写时,会对AOF进行rewrite进行压缩重写

优点:

  • 数据丢失概率低
  • 可读的日志文本

缺点:

  • 更耗费磁盘空间
  • 备份速度慢
  • 每次读写有性能压力

Q.E.D.


念念不忘,必有回响。