Redis

1.NoSQL数据库

  • 1.NoSQL(Not Only SQL),泛指非关系型的数据库NoSQL不依赖业务逻辑方式存储,以简单的kye-value模式存储

1.Memcache

  • 1.数据存储内存中,一般不持久化
  • 2.支持简单的key-value模式,支持类型单一,类似Redis中的String
  • 3.一般是作为缓存数据库辅助持久化的数据库
  • 4.Memcache采用 多线程+锁 技术

2.MongoDB

  • 1.高性能、开源、模式自由(schema free)的文档型数据库
  • 2.数据存储内存中, 如果内存不足,将不常用的数据保存到硬盘
  • 3.虽然是key-value模式,但是对value(尤其是json)提供了丰富的查询功能
  • 4.支持二进制数据及大型对象

3.Cassandra

  • 1.Apache Cassandra是一款免费的开源NoSQL数据库
  • 2.管理由大量商用服务器构建起来的庞大集群上的海量数据集

2.Redis

  • 1.Redis开源免费,遵循BSD协议,是一个高性能key-valueNoSQL数据库
  • 2.Redis采用单线程+IO多路复用技术,其端口号为6379
  • 3.多路复用:指使用一个线程来检查多个文件描述符(Socket)的就绪状态,如果有一个文件描述符就绪,则返回,否则阻塞直到超时;得到就绪状态后进行真正的操作可以在同一个线程里执行,也可以启动线程执行
  • 4.Redis是一个开源的内存数据结构存储,用作数据库,缓存和消息代理Pub/Sub
    • 1.数据结构上:支持 字符串,散列,列表,集合,带有范围查询的排序集,位图,超级日志,具有半径查询和流的地理空间索引等数据结构
    • 2.功能上:Redis具有内置复制,Lua脚本Lua scriptingLRU驱逐LRU eviction of keys,自动过期Keys with a limited time-to-live,事务Transactions和不同级别的磁盘持久性,并通过Redis Sentinel提供高可用性并使用Redis Cluster自动分区Automatic failover

1.作用

  • 1.数据中间件
    在这里插入图片描述
  • 2.解决IO压力
    在这里插入图片描述

2.特点

  • 1.存储非结构化的数据:key-value方式存储数据
  • 2.弱事务:无法保证ACID,但一般能保证最终一致
  • 3.持久化:内存中存储数据,但能自动持久化
  • 4.高性能:查询性能非常高
  • 5.特定命令:不支持SQL,需要使用特定的命令

3.场景

1.适用场景

  • 1.适合高频次,热门访问的数据,降低数据库IO
  • 2.适合配合关系型数据库做高速缓存
  • 3.适合分布式架构,做分布式中间件

2.不适用场景

  • 1.需要事务支持
  • 2.基于sql的结构化查询存储,处理复杂的关系
  • 3.需要频繁更改的数据

4.安装

1.安装步骤

  • 1.参考Linux软件安装文章中的Linux安装Redis

2.安装目录

  • 1.默认安装目录:/usr/local/bin
    在这里插入图片描述
  • 2.工具说明
    • 1.redis-benchmark:性能测试工具
      在这里插入图片描述
    • 2.redis-check-aof:修复有问题的aof持久化文件
    • 3.redis-check-dump:修复有问题的dump.rdb文件
    • 4.redis-sentinelRedis集群使用
    • 5.redis-serverRedis服务器启动命令
    • 6.redis-cliRedis客户端操作入口

3.文件目录

在这里插入图片描述

4.配置文件(redis.conf)

[root@localhost ~]# find / -name redis.conf
/etc/redis/redis.conf  # 备份文件,已修改
/opt/redis/redis-3.2.9/redis.conf  # 原文件,未修改

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5.基础数据库操作

  • 1.Redis默认16个数据库,类似数组下标从0~15,初始默认使用0号库
  • 2.select 数据库id:切换数据库
    在这里插入图片描述
  • 3.dbsize:查看当前数据库的key的数量
    在这里插入图片描述
  • 4.flushdb:清空当前库
    在这里插入图片描述
  • 5.flushall:清空全部库
  • 6.info:显示Redis服务器的各种信息
    在这里插入图片描述
  • 7.ping:测试客户端是否与redis-server连通,如果连通结果为PONG
    在这里插入图片描述

6.基础用户操作

  • 1.config get requirepass:查看密码,第二个结果表示密码
    在这里插入图片描述
  • 2.config set requirepass 密码:设置密码,临时生效
    在这里插入图片描述
  • 3.修改配置文件redis.conf,设置密码,永久生效
    在这里插入图片描述
    在这里插入图片描述
  • 4.auth 密码:设置密码,操作需验证身份,并且统一密码管理,所有库密码相同
    在这里插入图片描述
  • 5.上面设置的都是对默认用户default的密码
    • 1.Redis6.0版本之前只支持单用户访问,没有用户名,auth认证时候只要auth 密码
    • 2.Redis6.0版本及之后支持输入用户名auth 用户名 密码

7.基础Key操作

  • 1.keys pattern:查看当前库匹配模式的键,pattern*时表示查询全部key
  • 2.exists key [key...]:判断一个或多个key是否存在
  • 3.del key [key...]:删除一个或多个key
  • 4.type key:查看key的类型
  • 5.del key:删除指定key数据
  • 6.rename key:将key重命名
  • 7.unlink key:异步删除key数据
  • 8.expire key 时间:设置key过期时间,单位秒
  • 9.persist key:移除key的过期时间
  • 10.ttl key:查看key还剩多少秒过期,-1表示永不过期,-2表示已过期
# 1.查看当前库的所有键
127.0.0.1:6379> keys *
(empty list or set)

# 2. * 如果什么都不加表示匹配所有,如果配合字符串使用表示匹配前/后任意字符
127.0.0.1:6379> set aname lisi
OK
127.0.0.1:6379> set bname wangwu
OK
127.0.0.1:6379> set cname zhaoliu
OK
127.0.0.1:6379> keys *name
1) "cname"
2) "aname"
3) "bname"

# 3.查看指定key的类型(是键的类型而不是值的类型)
127.0.0.1:6379> set age 18
OK
127.0.0.1:6379> type age
string
127.0.0.1:6379> type aname
list

# 4.查看key是否存在
127.0.0.1:6379> exists name
(integer) 0
127.0.0.1:6379> set name 李四
OK
127.0.0.1:6379> exists name
(integer) 1
127.0.0.1:6379>

# 5.del是直接删除,unlink是异步删除,而且unlink只有4.0版本以上才能使用
127.0.0.1:6379> del name
(integer) 1
127.0.0.1:6379> unlink 18
(integer) 1

# 6.expire 设置key的过期时间,默认单位为秒(pexpire 单位毫秒)
127.0.0.1:6379> expire age 100
(integer) 1

# 7.ttl 查看key的过期时间 -1 表示永不过期;-2 表示已过期
127.0.0.1:6379> ttl age
(integer) 96

# 8.persist 取消键的过期时间,如果过期时间被成功清除则返回 1;否则返回 0
127.0.0.1:6379> expire course 200
(integer) 1
127.0.0.1:6379> ttl course
(integer) 195
127.0.0.1:6379> persist course
(integer) 1
127.0.0.1:6379> ttl course
(integer) -1

8.操作原子性

  • 1.原子操作:指不会被线程调度机制打断的操作,即不会发生线程切换
  • 2.单线程中能够在单条指令中完成的操作都是原子操作,因为中断只能发生于指令之间
  • 4.多线程中不能被其它进程(线程)打断的操作就叫原子操作。
  • 5.Redis的命令操作单线程的,因此具有操作原子性,但是其在版本4以后开始采用多线程

5.基本数据类型

  • 1.数据类型:指存储数据的类型,即 value 的类型,key 永远都是字符串
  • 2.命令可以查询Redis官网:http://www.redis.cn/commands.html
    在这里插入图片描述

1.String

  • 1.StringRedis最基本的类型,一个key对应一个value
  • 2.String二进制安全的,其keystring类型,其value类型是string,可以包含任何类型数据(例:图片,视频等可以转为二进制存入其中)
  • 3.String中的value最大可以存512M的内容
1.增
# 1.set key value(value默认不用带双引号,会自动添加,除非有特殊符号 例:"zhang san")
# 中间有空格所以需要加上双引号
127.0.0.1:6379> set name "李四"
OK
127.0.0.1:6379> set age 18
OK

># 2.NX XX EX PX 4种参数
127.0.0.1:6379> keys *
1) "aname"
2) "cname"
3) "bname"
127.0.0.1:6379> get aname
"lisi"
# NX:当数据库中key不存在时,可以将key-value添加数据库
127.0.0.1:6379> set aname wangwu NX # 或 setnx aname wangwu
(nil)
127.0.0.1:6379> get aname
"lisi"
# XX:当数据库中key存在时,可以将key-value添加数据库,与NX参数互斥
127.0.0.1:6379> set aname wagnwu XX
OK
127.0.0.1:6379> get aname
"wagnwu"
# EX:key的超时秒数
127.0.0.1:6379> set aname wangwu EX 50 # 或 setex aname 50 wangwu
OK
127.0.0.1:6379> ttl aname
(integer) 46
# PX:key的超时毫秒数,与EX互斥
127.0.0.1:6379> set aname wangwu PX 6000 # 或 psetex aname 6000 wangwu
OK
127.0.0.1:6379> ttl aname
(integer) 4

# 3.mset 批量添加键值对
127.0.0.1:6379> mset bname test1 cname test2
OK
127.0.0.1:6379> mget bname cname
1) "test1"
2) "test2"

# 4.setnx  只有key不存在时才能设置key的值
127.0.0.1:6379> keys *
1) "cname"
2) "bname"
127.0.0.1:6379> setnx bname test
(integer) 0

# 5.msetnx 同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在才能添加成功
# 原子性,有一个失败则都失败
127.0.0.1:6379> msetnx bname test1 dname test3
(integer) 0
127.0.0.1:6379> msetnx dname test3 ename test4
(integer) 1
127.0.0.1:6379> keys *
1) "cname"
2) "dname"
3) "bname"
4) "ename"

# 6.getset 以新换旧,设置了新值同时获得旧值
127.0.0.1:6379> get bname
"test1"
127.0.0.1:6379> getset bname newtest
"test1"
127.0.0.1:6379> get bname
"newtest"
2.删
# 1.删除指定键的值
127.0.0.1:6379> keys *
1) "name"
2) "age"
3) "sex"
127.0.0.1:6379> del sex
(integer) 1
127.0.0.1:6379> keys *
1) "name"
2) "age"
3.改
# 1.set 设置相同键的值,默认覆盖
127.0.0.1:6379> get name
李四
127.0.0.1:6379> set name lisi
OK
127.0.0.1:6379> get name
"lisi"

# 2.incr/decr 将 key中储存的数字值增/减1(只能对数字值操作,如果为空,新值为1/-1)
# 因为redis是单线程的,所以能单条指定完成的操作都是原子操作(单线程中断只能发生在多个指令之间)
127.0.0.1:6379> get bname
"wangwuhello,test"
127.0.0.1:6379> get cname
"1810"
127.0.0.1:6379> incr bname
(error) ERR value is not an integer or out of range
127.0.0.1:6379> incr cname
(integer) 1811
127.0.0.1:6379> incr bname
(error) ERR value is not an integer or out of range
127.0.0.1:6379> decr cname
(integer) 1810

# 3.incrby/decrby 将key中储存的数字值增减,自定义步长(只能对数字值操作,如果为空,新值为自定义步长/-自定义步长)
127.0.0.1:6379> incrby bname 2
(error) ERR value is not an integer or out of range
127.0.0.1:6379> incrby cname 2
(integer) 1812
127.0.0.1:6379> decrby bname 2
(error) ERR value is not an integer or out of range
127.0.0.1:6379> decrby cname 2
(integer) 1810

# 4.incrbyfloat key increment 指定key增长浮点数. 当键不存在时,先将其值设为0再操作
127.0.0.1:6379> set test 1
OK
127.0.0.1:6379> incrbyfloat test 10.5
"11.5"

# 5.append 将给定的值追加到原值的末尾(字符串拼接,无法加减)
127.0.0.1:6379> get bname
"wangwu"
127.0.0.1:6379> append bname hello
(integer) 11
127.0.0.1:6379> set cname 18
OK
127.0.0.1:6379> get cname
"18"
127.0.0.1:6379> append cname 10
(integer) 4
127.0.0.1:6379> get cname
"1810"

# 6.setrange  key 起始位置 value (覆写key所储存的字符串值,从起始位置开始,索引从0开始)
127.0.0.1:6379> get cname
"test2"
127.0.0.1:6379> setrange cname 1 cc
(integer) 5
127.0.0.1:6379> get cname
"tcct2"
4.查
# 1.查所有key
127.0.0.1:6379> keys *
1) "name"
2) "age"
3) "sex"

# 2.获取指定键的值,查询不存在的键值对,结果为nil
127.0.0.1:6379> get name
"\xe6\x9d\x8e\xe5\x9b\x9b"
127.0.0.1:6379> get sex
"\xe7\x94\xb7"
127.0.0.1:6379> get age
"18"
127.0.0.1:6379> get test
(nil)
# 注意:默认redis不转义中文,如果想看中文内容,打开客户端时 redis-cli  命令后面加上  --raw 即可
[root@localhost redis-3.2.9]# redis-cli --raw
127.0.0.1:6379> get name
李四

# 3.strlen 查询指定键对应值的长度
127.0.0.1:6379> strlen bname
(integer) 5

# 4.getrange  获得下标对应范围的值(闭合区间,下标从0开始,下标越界并不会报错)
127.0.0.1:6379> getrange bname 0 3
"test"
127.0.0.1:6379> getrange bname 0 4
"test1"
127.0.0.1:6379> getrange bname 0 5
"test1"
127.0.0.1:6379> getrange bname 1 4
"est1"
5.底层实现
  • 1.String的数据结构为简单动态字符串(Simple Dynamic StringSDS)
  • 2.SDS:指可修改的字符串,结构类似于JavaArrayList
  • 3.其采用预分配冗余空间的方式减少内存的频繁分配
  • 4.内部当前字符实际分配的空间capacity一般高于实际字符串长度
  • 5.当字符串长度小于1M时,扩容时加倍现有的空间;如果超过1M,扩容时一次只会多扩容1M的空间
  • 6.注意:字符串最大长度为512M
    在这里插入图片描述
6.应用场景(需完善)

* 1.

1.缓存
1.缓存分类
  • 1.客户端缓存
    • 1.浏览器第一次请求服务器时,浏览器中没有缓存数据,直接向服务器请求获取数据,获取到数据后将数据缓存下来
    • 2.当浏览器再次向服务器发送同一请求时,浏览器会自动检测缓存中有没有对应数据,如果有则查看是否过期
    • 3.过期则根据相关策略再次从服务器上获取新的数据,并将新的数据缓存到浏览器中
    • 4.如果缓存中的数据没有过期,则直接将缓存中的数据呈现到页面上
    • 5.浏览器能够在本地保存网站中的图片或者其他文件的副本,这样再次访问该网站的时候,浏览器就不用再下载全部的文件
    • 6.客户端缓存只影响当前用户
      在这里插入图片描述
      在这里插入图片描述
  • 2.CDN缓存
    • 1.没有使用CND缓存时,用户在浏览器中输入域名,本地DNS服务器会对域名进行解析并返回域名对应的IP地址
    • 2.浏览器获取到IP地址后访问该IP,服务器对请求作出相应并返回数据,浏览器将数据渲染到页面上
    • 3.使用CND缓存时,用户在浏览器中输入域名,本地DNS服务器解析(根据IP判断地理位置、接入网类型、选择路由最短和负载最轻的服务器),取得缓存服务器IP
    • 4.根据IP发出访问请求,如果缓存服务器中有相关内容,则直接把数据返回给客户端,如果没有,则向源站发送请求,将数据返回给用户,同时将结果数据存入缓存服务器
    • 5.CDN缓存可以对站点或应用中大量静态资源加速分发
    • 6.CDN缓存影响一批用户
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述
  • 3.反向代理缓存
  • 4.服务器本地缓存
  • 5.服务器分布式缓存
    在这里插入图片描述
2.缓存作用
  • 1.缩短网络路径,加快访问速度
  • 2.减少请求,降低服务器压力
3.Redis缓存
  • 1.缓存热点数据(经常查询,不经常修改或删除的数据),降低数据库IO压力,不仅适用单机系统也适用于分布式系统
  • 2.第一次查询数据库,查询完后存入redis中,后续查询可直接从redis中获取
  • 3.EhCache\Mybatis缓存只适用于单机系统中将数据库中的热点数据进行备份,减少IO操作,提高应用性能,但是不适用于分布式系统
    在这里插入图片描述
4.单机架构的缓存
  • 1.EhCacahe/Mybatis缓存直接将数据缓存在JVM中,速度快,效率高
  • 2.但不适用分布式集群系统,缓存管理非常麻烦,查询一条数据后,必须在所有服务器上都加上该缓存数据
    请添加图片描述
    请添加图片描述
5.分布式集群的缓存
  • 1.Redis基于内存操作,可以替代EhCache/Mybatis缓存
  • 2.Redis缓存基于内存,速度更快,并且适用于分布式系统
    在这里插入图片描述
6.理论基础(未实际使用过)
  • 1.执行查询时,先查询Redis(类名+方法名+参数)缓存,如果有数据则不再调用mapper直接返回,如果查不到,才调用mapper,并将数据保存到redis缓存
  • 2.执行增删改后,需要清空Redis中的缓存,避免脏数据
  • 3.避免操作缓存的代码和原始代码耦合,不能将操作缓存代理放入到业务方法中
  • 4.将操作Redis缓存的代码定义成Advice(增强),使用AOP的方式动态增强
7.实际操作(基于SpringBoot注解)
  • 1.Spring内置了缓存注解
    • 1.Cacheable:执行方法前先查询缓存,如果缓存有数据,直接返回,如果缓存没有数据,则执行查询方法,并将结果保存到缓存中
    • 2.CacheEvict:可以在方法执行前或后删除缓存
  • 2.Spring内置了缓存增强类(RedisCacheManager)
8.创建配置类
package com.wd.config;

import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;

import java.time.Duration;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class RedisCacheConfig {
   @Bean
   //配置缓存管理器
   public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory){

       //采用无锁写的缓存策略
       RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);

       //返回Redis缓存管理器(缓存写策略,缓存60秒过期,其他缓存配置)
       return new RedisCacheManager(redisCacheWriter, getRedisCacheConfiguration(60), getRedisCacheConfigurationMap());
   }

   //RedisCacheConfiguration 用于负责Redis的缓存配置,
   private RedisCacheConfiguration getRedisCacheConfiguration(int seconds){
       //1.获取Redis缓存配置对象
       RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
       //2.准备Json序列化方式
       GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
       return redisCacheConfiguration.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))//3.设定序列化值的方式采用json
               .entryTtl(Duration.ofSeconds(seconds));//4.设置过期时间
   }

   //其他缓存配置
   private Map<String, RedisCacheConfiguration> getRedisCacheConfigurationMap(){
       Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>();

       //以UserServiceImpl(一般为类名)开头的缓存要存储100秒
       redisCacheConfigurationMap.put("UserServiceImpl", getRedisCacheConfiguration(100));
       return redisCacheConfigurationMap;
   }
}
9.启动类上添加开启缓存的注解
package com.wd;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.cache.annotation.EnableCaching;

@EnableCaching //开启缓存
@org.springframework.boot.autoconfigure.SpringBootApplication
@MapperScan("com.wd.mapper")//扫描mapper接口动态生成的代理类,也可以在具体>mapper接口上使用mapper接口,二选一
public class SpringBootApplication {
   //启动tomcat部署项目
   public static void main(String[] args) {
       SpringApplication.run(SpringBootApplication.class,args);
   }
}
10.目标方法上添加使用缓存的注解
package com.wd.service.impl;

import com.wd.domains.User;
import com.wd.service.UserService;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService {

	//存入redis的键应该是类名+方法名+参数,value的值是类名,key的值是方法名加参数值
	//一般用在查询方法上,表示使用缓存
   @Cacheable(value = "UserServiceImpl",key = "#root.methodName+#id")
   public String selectHello(Integer id){
       System.out.println("----没有调用缓存,直接调用方法----selectHello");
       return "hello redis cache";
   }

	//增删改方法执行后,删除缓存,防止脏数据,value值为类名,allEntries表明删除该类所有的缓存,beforeInvocation表示在方法执行前执行
   @CacheEvict(value="UserServiceImpl",allEntries = true,beforeInvocation = true)
   public void saveHello(){
       System.out.println("----没有调用缓存,直接调用方法saveHello----");
   }
}
11.数据一致性问题
  • 1.数据获取流程图,导致数据不一致的情况有以下几种
    在这里插入图片描述
  • 2.读取缓存步骤一般没有问题,一旦涉及到数据更新:数据库和缓存更新,就容易出现缓存(Redis)和数据库(MySQL)间的数据一致性问题
  • 3.不管是先写MySQL数据库,再删除Redis缓存;还是先删除缓存,再写库,都有可能出现数据不一致的情况
    • 1.如果先删除Redis缓存,还没有来得及写库,另一个线程就来读取,发现缓存为空,则去数据库中读取数据写入缓存,此时缓存中为脏数据
    • 2.如果先写库,再删除缓存前,写库的线程宕机了,没有删除掉缓存,则也会出现数据不一致情况。
  • 4.因为写和读是并发的,没办法保证顺序,就会出现缓存和数据库的数据不一致的问题

缓存和数据库一致性解决方案

  • 1.采用延时双删策略
    • 1.在写库前后都进行redis.del(key)操作,并且设定合理的超时时间
      //先删除缓存
      //再写数据库
      //休眠500毫秒
      //再次删除缓存
      public void write(String key,Object data){
      	 redis.delKey(key);
      	 db.updateData(data);
      	 Thread.sleep(500);
      	 redis.delKey(key);
      }
      
    • 2.休眠时间如何确定,需要评估项目读数据业务逻辑的耗时,目的是确保读请求结束,写请求可以删除读请求造成的缓存脏数据
    • 3.设置缓存过期时间:从理论上来说,给缓存设置过期时间,是保证最终一致性的解决方案,所有的写操作以数据库为准,只要到达缓存过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存
    • 4.该方案的弊端,结合双删策略+缓存超时设置,这样最差的情况就是在超时时间内数据存在不一致,而且又增加了写请求的耗时
  • 2.异步更新缓存(基于订阅binlog的同步机制)
    • 1.MySQL binlog增量订阅消费+消息队列+增量数据更新到redis
      //读Redis:热数据基本都在Redis
      //写MySQL:增删改都是操作MySQL
      //更新Redis数据:MySQ的数据操作binlog,来更新到Redis
      //Redis更新
      
    • 2.数据操作主要分为两大块:一个是全量(将全部数据一次写入到redis),一个是增量(实时更新)
    • 3.读取binlog后分析 ,利用消息队列,推送更新各台的redis缓存数据
    • 4.这样一旦MySQL中产生了新的写入、更新、删除等操作,就可以把binlog相关的消息推送至RedisRedis再根据binlog中的记录,对Redis进行更新
    • 5.可以结合使用canal(阿里的一款开源框架),通过该框架可以对MySQLbinlog进行订阅,而canal正是模仿了mysqlslave数据库的备份请求,使得Redis的数据更新达到了相同的效果
12.缓存穿透,击穿,雪崩
  • 1.缓存穿透
    • 1.指当查询某个数据时,Redis中不存在该数据,即缓存没有命中,此时查询请求就会转向持久层数据库MySQL,结果发现MySQL中也不存在该数据,
    • 2.此时MySQL只能返回一个空对象,代表此次查询失败,如果这种类请求非常多或黑客利用这种请求进行恶意攻击,就会给MySQL数据库造成很大压力甚至于崩溃,这种现象就叫缓存穿透
  • 2.缓存穿透解决方案
    • 1.缓存空对象(不推荐)
      • 1.当MySQL返回空对象时,Redis将该对象缓存起来,同时为其设置一个过期时间
      • 2.当用户再次发起相同请求时,就会从缓存中拿到一个空对象,用户的请求被阻断在了缓存层,从而保护了后端数据库
      • 3.但是该方案也存在一些问题,虽然请求进不了MySQL,但是会占用Redis的缓存空间
    • 2.布隆过滤器(推荐)
      • 1.布隆过滤器判定不存在的数据,那么该数据一定不存在,利用它的这一特点可以防止缓存穿透
      • 2.缓存预热:是指系统启动时,提前将相关的数据加载到Redis缓存系统中,这样避免了用户请求时再加载数据
      • 3.首先将用户可能会访问的热点数据存储在布隆过滤器中(也称缓存预热)
      • 4.当有一个用户请求时会先经过布隆过滤器,如果请求的数据,布隆过滤器中不存在,那么该请求将直接被拒绝,否则将继续执行查询
      • 5.相较于第一种方法,布隆过滤器方法更为高效、实用
        在这里插入图片描述
  • 3.缓存击穿
    • 1.指查询的数据缓存中不存在,但是后端数据库却存在,这种现象出现原因是一般是由缓存中key过期导致的
    • 2.比如一个热点数据key,它无时无刻都在接受大量的并发访问,如果某一时刻这个key突然失效了,就致使大量的并发请求进入后端数据库,导致其压力瞬间增大,这种现象被称为缓存击穿
  • 4.缓存击穿解决方案
    • 1.改变过期时间:设置热点数据永不过期
    • 2.分布式锁:采用分布式锁的方法,重新设计缓存的使用方式
      • 1.上锁:当通过key去查询数据时,首先查询缓存,如果没有,就通过分布式锁进行加锁,第一个获取锁的进程进入后端数据库查询,并将查询结果缓到Redis
      • 2.解锁:当其他进程发现锁被某个进程占用时,就进入等待状态,直至解锁后,其余进程再依次访问被缓存的key
  • 5.缓存雪崩
    • 1.指缓存中大批量的key同时过期,此时数据访问量又非常大,从而导致后端数据库压力突然暴增,甚至会挂掉,这种现象被称为缓存雪崩
    • 2.与缓存击穿不同,缓存击穿是在并发量特别大时,某一个热点key突然过期,而缓存雪崩则是大量的key同时过期,因此根本不是一个量级
  • 6.​缓存雪崩解决方案
    • 1.缓存雪崩和缓存击穿有相似之处,所以也可采用热点数据永不过期的方法,来减少大批量的key同时过期
    • 2.为key设置随机过期时间,避免key集中过期
2.验证码
  • 1.手机号/id等唯一标识作为key,验证码作为value,存储在redis中,设置过期时间
  • 2.如果用户输入验证码,从redis中取值对比,如果过期则无效
3.计数器
  • 1.点赞数,访问量id作为key,数量作为value,记录在redis中并持久化或过一段时间同步到mysql
4.存储对象
  • 1.以json形式存储对象,key=id,value=json格式存储
5.共享session
  • 1.不同服务器有不同的会话session,会导致用户每次刷新网页又要重新登录
  • 2.因此用redis将用户session集中管理,每次获取用户更新或查询登录信息都直接从redis中集中获取
    在这里插入图片描述
6.分布式锁
  • 1.分布式系统中,当不同进程或线程一起访问共享资源时,会造成资源争抢,如果不加以控制的话,就会引发程序错乱
  • 2.此时使用分布式锁能够有效的解决这个问题,它采用了一种互斥机制来防止线程或进程间相互干扰,从而保证了数据的一致性
  • 3.分布式锁并非是Redis独有,比如MySQL关系型数据库,以及Zookeeper 分布式服务应用都实现分布式锁,只不过Redis是基于缓存实现的
  • 4.Redis分布式锁主要有以下特点
    • 1.互斥性:分布式锁的重要特点,在任意时刻,只有一个线程能够持有锁
    • 2.锁的超时时间:一个线程在持锁期间挂掉了而没主动释放锁,此时通过超时时间来保证该线程在超时后可以释放锁,这样其他线程才可以继续获取锁
    • 3.同一个线程加锁解锁必须是由同一个线程来设置,Redis中一个线程代表一个客户端
  • 5.集群环境下多个web应用对同一个商品进行抢购和减库存操作时,可能出现超卖时会用到分布式锁
  • 6.Redis分布式锁常用命令
    # 仅当key不存在时,设置一个key为value的字符串,返回1
    # 若 key 存在,设置失败,返回 0
    setnx key val
    # 为 key 设置一个超时时间,以秒为单位,超过这个时间锁会自动释放,避免死锁
    expire key timeout
    # 或直接使用 
    SET key value [expiration EX seconds|PX milliseconds] [NX|XX]  
    # 删除 key
    del key
    
    例:
    127.0.0.1:6379> setnx web www.baidu.com
    (integer) 1
    127.0.0.1:6379> expire web 60
    (integer) 1
    127.0.0.1:6379> get web
    "www.baidu.com"
    127.0.0.1:6379> ttl web
    (integer) 33
    127.0.0.1:6379> set name www.baidu.net ex 60 nx
    OK
    
    在这里插入图片描述

2.List

  • 1.单键多值,其keystring类型,其value是简单的字符串列表,一个列表最多可以存储2的32次方- 1个元素
  • 2.默认按照插入顺序排序,可以添加一个元素到列表的头部(左边)或尾部(右边)
  • 3.列表中的元素是有序的,可通过索引获取某个元素或者某个范围内的元素列表,列表中的元素是可重复
1.增
# 1.lpush/rpush 从左边/右边插入一个或多个值
# 注意:lpush从左边插入采用的是头插法所以获取的顺序与插入顺序相反
# 		rpush从右边插入采用的是尾插法所以获取的顺序与插入顺序一致
127.0.0.1:6379> lpush aname l1 l2 l3 l4
(integer) 4
127.0.0.1:6379> lrange aname 0 3
1) "l4"
2) "l3"
3) "l2"
4) "l1"
127.0.0.1:6379> rpush bname r1 r2 r3 r4
127.0.0.1:6379> lrange bname 0 3
1) "r1"
2) "r2"
3) "r3"
4) "r4"

# 2.lpushx/rpushx 从左边/右边插入一个值,只有在key存在时生效,否则不做改变
127.0.0.1:6379> rpushx bname v5
(integer) 5
127.0.0.1:6379> lrange bname 0 5
1) "r1"
2) "r2"
3) "r3"
4) "r4"
5) "v5"
127.0.0.1:6379> lpushx bname c1
(integer) 6
127.0.0.1:6379> lrange bname 0 5
1) "c1"
2) "r1"
3) "r2"
4) "r3"
5) "r4"
6) "v5"
2.删
# 1.lpop/rpop 从左边(头部)/右边(尾部)推出一个值
# 当列表中的值推完后,列表也不存在了
127.0.0.1:6379> lrange aname 0 -1
1) "l4"
2) "l3"
3) "l2"
4) "l1"
127.0.0.1:6379> lpop aname
"l4"
127.0.0.1:6379> rpop aname
"l1"

# 2.blpop/brpop 从左边(头部)/右边(尾部)推出一个值
# 如果无法弹出任何元素的时候阻塞连接

# 3.lrem 从左边删除n个指定值(从左到右)
127.0.0.1:6379> lrange aname 0 -1
1) "l4"
2) "after4"
3) "after4"
4) "l3"
5) "before2"
6) "l2"
7) "l1"
127.0.0.1:6379> lrem aname 1 after4
(integer) 1
127.0.0.1:6379> lrange aname 0 -1
1) "l4"
2) "after4"
3) "l3"
4) "before2"
5) "l2"
6) "l1"
3.改
# 1.rpoplpush 从列表1右边推出一个值,插到列表2左边
127.0.0.1:6379> lrange aname 0 -1
1) "l4"
2) "l3"
3) "l2"
4) "l1"
127.0.0.1:6379> lrange bname 0 -1
1) "r1"
2) "r2"
3) "r3"
4) "r4"
127.0.0.1:6379> rpoplpush aname bname
"l1"
127.0.0.1:6379> lrange aname 0 -1
1) "l4"
2) "l3"
3) "l2"
127.0.0.1:6379> lrange bname 0 -1
1) "l1"
2) "r1"
3) "r2"
4) "r3"
5) "r4"

# 2.linsert 指定列表的指定值的前/后面插入新值
127.0.0.1:6379> linsert aname before l2 before2
(integer) 5
127.0.0.1:6379> lrange aname 0 -1
1) "l4"
2) "l3"
3) "before2"
4) "l2"
5) "l1"
127.0.0.1:6379> linsert aname after l4 after4
(integer) 6
127.0.0.1:6379> lrange aname 0 -1
1) "l4"
2) "after4"
3) "l3"
4) "before2"
5) "l2"
6) "l1"

# 3.将列表key下标为index的值替换成指定值
127.0.0.1:6379> lrange aname 0 -1
1) "l4"
2) "after4"
3) "l3"
4) "before2"
5) "l2"
6) "l1"
127.0.0.1:6379> lset aname 1 new4
OK
127.0.0.1:6379> lrange aname 0 -1
1) "l4"
2) "new4"
3) "l3"
4) "before2"
5) "l2"
6) "l1"
4.查
# 1.lrange 按照索引下标获得元素(从左到右)
# 0左边第一个,-1右边第一个,(0到-1表示获取所有,无下标越界错误)
127.0.0.1:6379> lpush aname l1 l2 l3 l4
(integer) 4
127.0.0.1:6379> lrange aname 0 -1
1) "l4"
2) "l3"
3) "l2"
4) "l1"
127.0.0.1:6379> lrange aname 0 0
1) "l4"
127.0.0.1:6379> lrange aname 0 1
1) "l4"
2) "l3"
127.0.0.1:6379> lrange aname 0 2
1) "l4"
2) "l3"
3) "l2"
127.0.0.1:6379> lrange aname 0 3
1) "l4"
2) "l3"
3) "l2"
4) "l1"
127.0.0.1:6379> lrange aname 0 4
1) "l4"
2) "l3"
3) "l2"
4) "l1"

# 2.lindex 按照索引下标获得元素(从左到右,从0开始)
127.0.0.1:6379> lindex aname 0
"l4"
127.0.0.1:6379> lindex aname 1
"l3"
127.0.0.1:6379> lindex aname 2
"l2"
127.0.0.1:6379> lindex aname 3
"l1"
127.0.0.1:6379> lindex aname 4
(nil)

# 3.llen  获得指定列表长度 
127.0.0.1:6379> llen aname
(integer) 4
5.底层实现
  • 1.List的数据结构为快速链表(quicklist)
  • 2.列表元素较少的情况下会只用一块连续的内存存储,即压缩列表(ziplist:将所有元素紧挨着存储,分配的是一块连续的内存)
  • 3.列表元素较多的情况下会使用快速链表(quicklist),即将双向链表压缩列表(ziplist)结合起来组成了quicklist(将多个ziplist使用双向指针连接起来)
  • 4.优点是:满足了快速的插入删除性能,又不会出现太大的空间冗余
  • 5.快速链表
    • 1.Redis中的List数据结构是链表型,类似于LinkedList,但List结构不是一个简单的链表
    • 2.因为LinkedList的每个节点都要保存上一个节点和下一个节点的指针,相对来说比数组型的列表更占空间
    • 3.为了减少空间冗余采用压缩列表(zipList),zipList让少量的元素使用一个连续的内存空间,List结构由多个zipList串起来组成,被称为快速链表quickList
      在这里插入图片描述
      在这里插入图片描述
6.应用场景(需完善)
1.消息队列
  • 1.List类型的lpoprpush(或lpushrpop)能实现点对点队列
  • 2.List类型的lpushbrpop可实现阻塞队列
  • 3.不推荐项目中使用,因为已有KafkaRabbitMQ等成熟的消息队列,没必要去重复造轮子
    在这里插入图片描述
2.历史排行榜
  • 1.List类型的lrange命令可分页查看队列中的数据,可将每隔一段时间计算一次的排行榜存储在List类型中
  • 2.List类型并非适合所有的排行榜,只有定时计算的排行榜才适合使用List类型存储
  • 3.List类型不支持实时计算的排行榜
    在这里插入图片描述
3.朋友圈点赞列表
  • 1.要求按照点赞顺序展示点赞好友的信息
  • 2.使用List实现,发朋友圈的人用key表示,点赞的人为value
  • 3.点赞操作对应rpush,取消点赞操作可以对应lrem。评论信息可以通过List去查询关系型数据库
4.文章列表/数据分页
  • 1.List可以实现展示博客网站的文章列表
  • 2.List支持分页展示文章列表,List不但有序同时还支持按照范围内获取元素,可以完美解决分页查询功能

3.Set

  • 1.单键多值,其keystring类型,其valuestring类型的无序集合,一个集合最多可以存储2的32次方- 1个元素
  • 2.Set可以自动去重
  • 3.Set可以判断集合中是否存在某个元素
  • 4.Set底层是一个value为空的hash表,添加,删除,查找的复杂度都是O(1)
1.增
# sadd 将一个或多个 member 元素加入到集合 key 中,已经存在的 member 元素将被忽略
127.0.0.1:6379> sadd aname s1 s2 s3 s3 s4
(integer) 4
127.0.0.1:6379> smembers aname
1) "s2"
2) "s3"
3) "s1"
4) "s4"
2.删
# 1.srem 删除集合中的一个或多个元素
127.0.0.1:6379> srem aname s1
(integer) 1
127.0.0.1:6379> smembers aname
1) "s2"
2) "s3"
3) "s4"

# 2.spop 随机从该集合中推出一个值
127.0.0.1:6379> spop aname
"s2"
127.0.0.1:6379> smembers aname
1) "s3"
2) "s4"
3.改
# smove 把集合中一个值从一个集合移动到另一个集合
127.0.0.1:6379> smembers aname
1) "s3"
2) "s4"
127.0.0.1:6379> smembers bname
1) "10"
2) "20"
3) "30"
4) "40"
127.0.0.1:6379> smove aname bname s3
(integer) 1
127.0.0.1:6379> smembers aname
1) "s4"
127.0.0.1:6379> smembers bname
1) "40"
2) "20"
3) "30"
4) "10"
5) "s3"
4.查
# 1.smembers 取出该集合的所有值
127.0.0.1:6379> sadd aname s1 s2 s3 s3 s4
(integer) 4
127.0.0.1:6379> smembers aname
1) "s2"
2) "s3"
3) "s1"
4) "s4"

# 2.sismember 判断集合中是否为含有该值,有1,没有0
127.0.0.1:6379> sismember aname s3
(integer) 1
127.0.0.1:6379> sismember aname s5
(integer) 0

# 3.scard 返回该集合的元素个数
127.0.0.1:6379> scard aname
(integer) 4

# 4.srandmember 随机从该集合中取出n个值。不会从集合中删除 
127.0.0.1:6379> srandmember aname 1
1) "s4"
127.0.0.1:6379> smembers aname
1) "s3"
2) "s4"
5.集合操作
127.0.0.1:6379> sadd aname a1 a2 a3 a4 b1 b2
(integer) 6
127.0.0.1:6379> sadd bname b1 b2 b3 b4 a1 a2
(integer) 6
# 1.sinter 返回两个集合的交集元素
127.0.0.1:6379> sinter aname bname
1) "a2"
2) "a1"
3) "b1"
4) "b2"
# 2.sunion 返回两个集合的并集元素
127.0.0.1:6379> sunion aname bname
1) "a2"
2) "b4"
3) "a1"
4) "a3"
5) "a4"
6) "b1"
7) "b3"
8) "b2"
# 3.sdiff 返回两个集合的差集元素(key1中的,不包含key2中的)
# 返回多个集合的差集元素(第一个中,不包含其他中的)
127.0.0.1:6379> sdiff aname bname
1) "a4"
2) "a3"
# 4.sdiffstore destination key1 [key2...] 将多个集合的差集元素结果存放在destination集合中
# sinterstore destination key1 [key2] 将多个集合的交集元素结果存放在destination集合中
# sunionstore destination key1 [key2] 将多个集合的并集元素结果存放在destination集合中
127.0.0.1:6379> sdiffstore cname aname bname
(integer) 2
127.0.0.1:6379> smembers cname
1) "a4"
2) "a3"
6.底层实现
  • 1.Set类型的数据结构是dict字典,字典是用哈希表实现的
  • 2.类似JavaHashSet,内部使用的是hash结构,所有的value都指向同一内部值
  • 3.Set类型底层使用了intsethashtable两种数据结构存储
    • 1.intset类似数组
    • 2.hashtable是哈希表
  • 4.Set的底层存储intsethashtable存在编码转换,使用intset存储必须满足下面两个条件,否则使用hashtable
    • 1.结合对象保存的所有元素都是整数值
    • 2.集合对象保存的元素数量不超过512
7.应用场景(需完善)
1.共同特征
  • 1.将用户id作为key,标签,爱好,关注,好友等内容作为value存放到Set
  • 2.通过Set集合的sinter/sinterstore 命令可以查看指定的用户有哪些共同标签、爱好、关注、好友
    在这里插入图片描述
  • 3.将标签,爱好,关注等内容作为key用户id作为value存放到Set
  • 4.通过Set集合的sinter/sinterstore 命令可以查看同时标签、爱好、关注的有哪些用户
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
2.统计网站的访问PV、UV、IP
  • 1.统计网站的PV(访问量),UV(独立访客),IP(独立IP)
  • 2.PV:网站被访问次数,可通过刷新页面提高访问量
  • 3.UV:网站被不同用户访问的次数,可通过cookie统计访问量,相同用户切换IP地址,UV不变
  • 4.IP:网站被不同IP地址访问的总次数,可通过IP地址统计访问量,相同IP不同用户访问,IP不变
  • 5.利用Set集合的数据去重特征,记录各种访问数据
  • 6.建立String类型数据,利用incr统计日访问量(PV
  • 7.建立Set类型数据,记录不同cookie数量(UV
  • 8.建立Set类型数据,记录不同IP数量(IP
3.黑名单
  • 1.周期性更新满足规则的用户黑名单,加入Set集合
  • 2.用户行为信息达到后与黑名单进行比对,确认行为去向
  • 3.黑名单过滤IP地址:应用于开放游客访问权限的信息源
  • 4.黑名单过滤设备信息:应用于限定访问设备的信息源
  • 5.黑名单过滤用户:应用于基于访问权限的信息源

4.Hash

  • 1.Redis中的Hash是一个键值对集合,其keystring类型,其value是一个string类型的filedvalue的哈希表,类似Java中的Map<String,String>
  • 2.Hashvalue中最多可以存储2的32次方- 1个哈希表
    在这里插入图片描述
1.增
# 1.hset  给指定集合中的field键赋值
127.0.0.1:6379> hset user1 name 李四
(integer) 1
127.0.0.1:6379> hset user1 sex 男
(integer) 1
127.0.0.1:6379> hset user1 age 18
(integer) 1
127.0.0.1:6379> hset user2 name 王五
(integer) 1
127.0.0.1:6379> hset user2 sex 男
(integer) 1
127.0.0.1:6379> hset user2 age 20
(integer) 1
127.0.0.1:6379> keys *
1) "user2"
2) "user1"
127.0.0.1:6379> type user1
hash

# 2.hmset 批量给指定集合中的field键赋值
127.0.0.1:6379> hmset user3 name qiqi sex girl age 18
OK
127.0.0.1:6379> keys *
user2
user3
user1

# 3.hsetnx 将指定哈希表中的域 field的值设置为value ,当且仅当域 field 不存在时执行成功 .
127.0.0.1:6379> hsetnx user3 age 20
0
127.0.0.1:6379> hsetnx user3 height 175
1
2.删
# hdel 删除指定哈希表中的filed,当该哈希表中的filed都被删除后,该表也不存在了
127.0.0.1:6379> hkeys user3
name
sex
age
height
127.0.0.1:6379> hdel user3 name
1
127.0.0.1:6379> hdel user3 sex
1
127.0.0.1:6379> hdel user3 age
1
127.0.0.1:6379> hdel user3 height
1
127.0.0.1:6379> keys *
user2
user1
3.改
# hincrby 为哈希表中的域 field 的值加上增量
127.0.0.1:6379> hincrby user3 age 2
20
127.0.0.1:6379> hvals user3
qiqi
girl
20
127.0.0.1:6379> hincrby user3 age -2
18
127.0.0.1:6379> hvals user3
qiqi
girl
18
4.查
# 1.hget 从指定集合的field取出 value
[root@localhost redis]# redis-cli --raw
127.0.0.1:6379> hget user1 name
李四
127.0.0.1:6379> hget user1 sex
男
127.0.0.1:6379> hget user1 age
18
127.0.0.1:6379> hget user2 name
王五
127.0.0.1:6379> hget user2 sex
男
127.0.0.1:6379> hget user2 age
20

# 2.hexists 判断指定哈希表中的filed是否存在,存在为1,不存在为0
127.0.0.1:6379> hexists user3 sex
1
127.0.0.1:6379> hexists user3 height
0

# 3.hkeys 查询指定哈希表中的所有filed
127.0.0.1:6379> hkeys user3
name
sex
age

# 4.hvals 查询指定哈希表中的所有filed的值
127.0.0.1:6379> hvals user3
qiqi
girl
18

# 5.hgetall 返回 key 指定的哈希集中所有的字段和值
127.0.0.1:6379> hgetall user1
1) "name"
2) "lisi"
3) "sex"
4) "boy"

# 6.hlen 返回 key 指定的哈希集包含的字段的数量
127.0.0.1:6379> hlen user1
(integer) 2

# 7.hstrlen 返回hash指定field的value的字符串长度
127.0.0.1:6379> hstrlen user1 name
(integer) 4
5.底层实现
  • 1.Hash类型的hash对象底层存储使用ziplist(压缩列表)和hashtable
  • 2.当hash对象可以同时满足以下两个条件时,hash对象使用ziplist编码,否则使用hashtable
    • 1.hash对象保存的所有键值对的键和值的字符串长度都小于64字节
    • 2.hash对象保存的键值对数量小于512个
6.应用场景(需完善)
1.购物车
  • 1.用户idkey商品idfield,商品数量为value,恰好构成了购物车的3个要素
    在这里插入图片描述
2.存储对象
  • 1.Hash类型结构(keyfield-value)与对象结构(对象id属性-)相似,因此可用来存储对象
  • 2.String类型的key=string + value=json也是存储对象的一种方式,存储对象选择方案对比
    string + jsonhash
    效率很高
    容量
    灵活性
    序列化简单复杂
  • 3.String的存储通常用在频繁读操作,存储格式是json,即把java对象转换为json,然后存入Redis
    • 1.当对象的某个属性频繁修改时,不适合string+json的数据结构,因为不够灵活,每次修改都需要重新将整个对象序列化并赋值
  • 4.Hash的存储通常用在频繁写操作,可以针对某个属性单独修改,不用序列化整个对象修改
    • 1.商品的库存、价格、关注数、评价数经常变动时,就使用Hash存储结果
    • 2.Hash类型也可以存储不常变化的属性(商品名称、商品描述、上市日期等),但当对象的某个属性不是基本类型或字符串时,使用Hash类型必须手动序列
  • 5.总结:一般对象用string + json存储,对象中某些频繁变化的属性抽出来用Hash存储

5.ZSet(sorted set)

  • 1.ZSet是一个有序集合,其keystring类型,其valuestring类型,且集合元素不重复
  • 2.ZSetSet区别:ZSet每个成员都关联了一个评分(score),用来按照最低分到最高分的方式排序集合中的元素
  • 3.集合的元素是唯一的,但是元素的评分可以重复
1.增
# zadd 将一个或多个元素及其 score 值加入到有序集中(先输入分数再输值)
127.0.0.1:6379> zadd name 20 lisi 60 wangwu 100 zhaoliu 45 zhangsan
4
127.0.0.1:6379> zrange name 0 -1
lisi
zhangsan
wangwu
zhaoliu
2.删
# 1.zrem key member [member ...] 删除该集合下指定的元素 
127.0.0.1:6379> zrem name lisi
1
127.0.0.1:6379> zrange name 0 -1 withscores
zhangsan
45
wangwu
75
zhaoliu
100

# 2.zpopmax key [count] 删除并返回有序集合key中的最多count个具有最高得分的成员
# count的默认值为1,指定一个大于有序集合的基数的count不会产生错误, 当返回多个元素时候,得分最高的元素将是第一个元素,然后是分数较低的元素
redis> zadd myzset 1 "one"
(integer) 1
redis> zadd myzset 2 "two"
(integer) 1
redis> zadd myzset 3 "three"
(integer) 1
redis> zpopmax myzset
1) "3"
2) "three"
redis>

# 3.zpopmin key [count] 删除并返回有序集合key中的最多count个具有最低得分的成员。
# count的默认值为1,指定一个大于有序集合的基数的count不会产生错误,当返回多个元素时候,得分最低的元素将是第一个元素,然后是分数较高的元素
redis> zpopmin myzset
1) "1"
2) "one"
redis> 

# 4.zremrangebylex key min max 删除名称按字典由低到高排序成员之间所有成员
# 不要在成员分数不同的有序集合中使用此命令, 因为它是基于分数一致的有序集合设计的,如果使用,会导致删除的结果不正确
# 可以使用 “-“ 和 “+” 表示最小值和最大值

# 5.zremrangebyscire key min max 移除有序集key中所有score值介于min和max之间(包括等于min或max)的成员

# 6.zremrangebyrank key start stop 移除有序集key中指定排名(rank)区间内的所有成员
#下标参数start和stop都以0为底,0处是分数最小的那个元素
3.改
#  zincrby 为元素的score加上增量
127.0.0.1:6379> zincrby name 5 lisi
25
127.0.0.1:6379> zincrby name 15 wangwu
75
4.查
# 1.返回有序集中,下标在<start><stop>之间的元素
# 带WITHSCORES,可以让分数一起和值返回
127.0.0.1:6379> zrange name 0 -1
lisi
zhangsan
wangwu
zhaoliu
127.0.0.1:6379> zrange name 0 -1 withscores
lisi
20
zhangsan
45
wangwu
60
zhaoliu
100
127.0.0.1:6379> zrange name 0 0 withscores
lisi
20
127.0.0.1:6379> zrange name 0 1 withscores
lisi
20
zhangsan
45
127.0.0.1:6379> zrange name 0 2 withscores
lisi
20
zhangsan
45
wangwu
60
127.0.0.1:6379> zrange name 0 3 withscores
lisi
20
zhangsan
45
wangwu
60
zhaoliu
100
127.0.0.1:6379> zrange name 0 4 withscores
lisi
20
zhangsan
45
wangwu
60
zhaoliu
100

# 2.zrangebyscore  返回指定有序集中,所有score值介于 min 和 max 之间(包括等于 min 或 max )的成员,有序集成员按 score 值递增(从小到大)排列
127.0.0.1:6379> zrangebyscore  name 30 90 withscores
zhangsan
45
wangwu
60

# 3.zrevrangebyscore  返回指定有序集中,所有score值介于 min 和 max 之间(包括等于 min 或 max )的成员,有序集成员按 score 值递增(从大到小)排列
127.0.0.1:6379> zrevrangebyscore  name 90 30 withscores
wangwu
60
zhangsan
45
127.0.0.1:6379> zrevrangebyscore  name 90 30 withscores limit 0 2
wangwu
75
zhangsan
45
127.0.0.1:6379> zrevrangebyscore  name 90 30 withscores limit 1 1
zhangsan
45

# 4.zcount 统计该集合分数区间内的元素个数
127.0.0.1:6379> zcount name 50 100
2

# 5.zrank 返回该值在集合中的排名(从小到大),下标从0开始 
# zrevrank 返回有序集key中成员member的排名,其中有序集成员按score值从大到小排列
127.0.0.1:6379> zrank name zhaoliu
2

# 6.zscore 查询指定集合的值的分数
127.0.0.1:6379> zscore name zhangsan
45

# 7.zcard 返回key的有序集元素个数
127.0.0.1:6379> zcard name
(integer) 4
127.0.0.1:6379>

# 8.zrangebylex key min max [LIMIT offset count] 
# 返回指定成员区间内的成员,按成员字典正序排序, 分数必须相同
5.集合操作
# 1.zinterstore destination numskeys [key ...] [WEIGHTS weight] [SUM|MIN|MAX]
# 计算给定的numkeys个有序集合的交集,并且把结果放到destination中
# 给定要计算的key和其它参数之前,必须先给定key个数(numberkeys)
# 默认情况下,结果集中某个成员的score值是所有给定集下该成员score值之和
# WEIGHTS选项:可以为每个给定的有序集指定一个乘法因子,即每个给定有序集的所有成员的score值在传递给聚合函数之前都要先乘以该因子,如果WEIGHTS没有给定,默认就是1
# AGGREGATE选项:可以指定结果集的聚合方式,默认使用的参数SUM,可以将所有集合中某个成员的score值之和作为结果集中该成员的score值,如果使用参数MIN或者MAX,结果集就是所有集合中元素最小或最大的元素
# 如果key destination存在,就被覆盖
redis> zadd zset1 1 "one"
(integer) 1
redis> zadd  zset1 2 "two"
(integer) 1
redis> zadd  zset2 1 "one"
(integer) 1
redis> zadd  zset2 2 "two"
(integer) 1
redis> zadd  zset2 3 "three"
(integer) 1
redis> zinterstore out 2 zset1 zset2 WEIGHTS 2 3  # 即 1*2+1*3 和 2*2+2*3
(integer) 2
redis> zinterstore out 0 -1 WITHSCORES
1) "one"
2) "5"
3) "two"
4) "10"

# 2.zunionstore destination numkeys key [key ...] [WEIGHTS weight] [SUM|MIN|MAX] 同上述类似,不同的是求并集,具体案例可参考官网http://www.redis.cn/commands.html
6.底层实现
  • 1.ZSet底层使用了两个数据结构
    • 1.hashhash的作用是通过filed-value关联元素value和分数score,保证元素value的唯一性,且可以通过value找到对应的score
    • 2.跳跃表:跳跃表的作用在于给元素value排序,快速查找到指定元素
  • 2.跳跃表(跳表)
    在这里插入图片描述
7.应用场景
1.排行榜
  • 1.当前小时的时间戳作为 ZSetkey,把贴子ID 作为 member ,点击数评论数等作为 score,当 score 发生变化时更新 score
  • 2.利用 ZREVRANGE 或者 ZRANGE 查到对应数量的记录

6.普通数据类型

1.Bitmaps

  • 1.现代计算机用二进制(位) 作为信息的基础单位, 1个字节等于8位
  • 2.例:abc字符串是由3个字节组成, 但实际在计算机存储时将其用二进制表示
  • 3.abc分别对应的ASCII码分别是979899, 对应的二进制分别是011000010110001001100011
    在这里插入图片描述
  • 4.合理地使用操作位能够有效地提高内存使用率和开发效率
  • 5.Redis提供了Bitmaps实现对位的操作
    • 1.Bitmaps实际上是字符串String , 但是可以对字符串的进行操作。
    • 2.Bitmaps单独提供了一套命令,虽然也在String组中,但是在Redis中使用Bitmaps和使用String的方法并不相同
    • 3.Bitmaps类似一个以为单位的数组, 数组的每个单元只能存储0和1, 数组的下标在Bitmaps中叫做偏移量(offset)
      在这里插入图片描述
1.增
# 1.setbit key offset value 设置或者清空key的value(字符串)在offset处的bit值
# 那个位置的bit要么被设置,要么被清空,这个由value(只能是0或者1)来决定
# 当key不存在的时候,就创建一个新的字符串value,要确保这个字符串大到在offset处有bit值
# 参数offset需要大于等于0,并且小于等于2的32次方-1(限制bitmap大小为512MB)
# 当key对应的字符串增大的时候,新增的部分bit值都是设置为0
# 第一次初始化Bitmaps时, 假如偏移量非常大, 那么整个初始化过程执行会比较慢, 可能会造成Redis的阻塞
127.0.0.1:6379> setbit users:20230325 1 1
(integer) 0
127.0.0.1:6379> setbit users:20230325 6 1
(integer) 0
127.0.0.1:6379> setbit users:20230325 20 1
(integer) 0
127.0.0.1:6379> setbit users:20230325 12 1
(integer) 0
2.删
# 1.可以通过del删除
127.0.0.1:6379> del users:202303:two
(integer) 1
3.改
# 1.可以直接通过setbit命令进行修改
127.0.0.1:6379> setbit users:20230325 1 0
(integer) 1
4.查
# 1.getbit key offset 获取key的第offset位的值(从0开始算)
127.0.0.1:6379> getbit users:20230325 12
(integer) 1

# 2.bitcount key [start end] 统计字符串被设置为1的bit数
# 一般情况下,给定的整个字符串都会被进行计数
# 通过指定额外的 start 或 end 参数,可以让计数只在特定的位上进行
# start、end 是指bit组的字节的下标数,二者皆包含
# start 和 end 参数的设置都可以使用负数值:
# -1 表示最后一个位,-2 表示倒数第二个位
127.0.0.1:6379> bitcount users:20230325 0 20
(integer) 4

# 3.bitpos key bit [start] [end] 返回字符串里面第一个被设置为1或者0的bit位
# 默认情况下整个字符串都会被检索一次,只有在指定start和end参数(指定start和end位是可行的)
# 该范围被解释为一个字节的范围,而不是一系列的位
# 所以start=0 并且 end=2是指前三个字节范围内查找
# 注意:返回的位的位置始终是从0开始的,即使使用了start来指定了一个开始字节也是这样
127.0.0.1:6379> bitpos users:20230324 1 -1
(integer) 12

# 4.bitfield key [GET type offset] [SET type offset value] [INCRBY type offset increment] [OVERFLOW WRAP|SAT|FAIL]
# 把Redis字符串当作位数组,并能对变长位宽和任意未字节对齐的指定整型位域进行寻址
# bitfield 有三个子指令get,set,incrby都可以对指定位片段进行读写,但最多只能处理64个连续的位
# 有符号数最多可以取64位,无符号数只能取63位( redis 协议中的integer是有符号数,最大64位)
# 超过64位,则要使用多个子指令,bitfield可以一次执行多个子指令
# get <type> <offset> – 返回指定的位域
# set <type> <offset> <value> – 设置指定位域的值并返回它的原值
# incrby <type> <offset> <increment> – 自增或自减(如果increment为负数)指定位域的值并返回它的新值
# overflow [wrap|sat|fail] 通过设置溢出行为来改变调用incrby指令的后序操作
# 当需要一个整型时,有符号整型需在位数前加i,无符号在位数前加u
127.0.0.1:6379> set test he
OK
127.0.0.1:6379> bitfield test get u4 0 # 从第一个位开始取 4 个位,结果是无符>号数 (u)
1) (integer) 6
127.0.0.1:6379> bitfield test get u3 2 # 从第三个位开始取 3 个位,结果是无符号数 (u)
1) (integer) 5
127.0.0.1:6379> bitfield test get i4 0 # 从第一个位开始取 4 个位,结果是有符号数 (i)
1) (integer) 6
127.0.0.1:6379> bitfield test get i3 2 # 从第三个位开始取 3 个位,结果是有符号数 (i)
1) (integer) -3
# 首先获取 he 的 ASCII ,然后 通过 ASCII 计算出二进制值
# he的二进制值 01101000 01100101
# bitfield test get i3 2 中从第三个位开始取 3 个位,取到的是 101,第一个位为 1,是符号位数,表示这是个负数
# 然后把 01 取反得 10,然后加1得补码 11,结果是 -3 
5.集合操作
# bitop operation destkey key [key ...] 对一个或多个保存二进制位的字符串 key 进行位元操作,并将结果保存到 destkey 上
# bitop 命令支持 and、 or、 not、 xor 四种operation中的任意一种参数
# bitop and destkey srckey1 srckey2 srckey3 ... srckeyN ,对一个或多个 key 求逻辑并,并将结果保存到 destkey 
# bitop or destkey srckey1 srckey2 srckey3 ... srckeyN,对一个或多个 key 求逻辑或,并将结果保存到 destkey 
# bitop xor destkey srckey1 srckey2 srckey3 ... srckeyN,对一个或多个 key 求逻辑异或,并将结果保存到 destkey 
# bitop not destkey srckey,对给定 key 求逻辑非,并将结果保存到 destkey
# 除了 not 操作之外,其他操作都可以接受一个或多个 key 作为输入
# 当 bitop 处理不同长度的字符串时,较短的那个字符串所缺少的部分会被看作 0 ,空的 key 也被看作是包含 0 的字符串序列
127.0.0.1:6379> setbit users:20230324 1 1
(integer) 0
127.0.0.1:6379> setbit users:20230324 12 1
(integer) 0
127.0.0.1:6379> bitop and users:202303:2 users:20230324 users:20230325
(integer) 3
127.0.0.1:6379> bitcount users:202303:2
(integer) 2
6.底层实现
7.应用场景(需完善)
1.统计网站的访问PV、UV、IP

2.HyperLogLog

  • 1.求集合中不重复元素个数的问题称为基数问题,解决基数问题有很多种方案
    • 1.MySQL可使用distinct count计算不重复个数
    • 2.Redis可使用的hash、set、bitmaps等数据结构来处理
  • 2.Hash、Set、Bitmaps等数据结构虽然可以处理,但随着数据不断增加,会导致占用空间越来越大,对于非常大的数据集是不可取的
  • 3.Redis中的HyperLogLog是用来做基数统计的,其优点是:输入元素的数量或者体积非常大时,计算基数所需的空间总是固定的、并且很小的
  • 4.Redis中每个 HyperLogLog 键只需要花费 12 KB 内存,可以计算接近 2^64 个不同元素的基数,不会因为元素越多耗费内存就越多
  • 5.但是 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素
1.增
# pfadd key element [element ...] 添加指定元素到 HyperLogLog 中
# 如果执行命令后HLL估计的近似基数发生变化,则返回1,否则返回0
127.0.0.1:6379> pfadd h1 "redis" "mysql" "nginx" "mycat"
(integer) 1
127.0.0.1:6379> pfadd h1 "shardingsphere" "springsecurity"
(integer) 1
127.0.0.1:6379> pfadd h2 "mongdb" "shiro"
(integer) 1
2.删
# 1.可以通过del删除
127.0.0.1:6379> del h1
(integer) 1
127.0.0.1:6379> keys *
1) "h3"
2) "h2"
3.改
# pfmerge destkey sourcekey [sourcekey ...]
# 将一个或多个HLL合并后的结果存储在另一个HLL中
127.0.0.1:6379> pfmerge h3 h1 h2
OK
127.0.0.1:6379> pfcount h3
(integer) 8
4.查
# pfcount key [key ...] 
# 如果该变量不存在,则返回0 
# 当参数为一个key时,返回存储在HLL结构体的该变量的近似基数
# 当参数为多个key时,返回这些HLL并集的近似基数
127.0.0.1:6379> pfcount h1 h2
(integer) 8
5.底层实现
6.应用场景
1.统计网站的访问PV、UV、IP

3.Geo

  • 1.Redis 3.2 中增加了对GEO类型的支持
  • 2.GEO:地理信息的缩写(Geographic),该类型是元素的2维坐标,地图上指经纬度
  • 3.Redis基于该类型,提供了经纬度设置,查询,范围查询,距离查询等常见操作
1.增
# geoadd key longitude latitude member [longitude latitude member ...]
# 将指定的地理空间位置(纬度、经度、名称)添加到指定的key中
# 两极无法直接添加,一般会下载城市数据,直接通过 Java 程序一次性导入
# 有效的经度从 -180 度到 180 度,有效的纬度从 -85.05112878 度到 85.05112878 度
# 当坐标位置超出指定范围时,该命令将会返回一个错误
# 已经添加的数据,是无法再次往里面添加的
127.0.0.1:6379> geoadd china:city 121.47 31.23 shanghai
(integer) 1
127.0.0.1:6379> geoadd china:city 106.50 29.53 chonqing 114.05 22.52 >shenzhen 116.38 39.90 beijing
(integer) 3
2.删
# 可以通过del删除
127.0.0.1:6379> geoadd china:city 121.47 31.23 shanghai
(integer) 1
127.0.0.1:6379> geopos china:city shanghai
1) 1) "121.47000163793563843"
   2) "31.22999903975783553"
127.0.0.1:6379> del china:city
(integer) 1
127.0.0.1:6379> keys *
(empty list or set)
3.改
# 可以直接通过geopos重新设置
127.0.0.1:6379> geopos china:city shenzhen
1) 1) "114.04999762773513794"
   2) "22.5200000879503861"
127.0.0.1:6379> geoadd china:city 115.04 22.52 shenzhen
(integer) 0
127.0.0.1:6379> geopos china:city shenzhen
1) 1) "115.04000097513198853"
   2) "22.5200000879503861"
4.查
# 1.geopos key member [member ...] 从key里返回所有给定位置元素的位置(经度和纬度)
# geopos 命令返回一个数组, 数组中的每个项都由两个元素组成: 
# 第一个元素为给定位置元素的经度, 而第二个元素则为给定位置元素的纬度
# 当给定的位置元素不存在时, 对应的数组项为空值
127.0.0.1:6379> geopos china:city shanghai shenzhen
1) 1) "121.47000163793563843"
   2) "31.22999903975783553"
2) 1) "114.04999762773513794"
   2) "22.5200000879503861"

# 2.geodist key member1 member2 [unit] 返回两个给定位置之间的距离。
# 如果两个位置之间的其中一个不存在, 那么命令返回空值
# 指定单位的参数 unit 必须是以下单位的其中一个
# m 表示单位为米
# km 表示单位为千米
# mi 表示单位为英里
# ft 表示单位为英尺
# 如果用户没有显式地指定单位参数, 那么 GEODIST 默认使用米作为单位
127.0.0.1:6379> geodist china:city shanghai shenzhen km
"1215.9224"

# 3.georadius key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count]
# 以给定的经纬度为中心,找出某一半径内的元素
# 范围可以使用以上其中一个单位
# 给定以下可选项时, 命令会返回额外的信息
#	1.WITHDIST: 在返回位置元素的同时, 将位置元素与中心之间的距离也一并返回。 距离的单位和用户给定的范围单位保持一致。
#	2.WITHCOORD: 将位置元素的经度和维度也一并返回。
#	3.WITHHASH: 以 52 位有符号整数的形式, 返回位置元素经过原始 geohash 编码的有序集合分值。 这个选项主要用于底层应用或者调试, 实际中的作用并不大。
# 命令默认返回未排序的位置元素。 通过以下两个参数, 用户可以指定被返回位置元素的排序方式
# 	1.ASC: 根据中心的位置, 按照从近到远的方式返回位置元素。
# 	2.DESC: 根据中心的位置, 按照从远到近的方式返回位置元素
127.0.0.1:6379> georadius china:city 110 30 1000 km
1) "chonqing"
2) "shenzhen"
127.0.0.1:6379> georadius china:city 110 30 1000 km withdist
1) 1) "chonqing"
   2) "341.9374"
2) 1) "shenzhen"
   2) "924.6408"

# 4.georadiusbymember key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count]
# 以给定的元素为中心,找出某一半径内的元素
127.0.0.1:6379> georadiusbymember china:city shenzhen 2000 km withdist
1) 1) "chonqing"
   2) "1084.4275"
2) 1) "shenzhen"
   2) "0.0000"
3) 1) "shanghai"
   2) "1215.9224"
4) 1) "beijing"
   2) "1945.5740"
5.底层实现
6.应用场景

7.发布和订阅

  • 1.Redis发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息
  • 2.Redis发布订阅(pub/sub)通过频道进行信息的发送,即生产者生产完消息通过频道分发消息给订阅该频道的消费者
  • 3.Redis客户端可以订阅任意数量频道,其只能接收到已订阅频道的信息,未订阅频道的信息无法接收
  • 4.项目中可采用KafkaRabbitMQRocketMQActiveMQ等成熟的消息组件,也可采用Redis发布订阅模式
  • 5.Redis发布订阅优点:轻量直接使用,而上面几种消息组件适合大数据量数据准确性要求高的场景

1.客户端订阅频道

在这里插入图片描述

2.发布者发送消息

在这里插入图片描述

3.发布订阅命令行实现

  • 1.启动3个服务端都分别订阅channel1频道
    # 1.subscribe channel [channel ...] 订阅给指定频道的信息
    # 一旦客户端进入订阅状态,客户端就只可接受订阅相关的命令,其他命令一律失效
    127.0.0.1:6379> subscribe channel1
    Reading messages... (press Ctrl-C to quit)
    1) "subscribe"
    2) "channel1"
    3) (integer) 1
    
    # 2.psubscribe pattern [pattern ...]  订阅给定的模式(patterns)的频道
    # 支持的模式(patterns)
    #	1.h?llo subscribes to hello, hallo and hxllo 代表一个任意字符
    #	2.h*llo subscribes to hllo and heeeello 代表没有或任意多个字符
    #	3.h[ae]llo subscribes to hello and hallo, but not hillo 代表括号内任意字符
    # 如果想输入普通的字符,可以在前面添加\
    127.0.0.1:6379> psubscribe chann*
    Reading messages... (press Ctrl-C to quit)
    1) "psubscribe"
    2) "chann*"
    3) (integer) 1
    1) "pmessage"
    2) "chann*"
    3) "channel1"
    4) "test"
    
  • 2.启动1个客户端给channel1频道发布消息
    # publish channel message 将信息 message 发送到指定的频道 channel
    # 返回值 收到消息的客户端数量
    127.0.0.1:6379> publish channel1 testConn
    (integer) 3
    
  • 3.其中3个客户端都可以看到服务端发送的消息
    127.0.0.1:6379> subscribe channel1
    Reading messages... (press Ctrl-C to quit)
    1) "subscribe"
    2) "channel1"
    3) (integer) 1
    1) "message"
    2) "channel1"
    3) "testConn"
    
  • 4.停止订阅信息
    # 1.unsubscirbe [channel [channel ...]]
    # 指示客户端退订给定的频道,若没有指定频道,则退订所有频道
    127.0.0.1:6379> unsubscribe channel1
    1) "unsubscribe"
    2) "channel1"
    3) (integer) 0
    
    # 2.punsubscirbe  [pattern [pattern ...]]
    # 指示客户端退订指定模式,若果没有提供模式则退出所有模式
    
  • 5.查看当前发布订阅的信息
    # 1.pubsub 是自省命令,能够检测pub/sub子系统的状态
    # 它由分别详细描述的子命令(subcommand )组成
    # pubsub subcommand [argument [argument ...]]
    # 子命令subcommand 有以下选项
    #	1.pubsub channels [pattern] 列出当前活跃的频道,活跃是指信道含有一个或多个订阅者(不包括从模式接收订阅的客户端) 
    	# 如果pattern未提供,所有的信道都被列出,否则只列出匹配上指定全局-类型模式的信道被列出
    #	2.pubsub numsub [channel-1 ... channel-N] 列出指定信道的订阅者个数(不包括订阅模式的客户端订阅者)
    #	3.pubsub numpat 返回订阅模式的数量(使用命令PSUBSCRIBE实现的模式才会统计)
    	# 注意:这个命令返回的不是订阅模式的客户端的数量, 而是客户端订阅的所有模式的数量总和
    127.0.0.1:6379> pubsub channels
    1) "channel1"
    127.0.0.1:6379> pubsub numsub channel1
    1) "channel1"
    2) (integer) 2
    127.0.0.1:6379> pubsub numpat
    (integer) 1
    

4.底层实现

  • 1.频道的实现原理
    • 1.pubsub_channels定义的属性是一个字典类型,保存着客户端和频道信息,key 值保存的是频道名value 是一个链表,链表中保存的是客户端 id
    • 2.频道内部存储结构,Redis 源码路径:redis-5.0.7/src/server.h
      struct redisServer {
          /* General */
          pid_t pid;   
      
          //...
      
          // 将频道映射到已订阅客户端的列表(就是保存客户端和订阅的频道信息)
          dict *pubsub_channels;  /* Map channels to list of subscribed clients */
      }
      
      在这里插入图片描述
    • 3.频道订阅:订阅频道时先检查字段内部是否存在,不存在则为当前频道创建一个字典且创建一个链表存储客户端 id;否则直接将客户端 id 插入到链表中
    • 4.取消频道订阅:取消时将客户端 id 从对应的链表中删除,如果删除之后链表已经是空链表了,则将会把这个频道从字典中删除
    • 5.订阅的消费者需要一直执行,阻塞获取消息,如果断开则表示退订
    • 6.Channel 只接收生产者发送的消息,自身是不存储消息,如果 Channel 没有被订阅,则消息会被丢弃
  • 2.模式的实现原理
    • 1.pubsub_patterns定义的属性是一个列表,保存一个订阅模式结构订阅模式结构中保存着订阅模式客户端被订阅的模式
    • 2.模式内部存储结构,Redis 源码路径:redis-5.0.7/src/server.h
      struct redisServer {
          /* General */
          pid_t pid;   
      
          //......
      
          // pubsub订阅的列表信息(存储订阅模式的信息)
          list *pubsub_patterns;  /* A list of pubsub_patterns */
      }
      
      // 1303行订阅模式列表结构:
      typedef struct pubsubPattern {
          client *client;  -- 订阅模式客户端
          robj *pattern;   -- 被订阅的模式
      } pubsubPattern;
      
      在这里插入图片描述
    • 3.模式订阅:新增一个pubsub_pattern数据结构添加到链表的尾部,同时保存客户端id
    • 4.取消模式订阅:从当前的链表pubsub_patterns结构中删除需要取消的模式

5.应用场景

8.Java操作Redis

1.准备工作

1.关闭防火墙

在这里插入图片描述

2.修改配置文件
[root@node1 ~]# vim /etc/redis/redis.conf
# 具体含义可参考安装中的配置文件

# 1.注释掉bind,否则只能本机访问
# bind 127.0.0.1

# 2.关闭保护模式
protected-mode no
3.重启Redis
[root@node1 /]# redis-server /etc/redis/redis.conf
# 修改配置文件后,重新使用该配置文件启动redis服务器,使配置生效
# 如果想使用systemctl启动则需要先将redis加入服务
4.常见问题
  • 1.未关闭保护模式
    # 该错误是没有关闭保护模式
    redis.clients.jedis.exceptions.JedisDataException: 
    DENIED Redis is running in protected mode because protected mode is enabled, no bind address was specified, no authentication password is requested to clients. 
    In this mode connections are only accepted from the loopback interface. 
    If you want to connect from external computers to Redis you may adopt one of the following solutions: 
    
  • 2.未验证密码
    在这里插入图片描述
    在这里插入图片描述

2.Jedis操作

  • 1.Redis-cliRedis官方提供的客户端,可以在Linux上发送命令对Redis进行操作
  • 2.JedisRedis官方推出的面向Java的客户端,可以使用Java语言操作Redis,并且提供了连接池管理
  • 3.Jedis 实现上是直接连接的 redis-server,如果在多线程环境下是非线程安全的,这个时候只有使用连接池,为每个 Jedis 实例增加物理连接
1.创建SpringBoot项目
  • 1.参考SpringBoot文章中的第一个SpringBoot程序
2.导入依赖
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>
3.测试API
  • 1.String在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
  • 6.Bitmaps
    在这里插入图片描述
    在这里插入图片描述
4.创建连接池
	@Test
   public void JedisPool(){
       //创建连接池的配置对象
       JedisPoolConfig config = new JedisPoolConfig();
       //从连接池获取连接时,是否检测连接的有效性
       config.setTestOnBorrow(true);
       //可用连接实例的最大数目,默认值为8
       config.setMaxTotal(100);
       //最大空闲连接数, 默认8个
       config.setMaxIdle(8);
       //最小空闲连接数,默认8个
       config.setMinIdle(8);
       //没有空闲连接时,最大的等待毫秒数
       config.setMaxWaitMillis(60000);

       //创建连接池
       JedisPool jedisPool = new JedisPool(config,"192.168.73.130",6379,6,"root");
       //获取连接
       Jedis jedis = jedisPool.getResource();
       //测试连通性
       String ping = jedis.ping();
       //释放资源
       jedis.close();
   }
5.JedisUtils
  • 1.将配置提取到redis.properties文件中
    # redis机器ip
    redis.hostName=192.168.73.100
    # redis端口号
    redis.port=6379
    
    # 最大数量
    redis.maxTotal=500
    # 最大空闲数量
    redis.maxIdle=50
    # 最小空闲数量
    redis.minIdle=10
    # 建立连接最大等待时间,单位毫秒
    redis.maxWaitMillis=30000
    # 从连接池中获取连接时,是否检查连接的可用性
    redis.testOnBorrow=true
    
  • 2.创建Jedis工具类(注意:实际项目中不使用该方式而使用yml文件)
    package com.wd.util;
    
    import redis.clients.jedis.Jedis;
    import redis.clients.jedis.JedisPool;
    import redis.clients.jedis.JedisPoolConfig;
    
    import java.io.InputStream;
    import java.util.Properties;
    
    public class JedisUtils {
        private static JedisPool pool = null;
    
        static {
            InputStream inputStream = JedisUtil.class.getResourceAsStream("/redis.properties");
            Properties properties = new Properties();
            try {
                properties.load(inputStream);
                inputStream.close();
            } catch (Exception e) {
                e.printStackTrace();
                throw new RuntimeException(e);
            }
            JedisPoolConfig config = new JedisPoolConfig();
         
            String hostName = properties.getProperty("redis.hostName");
            String port = properties.getProperty("redis.port");
            String testOnBorrow = properties.getProperty("redis.testOnBorrow");
            String maxTotal = properties.getProperty("redis.maxTotal");
            String maxIdle = properties.getProperty("redis.maxIdle");
            String minIdle = properties.getProperty("redis.minIdle");
            String maxWaitMillis = properties.getProperty("redis.maxWaitMillis");
            
            if(testOnBorrow != null){
                config.setTestOnBorrow(Boolean.parseBoolean(testOnBorrow));
            }
            if(maxTotal != null){
                config.setMaxTotal(Integer.parseInt(maxTotal));
            }
            if(maxIdle != null){
                config.setMaxIdle(Integer.parseInt(maxIdle));
            }
            if(minIdle != null){
                config.setMinIdle(Integer.parseInt(minIdle));
            }
            if(maxWaitMillis != null){
                config.setMaxWaitMillis(Long.parseLong(maxIdle));
            }
    
            pool = new JedisPool(config, hostName, Integer.parseInt(port == null ? "6379" : port));
        }
    
        public static Jedis getJedis(){
            return pool.getResource();
        }
    
        public static void close(Jedis jedis){
            jedis.close();
        }
    }
    
     @Test
        public void testRedisUtil(){
            Jedis jedis = JedisUtils.getJedis();
            String ping = jedis.ping();
            jedis.close();
        }
    

3.Lettuce操作

  • 1.Lettuce也是面向Java的客户端,可以使用Java语言操作Redis,并且提供了连接池管理(注意:其连接池需要依赖commons-pool2架包)
  • 2.Lettuce 实现上是基于 Netty 连接的 redis-server,连接实例(StatefulRedisConnection)可以在多个线程间并发访问
  • 3.StatefulRedisConnection 是线程安全的,所以一个连接实例(StatefulRedisConnection)就可以满足多线程环境下的并发访问
  • 4.同时Lettuce是可伸缩的设计,一个连接实例不够的情况也可以按需增加连接实例
  • 5.Lettuce使用方式由4部分组成
    • 1.URI:定义连接信息
    • 2.ClientRedis客户端,集群使用专用的RedisClusterClient
    • 3.ConnectionRedis连接,有多种类型(单机,哨兵,集群,订阅发布)
    • 4.Command:用户操作api,基本包含了Redis全部命令,Command有三种类型
      • 1.sync:同步,跟Jedis类似
      • 2.async:异步
      • 3.reactive:响应式
1.创建SpringBoot项目
  • 1.参考SpringBoot文章中的第一个SpringBoot程序
2.导入依赖
<dependency>
    <groupId>io.lettuce</groupId>
    <artifactId>lettuce-core</artifactId>
    <version>5.2.1.RELEASE</version>
</dependency>
3.测试API
   @Test
   public void test(){
       //1.连接设置
       RedisURI uri = RedisURI.builder()
               .withHost("192.168.73.130")
               .withPort(6379)
               .withPassword("root")
               .build();
       //2.线程池设置
       ClientResources resources = DefaultClientResources.builder()
               .ioThreadPoolSize(4)	//设置I/O线程池大小(默认cpu数)仅在没有提供eventLoopGroupProvider时有效
               .computationThreadPoolSize(4)	//设置用于计算的任务线程数(默认cpu数)仅在没有提供eventExecutorGroup时有效
//                .reconnectDelay(Delay.constant(Duration.ofSeconds(10)))	//设置无状态尝试重连延迟,默认延迟上限30s
               .build();
       //3.客户端
       RedisClient client = RedisClient.create(resources, uri);
       //4.客户端操作
       ClientOptions options = ClientOptions.builder()
               .autoReconnect(true)	//设置自动重连
               .pingBeforeActivateConnection(true)	//激活连接前执行PING命令
//				.timeoutOptions(TimeoutOptions.enabled(Duration.ofSeconds(5)))	//命令超时
               .build();
       client.setOptions(options);
       client.setDefaultTimeout(Duration.ofSeconds(3));	//为客户端创建的连接设置默认超时时间,适用于尝试连接和非阻塞命令
       //5.客户端连接
       StatefulRedisConnection<String, String> conn = client.connect();

       System.out.println("==sync同步==");
       RedisCommands<String, String> syncCmd = conn.sync();
       String set1 = syncCmd.set("test1", "aaa");
       String test1 = syncCmd.get("test1");
       System.out.println(set1 + " : " + test1);

       System.out.println("==async异步==");
       RedisAsyncCommands<String, String> asyncCmd = conn.async();
       RedisFuture<String> test2 = asyncCmd.set("test2", "bbb");
       try {
           System.out.println(test2.get(2, TimeUnit.SECONDS));
       } catch (InterruptedException | ExecutionException | TimeoutException e1) {
           e1.printStackTrace();
       }
       RedisFuture<String> test3 = asyncCmd.get("test2");
       test3.whenCompleteAsync((x, t) -> {
           System.out.println("async异步: " + x + " : " + t);
       });

       System.out.println("==reactive反应式==");
       RedisReactiveCommands<String, String> reactiveCmd = conn.reactive();
       Mono<String> test5 = reactiveCmd.set("test5", "ccc");

       System.out.println(test5.block());

       Mono<String> test6 = reactiveCmd.get("test5");
       test6.subscribe(System.out::println);
       Flux<String> keys = reactiveCmd.keys("*");
       keys.subscribe(System.out::println);

       try {Thread.sleep(500);} catch (InterruptedException e) {}

       System.out.println("==pubsub发布订阅==");
       StatefulRedisPubSubConnection<String, String> pubsubConn = client.connectPubSub();
       pubsubConn.addListener(new RedisPubSubListener<String, String>() {
           @Override
           public void unsubscribed(String channel, long count) {
               System.out.println("[unsubscribed]" + channel);
           }
           @Override
           public void subscribed(String channel, long count) {
               System.out.println("[subscribed]" + channel);
           }
           @Override
           public void punsubscribed(String pattern, long count) {
           }
           @Override
           public void psubscribed(String pattern, long count) {
           }
           @Override
           public void message(String pattern, String channel, String message) {
               System.out.println("[message]" + channel + " -> " + message);
           }
           @Override
           public void message(String channel, String message) {
               System.out.println("[message]" + channel + " -> " + message);
           }
       });

       RedisPubSubAsyncCommands<String, String> pubsubCmd = pubsubConn.async();
       //订阅端
       pubsubCmd.subscribe("CH");
       pubsubCmd.subscribe("CH2");
       //发布端
       pubsubCmd.publish("CH", "hello");
       pubsubCmd.publish("CH2", "world");
       //取消订阅
       pubsubCmd.unsubscribe("CH");
       pubsubCmd.unsubscribe("CH2");
       try {Thread.sleep(500);} catch (InterruptedException e) {}
   }

在这里插入图片描述

9.SpringBoot操作Redis

  • 1.Java 实现对 Redis 的操作主要有两种方式:JedisLettuce
    • 1.JedisRedis官方推荐的面向Java操作Redis的客户端
    • 2.LettuceSpringBoot(2.x.x版本)默认使用的面向Java操作Redis的客户端
  • 2.Springspring-data-redis模块中封装了RedisTemplate对象来对Redis进行各种操作
  • 3.Spring整合到SpringBoot框架中则是spring-boot-starter-data-redis模块,底层实际还是spring-data-redis
    在这里插入图片描述
  • 4.SpringBoot框架中在1.x.x版本时默认使用的是Jedis客户端,2.x.x版本后默认使用的是lettuce客户端,使用的都是RedisTemplate工具类,只是底层的实现不同
  • 5.RedisTemplateRedis原生的api进行了封装,还提供了连接池自动管理,异常处理及序列化等操作,简化Java操作Redis的编码工作
  • 6.其中除了RedisTemplate还包括StringRedisTemplateRedisTemplate可以支持Redis没有的缓存对象的操作,而StringRedisTemplate用来存储字符串
  • 7.SpringBoot使用JedisLettuce的配置操作不同,具体可参考:https://my.oschina.net/u/4350311/blog/3311483

1.导入依赖

<!-- redis -->
<!-- 内部集成了Jedis与Lettuce,spring-boot-starter-data-redis版本需要和SpringBoot一致 -->
<dependency>
  <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-redis</artifactId>
   <version>2.6.3</version>
</dependency>
<!-- spring2.x.x集成redis底层默认使用Lettuce,需要连接池依赖common-pool2-->
<!-- spring1.x.x集成redis底层默认使用Jedis,不需要此连接池依赖common-pool2-->
<dependency>
   <groupId>org.apache.commons</groupId>
   <artifactId>commons-pool2</artifactId>
   <version>2.9.0</version>
</dependency>

2.修改配置

spring
 redis:
   # redis部署的服务器的ip
   host: 192.168.73.100
   # 端口号
   port: 6379
   # 验证
   password: root
   # 连接超时时间(毫秒)
   timeout: 2000
   lettuce: # 或者 jedis
     pool:
       # 最大数量
       max-active: 500
       # 最大空闲数量
       max-idle: 50
       # 最小空闲数量
       min-idle: 10
       # 建立连接最大等待时间,单位毫秒
       max-wait: 30000

3.配置类

  • 1.将RedisTemplate交给Spring工厂管理并设置序列化方式
  • 2.因为底层默认JDK二进制序列化 不能跨平台使用,所以需要序列化
  • 3.因为目前SpringBoot2.x.x版本默认使用Lettuce,所以该配置类的序列化方式是针对LettuceJedis可参考https://my.oschina.net/u/4350311/blog/3311483
    package com.config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.connection.RedisConnectionFactory;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
    import org.springframework.data.redis.serializer.StringRedisSerializer;
    
    @Configuration
    public class RedisConfig {
     @Bean
        public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){
            //创建Redis操作模板
            RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
            //设置连接工厂
            redisTemplate.setConnectionFactory(redisConnectionFactory);
    
            //创建字符序列化方式
            StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
            //创建Json序列化方式
            GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
    
            //设置Redis键的设置方式为字符串序列化
            redisTemplate.setKeySerializer(stringRedisSerializer);
            //设置Redis值的设置方式为Json序列化
            redisTemplate.setValueSerializer(genericJackson2JsonRedisSerializer);
            //设置Redis中Hash结构键的设置方式为字符串序列化
            redisTemplate.setHashKeySerializer(stringRedisSerializer);
            //设置Redis中Hash结构键的设置方式为Json序列化
            redisTemplate.setHashValueSerializer(genericJackson2JsonRedisSerializer);
            return redisTemplate;
        }
    }
    

4.序列化和反序列化

1.定义
  • 1.对象序列化:将对象的状态信息持久保存的过程
  • 2.对象反序列化:根据对象的状态信息恢复对象的过程
  • 3.需要序列化的对象必须实现Serializable接口
  • 4.Redis中有两种序列化方式
    • 1.字节数组:java对象在计算机中以字节数组的方式存储到Redis
    • 2.jsonjava对象在计算机中以Json字符串的方式存储到Redis
2.存储对象
  • 1.Redis中操作Java中的对象需要序列化后才能进行操作,否则会报错
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
3.序列化配置
  • 1.Lettuce序列化配置如上述Lettuce配置类

5.RestTemplate

  • 1.spring-data-redis中的RestTemplate针对Redis客户端中的Api进行了归类封装,将同一类型操作封装为Operations接口
    • 1.ValueOperations:提供了对Sring类型的操作
    • 2.ListOperations:提供了对List类型的数据操作
    • 3.SetOperations:提供了对Set类型的数据操作
    • 4.HashOperations:提供了对Hash类型的数据操作
    • 5.ZsetOperations:提供了对ZSet类型的数据操作
    • 6.opsForHyperLogLog 提供了对HyperLogLog类型的数据操作
    • 7.opsForGeo 提供了对Geo类型的数据操作
    • 8.opsForStream 提供了对Stream类型的数据操作
    • 9.opsForCluster() 提供了对Cluster类型的数据操作

6.测试RestTemplate

@RunWith(SpringRunner.class)
//入口类对象作为该注解的参数
@SpringBootTest(classes = SpringBootApplication.class)
public class TestSpringBoot {
	@Resource
   private RedisTemplate redisTemplate;
   
	@Test
   public void testStringOperations(){
       //获取五种常用操作类
       ValueOperations valueOperations = redisTemplate.opsForValue();
       ListOperations listOperations = redisTemplate.opsForList();
       SetOperations setOperations = redisTemplate.opsForSet();
       HashOperations hashOperations = redisTemplate.opsForHash();
       ZSetOperations zSetOperations = redisTemplate.opsForZSet();

       //添加(配置类中设置了添加获取值的格式为二进制json格式,所以手动存String类型的值获取时会报错)
       valueOperations.set("name","李四");//单个添加
       HashMap<String, Object> redisMap = new HashMap<>();
       redisMap.put("sex","男");
       redisMap.put("height",175);
       redisMap.put("weight",125);
       valueOperations.multiSet(redisMap);//批量添加

       //删除
       redisTemplate.delete("name");

       //修改
       valueOperations.set("age",18);//修改
       valueOperations.increment("age");//自增
       valueOperations.increment("age",10);//增加指定值
       valueOperations.decrement("age");//自减
       valueOperations.decrement("age",5);//减少指定值

       //查询
       Object age = valueOperations.get("age");
       ArrayList<String> redisArray = new ArrayList<>();
       redisArray.add("name");
       redisArray.add("sex");
       redisArray.add("age");
       redisArray.add("height");
       redisArray.add("weight");
       List list = valueOperations.multiGet(redisArray);//批量查询
       list.forEach(System.out::println);
   }
}

7.工具类

  • 1.实际工作中会将RestTemplate封装成工具类,方便使用
    package com.redis.util;
    
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.stereotype.Component;
    import org.springframework.util.CollectionUtils;
    
    import javax.annotation.Resource;
    import java.util.Collection;
    import java.util.List;
    import java.util.Map;
    import java.util.Set;
    import java.util.concurrent.TimeUnit;
    
    @Component
    public final class RedisUtils {
    
        @Resource
        private RedisTemplate<String, Object> redisTemplate;
    
    
        public Set<String> keys(String keys){
            try {
                return redisTemplate.keys(keys);
            }catch (Exception e){
                e.printStackTrace();
                return null;
            }
        }
    
        /**
         * 指定缓存失效时间
         * @param key 键
         * @param time 时间(秒)
         * @return true 表示设置成功
         */
        public boolean expire(String key, long time) {
            try {
                if (time > 0) {
                    redisTemplate.expire(key, time, TimeUnit.SECONDS);
                }
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
        /**
         * 根据key 获取过期时间
         * @param key 键 不能为null
         * @return 时间(秒) 返回0代表为永久有效
         */
        public long getExpire(String key) {
            return redisTemplate.getExpire(key, TimeUnit.SECONDS);
        }
        /**
         * 判断key是否存在
         * @param key 键
         * @return true 存在 false不存在
         */
        public boolean hasKey(String key) {
            try {
                return redisTemplate.hasKey(key);
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
        /**
         * 删除缓存
         * @param key 可以传一个值 或多个
         */
        @SuppressWarnings("unchecked")
        public void del(String... key) {
            if (key != null && key.length > 0) {
                if (key.length == 1) {
                    redisTemplate.delete(key[0]);
                } else {
                    redisTemplate.delete((Collection<String>) CollectionUtils.arrayToList(key));
                }
            }
        }
        /**
         * 普通缓存获取
         * @param key 键
         * @return 值
         */
        public Object get(String key) {
            return key == null ? null : redisTemplate.opsForValue().get(key);
        }
        /**
         * 普通缓存放入
         * @param key 键
         * @param value 值
         * @return true成功 false失败
         */
        public boolean set(String key, Object value) {
            try {
                redisTemplate.opsForValue().set(key, value);
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
        /**
         * 普通缓存放入, 不存在放入,存在返回
         * @param key 键
         * @param value 值
         * @return true成功 false失败
         */
        public boolean setnx(String key, Object value) {
            try {
                redisTemplate.opsForValue().setIfAbsent(key,value);
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
        /**
         * 普通缓存放入并设置时间
         * @param key 键
         * @param value 值
         * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
         * @return true成功 false 失败
         */
        public boolean set(String key, Object value, long time) {
            try {
                if (time > 0) {
                    redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
                } else {
                    set(key, value);
                }
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 普通缓存放入并设置时间,不存在放入,存在返回
         * @param key 键
         * @param value 值
         * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
         * @return true成功 false 失败
         */
        public boolean setnx(String key, Object value, long time) {
            try {
                if (time > 0) {
                    redisTemplate.opsForValue().setIfAbsent(key, value, time, TimeUnit.SECONDS);
                } else {
                    set(key, value);
                }
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 递增
         * @param key 键
         * @param delta 要增加几(大于0)
         * @return
         */
        public long incr(String key, long delta) {
            if (delta < 0) {
                throw new RuntimeException("递增因子必须大于0");
            }
            return redisTemplate.opsForValue().increment(key, delta);
        }
        /**
         * 递减
         * @param key 键
         * @param delta 要减少几(小于0)
         * @return
         */
        public long decr(String key, long delta) {
            if (delta < 0) {
                throw new RuntimeException("递减因子必须大于0");
            }
            return redisTemplate.opsForValue().increment(key, -delta);
        }
        /**
         * HashGet
         * @param key 键 不能为null
         * @param item 项 不能为null
         * @return 值
         */
        public Object hget(String key, String item) {
            return redisTemplate.opsForHash().get(key, item);
        }
        /**
         * 获取hashKey对应的所有键值
         * @param key 键
         * @return 对应的多个键值
         */
        public Map<Object, Object> hmget(String key) {
            return redisTemplate.opsForHash().entries(key);
        }
        /**
         * HashSet
         * @param key 键
         * @param map 对应多个键值
         * @return true 成功 false 失败
         */
        public boolean hmset(String key, Map<String, Object> map) {
            try {
                redisTemplate.opsForHash().putAll(key, map);
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
        /**
         * HashSet 并设置时间
         * @param key 键
         * @param map 对应多个键值
         * @param time 时间(秒)
         * @return true成功 false失败
         */
        public boolean hmset(String key, Map<String, Object> map, long time) {
            try {
                redisTemplate.opsForHash().putAll(key, map);
                if (time > 0) {
                  expire(key, time);
                }
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
        /**
         * 向一张hash表中放入数据,如果不存在将创建
         * @param key 键
         * @param item 项
         * @param value 值
         * @return true 成功 false失败
         */
        public boolean hset(String key, String item, Object value) {
            try {
                redisTemplate.opsForHash().put(key, item, value);
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
        /**
         * 向一张hash表中放入数据,如果不存在将创建
         * @param key 键
         * @param item 项
         * @param value 值
         * @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
         * @return true 成功 false失败
         */
        public boolean hset(String key, String item, Object value, long time) {
            try {
                redisTemplate.opsForHash().put(key, item, value);
                if (time > 0) {
                    expire(key, time);
                }
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
        /**
         * 删除hash表中的值
         * @param key 键 不能为null
         * @param item 项 可以使多个 不能为null
         */
        public void hdel(String key, Object... item) {
            redisTemplate.opsForHash().delete(key, item);
        }
        /**
         * 判断hash表中是否有该项的值
         * @param key 键 不能为null
         * @param item 项 不能为null
         * @return true 存在 false不存在
         */
        public boolean hHasKey(String key, String item) {
            return redisTemplate.opsForHash().hasKey(key, item);
        }
        /**
         * hash递增 如果不存在,就会创建一个 并把新增后的值返回
         * @param key 键
         * @param item 项
         * @param by 要增加几(大于0)
         * @return
         */
        public double hincr(String key, String item, double by) {
            return redisTemplate.opsForHash().increment(key, item, by);
        }
        /**
         * hash递减
         * @param key 键
         * @param item 项
         * @param by 要减少记(小于0)
         * @return
         */
        public double hdecr(String key, String item, double by) {
            return redisTemplate.opsForHash().increment(key, item, -by);
        }
        /**
         * 根据key获取Set中的所有值
         * @param key 键
         * @return
         */
        public Set<Object> sGet(String key) {
            try {
                return redisTemplate.opsForSet().members(key);
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
        /**
         * 根据value从一个set中查询,是否存在
         * @param key 键
         * @param value 值
         * @return true 存在 false不存在
         */
        public boolean sHasKey(String key, Object value) {
            try {
                return redisTemplate.opsForSet().isMember(key, value);
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
        /**
         * 将数据放入set缓存
         * @param key 键
         * @param values 值 可以是多个
         * @return 成功个数
         */
        public long sSet(String key, Object... values) {
            try {
                return redisTemplate.opsForSet().add(key, values);
            } catch (Exception e) {
                e.printStackTrace();
                return 0;
            }
        }
        /**
         * 将set数据放入缓存
         * @param key 键
         * @param time 时间(秒)
         * @param values 值 可以是多个
         * @return 成功个数
         */
        public long sSetAndTime(String key, long time, Object... values) {
            try {
                Long count = redisTemplate.opsForSet().add(key, values);
                if (time > 0){
                    expire(key, time);
                }
                return count;
            } catch (Exception e) {
                e.printStackTrace();
                return 0;
            }
        }
        /**
         * 获取set缓存的长度
         * @param key 键
         * @return
         */
        public long sGetSetSize(String key) {
            try {
                return redisTemplate.opsForSet().size(key);
            } catch (Exception e) {
                e.printStackTrace();
                return 0;
            }
        }
        /**
         * 移除值为value的
         * @param key 键
         * @param values 值 可以是多个
         * @return 移除的个数
         */
        public long setRemove(String key, Object... values) {
            try {
                Long count = redisTemplate.opsForSet().remove(key, values);
                return count;
            } catch (Exception e) {
                e.printStackTrace();
                return 0;
            }
        }
        // ===============================list=================================
        /**
         * 获取list缓存的内容
         * @param key 键
         * @param start 开始
         * @param end 结束 0 到 -1代表所有值
         * @return
         */
        public List<Object> lGet(String key, long start, long end) {
            try {
                return redisTemplate.opsForList().range(key, start, end);
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
        /**
         * 获取list缓存的长度
         * @param key 键
         * @return
         */
        public long lGetListSize(String key) {
            try {
                return redisTemplate.opsForList().size(key);
            } catch (Exception e) {
                e.printStackTrace();
                return 0;
            }
        }
        /**
         * 通过索引 获取list中的值
         * @param key 键
         * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
         * @return
         */
        public Object lGetIndex(String key, long index) {
            try {
                return redisTemplate.opsForList().index(key, index);
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
        /**
         * 将list放入缓存
         * @param key 键
         * @param value 值
         * @return
         */
        public boolean lSet(String key, Object value) {
            try {
                redisTemplate.opsForList().rightPush(key, value);
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
        /**
         * 将list放入缓存
         * @param key 键
         * @param value 值
         * @param time 时间(秒)
         * @return
         */
        public boolean lSet(String key, Object value, long time) {
            try {
                redisTemplate.opsForList().rightPush(key, value);
                if (time > 0){
                    expire(key, time);
                }
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
        /**
         * 将list放入缓存
         * @param key 键
         * @param value 值
         * @return
         */
        public boolean lSet(String key, List<Object> value) {
            try {
                redisTemplate.opsForList().rightPushAll(key, value);
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
        /**
         * 将list放入缓存
         *
         * @param key 键
         * @param value 值
         * @param time 时间(秒)
         * @return
         */
        public boolean lSet(String key, List<Object> value, long time) {
            try {
                redisTemplate.opsForList().rightPushAll(key, value);
                if (time > 0){
                    expire(key, time);
                }
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
        /**
         * 根据索引修改list中的某条数据
         * @param key 键
         * @param index 索引
         * @param value 值
         * @return
         */
        public boolean lUpdateIndex(String key, long index, Object value) {
            try {
                redisTemplate.opsForList().set(key, index, value);
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
        /**
         * 移除N个值为value
         * @param key 键
         * @param count 移除多少个
         * @param value 值
         * @return 移除的个数
         */
        public long lRemove(String key, long count, Object value) {
            try {
                Long remove = redisTemplate.opsForList().remove(key, count, value);
                return remove;
            } catch (Exception e) {
                e.printStackTrace();
                return 0;
            }
        }
    }
    

10.持久化

  • 1.Redis 提供了2种不同形式的持久化策略
    • 1.RDB(Redis DataBase):基于内存快照,默认开启
    • 2.AOF(Append Of File):基于操作日志,默认不开启

1.RDB

1.定义
  • 1.快照持久化策略:Redis指定的时间内发生给定的写操作,则将内存中的数据集快照(Snapshot)写入磁盘, 恢复时将磁盘中快照文件直接读到内存
  • 2.快照:把Redis内存中数据某一时刻的状态以文件的形式进行全量备份到磁盘
  • 3.RDB有两种调用方式
    • 1.同步:主动调用save命令即可触发Redis进行RDB文件备份,但是save同步命令,在备份完成之前,Redis服务器不响应客户端任何请求
      127.0.0.1:6379> save
      OK
      
    • 2.异步:主动调用bgsave命令,Redis服务器fork一个子进程异步进行RDB文件备份,与此同时主进程依然可以响应客户端请求
      127.0.0.1:6379> bgsave
      Background saving started
      
2.配置说明
  • 1.实际开发中使用异步RDB生成策略,以上两个命令手动的在客户端发送称为主动触发,通过配置文件设置称为被动触发,以下是被动触发的配置
    • 1.save <seconds> <changes>:配置Redis服务器在什么条件下自动触发 bgsave异步生成RDB备份文件,默认开启
      save 900 1      # 900秒内执行一次set操作 则持久化1次  
      save 300 10     # 300秒内执行10次set操作,则持久化1次
      save 60 10000   # 60秒内执行10000次set操作,则持久化1次
      
    • 2.stop-writes-on-bgsave-error yes(|no):生成RDB备份文件过程中,如果遭遇错误,是否停止Redis写服务,以警示用户RDB备份异常,默认开启
    • 3.rdbcompression yes(|no):生成RDB备份文件过程中,如果遇到字符串对象并且其中的字符串值超过 20 个字节,就会使用LZF算法对字符串进行压缩,默认开启
    • 4.rdbchecksum yes(|no):是否使用CRC64校验算法校验RDB文件是否发生损坏,默认开启
    • 5.dbfilename dump.rdb:指定生成的RDB文件名称,默认配置为dump.rdb
    • 6.dir ./:指定rdb文件存放的目录,默认是当前目录,即启动项目时所处目录
      在这里插入图片描述
      在这里插入图片描述
3.持久化流程

在这里插入图片描述

  • 1.Redis会单独fork一个子进程来进行持久化
  • 2.子进程会先将数据保存到一个临时文件中,确保持久化过程结束,临时文件写入成功后,再用临时文件替换上次持久化好的RDB文件
  • 3.最后退出子进程
4.原理
  • 1.整个持久化过程中,主进程不进行任何磁盘IO操作,确保了极高的性能
  • 2.Fork
    • 1.Redis中调用fork()函数会复制一个和父进程相同的子进程子进程可以共享主进程的所有内存数据
    • 2.fork()函数是阻塞的,当子进程复制完成后,程序的后续代码段会由父子进程并发执行,但系统不保证执行顺序
    • 3.fork()成功之后,子进程调用rdbSave进行RDB文件写入,并产生一个临时文件,确保临时文件写入成功后,再替换旧RDB文件,最后退出子进程
    • 4.fork()并不会带来明显的性能开销,因为不会立刻对内存进行拷贝,而是需要时才拷贝内存
  • 3.因为关系到Redis在快照过程中是否能正常处理写请求,因此采用了写时复制技术
    • 1.Redis写时复制技术:一般情况父进程子进程会共用同一段物理内存,只有内存中的数据内容要发生变化时,才会将父进程的内容复制一份给子进程
    • 2.本质:有写操作的时候复制一份
    • 3.步骤
      • 1.如果主进程是读取内存数据,那么和bgsave子进程并不冲突
      • 2.如果主进程是修改内存数据(图中数据C),操作系统内核会将被修改的内存数据复制一份(复制的是修改之前的数据
      • 3.未被修改的内存数据依然被父子两个进程共享,被主进程修改的内存空间归属于主进程,被复制出来的原始数据归属于子进程
      • 4.如此主进程可以在快照发生的过程中接受数据写入的请求,子进程也能够对某一时刻的内容做快照
      • 5.写时复制是建立在短时间内写请求不多的假设之下,如果写请求的量非常巨大,那么内存复制的压力自然也不会小
        在这里插入图片描述
5.优缺点
1.优点
  • 1.RDB文件是紧凑的二进制数据文件,节省磁盘空间,比较适合做冷备,全量复制(适合大规模的数据恢复)的场景
  • 2.恢复时直接加载到内存中即可,速度快
  • 3.RDBRedis对外提供的读写服务影响非常小,可以让Redis保持高性能
  • 4.RDB可以生成多个文件,每个文件都代表了某一个时刻的Redis完整的数据快照
2.缺点
  • 1.RDB最后一次持久化后的数据可能丢失
  • 2.RDB每次fork子进程生成RDB快照数据文件时,如果数据文件特别大,可能会导致对客户端提供的服务暂停
  • 3.RDB无法实现实时或者秒级持久化RDB是间隔一段时间进行持久化,如果持久化之间Redis发生故障,会发生数据丢失

2.AOF

1.定义
  • 1.以日志的形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来(读操作不记录), 只许追加文件但不可以改写文件
  • 2.Redis启动时会读取该文件重新构建数据,即Redis重启时会根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作
2.配置说明
  • 1.appendonly:默认no,即Redis 默认使用的是rdb方式持久化,如果想要开启 AOF 持久化方式,需要将 appendonly 修改为 yes
  • 2.appendfilenameaof文件名,默认appendonly.aof
  • 3.appendfsyncaof持久化策略,通常选择everysec,兼顾安全性和效率
    • 1.no:表示不主动执行同步(fsync),由操作系统保证数据同步到磁盘,速度最快但不安全,Linux的默认fsync策略是30秒,可能丢失30秒数据
    • 2.always:表示每次写入都fsync,以保证数据都同步到磁盘,安全但效率低
    • 3.everysec:表示每秒执行一次fsync,可能会导致丢失这1s数据,默认选择
  • 4.no-appendfsync-on-rewrite:重写时是否执行同步
    • 1.aof重写时会执行大量IO,此时对于everysecalwaysaof模式来说,执行fsync会造成阻塞过长时间
    • 2.设置为yes表示rewrite期间对新的写操作fsync,暂时保存在内存中,等rewrite完成后再写入,可能会丢失这部分数据
    • 3.默认为no,如果对延迟要求很高的应用,这个字段可以设置为yes,否则设置为no,这样更安全
  • 5.auto-aof-rewrite-percentage:自动重写触发百分比
    • 1.默认值为100aof自动重写配置,当目前aof文件大小超过上一次重写的aof文件大小的百分之多少进行重写,即当aof文件增长到一定大小的时候,Redis能够调用bgrewriteaof对日志文件进行重写
    • 2.当前AOF文件大小是上次日志重写得到AOF文件大小的二倍(设置为100)时,自动启动新的日志重写过程
  • 6.auto-aof-rewrite-min-size:64mb:设置允许重写的最小aof文件大小,避免了达到约定百分比但尺寸仍然很小的情况还要重写,因此重写需要同时满足以上两个条件
  • 7.aof-load-truncated:是否加载残缺aof文件
    • 1.aof文件可能在尾部是不完整的,当redis启动的时候,aof文件的数据被载入内存
    • 2.如果是yes:当截断的aof文件被导入的时候,会自动发布一个日志给客户端然后加载,默认值
    • 3.如果是no:用户必须手动使用redis-check-aof修复AOF文件才可以启动
      /usr/local/bin/redis-check-aof --fix appendonly.aof
      
  • 8.AOF文件的保存路径,同RDB的路径一致
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
3.持久化流程
  • 1.客户端的请求写命令会被append追加到AOF缓冲区内
  • 2.AOF缓冲区根据AOF持久化策略(always,everysec,no)将操作同步到磁盘的AOF`文件中
  • 3.AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量
  • 4.Redis服务重启时,会重新load加载AOF文件中的写操作达到数据恢复的目的
    在这里插入图片描述
4.原理
  • 1.Rewrite定义
    • 1.AOF 文件重写并不是对原文件进行重新整理,而是直接读取服务器现有的键值对,然后用一条命令去代替之前记录这个键值对的多条命令,生成一个新的文件后去替换原来的AOF文件
  • 1.Rewrite重写原理
    • 1.AOF采用文件追加方式,文件会越来越大,为避免出现此种情况,新增了重写(Rewrite)机制
    • 2.当AOF文件的大小超过所设定的阈值时,Redis就会启动AOF文件的内容压缩, 只保留可以恢复数据的最小指令集
    • 3.AOF文件持续增长而过大时,会fork出一条新进程来将文件重写(也是先写入临时文件最后再rename),redis4.0版本后的重写是把rdb的快照以二级制的形式附在新的aof头部,作为已有的历史数据,替换掉原来的流水账操作
  • 2.Rewrite触发机制
    • 1.重写虽然节约大量磁盘空间,减少恢复时间,但是每次重写也有一定的负担,因此设定Redis要满足一定条件才会进行重写
    • 2.Redis会记录上次重写时的AOF大小,默认配置是当AOF文件大小是上次rewrite后大小的一倍且文件大于64M时触发
      • 1.auto-aof-rewrite-percentage:设置重写的基准值,文件达到100%时开始重写(文件是原来重写后文件的2倍时触发)
      • 2.auto-aof-rewrite-min-size:设置重写的基准值,最小文件64MB,达到这个值开始重写。
      • 3.系统载入时或者上次重写完毕时,Redis会记录此时AOF大小,设为base_size
      • 4.如果RedisAOF当前大小>= base_size +base_size*100% (默认)且当前大小>=64mb(默认)的情况下,Redis会对AOF进行重写
  • 3.Rewrite流程
    • 1.bgrewriteaof触发重写,判断是否当前有bgsavebgrewriteaof在运行,如果有则等待该命令结束后再继续执行
    • 2.主进程fork出子进程执行重写操作,保证主进程不会阻塞
    • 3.子进程遍历redis内存中数据到临时文件,客户端的写请求同时写入aof_buf缓冲区和aof_rewrite_buf重写缓冲区保证原AOF文件完整以及新AOF文件生成期间的新的数据修改动作不会丢失
    • 4.子进程写完新的AOF文件后,向主进程发信号,父进程更新统计信息
    • 5.主进程把aof_rewrite_buf中的数据写入到新的AOF文件
    • 6.使用新的AOF文件覆盖旧的AOF文件,完成AOF重写
      在这里插入图片描述
  • 4.注意
    • 1.子进程进行AOF重写期间,服务器进程依然在处理其它命令,这新的命令有可能也对数据库进行了修改操作,使得当前数据库状态和重写后的 AOF 文件状态不一致
    • 2.为了解决这个数据状态不一致的问题,Redis 服务器设置了一个 AOF 重写缓冲区,这个缓冲区是在创建子进程后开始使用,当Redis服务器执行一个写命令之后,就会将这个写命令也发送到 AOF 重写缓冲区
    • 3.当子进程完成 AOF 重写之后,就会给父进程发送一个信号,父进程接收此信号后,就会调用函数将 AOF 重写缓冲区的内容都写到新的 AOF 文件中,这样将 AOF 重写对服务器造成的影响降到了最低
5.优缺点
1.优点
  • 1.AOF持久化方法提供了多种的同步频率,即使使用默认的同步频率每秒同步一次,Redis 最多丢失 1 秒的数据
  • 2.AOF文件使用Redis写命令追加的形式构造,可读性强,使用 redis-check-aof工具易修复
2.缺点
  • 1.对于具有相同数据的的RedisAOF文件通常会比 RDB 文件体积更大
  • 2.虽然AOF提供了多种同步的频率,但在Redis的负载较高时,RDBAOF具好更好的性能保证
  • 3.RDB使用快照的形式来持久化整个Redis数据,而 AOF只是将每次执行的命令追加到 AOF文件中,因此从理论上RDBAOF 方式更健壮,官方文档也指出AOF 的确也存在一些RDB没有BUG

3.RDB和AOF对比总结

  • 1.AOFRDB同时开启,系统默认取AOF的数据(数据不会存在丢失)
  • 2.官方推荐两个都启用
    • 1.如果对数据不敏感,可以选单独用RDB
    • 2.不建议单独用 AOF,因为可能会出现Bug
    • 3.如果只是做纯内存缓存,可以都不用

11.事务

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

1.命令

  • 1.Redis通过multi,exec,discard,watch等命令实现事务功能
  • 2.Redis事务从开始到执行会经历开始事务命令入队执行事务三个阶段
    • 1.开始事务:通过multi命令开始事务,等待命令入队
    • 2.命令入队:开始事务后,输入的命令并不会立即执行,而是加入队列并返回一个QUEUED(命令队列)
    • 3.执行事务:通过exec命令执行事务,命令队列中的命令才会依次执行
  • 3.放弃组队:组队的过程中可以通过discard命令来放弃组队
    在这里插入图片描述
    # 1.开启事务
    127.0.0.1:6379> multi
    OK
    127.0.0.1:6379> set name lisi
    QUEUED
    127.0.0.1:6379> set age 18
    QUEUED
    127.0.0.1:6379> set sex 男
    QUEUED
    # 2.执行事务
    127.0.0.1:6379> exec
    1) OK
    2) OK
    3) OK
    127.0.0.1:6379> keys *
    1) "age"
    2) "name"
    3) "sex"
    
  • 4.如果在组队阶段产生错误,则会导致整个命令队列的所有命令都执行失败
    127.0.0.1:6379> multi
    OK
    127.0.0.1:6379> set name 王五
    QUEUED
    127.0.0.1:6379> settt sex 男
    (error) ERR unknown command 'settt'
    127.0.0.1:6379> set age 18
    QUEUED
    127.0.0.1:6379> exec
    (error) EXECABORT Transaction discarded because of previous errors.
    
  • 5.如果在执行阶段产生错误,则命令队列的其他命令正常执行,不会回滚,产生错误的命令执行失败
    127.0.0.1:6379> multi
    OK
    127.0.0.1:6379> set name lisi
    QUEUED
    127.0.0.1:6379> set age 18 18
    QUEUED
    127.0.0.1:6379> set sex 男
    QUEUED
    127.0.0.1:6379> exec
    1) OK
    2) (error) ERR syntax error
    3) OK
    127.0.0.1:6379>
    
  • 6.组队阶段,放弃组队
    127.0.0.1:6379> multi
    OK
    127.0.0.1:6379> set name lisi
    QUEUED
    127.0.0.1:6379> set age 18
    QUEUED
    127.0.0.1:6379> discard
    OK
    127.0.0.1:6379> exec
    (error) ERR EXEC without MULTI
    

2.乐观锁

  • 1.乐观锁与悲观锁
    • 1.悲观锁(Pessimistic Lock):悲观的认为每次写数据时都会被其他线程修改,所以每次写数据时都会上锁,只有当前线程处理完才会解锁,其他线程才会有机会获取锁
    • 2.乐观锁(Optimistic Lock):乐观的认为每次写数据时其他线程都不会修改,所以每次写数据都不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,如果没有更新才会可以写入数据,使用版本号等机制可实现乐观锁
  • 2.watch命令使用的是乐观锁原理,在执行multi命令前,用于监控任意数量的键是否发生改变
  • 3.当执行exec命令时,将检查监视的键是否已经被修改,如果已经修改,服务器将拒绝执行事务,如果没修改,可以正常执行事务
  • 4.unwatch:取消watch命令对所有key的监视,如果在执行 watch命令之后,execdiscard命令先被执行,就不需要再执行unwatch
    # 单客户端修改
    127.0.0.1:6379> set age 20
    OK
    127.0.0.1:6379> set sex 男
    OK
    127.0.0.1:6379> watch age sex
    OK
    127.0.0.1:6379> multi
    OK
    127.0.0.1:6379> set age 18
    QUEUED
    127.0.0.1:6379> set name 丽水
    QUEUED
    127.0.0.1:6379> exec
    1) OK
    2) OK
    
    # 多客户端修改
    # 客户端1
    127.0.0.1:6379> set age 18
    QUEUED
    127.0.0.1:6379> set name 丽水
    QUEUED
    127.0.0.1:6379> exec
    1) OK
    2) OK
    127.0.0.1:6379> watch age sex
    OK
    127.0.0.1:6379> multi
    OK
    127.0.0.1:6379> set name 张三
    QUEUED
    127.0.0.1:6379> exec
    (nil)
    
    # 客户端2(客户端1执行事务的中间修改其监测的值)
    [root@localhost ~]# redis-cli
    127.0.0.1:6379> set age 20
    OK
    127.0.0.1:6379>
    

3.三大特性

  • 1.单独的隔离操作
    • 1.事务中的所有命令都会序列化、按顺序地执行,事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
  • 2.没有隔离级别的概念
    • 1.队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行
  • 3.不保证原子性
    • 1.事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚
  • 注意
    • 1.Redis事务并不能保证(ACID)
    • 2.Redis没有实现标准事务的原因
      • 1.目的性不同,Redis主要是为了解决性能问题,而标准事务会引入额外的复杂性,降低Redis的性能
      • 2.Redis适用的功能场景简单,使用Redis事务足以完成任务
    • 3.Redis不支持回滚事务
      • 1.因为多数事务失败是由语法错误或者数据结构类型错误导致的
      • 2.语法错误是在命令入队前进行检测,而类型错误是在执行时进行检测,Redis为提升性能而采用这种简单的事务,不同于关系型数据库

4.实现原理

  • Redis通过一个事务队列完成事务功能
    • 1.当一个客户端发送multi命令后,Redis服务器会将该客户端后续的命令保存到一个队列中
    • 2.当一个处于事务状态的客户端向Redis服务器发送exec命令时,服务器会遍历这个客户端的事务队列,执行队列中保存的所有命令,最后将执行命令的结果全部返回到客户端
    • 3.Redis在执行事务队列的命令前,如果发现入队的命令有语法错误或监控的值发生改变,将清空队列中的命令,拒绝执行
      在这里插入图片描述

5.应用场景

1.秒杀案例

13.主从复制

14.集群

15.分布式锁

场景应用

1.手机验证码

  • 1.需求
    • 1.输入手机号,点击发送后随机生成6位数字码,1分钟有效
    • 2.输入验证码,点击验证,返回成功或失败
    • 3.每个手机号每天只能输入3次
  • 2.实现
    • 1.使用Random生成随机6位数字验证码
    • 2.验证码保存到redis中,设置过期时间为60秒
    • 3.从redis获取验证码和输入验证码进行比较,判断验证码是否一致
    • 4.每次发送之后incr 1,大于3的时候,提交不能发送,一天后失效
    /**
      * 1.使用Random生成随机6位数字验证码
      * @return
      */
     public static String testVerifyCode(){
         Random random = new Random();
         StringBuilder str = new StringBuilder();
         for (int i=0; i<6; i++) {
             int num = random.nextInt(10);
             str.append(num);
         }
         System.out.println(str.toString());
         return str.toString();
     }
    
     /**
      * 2.验证码存入redis中,存储60秒,并限制每天只能发送3次
      * @param phone
      */
     public static void testSendRedis(String phone){
         //随机验证码
         String verifyCode = testVerifyCode();
    
         /** redis 键 值
          * verifyCode手机号:code  验证码
          * verifyCode手机号:count 今日该手机发送验证码次数
          */
         String codeKey = "verifyCode" + phone + ":code";
         String countKey = "verifyCode" + phone + ":count";
    
         //工具类获取连接
         Jedis jedis = JedisUtil.getJedis();
         String countValue = jedis.get(countKey);
    
         //每个手机只能发送三次
         if(countValue == null){
             //没有发送次数,表明第一次发出验证码
             jedis.setex(countKey,24*60*60,"1");
         }else if (Integer.parseInt(countValue) < 3) {
             //发送次数+1
             jedis.incr(countKey);
         }else {
             //发送三次,不能再发送
             System.out.println("今日验证码次数达到上限3次,不可以再发送验证码!");
             JedisUtil.close(jedis);
             return;
         }
    
         //发送验证码到redis中,并设置1分钟过期
         jedis.setex(codeKey,60,verifyCode);
         JedisUtil.close(jedis);
     }
    
     /**
      * 3.验证码效验
      * @param phone 用户手机号
      * @param userCode 用户输入的验证码
      */
     public static void compareVerifyCode(String phone, String userCode){
         String codeKey = "verifyCode" + phone + ":code";
         Jedis jedis = JedisUtil.getJedis();
         String realCode = jedis.get(codeKey);
         if(userCode.equals(realCode)){
             System.out.println("验证通过!");
         }else {
             System.out.println("验证失败!");
         }
         JedisUtil.close(jedis);
     }
    
     @Test
     //测试验证码是否保存到redis,并且只能发送三次(问题:一天能发送三次,而不是当天能发送三次,时间是从发送开始算起)
     public void testSendVerify(){
         testSendRedis("15340506070");
     }
    
     @Test
     //测试输入的验证码是否正确
     public void testCompareVerify(){
         compareVerifyCode("15340506070","043895");
     }
    
    127.0.0.1:6379> get verifyCode15340506070:count
    "1"
    127.0.0.1:6379> get verifyCode15340506070:code
    "524139"
    127.0.0.1:6379> ttl verifyCode15340506070:count
    (integer) 86333
    127.0.0.1:6379> ttl verifyCode15340506070:code
    (integer) 39
    

2.redis统一管理session

  • 通过nginx分发请求如果发送到不同tomcat上可能会导致session不可共用
  • 解决方法:
    • 1.通过ip_hash 将同一个ip发送的请求固定到同一个服务器上,缺点是可能会负载不均匀
    • 2.通过redis统一管理session
      请添加图片描述

3.秒杀案例

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值