定义:
NoSQL,表示“Not Only SQL”,泛指非关系型数据库,表示其数据的存储不是按照业务的逻辑来的,而是简单的以 key-value 的形式存储,因此加大了数据库的扩展性能。
作用:
1.在分布式开发中,一个请求可能会被分配到不同的服务器中,但是用户在不同服务器中的session不能被共享。为解决这个问题:首先可以将session存在cookie中,但是这样安全性不是很好;然后可以复制session,但是这样会有很多的重复冗余;最优的解决办法是将数据存在nosql中,如果另一个服务器需要就直接在nosql中取出。来缓解cpu或内存的压力
2.NoSQL可作为缓存数据库,减少io的读取,可以将一些经常被读取的数据存放进NoSQL中。
特色:
不遵循SQL标准
不支持ACID(但不代表不支持事务操作)
性能高效
适用场景:
对数据高并发的读写、海量数据的读写。
常用的NoSQL数据库:
Redis、MongoDB
1.安装:
参考文章:https://www.linuxprobe.com/centos-redis.html
String 是 Redis 最基本的类型,是一个key对应一个value;并且他是二进制安全的,说明他的string可以包含任何的数据,比如 jpg图片或者序列化对象;一个redis中字符串的value最多可以是512M。
常用命令:
set key value :添加键值对(如果添加的key已经有value值了则直接覆盖)
get key:通过 key 寻找值
append key value:将指定的value添加到指定的key的value后面
strlen key:获得键所对应的值的长度
setnx key value:只为不存在的key设置value值
incr key:将指定key中存储的数字加1(仅限数字类型)
decr key:将指定key中存储的数字减1(仅限数字类型)
incrby/decrby key 步长:将key中存储的值增减,自定义步长。
这里的增减操作是原子性的,因为redis是单线程的,对值的修改结果是相互不影响的。
mset、mget:批量添加key-v、获得批量key对应的值
msetnx:批量设置不存在的key
getrange key 起始位置 结束位置:截取指定key中的值的范围。
setex key 过期时间 value :添加一个kv,并给其设定过期时间。
getset key value:新值替换旧值,并会返回旧值。
数据结构:
string的数据结构是 简单动态字符串(缩写 SDS),是可以修改的字符串,内部结构类似ArrayList,分配的空间要比实际字符串长度大。当字符串长度小于 1M 时,扩容是加倍现有的空间,如果超过1M,扩容一次只会多扩1M空间。并且字符串最大长度是512M。
redis中的list是一个key对应了多个value。
常用命令:
lpush / rpush key value value:从左边/右边插入一个值(有点像栈的思想,例:从左边插入一个值,那最后插入的值就在最左边;如果从右边插入,则最后插入的值就在最右边)
lpop / rpop key :从左边 / 右边弹出一个值
lrange key 开始位置 结束位置:从最左边的开始展示 list 中的值,如果写成 lrange key 0 -1,则表示展示所有的值。
rpoplpush key1 key2:将k2最右边的值弹出到k1最左边。
lindex key index:按照索引下标获得元素(从左到右)
llen key:获得长度
linsert key before value newvalue:在value后面插入newvalue插入值
lrem key n value:从左边删除 n 个value(从左到右)
lset key index value 将列表key下标为index的值替换成value
数据结构:
list的数据结构为快速链表 quickList。
首先在列表元素较少的时候回使用一块连续的内存存储地址,这个结果是ziplist,也是压缩列表,它是所有元素紧挨着存储,分配的是一块连续的内存。
但元素数据多时改为 quicklist
redis将链表和ziplist结合起来变成 了quicklist,将多个ziplist使用双向指针串起来使用。
与 list功能类似,不同的地方在set可以自动排重,无序且不重复的。底层是一个value为null的hash表,所以操作的复杂度都是O(1).
常用命令:
SADD key member1 [member2] :向集合添加一个或多个成员
SCARD key :获取集合的成员数
SDIFF key1 [key2]:返回第一个集合与其他集合之间的差异。
SDIFFSTORE destination key1 [key2]:返回给定所有集合的差集并存储在 destination 中
SINTER key1 [key2]:返回给定所有集合的交集
SINTERSTORE destination key1 [key2]:返回给定所有集合的交集并存储在 destination 中
SISMEMBER key member:判断 member 元素是否是集合 key 的成员
SMEMBERS key:返回集合中的所有成员
SMOVE source destination member:将 member 元素从 source 集合移动到 destination 集合
SPOP key:移除并返回集合中的一个随机元素
SRANDMEMBER key [count]:返回集合中一个或多个随机数
SREM key member1 [member2]:移除集合中一个或多个成员
SUNION key1 [key2]:返回所有给定集合的并集
SUNIONSTORE destination key1 [key2]:所有给定集合的并集存储在 destination 集合中
SSCAN key cursor [MATCH pattern] [COUNT count]:迭代集合中的元素
redis hash是一个键值对的集合,是一个string类型的 field(字段) 和 value(值) 的映射表,它特别适合存储对象,因为其value也是一个类似键值对的形式。
常用命令:
HDEL key field1 [field2]:删除一个或多个哈希表字段
HEXISTS key field:查看哈希表 key 中,指定的字段是否存在。
HGET key field:获取存储在哈希表中指定字段的值。
HGETALL key:获取在哈希表中指定 key 的所有字段和值
HINCRBY key field increment:为哈希表 key 中的指定字段的整数值加上增量 increment 。
HINCRBYFLOAT key field increment:为哈希表 key 中的指定字段的浮点数值加上增量 increment 。
HKEYS key:获取所有哈希表中的字段
HLEN key:获取哈希表中字段的数量
HMGET key field1 [field2]:获取所有给定字段的值
HMSET key field1 value1 [field2 value2 ]:同时将多个 field-value (域-值)对设置到哈希表 key 中。
HSET key field value:将哈希表 key 中的字段 field 的值设为 value 。
HSETNX key field value:只有在字段 field 不存在时,设置哈希表字段的值。
HVALS key:获取哈希表中所有值。
HSCAN key cursor [MATCH pattern] [COUNT count]:迭代哈希表中的键值对。
有序集合,就是在set的基础上有了顺序。每一个元素都会关联一个double类型的分数,redis通过用户为其设定的分数来进行由小到大的排序。并且分数是可以重复的。
常用命令:
ZADD key score1 member1 [score2 member2]:向有序集合添加一个或多个成员,或者更新已存在成员的分数
ZCARD key:获取有序集合的成员数
ZCOUNT key min max:计算在有序集合中指定区间分数的成员数
ZINCRBY key increment member:有序集合中对指定成员的分数加上增量 increment
ZINTERSTORE destination numkeys key [key …]:计算给定的一个或多个有序集的交集并将结果集存储在新的有序集合 destination 中
ZLEXCOUNT key min max:在有序集合中计算指定字典区间内成员数量
ZRANGE key start stop [WITHSCORES]:通过索引区间返回有序集合指定区间内的成员
ZRANGEBYLEX key min max [LIMIT offset count]:通过字典区间返回有序集合的成员
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT]:通过分数返回有序集合指定区间内的成员
ZRANK key member:返回有序集合中指定成员的索引
ZREM key member [member …]:移除有序集合中的一个或多个成员
ZREMRANGEBYLEX key min max:移除有序集合中给定的字典区间的所有成员
ZREMRANGEBYRANK key start stop:移除有序集合中给定的排名区间的所有成员ZREMRANGEBYSCORE key min max:移除有序集合中给定的分数区间的所有成员
ZREVRANGE key start stop [WITHSCORES]:返回有序集中指定区间内的成员,通过索引,分数从高到低
ZREVRANGEBYSCORE key max min [WITHSCORES]:返回有序集中指定分数区间内的成员,分数从高到低排序
ZREVRANK key member:返回有序集合中指定成员的排名,有序集成员按分数值递减(从大到小)排序
ZSCORE key member:返回有序集中,成员的分数值
ZUNIONSTORE destination numkeys key [key …]:计算给定的一个或多个有序集的并集,并存储在新的 key 中
ZSCAN key cursor [MATCH pattern] [COUNT count]:迭代有序集合中的元素(包括元素成员和元素分值)
数据结构:
1.hash
2.跳跃表
redis是将数据存在了内存中,内存只是临时储存器,断电就什么都没有了,所以持久化操作就是将数据存在硬盘上,同时redis官方也提供了两种不同的持久化方法来将数据存在硬盘中:
快照,就是在某一时刻将所有的数据写进硬盘中,就像拍一张照片一样将所有数据保存下来,这也是 redis 默认开启的持久化方法,所以我们在退出程序后下一次再打开 redis 依旧可以看到上次操作的数据。
保存的文件是以 .rdb 形式结尾的文件,所以这种形式也被称为RDB方式。我的这个文件是存在 /usr/local/bin 目录下。
快照的生成方式:
1.客户端方式:BGSAVE 和 SAVE 指令
区别:
127.0.0.1:6379> BGSAVE
Background saving started
127.0.0.1:6379> save
OK
2.服务器主动快照:
在我们使用 shutdown命令或者使用 ctrl + c 关闭redis时,redis会主动给我们做一次快照,形式是SAVE。
但是如果是突然断电宕机了,则不会生成快照,这也是快照的一个弊端,因为快照只能记录某一时刻的数据,如果突然宕机了,可能后面的数据就没有记录下来。
这种方法会使用一个aof文件记录每一次redis的写命令,将每一次的写命令都写到aop文件的末端,因此redis只要从头到尾的执行一遍aof中的写命令,就可以恢复原来的aof文件对应的数据值。
这个操作需要在conf文件中的APPEND ONLY MODE 自行打开,默认是关闭的 appendonly no,我们需要将 no 设置为 yes。
appendonly yes
appendfilename "appendonly.aof"
appenddirname "appendonlydir"
这里可以设置aof文件的读取设置:
1.always:每个redis写命令是都会被写进硬盘,从而当系统发送崩溃时的数据丢失会减到最少,但因为这种同步策略需要对硬盘进行大量的读写,所以redis的处理命令的速度会受到硬盘性能的限制。【谨慎使用!】
2.everysec:每秒执行一次同步显式的将多个命令同步到磁盘,这里是以每一秒的频率对aof文件进行同步,对于redis的性能影响也不是很大,同时可以保证即使系统崩溃,用户最多也就丢失一秒之内产生的数据。
3.no:由操作系统决定何时同步【不推荐】
如果两种持久化方式都被选择,则redis会优先选择aof方法,因为这种方法更加的安全。
但是使用aof方式也是有问题的,因为aof会记录每一次写的命令,但是随着时间的积累,aof文件的体积会变的越来越大。
aof的重写:
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
public static void main(String[] args) {
Jedis jedis = new Jedis("192.168.17.128",6379);
//如果设置了密码就需要加这句
jedis.auth("xxxx");
jedis.close();
}
注:在连接过程中遇到了问题,就是显示连接超时了,解决步骤如下:
1.进入redis.conf文件,将bind xxxx 注释掉,然后将下面的protectedxxx 设置为no
2.将6379端口放开:用命令 firewall-cmd --zone=public --add-port=6379/tcp --permanent 将对应端口放开。
参考文章:https://www.cnblogs.com/lirhbky/p/15182657.html
安全考虑,还可以给redis添加一个密码:
参考文章:https://blog.csdn.net/weixin_55229531/article/details/125218284
jedis在java中的操作和原来的命令差不多。
jedis.set("k1","wyh");
jedis.set("k2","wqj");
Set<String> keys = jedis.keys("*");
for (String key : keys) {
System.out.println(key);
}
System.out.println(jedis.get("k2"));
Spring Boot Data(数据) Redis 中提供了RedisTemplate和StringRedisTemplate
StringRedisTemplate是RedisTemplate的子类,两个方法基本一致,不同之处主要体现在操作的数据类型不同。
RedisTemplate中的两个泛型都是Object,意味着存储的key和value都可以是一个对象,而StringRedisTemplate的两个泛型都是String,意味着StringRedisTemplate的key和value都只能是字符串。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.7.1</version>
</dependency>
server.port=1126
spring.redis.port=7000
spring.redis.host=192.168.17.130
spring.redis.timeout=60000
@SpringBootTest()
public class WyhTest {
@Resource
private RedisTemplate RedisTemplate;
@Test
public void wyhRedisTest(){
System.out.println(RedisTemplate.opsForValue().get("k1"));
}
}
遇到的问题:
1.包导入问题:
老是报 junit 找不到的错,
java.lang.ClassNotFoundException: org.junit.jupiter.api.ClassOrderer
遇到报错:明明导了包但是还是报错:程序包org.springframework.data.redis.core不存在
反正就是不停的刷新maven等,最后不知道为啥就解决了。
参考:https://blog.csdn.net/chengtry/article/details/121694025?utm_medium=distribute.pc_relevant.none-task-blog-2defaultbaidujs_title~default-4-121694025-blog-90751976.pc_relevant_aa&spm=1001.2101.3001.4242.3&utm_relevant_index=6
# bind 192.168.1.100 10.0.0.1 # listens on two specific IPv4 addresses
# bind 127.0.0.1 ::1 # listens on loopback IPv4 and IPv6
# bind * -:: *
相关操作:
redis在存入相关数据前会对数据进行序列化后再将数据存入redis中
1.对 StringRedisTemplate的默认序列化的方法是 StringRedisSerializer,序列化是写在了构造方法中。
public class StringRedisTemplate extends RedisTemplate<String, String> {
public StringRedisTemplate() {
setKeySerializer(RedisSerializer.string());
setValueSerializer(RedisSerializer.string());
setHashKeySerializer(RedisSerializer.string());
setHashValueSerializer(RedisSerializer.string());
}
RedisSerializer的string()方法 源码
static RedisSerializer<String> string() {
return StringRedisSerializer.UTF_8;
}
2.对于RedisTemplate的默认序列化的方法是JdkSerializationRedisSerializer,因为redisTemplate处理的是对象相关的数据。
如果想要存储key是string,value是object的数据的话可以对序列化方式进行设置,将key的序列化方式改为string。
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.opsForValue().set("book1",new Books("1q84","村上春树"));
System.out.println(redisTemplate.opsForValue().get("1q84"));
示例:
如果键和值都不是对象的形式,那就是字符串的形式,应该用StringRedisTemplate 来获取对应的值;如果键是string,值是对象类型的,就给RedisTemplate 设置key的序列化方法;如果键是string、值也是string,那就直接使用RedisTemplate 不用修改其他
@Resource
private RedisTemplate redisTemplate;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Test
public void wyhRedisTest() {
stringRedisTemplate.opsForValue().set("wyh","want to sleep");
System.out.println(stringRedisTemplate.opsForValue().get("wyh"));
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.opsForValue().set("book1",new Books("1q84","村上春树"));
System.out.println(redisTemplate.opsForValue().get("book1"));
//总是报错
redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
}
如果使用 RedisTemplate.opsForValue().get(“wyh”) 这一行,用redistemplate 去拿键值都是string的类型的数据,则会报错。
如果我存储的Books对象类中没有做序列化的操作,在放入redis中就会出现以下错误。
org.springframework.data.redis.serializer.SerializationException: Cannot serialize;
nested exception is org.springframework.core.serializer.support.SerializationFailedException: Failed to serialize object using DefaultSerializer; nested exception is java.lang.IllegalArgumentException: DefaultSerializer requires a Serializable payload but received an object of type [com.redis2.entity.Books]
在类中添加继承序列号的部分,就成功了。
public class Books implements Serializable {
private String name;
private String author;
}
特别的:
因为 hash 比较特别,它的值也是一个键值对,所以对于hash,可以用 setHashKeySerializer 来设置hash函数值中的key的序列化方法。同理也可以设置hash的值中的value的序列化方法。
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
绑定key:
在之前我们对一个key进行操作时,可能需要写很多遍这个key值,我们可以使用boundValueOps方法来绑定一个key,如下列代码所示,就不用一直输入key值了。
// stringRedisTemplate.opsForValue().set("bf","0602");
// stringRedisTemplate.opsForValue().append("bf","好人");
BoundValueOperations<String, String> bf = stringRedisTemplate.boundValueOps("bf");
bf.set("wqj");
bf.append("臭狗屎");
System.out.println(stringRedisTemplate.opsForValue().get("bf"));
1.缓存:计算机内存中的一段数据。
2.内存特点:读写快,断电立即丢失。
3.缓存解决的问题:将常用的数据存储在缓存中,大大提高了效率,同时也可以减轻被访问程序(如数据库)的访问压力。
4.使用场景:缓存中适合存储不常变化的数据。
5.本地缓存和分布式缓存的区别:
本地缓存:存放在应用服务器内存中的数据。
分布式缓存:存放在当前应用服务器内存之外的数据称为分布式缓存。
集群:将同一种服务的多个节点放在一起共同对系统提供服务的过程称为集群
分布式:有多个不同服务集群共同对系统提供服务这个系统称为分布式系统。
在mapper配置文件中添加 < cache/>,即可开启缓存,mybatis中的二级缓存是sqlsessionfactory级别的缓存,并且是所有会话共享的。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.redis2.dao.AdminDao">
<cache/>
<select id="findAll" resultType="Admin">
select * from books_system.admin
</select>
</mapper>
@SpringBootTest
public class MysqlCacheTest {
@Autowired
private AdminServiceImpl adminService;
@Test
public void MysqlCache(){
adminService.getAllAdmin();
System.out.println("=====================================");
adminService.getAllAdmin();
System.out.println("=====================================");
adminService.getAllAdmin();
}
}
在开启了缓存,并且我们进行多次的查询可以发现,第一次是走了数据库的,但是第二次和第三次是没有走数据库的,是从缓存中获得数据的。
报了一个序列化的错,提示实体类admin没有序列化,这也证明了mysql进行缓存时也需要对象进行序列化。要让实体类继承序列化接口。(这也解释了一些代码生成器生成的entity都是继承了序列化接口的)
总结:mybatis 也可以实现缓存,是本地缓存,一旦jvm关闭时缓存也不复存在。
源码分析:
mybatis中的缓存提供了一个单独的cache接口,其中的 perpetualcache 实现类就是我们使用到的。
可以看出,mybatis中的缓存底层是map的形式,通过键值对存放缓存值。
private final Map<Object, Object> cache = new HashMap();
public void putObject(Object key, Object value) {
this.cache.put(key, value);
}
public Object getObject(Object key) {
return this.cache.get(key);
}
并且我们可以在mapper的 cache 标签中设置 type 为指定的 cache 类型
<cache type="org.apache.ibatis.cache.impl.PerpetualCache"/>
我们了解mybatis缓存的执行流程后,可将缓存换为redis缓存。
首先创建一个rediscache 继承一下cache接口,然后将xml配置文件中的缓存类型改为我们自定义的缓存。
public class RedisCache implements Cache
<cache type="com.redis2.cache.RedisCache"/>
运行时发现报了错,提示缺少一个id
Invalid base cache implementation (class com.redis2.cache.RedisCache). Base cache implementations must have a constructor that takes a String id as a parameter.
我们按照 perpetualcache 实现类的写法,将id添加进去,并打印id,发现id就是我们当前放入缓存的 mapper 的 namespace,cache标签是写在一个mapper标签中的,就说明一个cache是对应一个 namespace 的。
id:com.redis2.dao.AdminDao
因为还没有重写get和put的方法,所以当前缓存还不生效,但因为我们开启缓存是在mybatis的xml配置文件中写的,并不是在spring的容器中写的,所以没法利用自动装配启动 redis,我们可以通过一个工具类 SpringUtil 来获得 redisTemplate 的 bean 对象。
使用的是 ApplicationContextAware 类:
@Component
public class SpringUtil implements ApplicationContextAware {
private static ApplicationContext applicationContextUtil;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
applicationContextUtil = applicationContext;
}
public static Object getBean(String name){
return applicationContextUtil.getBean(name);
}
}
写好工具类后我们来完善redis缓存的功能:
因为机制是根据id就是命名空间来存储缓存,所以使用了redis的hash来存储,最大的key就是存储id,hash的key就存储缓存的key,value存储缓存的value。
@Override
public void putObject(Object key, Object value) {
RedisTemplate redisTemplate = (RedisTemplate) SpringUtil.getBean("redisTemplate");
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
//通过一个redis,hash进行存储,我们获得的key
redisTemplate.opsForHash().put(id,key.toString(),value);
System.out.println("key: "+key);
System.out.println("value: "+key);
}
@Override
public Object getObject(Object key) {
RedisTemplate redisTemplate = (RedisTemplate) SpringUtil.getBean("redisTemplate");
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
return redisTemplate.opsForHash().get(id,key.toString());
}
结果显示:可以看见所有的数据都是从缓存中拿到的。
问题:在对数据进行增删改的时候,我们发现需要修改更新缓存,因为缓存的获取是根据key进行获取,在对数据更新时,key不会更新,只会更新value,所以不会更新redis中的数据。
删除数据:清空redis中对应id的数据,全部清空
//这个方法没有使用
@Override
public Object removeObject(Object o) {
return null;
}
@Override
public void clear() {
getRedisTemplate().delete(id);
System.out.println("我执行了");
}
@Override
public int getSize() {
return getRedisTemplate().opsForHash().size(id).intValue();
}
在向表中添加数据时,可以发现默认调用了 clear 方法,在添加和修改数据时,会默认调用clear方法,删除相应id下的缓存,这样做是为了刷新数据,避免数据错误。
public void addAdmin(){
adminService.addAdmin(new Admin("wqj",2000));
}
总结:
redis作为缓存适合查询多修改少的数据,因为每次修改数据(增改删)就会删除一次原来的缓存,他是利用了mybatis的缓存机制,将原来的mybatis的缓存替换为了redis的缓存。
同时也存在一个问题:这种情况适用于单表查询,数据互不关联;当我们面对多表查询,查出的数据具有关联性时:当一个的数据做了改动,缓存中的其他id中也缓存了这个表的相关信息,此时其他缓存中的此表的相关信息就无法进行更新,此时可以使用 cache-ref 来关联其他表。
<cache-ref = />
关于key过长的问题:
可以看见,每次的生成的hashkey的长度都很长,为了方便,可以先对存入的key添加MD5加密。
MD5加密的特点:md5的加密方法是根据文件内容进行加密的,内容相同的文件md5是一样的。并且都会生成一串32位16进制的字符串。
所以建议将存入redis的key进行md5优化。
面试相关问题: