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-value
的NoSQL
数据库- 2.
Redis
采用单线程+IO
多路复用技术,其端口号为6379
- 3.
多路复用
:指使用一个线程来检查多个文件描述符(Socket
)的就绪状态,如果有一个文件描述符就绪,则返回,否则阻塞直到超时;得到就绪状态
后进行真正的操作可以在同一个线程里执行,也可以启动线程执行- 4.
Redis
是一个开源的内存数据结构存储,用作数据库,缓存和消息代理Pub/Sub
- 1.数据结构上:支持 字符串,散列,列表,集合,带有范围查询的排序集,位图,超级日志,具有半径查询和流的地理空间索引等数据结构
- 2.功能上:
Redis
具有内置复制,Lua
脚本Lua scripting
,LRU
驱逐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-sentinel
:Redis
集群使用- 5.
redis-server
:Redis
服务器启动命令- 6.
redis-cli
:Redis
客户端操作入口
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.
String
是Redis
最基本的类型,一个key
对应一个value
- 2.
String
是二进制安全
的,其key
是string
类型,其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 String
,SDS
)- 2.
SDS
:指可修改的字符串,结构类似于Java
的ArrayList
- 3.其采用
预分配冗余空间
的方式减少内存的频繁分配- 4.内部当前字符实际分配的空间
capacity
一般高于实际字符串长度- 5.当字符串长度小于
1M
时,扩容时加倍
现有的空间;如果超过1M
,扩容时一次只会多扩容1M
的空间- 6.注意:字符串最大长度为
512M
6.应用场景(需完善)
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
相关的消息推送至Redis
,Redis
再根据binlog
中的记录,对Redis
进行更新- 5.可以结合使用
canal
(阿里的一款开源框架),通过该框架可以对MySQL
的binlog
进行订阅,而canal
正是模仿了mysql
的slave
数据库的备份请求,使得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.单键多值,其
key
是string
类型,其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
类型的lpop
和rpush
(或lpush
和rpop
)能实现点对点队列
- 2.
List
类型的lpush
和brpop
可实现阻塞队列
- 3.不推荐项目中使用,因为已有
Kafka
、RabbitMQ
等成熟的消息队列,没必要去重复造轮子
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.单键多值,其
key
是string
类型,其value
是string
类型的无序集合
,一个集合最多可以存储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.类似
Java
中HashSet
,内部使用的是hash
结构,所有的value
都指向同一内部值- 3.
Set
类型底层使用了intset
和hashtable
两种数据结构存储
- 1.
intset
类似数组- 2.
hashtable
是哈希表- 4.
Set
的底层存储intset
和hashtable
存在编码转换,使用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
是一个键值对集合,其key
是string
类型,其value
是一个string
类型的filed
和value
的哈希表,类似Java
中的Map<String,String>
- 2.
Hash
中value
中最多可以存储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.
用户id
为key
,商品id
为field
,商品数量为value
,恰好构成了购物车的3个要素
2.存储对象
- 1.
Hash
类型结构(key
,field
-value
)与对象结构(对象id
,属性
-值
)相似,因此可用来存储对象- 2.
String
类型的key=string + value=json
也是存储对象的一种方式,存储对象选择方案对比
string + json hash 效率 很高 高 容量 低 低 灵活性 低 高 序列化 简单 复杂 - 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
是一个有序集合
,其key
是string
类型,其value
是string
类型,且集合元素不重复
- 2.
ZSet
和Set
区别: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.
hash
:hash
的作用是通过filed-value
关联元素value
和分数score
,保证元素value
的唯一性,且可以通过value
找到对应的score
- 2.
跳跃表
:跳跃表的作用在于给元素value
排序,快速查找到指定元素- 2.
跳跃表(跳表)
7.应用场景
1.排行榜
- 1.当前小时的时间戳作为
ZSet
的key
,把贴子ID
作为member
,点击数评论数等作为score
,当score
发生变化时更新score
- 2.利用
ZREVRANGE
或者ZRANGE
查到对应数量的记录
6.普通数据类型
1.Bitmaps
- 1.现代计算机用
二进制(位)
作为信息的基础单位,1个字节
等于8位
- 2.例:
abc
字符串是由3个字节
组成, 但实际在计算机存储时将其用二进制表示- 3.
abc
分别对应的ASCII
码分别是97
、98
、99
, 对应的二进制
分别是01100001
、01100010
、01100011
- 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.项目中可采用
Kafka
、RabbitMQ
、RocketMQ
、ActiveMQ
等成熟的消息组件,也可采用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-cli
是Redis
官方提供的客户端,可以在Linux
上发送命令对Redis
进行操作- 2.
Jedis
是Redis
官方推出的面向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.
Client
:Redis
客户端,集群使用专用的RedisClusterClient
- 3.
Connection
:Redis
连接,有多种类型(单机,哨兵,集群,订阅发布)- 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
的操作主要有两种方式:Jedis
和Lettuce
- 1.
Jedis
是Redis
官方推荐的面向Java
操作Redis
的客户端- 2.
Lettuce
是SpringBoot(2.x.x版本)
默认使用的面向Java
操作Redis
的客户端- 2.
Spring
的spring-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.
RedisTemplate
对Redis
原生的api
进行了封装,还提供了连接池自动管理,异常处理及序列化等操作,简化Java
操作Redis
的编码工作- 6.其中除了
RedisTemplate
还包括StringRedisTemplate
,RedisTemplate
可以支持Redis
没有的缓存对象的操作,而StringRedisTemplate
用来存储字符串- 7.
SpringBoot
使用Jedis
和Lettuce
的配置操作不同,具体可参考: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
,所以该配置类的序列化方式是针对Lettuce
,Jedis
可参考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.
json
:java对象
在计算机中以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.
RDB
对Redis
对外提供的读写服务影响非常小,可以让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.
appendfilename
:aof
文件名,默认appendonly.aof
- 3.
appendfsync
:aof
持久化策略,通常选择everysec
,兼顾安全性和效率
- 1.
no
:表示不主动执行同步(fsync
),由操作系统保证数据同步到磁盘,速度最快但不安全,Linux
的默认fsync
策略是30
秒,可能丢失30
秒数据- 2.
always
:表示每次写入都fsync
,以保证数据都同步到磁盘,安全但效率低- 3.
everysec
:表示每秒执行一次fsync
,可能会导致丢失这1s
数据,默认选择- 4.
no-appendfsync-on-rewrite
:重写时是否执行同步
- 1.
aof
重写时会执行大量IO
,此时对于everysec
和always
的aof
模式来说,执行fsync
会造成阻塞过长时间- 2.设置为
yes
表示rewrite
期间对新的写操作
不fsync
,暂时保存在内存中,等rewrite
完成后再写入,可能会丢失这部分数据- 3.默认为
no
,如果对延迟要求很高的应用,这个字段可以设置为yes
,否则设置为no
,这样更安全- 5.
auto-aof-rewrite-percentage
:自动重写触发百分比
- 1.默认值为
100
,aof
自动重写配置,当目前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.如果
Redis
的AOF
当前大小>=base_size
+base_size*100%
(默认)且当前大小>=64mb
(默认)的情况下,Redis
会对AOF
进行重写- 3.
Rewrite
流程
- 1.
bgrewriteaof
触发重写,判断是否当前有bgsave
或bgrewriteaof
在运行,如果有则等待该命令结束后再继续执行- 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.对于具有相同数据的的
Redis
,AOF
文件通常会比RDB
文件体积更大- 2.虽然
AOF
提供了多种同步的频率,但在Redis
的负载较高时,RDB
比AOF
具好更好的性能保证- 3.
RDB
使用快照
的形式来持久化整个Redis
数据,而AOF
只是将每次执行的命令追加到AOF
文件中,因此从理论上RDB
比AOF
方式更健壮,官方文档也指出AOF
的确也存在一些RDB
没有BUG
3.RDB和AOF对比总结
- 1.
AOF
和RDB
同时开启,系统默认取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
命令之后,exec
或discard
命令先被执行,就不需要再执行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.秒杀案例