# Redis笔记 **Repository Path**: azin-cn/redis-notes ## Basic Information - **Project Name**: Redis笔记 - **Description**: Redis笔记 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2022-03-06 - **Last Updated**: 2022-03-06 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README > 互联网技术中,终极解决方案:加一层 # 一. Java开发中技术分析 img 从开发角度的技术选型来说,只需要功能性的技术即可完成业务的开发,但是仅仅只有功能性的技术不足以满足快速开发或者可维护的需求,所以需要引入扩展性的技术和解决性能类型的技术。 # 二. 为什么添加NOSQL数据库 img ### (1). 负载均衡时,session对象的共享问题 做负载均衡时,如果session不能共享,那么会导致在一台服务器上有数据,另外一台服务器上没有数据,显然这是不允许的。 ### (2). session共享引发的不安全、大量IO操作问题 如果直接在每台服务器上做session复制,每一台都储存session对象会造成空间的大量浪费,如果储存到数据库中判断是否已经登录(是否有session),那么又会造成大量的IO操作,硬盘的速度限制了访问的速度。 img # 三. Redis数据库 - 内存中的数据库(高读写,低延迟,做缓存) ## 1. Redis的简介 > Redis是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。Redis就像是一个HashMap,不过不是在JVM中运行,而是以一个独立进程的形式运行。 img ## 2. Redis的安装 参考链接🔗 [Redis安装](https://www.cnblogs.com/ifme/p/13903017.html) ## 3. Redis的结构 img ## 4. Redis的启动 > 端口:6379 ### 1). Redis的前台启动 ### 2). Redis的后台启动 `redis-server` img ## 5. Redis的技术策略 IMG-20220113161601 单线程+多路IO复用的效率更好,类似线程中分出的协程思想,复用线程。 - 单线程IO复用的举例IMG-20220113192158 这里省下的就是线程切换时的成本,如果时多线程,即使线程的切换成本比进程低了很多,但线程的切换消耗在今天高并发的角度来看,还是很高的。 线程的切换成本简单来说是:切换线程时,CPU需要保存一个线程状态和加载另外一个线程花费的时间和耗费的能量(上下文切换)。 以前只需要考虑线程比进程轻量,切换线程即可,但是对于高并发的今天,线程也显得是重量级的过程,所以也出现了更轻量的协程(用户态的线程)。 ## 6. Redis的使用 > Redis是单线程操作,所以不需要考虑多线程的数据锁。 > > 给定的指令不一样,Redis形成的数据结构就不一样,不需要显式的声明数据类型 > > (set,lpush,sadd) ### 1. 五大基本数据类型 - String(字符串)string 类型是 Redis 最基本的数据类型,可以包含任何数据,最大能存储 512MB - Hash(哈希)Redis hash 是一个键值对集合,string类型的key,hash 特别适合用于存储对象 - List(列表)Redis 列表是简单的字符串列表,按照插入顺序排序,按照左右进行区分 - Set(集合)Redis 的 Set 是 string 类型的无序集合,通过哈希实现,添加,删除,查找的复杂度都是 O(1) - Zset(sorted set:有序集合) Redis zset 和 set 一样也是string类型元素的集合,且不允许重复的成员,有序树 ### 2. 常用的命令 不需要记住所有的命令,但是最起码得了解数据类型有什么方法,适合做什么。 常用命令查找:链接🔗 [Redis命令中心(Redis commands) -- Redis中国用户组(CRUG)](http://redis.cn/commands.html) 避免中文乱码:`redis-cli --raw` #### (1)String字符串数据常用命令 String在Redis中采用的是动态字符串,相当于Java中的ArrayList结构,有容量和实际大小。当数据大小小于1M时,每次双倍扩容,大于1M时,每次扩容只扩大1M,最大只能到512M。 IMG-20220113221206 | 命令(忽略大小写)增删改查 | 描述 | | ---------------------------------- | --------------------------------------------------------- | | `SET` key value | 设置key=value | | `SETRANGE` key start value | 从指定位置开始,插入value | | `SETNX` key value | 只有在 key 不存在时设置 key 的值 | | `MSET` k1 v1 k2 v2 ... | 设置多个键和多个值 | | `MSETNX` k1 v1 k2 v2 ... | 只要有一个失败,那么所有的都失败 | | `SETEX` key seconds value | 指定的 key 设置值及其过期时间,存在则覆盖 | | | | | `DEL` key | 如果存在则删除键,同步删除,删除后再响应 | | `UNLINK` key | 如果存在则删除键,异步删除,直接响应删除,后续再删 | | `FLUSHDB` / `FLUSHALL` | 清空当前库/清空所有库 | | | | | **一些修改方法五大数据通用** | | | `APPEND` key value | 如果 key 已经存在并且是一个字符串,拼接上原字符串 | | `INCR` key | 将 key 中储存的数字类型的value增加1 | | `INCRBY` key increment | 将 key 中储存的数字类型的value增加给定大小 | | `INCRBYFLOAT` key increment | 将 key 中储存的数字类型的value增加给定大小浮点数值 | | `DECR` key | 将 key 中储存的数字类型的value减去1 | | `DECRBY` key decrement | 将 key 中储存的数字类型的value减去给定大小 | | `EXPIRE` key seconds | 指定键的过期时间,秒为单位 | | `EXPIREAT` key timestamp | 指定的键过期时间expire at。在这里,时间是在Unix时间戳格式 | | `PEXPIRE` key milliseconds | 指定键的过期时间,毫秒为单位 | | `TTL` key | 查询key还剩多少时间过期,-1永不过期,-2已过期 | | | | | **一些查询方法为无大数据通用方法** | | | `GET` key | 获得键key对应的值 | | `MGET` k1 k2 ... | 得到所有的给定键的值 | | `GETRANGE` key start end | 得到value为字符串的子字符串,字符串索引从0开始,左闭右闭 | | `GETSET` key value | 返回原来的值后,设置新的值 | | `STRLEN` key | 返回给定的key储存的字符串类型value的长度 | | `EXISTS` key | 判断给定的key是否存在 | | `TYPE` key | 返回键对应的value数据类型,五大数据类型 | | `KEYS` * | 查询所有存在的key | #### (2)List列表常用命令 双向链表实现实现的栈,头插法和尾插法。命令方法中分出了左和右 `lpush,rpush,lpop,rpop`,注意实现的是双向的栈,左边插入和右边插入不一样,`lpop`和`rpop`的顺序不一样 IMG-20220113221539 Redis中,`list`使用的是`quicklist`,即单个块使用的是`压缩链表ziplist(连续的块)`,数据量变大时,多个`ziplist`前后相连形成了`quicklist`。这样避免了每一个节点都储存下一节点的指针冗余。 IMG-20220114112220 | 序号 | 命令及描述 | | :--- | :----------------------------------------------------------- | | 1 | [BLPOP key1 [key2 ] timeout](https://www.runoob.com/redis/lists-blpop.html) 移出并获取列表的第一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。 | | 2 | [BRPOP key1 [key2 ] timeout](https://www.runoob.com/redis/lists-brpop.html) 移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。 | | 3 | [BRPOPLPUSH source destination timeout](https://www.runoob.com/redis/lists-brpoplpush.html) 从列表中弹出一个值,将弹出的元素插入到另外一个列表中并返回它; 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。 | | 4 | [LINDEX key index](https://www.runoob.com/redis/lists-lindex.html) 通过索引获取列表中的元素 | | 5 | [LINSERT key BEFORE\|AFTER pivot value](https://www.runoob.com/redis/lists-linsert.html) 在列表的元素前或者后插入元素 | | 6 | [LLEN key](https://www.runoob.com/redis/lists-llen.html) 获取列表长度 | | 7 | [LPOP key](https://www.runoob.com/redis/lists-lpop.html) 移出并获取列表的第一个元素 | | 8 | [LPUSH key value1 [value2]](https://www.runoob.com/redis/lists-lpush.html) 将一个或多个值插入到列表头部 | | 9 | [LPUSHX key value](https://www.runoob.com/redis/lists-lpushx.html) 将一个值插入到已存在的列表头部 | | 10 | [LRANGE key start stop](https://www.runoob.com/redis/lists-lrange.html) 获取列表指定范围内的元素,0,-1表示所有的值 | | 11 | [LREM key count value](https://www.runoob.com/redis/lists-lrem.html) 移除列表元素,从索引0开始删除 | | 12 | [LSET key index value](https://www.runoob.com/redis/lists-lset.html) 通过索引设置列表元素的值,修改原有的value | | 13 | [LTRIM key start stop](https://www.runoob.com/redis/lists-ltrim.html) 对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。 | | 14 | [RPOP key](https://www.runoob.com/redis/lists-rpop.html) 移除列表的最后一个元素,返回值为移除的元素。 | | 15 | [RPOPLPUSH source destination](https://www.runoob.com/redis/lists-rpoplpush.html) 移除列表的最后一个元素,并将该元素添加到另一个列表并返回 | | 16 | [RPUSH key value1 [value2]](https://www.runoob.com/redis/lists-rpush.html) 在列表中添加一个或多个值 | | 17 | [RPUSHX key value](https://www.runoob.com/redis/lists-rpushx.html) 为已存在的列表添加值 | #### (3)Hash哈希字典常用命令 底层就是一个哈希表,快速定位到需要查找的元素。 > 常用的方式是: > > key存给定的索引(地址),value存对象的(地址),对象也是一个Map结构,也有对象对应的字段 \ 属性的key和value,这样索引比较快。 索引(哪个对象) + 字段名(对象的哪个字段 \ 属性) | 序号 | 命令及描述 | | :--- | :----------------------------------------------------------- | | 1 | [ HDEL key field1 [field2]](https://www.runoob.com/redis/hashes-hdel.html) 删除一个或多个哈希表字段 | | 2 | [HEXISTS key field](https://www.runoob.com/redis/hashes-hexists.html) 查看哈希表 key 中,指定的字段是否存在。 | | 3 | [HGET key field](https://www.runoob.com/redis/hashes-hget.html) 获取存储在哈希表中指定字段的值。 | | 4 | [HGETALL key](https://www.runoob.com/redis/hashes-hgetall.html) 获取在哈希表中指定 key 的所有字段和值 | | 5 | [HINCRBY key field increment](https://www.runoob.com/redis/hashes-hincrby.html) 为哈希表 key 中的指定字段的整数值加上增量 increment 。 | | 6 | [HINCRBYFLOAT key field increment](https://www.runoob.com/redis/hashes-hincrbyfloat.html) 为哈希表 key 中的指定字段的浮点数值加上增量 increment 。 | | 7 | [HKEYS key](https://www.runoob.com/redis/hashes-hkeys.html) 获取所有哈希表中的字段 | | 8 | [HLEN key](https://www.runoob.com/redis/hashes-hlen.html) 获取哈希表中字段的数量 | | 9 | [HMGET key field1 [field2]](https://www.runoob.com/redis/hashes-hmget.html) 获取所有给定字段的值 | | 10 | [HMSET key field1 value1 [field2 value2 ]](https://www.runoob.com/redis/hashes-hmset.html) 同时将多个 field-value (域-值)对设置到哈希表 key 中。 | | 11 | [HSET key field value](https://www.runoob.com/redis/hashes-hset.html) 将哈希表 key 中的字段 field 的值设为 value 。
格式:hset user:100 id 1 name lisi age 20 语意:存储了一个对象为100,有属性id,name,age
hset 和 hmset基本一样 | | 12 | [HSETNX key field value](https://www.runoob.com/redis/hashes-hsetnx.html) 只有在字段 field 不存在时,设置哈希表字段的值。 | | 13 | [HVALS key](https://www.runoob.com/redis/hashes-hvals.html) 获取哈希表中所有值。 | | 14 | [HSCAN key cursor [MATCH pattern\] [COUNT count]](https://www.runoob.com/redis/hashes-hscan.html) 迭代哈希表中的键值对。 | #### (4) Redis 集合(Set) | 序号 | 命令及描述 | | :--- | :----------------------------------------------------------- | | 1 | [SADD key member1 [member2]](https://www.runoob.com/redis/sets-sadd.html) 向集合添加一个或多个成员 | | 2 | [SCARD key](https://www.runoob.com/redis/sets-scard.html) 获取集合的成员数 | | 3 | [SDIFF key1 [key2]](https://www.runoob.com/redis/sets-sdiff.html) 返回第一个集合与其他集合之间的差异。 | | 4 | [SDIFFSTORE destination key1 [key2]](https://www.runoob.com/redis/sets-sdiffstore.html) 返回给定所有集合的差集并存储在 destination 中 | | 5 | [SINTER key1 [key2]](https://www.runoob.com/redis/sets-sinter.html) 返回给定所有集合的交集 | | 6 | [SINTERSTORE destination key1 [key2]](https://www.runoob.com/redis/sets-sinterstore.html) 返回给定所有集合的交集并存储在 destination 中 | | 7 | [SISMEMBER key member](https://www.runoob.com/redis/sets-sismember.html) 判断 member 元素是否是集合 key 的成员 | | 8 | [SMEMBERS key](https://www.runoob.com/redis/sets-smembers.html) 返回集合中的所有成员 | | 9 | [SMOVE source destination member](https://www.runoob.com/redis/sets-smove.html) 将 member 元素从 source 集合移动到 destination 集合 | | 10 | [SPOP key](https://www.runoob.com/redis/sets-spop.html) 移除并返回集合中的一个随机元素,随机的一个元素,并删除 | | 11 | [SRANDMEMBER key [count]](https://www.runoob.com/redis/sets-srandmember.html) 返回集合中一个或多个随机数,返回随机的一个元素,但不删除 | | 12 | [SREM key member1 [member2]](https://www.runoob.com/redis/sets-srem.html) 移除集合中一个或多个成员 | | 13 | [SUNION key1 [key2]](https://www.runoob.com/redis/sets-sunion.html) 返回所有给定集合的并集,与差集等类似 | | 14 | [SUNIONSTORE destination key1 [key2]](https://www.runoob.com/redis/sets-sunionstore.html) 所有给定集合的并集存储在 destination 集合中 | | 15 | [SSCAN key cursor [MATCH pattern\] [COUNT count]](https://www.runoob.com/redis/sets-sscan.html) 迭代集合中的元素 | #### (5)Redis 有序集合(sorted set)常用命令 了解: IMG-20220114115343 | 序号 | 命令及描述 | | :--- | :----------------------------------------------------------- | | 1 | [ZADD key score1 member1 [score2 member2]](https://www.runoob.com/redis/sorted-sets-zadd.html) 向有序集合添加一个或多个成员,或者更新已存在成员的分数 | | 2 | [ZCARD key](https://www.runoob.com/redis/sorted-sets-zcard.html) 获取有序集合的成员数 | | 3 | [ZCOUNT key min max](https://www.runoob.com/redis/sorted-sets-zcount.html) 计算在有序集合中指定区间分数的成员数 | | 4 | [ZINCRBY key increment member](https://www.runoob.com/redis/sorted-sets-zincrby.html) 有序集合中对指定成员的分数加上增量 increment | | 5 | [ZINTERSTORE destination numkeys key [key ...]](https://www.runoob.com/redis/sorted-sets-zinterstore.html) 计算给定的一个或多个有序集的交集并将结果集存储在新的有序集合 destination 中 | | 6 | [ZLEXCOUNT key min max](https://www.runoob.com/redis/sorted-sets-zlexcount.html) 在有序集合中计算指定字典区间内成员数量 | | 7 | [ZRANGE key start stop [WITHSCORES]](https://www.runoob.com/redis/sorted-sets-zrange.html) 通过索引区间返回有序集合指定区间内的成员 | | 8 | [ZRANGEBYLEX key min max [LIMIT offset count]](https://www.runoob.com/redis/sorted-sets-zrangebylex.html) 通过字典区间返回有序集合的成员 | | 9 | [ZRANGEBYSCORE key min max [WITHSCORES\] [LIMIT]](https://www.runoob.com/redis/sorted-sets-zrangebyscore.html) 通过分数返回有序集合指定区间内的成员 | | 10 | [ZRANK key member](https://www.runoob.com/redis/sorted-sets-zrank.html) 返回有序集合中指定成员的索引 | | 11 | [ZREM key member [member ...]](https://www.runoob.com/redis/sorted-sets-zrem.html) 移除有序集合中的一个或多个成员 | | 12 | [ZREMRANGEBYLEX key min max](https://www.runoob.com/redis/sorted-sets-zremrangebylex.html) 移除有序集合中给定的字典区间的所有成员 | | 13 | [ZREMRANGEBYRANK key start stop](https://www.runoob.com/redis/sorted-sets-zremrangebyrank.html) 移除有序集合中给定的排名区间的所有成员 | | 14 | [ZREMRANGEBYSCORE key min max](https://www.runoob.com/redis/sorted-sets-zremrangebyscore.html) 移除有序集合中给定的分数区间的所有成员 | | 15 | [ZREVRANGE key start stop [WITHSCORES]](https://www.runoob.com/redis/sorted-sets-zrevrange.html) 返回有序集中指定区间内的成员,通过索引,分数从高到低 | | 16 | [ZREVRANGEBYSCORE key max min [WITHSCORES]](https://www.runoob.com/redis/sorted-sets-zrevrangebyscore.html) 返回有序集中指定分数区间内的成员,分数从高到低排序 | | 17 | [ZREVRANK key member](https://www.runoob.com/redis/sorted-sets-zrevrank.html) 返回有序集合中指定成员的排名,有序集成员按分数值递减(从大到小)排序 | | 18 | [ZSCORE key member](https://www.runoob.com/redis/sorted-sets-zscore.html) 返回有序集中,成员的分数值 | | 19 | [ZUNIONSTORE destination numkeys key [key ...]](https://www.runoob.com/redis/sorted-sets-zunionstore.html) 计算给定的一个或多个有序集的并集,并存储在新的 key 中 | | 20 | [ZSCAN key cursor [MATCH pattern\] [COUNT count]](https://www.runoob.com/redis/sorted-sets-zscan.html) 迭代有序集合中的元素(包括元素成员和元素分值) | ### 3. 统计数量(菜鸟教程) #### Redis HyperLogLog Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定 的、并且是很小的。 在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基 数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。 但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。 #### 什么是基数? 比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 基数(不重复元素)为5。 基数估计就是在误差可接受的范围内,快速计算基数。 | 序号 | 命令及描述 | | :--- | :----------------------------------------------------------- | | 1 | [PFADD key element [element ...]](https://www.runoob.com/redis/hyperloglog-pfadd.html) 添加指定元素到 HyperLogLog 中。 | | 2 | [PFCOUNT key [key ...]](https://www.runoob.com/redis/hyperloglog-pfcount.html) 返回给定 HyperLogLog 的基数估算值。 | | 3 | [PFMERGE destkey sourcekey [sourcekey ...]](https://www.runoob.com/redis/hyperloglog-pfmerge.html) 将多个 HyperLogLog 合并为一个 HyperLogLog | # 四. Java操作Redis Jedis ### 准备工作 linux中打开防火墙 ```shell 打开端口 firewall-cmd --zone=public --add-port=6379/tcp --permanent 检查端口 firewall-cmd --zone=public --query-port=6379/tcp 重启防火墙 systemctl restart firewalld.service ``` ### 普通的maven工程操作redis 1. `pom`导入依赖 ```xml redis.clients jedis 3.2.0 ``` 2. 测试是否连接成功 ```java public class TestRedis { public static void main(String[] args) { //ip 端口 Jedis jedis = new Jedis("你的ip", 6379); //密码 jedis.auth("mypassword"); //测试连接 String ping = jedis.ping(); System.out.println(ping); // 返回的值是pong } } ``` ### Jedis使用 Jedis提供的API和Redis的原生方法基本一致。 如Redis中的`set`方法,在Jedis中,直接是`jedis.set()` # 五 Java操作Redis SpringBoot 说明: 在 SpringBoot2.x 之后, 原来使用的 Jedis 被替换成了 lettuce。 如果不是很高的性能要求,可以采用Jedis,否则需要编写Utils工具类实现序列化。 > jedis: 采用直连, 多个线程操作的话, 是不安全的, 如果想要避免不安全, 使用 jedis pool 连接池 它更像BIO > lettuce: 采用netty 实例可以多个线程中进行共享, 不存在线程不安全的情况, 可以减少线程数据 它更像NIO ### 1.pom导入依赖 Jedis版本 ```xml org.springframework.boot spring-boot-starter-data-redis org.apache.commons commons-pool2 2.6.0 ``` Lettuce版本 ```xml org.springframework.boot spring-boot-starter-data-redis ``` ### 2. 配置连接信息 ```properties # SpringBoot 所有的配置类, 都有一个自动配置类 RedisAutoConfiguration # 自动配置类都会绑定一个 peoperties 配置文件 RedisProperties # 正常使用即可,更多的配置详情redis.cn spring.redis.host=127.0.0.1 spring.redis.port=6379 ``` ### 3. Jedis 与 Lettuce 如果采用 `Jedis+Jedis Pool` 可以不用写utils工具类,jedis已经封装完成,如果采用的是 `Lettuce`,仍需要编写utils工具类实现序列化,否则会出现错误。 采用Lettuce版本 #### 自定义Template ```java @Bean @SuppressWarnings("all") public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException { // 自定义 String Object RedisTemplate template = new RedisTemplate(); template.setConnectionFactory(redisConnectionFactory); // Json 序列化配置 Jackson2JsonRedisSerializer objectJackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); // ObjectMapper 转译 ObjectMapper objectMapper = new ObjectMapper(); objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); objectJackson2JsonRedisSerializer.setObjectMapper(objectMapper); // String 的序列化 StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); // key 采用String的序列化方式 template.setKeySerializer(stringRedisSerializer); // hash 的key也采用 String 的序列化方式 template.setHashKeySerializer(stringRedisSerializer); // value 序列化方式采用 jackson template.setValueSerializer(objectJackson2JsonRedisSerializer); // hash 的 value 采用 jackson template.setHashValueSerializer(objectJackson2JsonRedisSerializer); template.afterPropertiesSet(); return template; } ``` #### 自定义RedisUtils 复制粘贴即可,不需要手动编写 ```java // package 包名 import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; /** * @author mxz */ @Component public final class RedisUtils { @Autowired private RedisTemplate redisTemplate; /** * 指定缓存失效时间 * * @param key 键 * @param time 时间(秒) * @return */ 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 键(不能为 Null) * @return true 存在 false 不存在 */ public boolean hashKey(String key) { try { return redisTemplate.hasKey(key); } catch (Exception e) { e.printStackTrace(); return false; } } /** * 删除缓存 * * @param key 可以传一个值 或多个 */ public void del(String... key) { if (key != null && key.length > 0) { redisTemplate.delete(key[0]); } else { redisTemplate.delete(CollectionUtils.arrayToList(key)); } } //==================================String==================================== /** * 普通缓存获取 * * @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 值 * @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 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().decrement(key, delta); } // ================================Map================================= /** * HashGet * * @param key 键 不能为null * @param item 项 不能为null */ public Object hget(String key, String item) { return redisTemplate.opsForHash().get(key, item); } /** * 获取hashKey对应的所有键值 * * @param key 键 * @return 对应的多个键值 */ public Map hmget(String key) { return redisTemplate.opsForHash().entries(key); } /** * HashSet * * @param key 键 * @param map 对应多个键值 */ public boolean hmset(String key, Map 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 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) */ public double hincr(String key, String item, double by) { return redisTemplate.opsForHash().increment(key, item, by); } /** * hash递减 * * @param key 键 * @param item 项 * @param by 要减少记(小于0) */ public double hdecr(String key, String item, double by) { return redisTemplate.opsForHash().increment(key, item, -by); } // ============================set============================= /** * 根据key获取Set中的所有值 * * @param key 键 */ public Set 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 键 */ 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代表所有值 */ public List 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 键 */ 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倒数第二个元素,依次类推 */ 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 值 */ 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 时间(秒) */ 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 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 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; } } // ===============================HyperLogLog================================= public long pfadd(String key, String value) { return redisTemplate.opsForHyperLogLog().add(key, value); } public long pfcount(String key) { return redisTemplate.opsForHyperLogLog().size(key); } public void pfremove(String key) { redisTemplate.opsForHyperLogLog().delete(key); } public void pfmerge(String key1, String key2) { redisTemplate.opsForHyperLogLog().union(key1, key2); } } ``` # 六. Redis的事务 区分与MySQL(各类SQL关系型数据库的事务),Redis的事务比较简单。 仅仅只是命令的组队(Queue),即Redis在执行时,不会有其他的命令来打断当前执行的命令,因为Redis满足单线程+多路IO复用的模型,同一时间按只会有一个在运行。 > MULIT 开启组队 > > EXEC 开始执行(提交) > > DISCOAD 组队未完成(取消组队) 如果在MULIT中出现了错误,那么后续的所有操作都会出现错误。 如果在EXEC中出现了某一个指令的错误,那么会跳过当前的指令,其他指令不影响 ### 悲观锁 > 悲观锁是指在访问共享变量之前就认为会出现线程安全问题,所以需要在上锁之后再访问共享变量,由于悲观锁每次访问时都会上锁,就会造成程序的并发性降低 IMG-20220115002506 乐观锁 > 乐观锁是指如果在访问共享变量时没有出现线程安全问题就可以继续操作,如果出现线程安全问题,就取消这次对共享变量的操作 乐观锁 ->ABA问题->版本号时间戳 IMG-20220115002932 ### Redis的事务使用乐观锁实现 ### 开启乐观锁 > watch 监视某个变量 > > unwatch 取消监视某个变量 IMG-20220115005320 > 具体的事务案列可以寻找秒杀,或银行判断余额的流程。 # 七. Redis持久化 详情:[RDB和AOF的区别 - 博客园](https://www.cnblogs.com/zxs117/p/11242026.html) + [REDIS persistence -- Redis中国用户组(CRUG)](http://redis.cn/topics/persistence.html) Redis 提供了不同级别的持久化方式: - RDB持久化方式能够在指定的时间间隔能对你的数据进行快照存储。 - AOF持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以redis协议追加保存每次写的操作到文件末尾.Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大。 - 如果只希望你的数据在服务器运行的时候存在,可以不使用任何持久化方式。 - 当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整。 Redis提供了两种方式进行数据的持久化 > RDB (Redis DataBase) > > 原理是将Reids在内存中的数据库记录定时dump到磁盘上的RDB持久化 > > AOF(Append Of File / Append Only File) > > 原理是将Reids的操作日志以追加的方式写入文件 两个文件默认都在安装路径下生成 /usr/local/bin ## RDB持久化 IMG-20220115121125 劣势 IMG-20220115165308 备份相关资料查找即可 [RDB和AOF的区别 - 博客园](https://www.cnblogs.com/zxs117/p/11242026.html) ## AOF持久化 IMG-20220115170443 # 八. Redis的主从复制 IMG-20220115180219 目的:提供性能,保护数据得完整性 IMG-20220115180623 > 1. Master可读可写,Slaver只能读,不能写 > 2. Master可以对应多个Slaver,但是数量越多压力越大,延迟就可能越严重 > 3. Master写入后立即返回,几乎同时将写入异步同步到各个Slaver,所以基本上延迟可以忽略 > 4. 可以通过slaveof no one命令将Slaver升级为Master(当Master挂掉时,手动将某个Slaver变为Master) > 5. 可以通过sentinel哨兵模式监控Master,当Master挂掉时自动选举Slaver变为Master,其它Slaver自动重连新的Master 配置主从信息需要更改配置文件 参考链接:🔗 [Redis主从配置_·欣·的博客-redis主从配置](https://blog.csdn.net/weixin_39764056/article/details/104345786) [Redis集群(三):主从配置一 ](https://www.cnblogs.com/gossip/p/5992716.html) 设置密码了需要在从机配置文件中修改 masterauth 选项 查看Redis 主从 信息 ```shell info replication ``` ```shell redis-cli -p 端口号 ``` # 九. Redis的集群 > Redis通过主从服务器,可以解决如果主服务器出现问题,备用服务器可以马上替代,保证服务不间断提供。同时Redis通过主从服务器,将读写操作分离,提高了性能。 > > 但是对于Redis服务器来说,仅仅有一台主服务器不能承受并发越来越高的业务,那么就出现集群,不同的业务分配到不同的服务器上,每个服务器负责的业务单一,这就是集群的意义,提高了业务的性能同时降低了业务之间的耦合度。 Redis集群的优势 IMG-20220115214410 Redis集群的缺点 IMG-20220115223831 # 十. 应用问题 ## 缓存穿透 IMG-20220115233633 查缓存中没有的数据,从而导致大量请求直接访问数据库导致压力过大。简单来说就是所有的请求直接访问持久化的数据库,而不是先通过缓存。 > 常见的场景如: > > 双十一大量的访问,机器不可能把数据库中所有的内容都缓存下来,在这种情况下,必然会造成对数据库的大量读写操作。 > > 对某一个不存在的页面重复访问(不存在的内容等),那么缓存中没有这个内容,那么就会查数据库,此时又查不到,返回空,如果此时又有新的访问访问不存在的内容,那么势必会造成访问数据库,造成数据库的访问压力过大。也就是常见的攻击 IMG-20220115233345 ### 常见的场景(出现问题的原因) IMG-20220115232246 ### 解决方案 IMG-20220115232847 对空值做缓存是指:对查不到的所有数据(不存在的url不仅仅只是一个,访问得到的记过也即是空)做缓存,不仅仅只是一个空值,而是对于所有的查不到的信息都进行空值的缓存。 > 布隆过滤器:不存在则一定不存在,存在则有可能存在。 ## 缓存击穿 > 这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮,来不及回设。 > > 简单的理解就是:在某一个时刻,热点的key过期了,所有的访问需求直接奔向数据库,造成数据库压力过大。 > > 如某某明星的私事被曝光了,造成大量访问,关键时刻缓存中的key过期,此时大量的用户仍在访问,这些访问的需求直接奔向数据库,造成数据库读写压力大,压垮数据库。 IMG-20220115233557 IMG-20220115233218 ### 出现的场景(问题引发的原因) IMG-20220115233823 ### 解决方案 IMG-20220115233737 ## 缓存雪崩 > 大量用户访问缓存中不存在但数据库中存在的数据,并且这些数据不一样 > > 在极短的时间段内,查询的key大量过期,并且大量的访问内日那个不相同(如果相同可以做缓存),因为大量访问不相同内容,造成数据库的读写压力过大,最终压垮数据库。 IMG-20220115234406 ### 图解 IMG-20220115234612 雪崩对于底层应用冲击非常大,所以应该尽量避免缓存雪崩情况的发生。 ### 解决方案 IMG-20220116091220 ## 分布式锁 重点:分布式锁特点之一就是互斥性,所以锁必须唯一。 IMG-20220116092440 > 随着业务的不断发展,原有的单体应用变为分布式集群系统后,由于分布式系统多线程,多进程分布在不同的机器上,这使原单机部署情况下的并发控制锁策略失效,单纯的Java API不能提供分布式锁的能力,为了解决这个问题就需要一种跨JVM的互斥锁来控制共享资源的访问,这就是分布式锁要解决的问题。 为了防止出现设置锁但是一直不释放锁的情况,一般在设置锁时有两种释放锁的方法。 - `del key` 直接使用 del 方法 - `expire key seconds` 设置过期时间 真正落地不用采用设置UUID的值,分布式应用引发的的各种问题通常由分布式锁完成,而应用分布式锁过程中出现的问题,由以下完成。 1、过期时间没法合理设置 2、集群不支持lua脚本,常规考虑redssion和zk的锁方案,通常是前者,如果后者还要刻意引入zk那显然成本过高。