一、NoSql
Not Only Sql (不仅仅是SQL)
很多数据类型如用户的个人信息,社交网络,地理位置。这些数据类型的存储不需要一个固定的格式,不需要多余的操作就可以横向扩展。
1. NoSQL 特点
-
方便扩展(数据之间没有关系,很好扩展);
-
大数据量高性能(Redis 一秒写8万次,读11万次);
-
数据类型是多样型的(不需要事先设计数据库);
-
传统的 RDBMS 和 NoSQL的区别:
传统的 关系型数据库: - 结构化组织; - SQL; - 数据和关系存在单独的表中; - 严格的一致性; NoSQL: - 不仅仅是数据; - 没有固定的查询语言; - 键值对存储,列存储,文档存储,图形数据库(社交关系); - CAP 定理 和 BASE (异地多活) - 高性能,高可用,可扩展
真正的实践:NoSQL + RDBMS 一起使用才是最强的。
2. NoSQL 的四大分类
-
KV键值对:Redis ;
-
文档型数据库:(bson 格式 和 json 一样): MongoDB ;
- MongoDB 是一个基于分布式文件存储的数据库,C++ 编写,主要用来存储大量的文档; - MongoDB 是一个介于关系型数据库和非关系型数据库之间的一个产品; - MongoDB 是非关系型数据库中功能最丰富的,最像关系型数据库的!3
-
列存储数据库:HBase ;
-
图关系数据库:Neo4j , InfoGrid ;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sgA4Gonk-1642057063675)(C:\Users\13965\AppData\Roaming\Typora\typora-user-images\image-20211228110501095.png)]
二、Redis 基础
1. redis 是什么?
- 即远程字典服务!免费、开源,当下最热门的的技术之一;
- 可以内存存储、持久化;
- 效率高,可以用于高速缓存;
- 发布订阅系统 ;
- 地图信息分析 ;
- 计时器、计数器等;
在redis目录下,在命令窗口执行:redis-server.exe redis.windows.conf
中文官网:http://www.redis.cn/
2. windows 安装
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zO0keNRi-1642057063676)(C:\Users\13965\AppData\Roaming\Typora\typora-user-images\image-20211228141550178.png)]
测试连接: 通过 ping 如果输出 PONG 表示连接成功
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Hb9R0DCV-1642057063677)(C:\Users\13965\AppData\Roaming\Typora\typora-user-images\image-20211228152716514.png)]
3. reis-benchmark 性能测试
可选参数如下:
举例:本地 redis 100个并发连接,1000次的请求
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LT3yUlTz-1642057063678)(C:\Users\13965\AppData\Roaming\Typora\typora-user-images\image-20211229095213732.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aTUiiFys-1642057063679)(C:\Users\13965\AppData\Roaming\Typora\typora-user-images\image-20211229094224333.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9VUYf15i-1642057063680)(C:\Users\13965\AppData\Roaming\Typora\typora-user-images\image-20211229095127583.png)]
4. 基础知识
- redis 有16个数据库,默认使用的是第 1 个;
- redis 是单线程的,基于内存操作的,是将多有的数据放在内存中的;
- redis 的瓶颈是机器的内存和网络带宽;
5. 数据类型
(1)redis-key
- select 【数据库索引】:切换到对应索引值的数据库 ;
- flushdb :清空当前数据库 ;
- flushall :清空所有数据库;
- keys * : 查询所有的key ;
- exists 【key】:判断当前 key 是否存在 1表示存在 0表示不存在 ;
- set 【key】 【values】 :设置键值对,如果已经存在对应 key 会覆盖 ;
- setnx 【key】【values】:不存在在设置,如果已经存在,不会设置成功 ;
- mset 【key1】【values1】【keys2】【values2】……: 批量设置值,会覆盖 ;
- msetnx ……: 批量设置,如果存在则不会设置成功 ;
- get 【key】:获取对应 key 的值 ;
- mget 【k1】【k2】【k3】……: 批量获取对应 key 的值 ;
- getset 【key】【value】:先 get 然后再 set ;
- move 【key】 【数据库】:从某个数据库移除 key 值 ;
- expire 【key】 【times】:设置 key 的过期时间,单位是 秒 ;
- ttl 【key】:查看当前 key 的剩余时间 ;
- type 【key】:查看当前 key 的类型 ;
(2)类型1:String类型
- append 【key】【values】:为字符串追加内容,如果不存在,则新建 ;
- strlen 【key】:获取指定 key 值的长度 ;
- getrange 【key】【start】【end】:获取 key 上值起始和结束长度的一个字符串 ;
- setrange 【key】【start】【values】:替换 key 指定位置开始的字符串 ;
(3)类型2:List 类型
- lpush 【list】【value】:将一个或多个值插入到列表的头部(头);
- rpush 【list】【value】:将一个或多个值插入到列表的尾部(尾);
- lrange 【list】【start】【end】:从列表中获取指定返回的元素 0 到 -1 是取所有;
- lpop 【list】:移除列表中 头部的 第一个值 ;
- rpop【list】:移除列表中 尾部的 第一个值 ;
- lindex 【list】【下标】:根据下标获取元素 ;
- llen 【list】:返回列表的长度 ;
- lrem 【list】【count】【value】:移除指定的数据 ;
- ltrim 【list】【start】【end】:通过下标截取指定的长度操作,原 list 被修改;
- rpoplpush 【原list】【目标 list】:移除尾部元素,并赋值给新的列表 ;
- lset 【list】【index】【value】:为指定下标赋值 ,下标不存在会报错;
- linsert 【list】【before / after】【目标值】【value】:在指定目标值的前或后插入内容 ;
(4)类型3 :Set 类型
- sadd 【set】【value】:set中添加一个值 ;
- smembers 【set】 : 查看set中的值列表 ;
- sismember 【set】 【value】:判断某个值是否在set中 ;
- scard 【set】:获取 set 集合中的元素个数 ;
- srem 【set】【value】:移除 set 集合中的元素 ;
- spop 【set】:随机移除 set 集合中的元素 ;
- srandmember 【set】【count】:随机取出 set 集合中的 几个 元素;
- smove 【原set】【目标set】【value】:移动元素;
- sdiff 【set1】【set2】:筛选出 set1 和 set2 不一样的内容;
- sinter 【set1】【set2】:筛选出 set1 和 set2 中交集的内容;
- sunion 【set1】【set2】:筛选出 set1 和 set2 的并集;
(5)类型 4:Hash 类型:
<key,Map>
- hset 【Hash 】【field字段名】【value字段值】:设置 hash 的值 ,同一个 key 相同的 name 会覆盖器内容;
- hget 【Hash 】【field 字段名】:取出值;
- hgetall 【Hash 】:获取 key 中所有的 值,包括了 map 的key和value ;
- hkeys 【Hash 】:获取 hash 中所有的 keys ;
- hvals 【Hash】:获取 hash 中所有的 values ;
- hmset :设置多个值 ;
- hmget : 获取多个值 ;
- hdel 【Hash 】【field】:删除 hash 指定的字段;
- hlen 【Hash 】:获取 hash 表的字段数量 ;
- hexists 【Hash 】【field】:判断 hash 中指定的 字段 是否存在;
(6)类型 5 :Zset 类型
在 set 的基础上,可以进行排序;
- zadd 【set】【序号】【value】:set 中添加一个有序的值,可以添加多个 ;
- zrange 【set】【start】【end】:获取 zset 中的值,0 ,-1 是获取所有的 ;
- zrangebyscore 【set】【-inf 无穷小】【+inf 无穷大】:显示全部的数据,从小到大 ;
- zrevrange 【set】【0】【-1】:显示全部数据,从大到小 ;
- zrem 【set】【value】:移除元素 ;
- zcard 【set】:查看元素数量 ;
- zcount 【set】【start】【end】:统计指定区间的成员数量 ;
(6)类型 6: Integer 类型
- incr 【key】:key 值自增 1 ;
- decr 【key】:key 值自减 1 ;
- incrby 【key】 【量】:key 值按照自增量进行自增 ;
- decrby 【key】 【量】:key 值按照自减量进行自减 ;
6. 三种特殊数据类型
(1)geospatial 地理位置
有效的经度 -180度 到180度,有效的维度 -85.05112878度 到 85.05112878度
- geoadd 【key】【维度】【经度】【名称】:添加城市地理位置;
- geopos 【key】 【名称】:获取经纬度 ;
- geodist 【key】【名称1】【名称2】【单位 km / m】:获取两个地点的直线距离;
- georadius 【key】【经度】【维度】 【半径】【单位】【withdist经度】【withcoord维度】【count数量】:查找给你经纬度位置半径内的位置;
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Hq4qRI4A-1642057063684)(C:\Users\13965\AppData\Roaming\Typora\typora-user-images\image-20220104151920113.png)]
- georadiusbymember 【key】【名称】【距离】【单位】:查找以某个城市为中心半径为多少所覆盖的城市;
(2)hyperloglog 数据结构
用来做基数统计的算法。
优点:占用内存是固定的
- pfadd 【key】【values】: 添加元素;
- pfcount 【key】:统计元素个数;
- pfmerge 【新key】【key1】【key2】:合并两个集合生成第三个集合,取的是并集;
(3)bitmap :位存储
都是操作二进制位来进行记录,只有 0 和 1 两个状态;
- setbit 【key】【位置】【0/1】:设置值;
- getbit 【key】【位置】:获取对应位置的值;
- bitcount 【key】:统计里面为 1 的数量;
7. 事务
事务:一组命令的集合;
redis 单条命令是保持原子性的,但是redis事务不保证原子性,redis事务没有隔离级别的概念,所有的命令在事物中,并没有被直接执行,只有发起执行命令的时候才会执行;
- 开启事务(multi )
- 命令入队(其他命令……)
- 执行事务(exec)
- 取消事务(discard)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C8bly8XZ-1642057063686)(C:\Users\13965\AppData\Roaming\Typora\typora-user-images\image-20220104162947848.png)]
- 错误,指令语法错误,虽然事务不会停止,提交事务时所有的命令都不会运行(编译时异常);
- 错误,操作错误,事务不会停止,提交事务时,只有操作错误的指令报错,其他指令正常运行(运行时异常);
8. 乐观锁 - 监视 Watch
-
第一步:获取 ; 第二步:更新 ;
-
加锁: watch 【对象】
-
解锁:unwatch
9. Jedis
是 redis 官方推荐的 java 连接开发工具,使用 java 操作redis 中间件。
-
导入对应的依赖:
<dependencies> <!--导入 jedis 的包--> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>3.2.0</version> </dependency> <!--json 包--> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.62</version> </dependency> </dependencies>
-
编码测试:
- 连接数据库;
- 操作命令;
- 断开连接;
-
Jedis 常用的 API 和 redis 指令是一样的;
10. SpringBoot 整合
新建 springboot 项目,选择 nosql 中的 redis 组件 :
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
springboot 2.x 之后,原来使用的 jedis 被替换为了 lettuce 驱动框架
jedis:采用的直连,多个线程操作的话,是不安全到的,,如果想要避免不安全的,就要使用 jedis pool连接池,更像 BIO 模式;
lettuce:采用 netty,实例可以在多个线程中进行共享,不存在线程不安全的情况,可以减少线程数据了,更像 NIO 模式;
- 导入依赖;
- 配置连接;
########## redis 配置
# ip地址
spring.redis.host=172.31.240.211
# 端口号
spring.redis.port=6379
# 默认的数据库
spring.redis.database=0
- 代码测试;
(1) opsFor - 方法:
- opsForValue() : 操作字符串 ;
- opsForZSet():操作 set ;
- opsForList() : 操作 list ;
- opsForGeo() :操作 地图;
- ……
除了上面的进入操作,可以之间通过 redisTemplate 进行操作;
(2)自定义redis配置类 RedisConfig
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k1WNSpuC-1642057063687)(C:\Users\13965\AppData\Roaming\Typora\typora-user-images\image-20220106152017364.png)]
因为自带的redis配置类,编码方式会导致中文乱码的问题;
-
自定义配置类 RedisConfig.java:
import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.annotation.Autowired; 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.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; /** * Redis 配置 */ @Configuration public class RedisConfig { @Autowired private RedisConnectionFactory factory; @Bean("myRedisTemplate") @SuppressWarnings("all") public RedisTemplate<String, Object> redisTemplate() { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); // 序列化配置 // 1.所有的对象通过 json 进行序列化 Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); // 2.通过 ObjectMapper 进行转义 ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); // om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY); jackson2JsonRedisSerializer.setObjectMapper(om); StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); // key采用String的序列化方式 template.setKeySerializer(stringRedisSerializer); // hash的key也采用String的序列化方式 template.setHashKeySerializer(stringRedisSerializer); // value序列化方式采用jackson template.setValueSerializer(jackson2JsonRedisSerializer); // hash的value序列化方式采用jackson template.setHashValueSerializer(jackson2JsonRedisSerializer); template.afterPropertiesSet(); return template; } }
(3)自定义 redis 工具类 RedisUtils.java
/**
* Redis工具类
*/
@Component
public class RedisUtils {
@Autowired
@Qualifier("myRedisTemplate")
private RedisTemplate<String, Object> redisTemplate;
/**
* 指定缓存失效时间
*
* @param key 键
* @param time 时间(秒)
*/
public boolean setExpireTime(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 getExpireTime(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
public boolean isHasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
*
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void delCache(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete((Collection<String>) CollectionUtils.arrayToList(key));
}
}
}
// ============================String=============================
/**
* 普通缓存获取
*
* @param key 键
* @return 值
*/
public Object getCacheString(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
*
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean setCacheString(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 setCacheAndExpireTimeString(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
setCacheString(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 递增
*
* @param key 键
* @param delta 要增加几(大于0)
*/
public long incrCacheString(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
*
* @param key 键
* @param delta 要减少几(小于0)
*/
public long decrCacheString(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
// ================================Map=================================
/**
* HashGet
*
* @param key 键 不能为null
* @param item 项 不能为null
*/
public Object hgetCacheMap(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
*
* @param key 键
* @return 对应的多个键值
*/
public Map<Object, Object> hmgetCacheMap(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
*
* @param key 键
* @param map 对应多个键值
*/
public boolean hmsetCacheMap(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 hmsetCacheAndExpireTimeMap(String key, Map<String, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
setExpireTime(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 false失败
*/
public boolean hsetCacheMap(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 hsetCacheAndExpireTimeMap(String key, String item, Object value, long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
setExpireTime(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除hash表中的值
*
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
public void hdelCacheMap(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKeyCahcheMap(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
*
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
*/
public double hincrCacheMap(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
*
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
*/
public double hdecrCacheMap(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
// ============================set=============================
/**
* 根据key获取Set中的所有值
*
* @param key 键
*/
public Set<Object> sGetCacheSet(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 sHasKeyCacheSet(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 sSetCacheSet(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)
setExpireTime(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 setRemoveCacheSet(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<Object> lGetCacheList(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 lGetIndexCacheList(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 lSetCacheList(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 lSetCacheList(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0)
setExpireTime(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSetCacheList(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 lSetCacheList(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0)
setExpireTime(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据索引修改list中的某条数据
*
* @param key 键
* @param index 索引
* @param value 值
* @return
*/
public boolean lUpdateIndexCacheList(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 lRemoveCacheList(String key, long count, Object value) {
try {
Long remove = redisTemplate.opsForList().remove(key, count, value);
return remove;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
}
三、Redis 进阶
1. redis.config 详解
- 对大小写是不敏感的;
- 可以把多个配置文件都包含进来 通过 include
网络
bing 127.0.0.1 # 绑定的 ip
protected-mode yes # 保护模式
port 6379 # 端口设置
-
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JLHibyPl-1642057063688)(C:\Users\13965\AppData\Roaming\Typora\typora-user-images\image-20220110110537732.png)]
-
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hR8HR7bM-1642057063689)(C:\Users\13965\AppData\Roaming\Typora\typora-user-images\image-20220110111339659.png)]
-
设置 redis 的密码,默认是没有的
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-clHrrHyC-1642057063690)(C:\Users\13965\AppData\Roaming\Typora\typora-user-images\image-20220110110959548.png)]
设置空密码: config set requirepass "" 这样就把密码设置空了
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-70e6YpmA-1642057063692)(C:\Users\13965\AppData\Roaming\Typora\typora-user-images\image-20220110111717643.png)]
2. Redis 持久化
redis 是内存数据库,如果不将内存中的数据库状态保存到磁盘,那么一旦服务器进程退出,服务器中的数据库状态也会消失。所以 redis 提供持久化的功能。
(1)RDB 文件
在指定的时间间隔内,将内存中的数据集快照写入磁盘,它恢复时是将快照文件直接读到内存里。
redis 会单独床阿金 一个子进程进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程,主进程是不进行任何的IO操作的,这就确保了性能,如果需要进行大规模的数据恢复,且对于数据护肤的完整性不是非常敏感,那 RDB 方式要比 AOF 方式更加的高效。RDB的缺点是最后一次持久化的数据可能会丢失。rdb 保存的文件是 dump.rdb 文件
触发机制:
- save 的规则满足的情况下,会自动出发 rdb 规则;
- 执行 flushall 命令,也会触发我们的 rdb 规则;
- 退出 redis ,也会产生 rdb 文件 ;
恢复 rdb 文件:
- 只要将 rdb 文件放在我们 redis 启动目录就可以,redis 启动的时候会自动检查 dump.rdb 恢复其中数据;
- 优点:适合大规模的数据恢复;对数据的完整性要求不高;
- 缺点:需要一定的时间间隔;如果意外宕机,最后一次修改数据就没有了;fork进程的时候会占用一定的内存空间;
(2)AOF 文件
将我们所有的命令都记录下来,history 文件,恢复的时候就把这个文件全部在执行一遍;
以日志的形式来记录每个写操作,将 redis 执行过的所有指令记录下来(读操作不记录),只许追加文件但不可以改文件,redis 启动之初会读取该文件重新构建数据,换言之,redis 重启的话就会根据日志文件的内容,将指令从前到后执行一次以完成数据的恢复工作。 AOF 保存的是 appendonly.aof 文件
默认是不开启的 ,需要将配置文件中的 appendonly 改为 yes 就开启了 aof ,重启 redis 即可生效 !
如果说 appendonly.aof 文件被破坏,那么将无法启动 redis,可以通过 redis-check-aof 工具进行自动修复
- 有点:每一次修改都同步,文件的完整会更加好;默认开启的是每秒同步一次;
- 缺点:相对于 rdb 来说,aof 远远大于 rdb,修复的速度也比较慢;运行效率也比较慢
3. 主从复制
是指将一台 redis 服务器的数据,复制到其他的 redis 服务器,前者称为 主节点 Master,后者称为 从节点 Slave,数据的复制是单向的,只能由主节点到从节点, 主节点以写为主,从节点以读为主;
默认情况下,每天 redis 服务器都是主节点,且一个主节点可以有多个从节点,但一个从节点只能有一个主节点;
主从复制的作用主要包括:
- 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式;
- 故障恢复:当节点出现问题时,可以有从节点提供服务,实现快速的故障恢复,实际上是一种服务的冗余;
- 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,从节点提供读服务,分担服务器的负载;
- 高可用基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是 redis 高可用的基础;
(1)环境配置
只配置从库,不用配置主库 一主二从; 目前在同一台window下安装三个redis实例 其中主服务端口6379,从服务slave1为6380,从服务slave2为6381. 单机多集群
a). 三个服务情况如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SNIVOgQp-1642057063693)(C:\Users\13965\AppData\Roaming\Typora\typora-user-images\image-20220111095450588.png)]
b). 主服务进行配置 6379: redis.windows-service.conf 和 redis.windows.conf 都要做修改
-
redis.windows.conf
-
logfile :原默认没有名次,指定名次为 “6379.log” ,此处建议不动;
注意:如果这里指定了日志名,就会在目录下生产对应的日志文件,而不会在命令行窗口进行打印了
-
dbfilename dump.rdb :修改为 dbfilename dump6379.rdb
-
redis.windows-service.conf
- logfile “server_log.txt” : 修改为 logfile “server_log6379.txt”
- dbfilename dump.rdb :修改为 dbfilename dump6379.rdb
c). 从服务 6380 ……
- port 6379 : 端口改为 port 6380;
- pidfile /var/run/redis.pid :改为 pidfile /var/run/redis6380.pid
- logfile “server_log.txt” : 改为 logfile “server_log6380.txt”
- dbfilename dump.rdb :修改为 dbfilename dump6380.rdb
- slaveof :修改为 slaveof 127.0.0.1 6379
d). 从服务 6381
- port 6379 : 端口改为 port 6381;
- pidfile /var/run/redis.pid :改为 pidfile /var/run/redis6380.pid
- logfile “server_log.txt” : 改为 logfile “server_log6380.txt”
- dbfilename dump.rdb :修改为 dbfilename dump6380.rdb
- slaveof :修改为 slaveof 127.0.0.1 6379
(2)启动和关闭
分别切换到 三个 服务的目录下,进行启动 redis 服务:redis-server.exe redis.windows.conf
关闭服务,在 cli 客户端中输入 shutdown 再输出 exit
启动客户端,进入到服务目录下,通过 redis-cli.exe -p 6380 这种方式进行客户端的切换
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ebYM9M4f-1642057063694)(C:\Users\13965\AppData\Roaming\Typora\typora-user-images\image-20220111103952684.png)]
(3)验证
打开主服务的客户端,输入 info replication 可以看到从服务的信息
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lw6wO2nv-1642057063695)(C:\Users\13965\AppData\Roaming\Typora\typora-user-images\image-20220111105141837.png)]
从机启动之后,成功连接到 主机后 会发送一个 sync 命令;主机在接到命令后,启动后台的存盘进程,同时收集所有接收到的用于修改数据集的命令,在后台进程执行完毕后, 主机将传送整个数据文件到 从机上,并完成一次同步。
默认情况下,主服务 负责写,从 服务负责读
(4)主机宕掉
如果主机宕掉,我们可以使用 slaveof no one ,进行修改配置文件,让自己变成主机!其他节点可以手动连接到这个节点。
4. 哨兵模式
自动选取老大的模式(主服务);
- redis 从 2.8 开始 正式提供了 Sentinel (哨兵)架构来解决这个问题;
- 能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换成主库;
- 哨兵模式是一种特殊的模式,首先 redis 提供了哨兵的命令,哨兵是一个独立的进程,作为进程,他会独立运行。
- 其原理是哨兵通过发送命令,等待 redis 服务器响应,从而监控运行的多个 redis 实例;
-
然而一个哨兵进程 对redis 服务进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控,各个哨兵之间还会进行监控,这就形成了多哨兵模式。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uJQrt5r8-1642057063696)(C:\Users\13965\AppData\Roaming\Typora\typora-user-images\image-20220112092421842.png)]
假设注解服务器宕机,哨兵1先检测到这种结果,系统不会马上进行 failover 过程,仅仅是哨兵1 主观认为主服务器不可用,这个现象称为主观下线。当后面的哨兵也检测到主服务器不可用时,并且数量达到一定的值时,那么哨兵之间就会进行一次投票(投票算法),投票的结果由一个哨兵发起,进行 failover 操作,切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线。
(1)哨兵配置
a). 三个实例目录下分别新建sentinel.conf, 端口为6379, 6380, 6381,其他一致
port 26379
sentinel monitor mymaster 127.0.0.1 6379 2
daemonize yes
最后的 2: 是一个数字,指明当有多少个sentinel认为一个master失效时,master才算真正失效;
哨兵就是个独立的进程,用来监听实例信息的变化,且对相关操作做出反应,这里对每个实例都配一个哨兵进程
b). 启动三个哨兵进程
C:\Redis-x64-3.2.100(slave6379)>redis-server.exe sentinel.conf --sentinel
C:\Redis-x64-3.2.100(slave6380)>redis-server.exe sentinel.conf --sentinel
C:\Redis-x64-3.2.100(slave6381)>redis-server.exe sentinel.conf --sentinel
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HbEmSWao-1642057063696)(C:\Users\13965\AppData\Roaming\Typora\typora-user-images\image-20220112105344879.png)]
至此,哨兵配置且启动完成.
(2)测试 停掉 6379主服务
停掉主库79的进程,这里就是在79实例的cmd(非哨兵cmd)下ctrl+c
;
在进行短暂的后台调整后的结果 ;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EpTSNAT3-1642057063697)(C:\Users\13965\AppData\Roaming\Typora\typora-user-images\image-20220112111119342.png)]
81的日志:简单分析下日志:
- 老大下线了
- 疯狂的发送心跳进行询问(老大,你死了没?没回答,就应该是死了,哨兵进程辅助slave(s)建立新的老大)
- 自己成为老大 MASTER MODE enabled
- 其他小弟进行数据请求
- 新的主从建立
[15112] 09 Sep 14:57:23.699 # Connection with master lost.
[15112] 09 Sep 14:57:23.700 * Caching the disconnected master state.
[15112] 09 Sep 14:57:24.420 * Connecting to MASTER 127.0.0.1:6379
[15112] 09 Sep 14:57:24.420 * MASTER <-> SLAVE sync started
[15112] 09 Sep 14:57:25.421 * Non blocking connect for SYNC fired the event.
[15112] 09 Sep 14:57:25.421 # Sending command to master in replication handshake: -Writing to master: Unknown error
[15112] 09 Sep 14:57:25.423 * Connecting to MASTER 127.0.0.1:6379
[15112] 09 Sep 14:57:25.423 * MASTER <-> SLAVE sync started
[15112] 09 Sep 14:57:26.424 * Non blocking connect for SYNC fired the event.
[15112] 09 Sep 14:57:26.424 # Sending command to master in replication handshake: -Writing to master: Unknown error
[15112] 09 Sep 14:57:26.426 * Connecting to MASTER 127.0.0.1:6379
[15112] 09 Sep 14:57:26.426 * MASTER <-> SLAVE sync started
[15112] 09 Sep 14:57:27.427 * Non blocking connect for SYNC fired the event.
[15112] 09 Sep 14:57:27.427 # Sending command to master in replication handshake: -Writing to master: Unknown error
。。。。。。。。。。。。。。。。。。。。
[15112] 09 Sep 14:57:53.517 * Connecting to MASTER 127.0.0.1:6379
[15112] 09 Sep 14:57:53.517 * MASTER <-> SLAVE sync started
[15112] 09 Sep 14:57:54.006 * Discarding previously cached master state.
[15112] 09 Sep 14:57:54.007 * MASTER MODE enabled (user request from 'id=8 addr=127.0.0.1:51668 fd=14 name=sentinel-efc4be2b-cmd age=70 idle=0 flags=x db=0 sub=0 psub=0 multi=3 qbuf=0 qbuf-free=32768 obl=36 oll=0 omem=0 events=rw cmd=exec')
[15112] 09 Sep 14:57:54.009 # CONFIG REWRITE executed with success.
[15112] 09 Sep 14:57:54.855 * Slave 127.0.0.1:6380 asks for synchronization
[15112] 09 Sep 14:57:54.855 * Full resync requested by slave 127.0.0.1:6380
[15112] 09 Sep 14:57:54.856 * Starting BGSAVE for SYNC with target: disk
[15112] 09 Sep 14:57:54.863 * Background saving started by pid 15712
[15112] 09 Sep 14:57:55.125 # fork operation complete
[15112] 09 Sep 14:57:55.126 * Background saving terminated with success
[15112] 09 Sep 14:57:55.129 * Synchronization with slave 127.0.0.1:6380 succeeded
redis-cli.exe -p 6380 进入到对应的客户端, info replication 看一下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JgfqZNgQ-1642057063698)(C:\Users\13965\AppData\Roaming\Typora\typora-user-images\image-20220112112029677.png)]
可以看到 当 6379 的挂了之后,经过哨兵的自动调整,主服务 由 6379 变成了 6381 , 6380 变成了从服务 。
(3)测试 重启 6379 主服务
这里我们再重新启动 6379 主服务,看一下会不会 哨兵会不会把主服务调成 6379, 6380 和 6381 又变成从服务;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ICpnqTvP-1642057063699)(C:\Users\13965\AppData\Roaming\Typora\typora-user-images\image-20220112112444379.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P45ssEeG-1642057063699)(C:\Users\13965\AppData\Roaming\Typora\typora-user-images\image-20220112113434427.png)]
重启之后会发现,79 的服务正常使用,但是值得注意的是,6379 的服务变成了 从服务,之前的 6381 服务,依然是主服务,并没有变成从服务。
(4)优缺点
- 优点:基于主从复制模式,所有的主从配置优点,他全有;主从可以切换,故障可以转移,系统的可用性就会更好;
- 缺点:redis 不好在线扩容,集群容量一旦达到上限,在线扩容就十分麻烦;配置其实很麻烦;
5. redis 缓存穿透和雪崩
概念:用户想要查询一个数据,发现redis 内存没有,于是向持久层数据库查询,发现也没有,于是本次查询失败。当用户很多的时候,缓存都没有命中,于是都去请求持久层的数据库,这会给持久层数据库造成很大的压力,这时候就出现了缓存穿透。
解决方案:
(1)布隆过滤器:
是一种数据结构,对所有可能查询的参数以 hash 形式存储,在控制层先进校验,不符合则丢弃,从而避免了对底层存储系统查询压力。
(2)缓存空对象:
当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了后端数据源。
但是这种方法会存在以下问题:
- 如果空值能被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键;
主服务
这里我们再重新启动 6379 主服务,看一下会不会 哨兵会不会把主服务调成 6379, 6380 和 6381 又变成从服务;
[外链图片转存中…(img-ICpnqTvP-1642057063699)]
[外链图片转存中…(img-P45ssEeG-1642057063699)]
重启之后会发现,79 的服务正常使用,但是值得注意的是,6379 的服务变成了 从服务,之前的 6381 服务,依然是主服务,并没有变成从服务。
(4)优缺点
- 优点:基于主从复制模式,所有的主从配置优点,他全有;主从可以切换,故障可以转移,系统的可用性就会更好;
- 缺点:redis 不好在线扩容,集群容量一旦达到上限,在线扩容就十分麻烦;配置其实很麻烦;
5. redis 缓存穿透和雪崩
概念:用户想要查询一个数据,发现redis 内存没有,于是向持久层数据库查询,发现也没有,于是本次查询失败。当用户很多的时候,缓存都没有命中,于是都去请求持久层的数据库,这会给持久层数据库造成很大的压力,这时候就出现了缓存穿透。
解决方案:
(1)布隆过滤器:
是一种数据结构,对所有可能查询的参数以 hash 形式存储,在控制层先进校验,不符合则丢弃,从而避免了对底层存储系统查询压力。
(2)缓存空对象:
当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了后端数据源。
但是这种方法会存在以下问题:
- 如果空值能被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键;
- 即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响;