前言
来好好学一下,缓存到底该怎么写,并且记录一下,以前没有注意到的方法
缓存
什么时候用缓存?
业务数据常用吗?如果查询的频率不高,或者对数据库的影响不大,就没有必要做缓存,因为内存是很宝贵的
而且,如果我们读的数据非常大,那么也不能轻易用缓存,因为这会对内存造成较大的占用
一般流程是什么?
我们会优先读取redis,如果redis没有,那么就会读取数据库,然后把这些信息写入redis,这样,我们下次就可以读redis了
写的业务需要缓存吗?
如果我们的业务,需要高速的写入,或者写入一次对数据的压力很大,或者说,一堆数据瞬间写入,在这种情况下,我们可以选择把这些数据暂时写入缓存,然后在一定条件下,我们再写入数据库
为什么需要把数据库写进数据库
因为redis中存的数据结构化不够好,复杂的查询的时候很不方便
因为redis中的数据是会占用内存的,把这些数据库写进磁盘,不但数据的持久性好,而且会减少内存的占用
而且redis在写入的时候,可以批量写入,我们知道,批量写入的性能是要比分开写入的性能要好,当然一次性写的肯定不能太多
JAVA API
我发现好多工具类都有一个configuration的配置类,然后通过这个配置类来创建对象,不过,spring boot在这个基础上做了优化,它把配置对象放在了yaml中,不得不说,我们常见的配置,无非就是一些字符串,数字,数组而已
redis也是有连接池的,我们知道连接池的目的就是更快的获取一个链接
redis中的数据怎么和java对象互相转换
这是我们最应该关注的一点,spring早已经为我们提供了好多序列化实现类,而且还有一个问题需要我们注意,就是redis的key和value的序列化是分开的,因为我们的key一般就是字符串,所以不用太复杂的序列化,而且因为是字符串,我甚至都觉得不用序列化,但我们的value就不一样了
数据类型
String 字符串
不仅仅是字符串,还可以是整数和浮点数,而且看也对数字进行一定的运算
List 列表
可以排序,内部的泛型是String,可以说这个列表是支持普通列表,队列和栈
Set 集合
无序,高效,随机读取,交并补差的集合运算
Hash 哈希散列表
类似于java的Map
Zset 有序集合
可以根据一定的范围获取对应的元素
HyperLogLog 基数
计算重复的值,以确定存储的数量
常用命令
它提供了一个类似save_or_update的功能,如果存在则返回旧值,否则返回null,其实本质就是返回原来key的值,这个命令可以获取以前的值,也许我觉得这个命令执行的效率要比,获取值后再覆盖更好一些
而且我发现这些数据库,甚至包括MySQL和redis,他们都为那些常见的需要查而后改的变成了一条指令的一次性操作,最直观的就是a=a+1
哈希适合于存储对象,但我觉得不仅仅适合于存储对象,它完全可以成为更灵活的数据结构,简而言之,它可以对我们存储的string类型分类
和类型有关的操作指令,比如加一个整数或者加一个小数,在java中这些是能区别出类型的,所以spring能够帮助我们屏蔽这些底层的差异,这也就是强类型语言的好处
而且redisTemplate可以和java的list和map相结合,来帮助我们简化一些操作
redis的列表,底层是双向链表,显然,它查找的效率是不高的,而且会遇到并发冲突的问题,不过redis在底层上也为我们提供了加锁的操作
如果我们要操作的数据量比较大,那么我们可以进行分批操作,不然我们的系统出现卡顿的现象
不得不说,还是java的全名称api看起来比较方便,而且很容易就能掌握这些api的使用,然后就是比如redis中需要用字符串来指定的操作比如是在列表某个节点的左边还是右边插入,而spring为我们直接提供了字符串常量,你说和原来的名称一样更容易学,不,我spring告诉你,这样的命名根本就不用学
HyperLogLog可以为一个重复的集合确定存储的空间,我觉得这个其实没什么用,我觉得真正有用的是,它作为布隆过滤器存在,它可以判断我们添加的元素是否存在,我们当然可以用集合,但是,如果我们集合的内存非常大呢,比如说一个人不能重复的点赞,不能重复的增加一篇文章的阅读量,问一下,这个就算出错了,会对系统造成影响吗,尤其是阅读量,没有人会在乎那一点点的误差,而且我们这个可以作为预判断,进行短暂的缓存
互联网考虑的就应该是性能和数据一致性,而我们的redis兼而有之,甚至,分布式锁就是用这个来实现的
常用技术
事务
注意一下,只有在一个redis链接中,才会有事务的一致性
流程:开启事务->命令进入队列->执行事务
redis可以指定监视键,如果其中某个键的值被修改了,那么就回滚整个事务,你仔细想这个思路,就是你可以访问我这个值甚至可以修改它,但是一旦检查出修改,我们就回滚,这么说,好像是没有锁的唉,而且我在想,会不会出现两个人同时不断的修改某个数据,然后互相直接造成冲突,导致谁也修改不了
再补充一些细节,开始的命令的时候,所有的命令都会进入一个队列,而这些命令是不会执行的,在提交事务的时候,才会统一的执行,但是呢,之后的命令是无法继续继续插入的
SessionCallBack是spring用来执行事务的方法,你要注意一点,就是前面的命令,是不会执行的,所以我们不能获取这个事务刚刚设置的值必须执行提交事务exec才可以
如果我们命令的格式错误,那么我之前和之后的命令都会被事务回滚,不过在java代码中,这是不可能的,我们要关注的就是,比如数据结构的错误,它之前和之后的事务是可以正常执行的,也就是说,这些你人为犯的逻辑错误,redis为了性能考虑是不会负责的,所以我们要在代码层面做好逻辑检查
watch
一般是在multi开启事务的时候,执行watch命令来指定监听的某些键值对
CAS(Compare And Swap),这个其实就是我们说的乐观锁。也就是说,但线程开始读取数据的时候,它会把监控的值,保存在副本中,这些值被我们称为旧值,这个是和watch有关的
ABA问题
ABA就是我们一开始获取的时候,值是A,然后我们开始用这个值进行运算,但是运算过程中,被另一个线程改为B,这时候就开始出错了,本来这种情况我们最后核对,发现这个值不是A,我们是能回滚这个出错的事务,但是,不巧的时候,这个值被其他线程改为A了,所以我们在核对的时候,是没有回滚事务的
怎么避免,乐观锁告诉我们,用一个version,每次操作一次,就对version+1,如果version的版本和我们想想的不一致,那说明有其他线程对我们的值进行了修改
所以redis在执行并发的过程中,并不会阻塞其他连接的并发,而且redis的事务机制是不需要处理ABA问题的,它是不会出现ABA
问题的,比如我监控的值是A,那么我开始执行,然后被另一个命令改成B了,这个时候,如果我执行的和A无关了,那么被其他人修改了也没关系,但是我真要执行的与A相关的操作,我发现被人修改成了B,那么我肯定是要回滚事务的,所以是不会存在ABA问题的
多线程的问题,有的时候你一下子想不明白,可以动手写一下执行的流程。
pipelined 流水线
multi和exec是有系统开销的,因为会涉及到锁的检查和序列化。那怎么在没有附加条件的情况下批量执行呢。平时我们的命令是一条一条被送到redis服务器执行的,我们可以通过pipelined方法,一次性送多条命令过去,我们可以在最后把所有的结果返回给一个list不过要注意一下对内存的占用,一般是涉及批量读写的时候要这么做
发布订阅
channel 就是我们要订阅的名称,在spring中,通过实现接口MessageListener
来实现监听的方法,发送消息的话,我们直接通过redis.convertAndSend(“通道名称”,“消息内容”);
超时命令
这个主要是为了内存,内存实在太宝贵了,我们简单聊一下java的垃圾回收,它会在内存占用满的时候,启动垃圾回收的机制,帮助我们空出更多的内存,同时它也提供了一个方法,System.gc()
来回收垃圾,但这个仅仅是一个对虚拟机的建议,因为垃圾回收过程中,对性能是会产生影响的,比如我的一个线上项目,自习室预约系统,它就会在垃圾回收期间,这个时候访问的话,反馈会变慢的
同时,redis还支持对key设置超时时间,具体而言,就是定时删除key
在用java8的lamba表达式的时候,你是可以在里面指定类型的,这样的情况下,就可以使用这个参数的类型,不要在里面再做一次转换了
redis提供了两种回收的机制,一种是定时回收,一般选择在没有访问的时间段,把那些比较大的内存回收掉,因为回收大内存的时候,是非常耗时的,然后就是惰性回收,这种回收,会在get的时候判断,如果超时,你就获取不到了,然后我把这个key来删掉
Lua 脚本
这个还支持执行一个语言的脚本,我觉得还是很牛的,暂时先了解一下
Redis 配置
持久化
我redis也是有持久化的,有两个方法,一种是对内存的快照,它在保存的时候慢,启动的时候快。另一种是AOF,追加程序输入的命令,就是说,保存的时候快,但是启动的时候要一条条执行命令,即使有些命令是重复的删除和添加一个值,redis也会不断的执行,所以启动比较慢,而我们的redis可以让你以任意的方法来搭配这两种方法,你可以两个都用,甚至一个都不用
save 900 1 这个配置的含义就是当900s执行一个写的命令,就启用快照备份一下, 而且redis在save的时候,是不容许写入命令的,所以我们要特别注意这一点,而且save可以配置不止一条,也就是说我们可以同时配置多个出发条件
但是呢,bgsave是不会影响写入的,因为它启动了另一个进程来执行写的命令,但是一但它保存失败了,就会停止接受写的操作,以这种强硬的方法告诉用户,所以这个最好不要启用
rdb文件是redis是持久化数据文件,rdbchecksum是配置是否对rdb文件进行检查
appendonly no 就是不采用AOF 的方法持久化
appendfsync 配置always,就是每执行一条命令在文件中追加一次,很明显,性能消耗太大了,但是配置everysec可以保证这一点,它每秒钟追加一次,当然也可以配置成no,让它不追加
内存回收策略
- volatile-lru:只会淘汰超时的,而且是最近使用最少的
- allkeys-lru:和上面的一样,不过不仅仅是超时的,所以没有设定超时的,但是因为redis的内存满了,所以要删除一些不常用的
- (volatile/allkeys)-random:随机删除,我觉得好奇怪
- volatile-ttl:随机删除存活时间最短的,还是有一定道理的
- noeviction:我不删除,如果满了,就只能读,但是不能写,写的话会返回错误,这种应该就是要在某些特殊情况下使用的吧(没想到这个竟然是redis的默认配置,也就是说,redis一般是不会删除你的数据)
补充一点,redis中这些都是近似算法,就是它只会在指定的几个数量之间比较,我们可以指定maxmemory-samples的值来确定,但是如果这个值太大的话,就容易造成我们一次性扫描的太多,性能不好,如果太小,那我们扫描的可能就不精确
备份
如果我们能把一个redis的数据备份到多台机器上,这样当我们一台redis出错的时候,其他的redis能立马补充上来作为灾备。同时,这样也能支持读写分离,这个很在于数据库的一致性问题
主从同步
我再想,三级标题是不是更好,我下次会把这些弄成三级标题的,主要是为了方便定位某些重要的概念
多台数据库服务器,只有一台主服务器,而主服务器只负责写入数据,不负责让外部程序读数据
从服务器有多个,但是他们不写,只是负责同步主服务器的数据,并让外部程序读取数据
主服务器写入数据成功后,将这些命令发送给从服务器,然后实现数据同步
当我们读取数据的时候,找其中的一台服务器读取数据,这样就分摊了数据的压力
从服务器不能工作的话,是不受任何影响的,如果主服务器不能工作的话,因为从服务器的数据和它一样,所以我们在从服务器里面选择一个作为新的主服务器
redis的主从同步
上面的只不过是一个大体的流程,不同的应用有自己独特的地方,我们来了解一下redis的主从同步
slaveof server port
server:主机,port:端口,当从机启动的时候,就会主动同步主机的数据
如果你配置的是127.0.0.1那么是只容许主机访问的,所以,你需要把它配置成0.0.0.0这样,其他机器也会访问
过程
首先要保证主服务器的开启
从服务器启动,读取同步的配置,决定是否使用当前的数据响应给客户端
然后从服务器发送SYNC命令,当主服务器接收到同步命令的时候,就会执行bgsave命令备份数据,但是主服务器并不会拒绝客户端的读写,而是将这些命令写入了缓存区,同时,对于从服务器来说,在没有接受到这个bgsave文件的时候,它会根据上面的配置,决定是否响应客户端
当bgsave命令执行完,会把文件发给从服务器,这个时候,从服务器会丢弃自己现有的数据,然后开始载入快照文件
然后主服务器会把刚刚在缓存区的写命令也发送给从服务器,在从服务器完成快照文件的载入后,就执行这些命令,执行完成后,就开始等待命令的写入
此后,每当主服务器执行一条写命令,就会往从服务器发送同步写入命令,这样就能保持数据的一致了
其他
主从同步的时候,我们要预留一些内存空间,但是如果是多台同步,可能会出现频繁等待和频繁操作bgsave命令的情况,所以我们要考虑用主从链进行同步
没看懂这个图,先鸽了
哨兵模式
Sentinel,哨兵模式就是主机失败后,会自动选一个新的主机出来。
原理
哨兵会独立运行,它是一个独立的进程,然后它会通过发送命令,等待Redis服务器的响应,从而监控多个Redis实例,用好一点的词语来表达的话,就是心跳检查
哨兵会向主从机都询问状态,如果哨兵发现主机宕机了,它就会把其中一个slave切换成master,所以哨兵会向slave询问状态,然后哨兵会利用redis的发布订阅模式,来修改从机的配置文件,让他们选新的主机
要是哨兵宕机怎么办?其中在互联网中,这种很好解决,弄多个哨兵就行了
哨兵集群
如果一个哨兵发现主机宕机,是不管用的,在微服务中这是很常见的,你不能因为我憋一下气,就认为我不会出气,就把我埋了。我记得在微服务中,服务注册中心用的是,认为你死了之后,先等一等,如果你长时间没有复活的话,才会把你踢出去。而熔断机制那边则是,隔一段时间,发一条请求过来,看看你是否还活着,而且这些请求,也最好不要是实际用户的请求,只要能检查出状态即可。
那一个哨兵发生主机不可用,会怎么办呢,它会告诉其他的哨兵,让他们一起检查这个主机,然后发起投票,当一定数量的哨兵认为它不可用的时候,就会触发实际的failover
故障切换,这种叫做客观下线
,而前面那个则是主观下线
就是你一个人的判断并不能说是准确的,只是一种主观的判断而已
配置实战
从服务器
bind 0.0.0.0 #使得别人也可以访问redis
requirepass "密码" #这个是因为需要外界访问,配置的密码
slave 主服务器ip 主服务器端口
masterauth 主服务器密码
sentinel.conf 哨兵配置
sentinel monitor 自定义的服务器名称 主服务器ip 主服务器端口 投票切换的最少数量
sentinel auth-pass 服务名称:就是你上面定义的那个 密码:主服务器的密码
启动过程
先是启动主机,然后是启动从机,最后是哨兵,这个貌似输入配置文件的地址就行
其他
哨兵投票成功后,默认3分钟后切换主机
与spring相结合
redis和数据库的一致性
一致性是相当重要的,这也就是我使用缓存的时候,最担心的问题!
书的作者说,如果我们对某个数据的实时性要求不高,容许有一定的延迟,可以redis为主,但是如果要求比较高,那么就得保证读写数据库然后更新缓存了
读操作
如果我们的数据是读,而且对实时性要求不高,我们可以优先读redis,然后redis有个自动过期时间,一旦过期了,我们才去读数据库获取最新的记录,然后再把它写在redis里面,也就是说,刚刚redis读取的数据可能不是最新的
写操作
先从数据库读最新的数据,然后完成业务操作,完成后更新业务数据到数据库,然后再将数据刷新到reids缓存,这样就能避免脏数据写进数据库里面
整合spring缓存机制
首先要注意一点,你要序列化的这个类,必须要实现Serializable
接口,所以你在用redis的时候,要特别注意你这个类是否实现了接口,最好在自己写的工具类上,明确的指出,必须是Serializable
的实现类,这样编译器就会帮助我们来避免这样的错误了
配置有点多,我去搜索一下spring boot的
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
开启缓存
@EnableCaching
简单使用
@Cacheable(value = "emp" ,key = "targetClass + methodName +#p0")
public List<NewJob> queryAll(User uid) {
return newJobDao.findAllByUid(uid);
}
spring boot就是这么简单,说明一下Cacheable,这里的value是必须的,它是你缓存的上下文,然后key是spring的表达式,它的目的是想指定,根据方法入参的不同,而自动为我们指定缓存,而我扫描了一眼spring的配置,我发现大部分都对象的组装和常量配置,所以spring boot yyds
缓存注解
@Cacheable和@CachePut都可以保存键值对,但是要求方法必须要有返回值,而@CacheEvict则可以用在void方法上,因为它不需要保存任何值
下面的注解可以标准到类或者方法之上,一般都在方法上
@Cacheable 查询
进入方法之前,spring会先去缓存服务器里面查找对应key的缓存值,如果找到就不会调用对应的方法,直接返回缓存给调用者,如果没有找到,就执行方法,并且讲结果放到缓存服务器中
key,缓存的键名称,可以根据参数的不同,而一一对应起来,不得不说,spring确实牛逼,能把缓存这种我感觉有一定复杂度的抽取出来,让我能够专注与业务逻辑的修改,一般用#id或者#user.id都是支持的
value,命名隔离空间
@CachePut 插入和修改
和上面有所不同的是,spring无论如何都要执行实际的方法,然后在放到缓存里面
插入和修改完也是能做缓存的,不一定是非要查询的时候才做,修改的时候,你的id可以从对象里面获取,那添加的时候怎么办?
@CacheEvict 删除
移除缓存对应的key值
可以通过condition判断删除成功与否的条件,然后再删除缓存
@Caching 不常用
分组注解,能够同时应用在其他缓存的注解上面。
其他
从别人项目学到的东西,可能和redis无关
mybatis-plus:
typeAliasesPackage: com.xxx.xxx.model
这个可以让我们在mybatis的xml里面少写东西
其他也没什么好学的,我以后会写开源项目学习的笔记,欢迎关注我