技术标签: spring boot java 【Spring Boot2.X】
上篇博文,我们深入的介绍了SpringBoot整合Redis的相关内容,处理缓存我们使用RedisTemplate或者StringRedisTemplate结合场景选择不同的数据结构,会造成缓存代码和业务代码会紧耦合在一起。有没有更加简便的方式呢?
答案:有,SpringCache。
这篇博文,我们介绍,SpringCache,以及SpringCache是如何来统一不同的缓存技术以高效便捷的方式接入到项目中,最后,深入讲解SpringCache是如何解决缓存击穿,缓存穿透,缓存雪崩的,还有哪些不足。
Spring Data Redis对Redis底层开发包(Jedis, JRedis, and RJC)进行了高度封装,RedisTemplate提供了redis各种操作、异常处理及序列化,支持发布订阅,并对spring 3.1 Cache进行了实现。
SpringCache并非某一种Cache实现的技术,SpringCache是一种缓存实现的通用技术,基于Spring提供的Cache框架,让开发者更容易将自己的缓存实现高效便捷的嵌入到自己的项目中。当然,SpringCache也提供了本身的简单实现NoOpCacheManager、ConcurrentMapCacheManager 等。通过SpringCache,可以快速嵌入自己的Cache实现。
SpringCache是缓存的上层封装,RedisCache是底层实现,这篇博文,我们就结合Redis来实现分布式缓存。我们以缓存用户数据为例,来实现我们的案例。建表语句以及mybatis的相关内容在源码中都有,我们就一一展示了,大家可以在源码中查看,项目整体目录如下图所示:
<!--引入缓存场景-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!--使用redis作为缓存中间件-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
使用SpringCache其实特别简单,就跟把大象装进冰箱一样,就两步。
1、开启缓存功能@EnableCaching
@EnableConfigurationProperties(CacheProperties.class)
@Configuration
@EnableCaching
public class CacheConfig {
//其他内容,暂时略过
}
2、使用注解完成缓存操作
@Repository
@CacheConfig(cacheNames = "users")
public class UserDao implements IUserDao{
@Autowired
private UserMapper userMapper;
@Cacheable(key = "'getTotalCount'")
@Override
public int getTotalCount(){
int totalCount = userMapper.getTotalCount();
return totalCount;
}
@Cacheable(key = "#userId")
@Override
public User getUser(Integer userId){
return userMapper.getUser(userId);
}
@Caching(evict = {
@CacheEvict(key = "'getUsers'"),
@CacheEvict(key = "'getTotalCount'")
})
@Override
public void insertUser(User u){
userMapper.insertUser(u);
}
@Cacheable(key = "'getUsers'")
@Override
public List<User> getUsers(){
return userMapper.getUsers();
}
@Caching(evict = {
@CacheEvict(key = "'getUsers'")
})
@Override
public void updateUserNameById(Integer userId, String name){
userMapper.updateUserNameById(userId, name);
}
@Caching(evict = {
@CacheEvict(key = "'getUsers'"),
@CacheEvict(key = "'getTotalCount'"),
@CacheEvict(key = "#userId")
})
@Override
public void deleteUser(Integer userId){
userMapper.deleteUser(userId);
}
/**
* 调用方法,有更新缓存的数据 修改数据库的数据同时更新新缓存。
*/
@Caching(evict = {
@CacheEvict(key="'getUsers'")
},put = {
@CachePut(key = "#result.id")})
@Override
public User updateUser(User user){
userMapper.updateUser(user);
return user;
}
}
测试用例,都在源码示例。
server:
port: 8084
spring:
application:
name: springboot-cache
datasource:
url: jdbc:mysql://localhost:3306/user_db_test
username: root
password: admin123
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
redis:
# Redis服务器地址
host: localhost
# Redis服务器连接端口
port: 6379
# Redis服务器连接密码(默认为空)
password:
# Redis数据库索引(默认为0)
database: 0
# 连接超时时间(毫秒)
timeout : 300
client-type: lettuce #切换jedis客户端,改成jedis
lettuce: #切换jedis客户端,改成jedis
pool:
# 连接池最大连接数(使用负值表示没有限制)
max-active: 8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1
# 连接池中的最大空闲连接
max-idle: 8
# 连接池中的最小空闲连接
min-idle: 0
cache:
type: redis
redis:
#是否缓存空值,防止缓存穿透
cache-null-values: true
#缓存过期时间(单位为毫秒)
time-to-live: 100000
#缓存前缀,用于区分其他缓存,不指定前缀,默认使用缓存的名字作为前缀
# key-prefix: CACHE_
#是否使用缓存前缀,false不使用任何缓存前缀
# use-key-prefix: false
# 配置mybatis规则
mybatis:
config-location: classpath:mybatis/mybatis-config.xml #全局配置文件位置
mapper-locations: classpath:mybatis/mapper/*.xml #sql映射文件位置
@Cacheable
: Triggers cache population.(将数据保存到缓存操作)@CacheEvict
: Triggers cache eviction.(将数据从缓存删除操作;失效模式)@CachePut
: Updates the cache without interfering with the method execution.(不影响方法执行更新缓存;双写模式)@Caching
: Regroups multiple cache operations to be applied on a method.(组合以上多个缓存操作)@CacheConfig
: Shares some common cache-related settings at class-level.(在类级别共享缓存的相同配置)@Cacheable
代表当前方法的结果需要缓存,如果缓存中有,方法不用调用。如果缓存中没有,会调用方法最后将方法的结果放入缓存。
value:每一个需要缓存的数据我们都来指定要放到哪个名字的缓存。【缓存的分区(按照业务类型分)】。如果指定缓存前缀 spring.cache.redis.key-prefix=CACHE_
, @Cacheable(value={"user"})
中的 value会失效!
key:缓存对象存储在Map集合中的key值,缺省按照函数的所有参数组合作为key值,若自己配置需使用SpEL表达式;注意:使用的SpEL表达式,字符串一定要加单引号。
condition:额外添加缓存的条件,满足条件的数据才会被缓存。语法为SpEL。
unless:配置哪些条件下的记录不缓存。语法为SpEL。
sync:加同步锁的同步获取,更新操作。
默认行为:
@CacheEvict
删除缓存,【失效模式】
allEntries:表示是否需要清除缓存中的所有元素。默认为false,表示不需要。当指定了allEntries为true时,Spring Cache将忽略指定的key。有的时候我们需要Cache一下清除所有的元素,这比一个一个清除元素更有效率。
befareInvocation:清除操作默认是在对应方法成功执行之后触发的,即方法如果因为抛出异常而未能成功返回时也不会触发清除操作。使用beforeInvocation可以改变触发清除操作的时间,当我们指定该属性值为true时,Spring会在调用该方法之前清除缓存中的指定元素。
@CachePut
根据返回值更新缓存,【双写模式】
@Caching
组合多个缓存操作;
@Caching
允许在同一方法上使用多个嵌套的 @Cacheable
、@CachePut
和@CacheEvict
注释
SpEL
表达式每个SpEL
表达式都有一个专门的context
。除了采用参数构建表达式,框架提供了专门的与caching相关的元数据,比如参数名。下表列出了在context中可用的参数,你可以用来当做key和conditional 处理。
Name | Location | Description | Example |
---|---|---|---|
methodName | root object | 被执行的method的名字 | #root.methodName |
method | root object | 被执行的method | #root.method.name |
target | root object | 执行的对象 | #root.target |
targetClass | root object | 执行对象的class | #root.targetClass |
args | root object | 执行对象的参数们(数组) | #root.args[0] |
caches | root object | 当前method对应的缓存集合 | #root.caches[0].name |
argument name | evaluation context | 任意method的参数。如果特殊情况下参数还没有被赋值(e.g. 没有debug信息),参数可以使用#a<#arg>来表示,其中#arg代表参数顺序,从0开始 | #iban或者#a0(也可以使用#p0或者#p<#arg>注解来启用别名) |
result | evaluation context | method执行的结果(要缓存的对象),仅仅在unless表达式中可以使用,或者cache put(用来计算key),或者cache evict表达式(当beforeInvocation=false). 为了支持wrapper,比如Optional,#result指向世纪的对象,不是wrapper. | #result |
自定义序列化方式,缓存的前缀,默认使用分区名,缓存的过期时间,是否缓存空值等。
@EnableConfigurationProperties(CacheProperties.class)//开启属性配置绑定功能
@Configuration
@EnableCaching //开启缓存启动类的注解从启动类移到这里
public class CacheConfig {
/**
* 配置文件中的东西没有用上;
* 1、原来文件中的东西没有用上
* @ConfigurationProperties(prefix = "spring.cache")
* public class CacheProperties {
*
* 2、要让他生效
* @return
*/
@Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory,CacheProperties cacheProperties){
//缓存配置对象
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
redisCacheConfiguration = redisCacheConfiguration
//序列化方式:new GenericJackson2JsonRedisSerializer();
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new FastJsonRedisSerializer<>(Object.class)));
CacheProperties.Redis redisProperties = cacheProperties.getRedis();
if (redisProperties.getTimeToLive() != null) {
redisCacheConfiguration = redisCacheConfiguration.entryTtl(redisProperties.getTimeToLive());
}
if (redisProperties.getKeyPrefix() != null) {
redisCacheConfiguration = redisCacheConfiguration.prefixCacheNameWith(redisProperties.getKeyPrefix());
}
if (!redisProperties.isCacheNullValues()) {
redisCacheConfiguration = redisCacheConfiguration.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
redisCacheConfiguration = redisCacheConfiguration.disableKeyPrefix();
}
return RedisCacheManager
.builder(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory))
.cacheDefaults(redisCacheConfiguration).build();
}
}
1、自动配置
CacheAutoConfiguration会导入CacheProperties。CacheProperties用于配置缓存的基本属性。通过Import导入CacheConfigurationImportSelector,通过用户设置缓存类型,导入响应的缓存配置。
2、配置使用Redis作为缓存
会自动导入RedisCacheConfiguration;RedisCacheConfiguration自动配好了缓存管理器RedisCacheManager,RedisProperties。
为了系统性能的提升,我们一般都会将部分数据放入缓存中,加速访问,而db承担数据落盘工作。但是在现实业务中,缓存场景按照读写分,可以分成读环境场景和写缓存场景,各自又有需要注意的问题。
哪些数据适合放入缓存?
描述:
缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,请求会直接打到数据库上,并且查不到数据,没法写缓存,所以下一次同样会打到数据库上。
此时,缓存起不到作用,请求每次都会走到数据库,流量大时数据库可能会被打挂。此时缓存就好像被“穿透”了一样,起不到任何作用。
解决方案:
描述:
缓存击穿是指某一个热点key,缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力
解决方案:
设置热点数据永远不过期。直接将缓存设置为不过期,然后由定时任务去异步加载数据,更新缓存。
这种方式适用于比较极端的场景,例如流量特别特别大的场景,使用时需要考虑业务能接受数据不一致的时间,还有就是异常情况的处理,不要到时候缓存刷新不上,一直是脏数据,那就凉了。
加互斥锁。该方式和缓存击穿一样,按 key 维度加锁,对于同一个 key,只允许一个线程去计算,其他线程原地阻塞等待第一个线程的计算结果,然后直接走缓存即可。。在并发的多个请求中,只有第一个请求线程能拿到锁并执行数据库查询操作,其他的线程拿不到锁就阻塞等着,等到第一个线程将数据写入缓存后,直接走缓存。
描述:
大量的热点 key 设置了相同的过期时间,导在缓存在同一时刻全部失效,造成瞬时数据库请求量大、压力骤增,引起雪崩,甚至导致数据库被打挂。
缓存雪崩其实有点像“升级版的缓存击穿”,缓存击穿是一个热点 key,缓存雪崩是一组热点 key。
解决方案:
对于读取缓存步骤一般没有什么问题,但是一旦涉及到数据更新:数据库和缓存更新,就容易出现缓存(Redis)和数据库(MySQL)间的数据一致性问题。缓存不一致有两种模式:双写模式、失效模式。
写数据库的,同时写缓存。
问题1:单线程,更新数据成功,更新缓存失败,导致数据出现不一致。
问题2:多线程,由于卡顿问题,导致写缓存2在最前,写缓存1在后面出现不一致。
如下图:
不管是先写MySQL数据库,再删除Redis缓存;还是先删除缓存,再写库,都有可能出现数据不一致的情况。
问题1:数据发生了变更,先删除缓存,然后要修改数据库,此时还没有修改。一个请求过来,去读缓存,发现缓存为空了,去查询数据库,查到了修改前的旧数据,放到缓存中。随后数据变更的程序完成了数据库的修改。此时出现数据不一致的情况。
如下图:
无论是双写模式,还是失效模式,都会导致缓存的不一致问题。类似的问题,如何处理呢?
1、缓存数据本就不应该是实时性,一致性要求超高的。所以缓存数据的时候加上过期时间,保证每天拿到当前最新数据即可。
2、遇到实时性,一致性要求高的数据,就应该查数据库,即使慢点。
3、通过加锁保证并发读写,写写的时候按顺序排好队,读读无所谓。所以适合使用读写锁。
4、如果现实业务场景中确实有需要,可以参考终极解决方案。
异步更新缓存(基于订阅binlog的同步机制)
技术整体思路:
MySQL binlog增量订阅消费+消息队列+增量数据更新到redis
1)读Redis:热数据基本都在Redis
2)写MySQL:增删改都是操作MySQL
3)更新Redis数据:MySQ的数据操作binlog,来更新到Redis
这样一旦MySQL中产生了新的写入、更新、删除等操作,读取binlog后分析 ,利用消息队列,推送更新各台的redis缓存数据。
本文示例读者可以通过查看下面仓库中的项目,如下所示:
<module>springboot-cache</module>
如果您对这些感兴趣,欢迎star、follow、收藏、转发给予支持!
作者:程序猿小亮
博主写作不易,加个关注呗
求关注、求点赞,加个关注不迷路,感谢
点赞是对我最大的鼓励
↓↓↓↓↓↓
文章浏览阅读1.1k次。开学以来,由于课程压力比较大,加上自己在参加一些项目,未能及时更新博客。深有荒废之感,只能多积累,暂时只输入,不输出,等下学期时间充足了再多写博客。转载自:http://blog.csdn.net/playoffs/article/details/7588597推荐几个机器学习和数据挖掘领域相关的中国大牛:李航:http://research.microsoft.com/e_数据挖掘算法中国人
文章浏览阅读1.5k次。当利用归纳法证明一个商界时,实际上证明一个更弱的上界可能会gengnan_通过减去一个低级项完成代入法证明
文章浏览阅读3.6k次。一、MyEclipse: 1、先找到xml文件对应的DTD文件 (一般的文件头都可以找到该信息) (web.xml 的 dtd 约束文件在servlet.jar里面;struts和hibernate都在自己的jar包里面) 2、window->preference->Myeclipse Enterprise->Files _esclipse 添加xml约束
文章浏览阅读4.7k次,点赞2次,收藏3次。与 用在网页上都能式字体加粗,二者的不同是:是物理元素 ;是逻辑元素。 物理元素强调的是一种物理行为。比如说,把一段文字用b加粗,意识是告诉浏览器应该加粗显示,没有其他作用。而可以从字面理解知道他是强调的意识,是逻辑标签,强调文档逻辑。 对于搜索引擎(SEO)来说,比重视的多。和都是斜体,但是是逻辑元素,是物理元素。_sonar 和
文章浏览阅读387次。案例:根据用户的通知设置去通知用户,设置接收Email的用户只接收Email,设置接收sms的用户只接收sms,设置两种 通知类型都接收的则两种通知都有效。生产者:package com.xuecheng.rabbitmq.producer;import com.rabbitmq.client.BuiltinExchangeType;import com.rabbitmq.client..._rabbitmq 通配符模式 唯一
文章浏览阅读367次。起因业务需求要集成Paypal,实现循环扣款功能,然而百度和GOOGLE了一圈,除官网外,没找到相关开发教程,只好在Paypal上看,花了两天后集成成功,这里对如何使用Paypal的支付接口做下总结。Paypal现在有多套接口:通过Braintree(后面会谈Braintree)实现Express Checkout;创建App,通过REST Api的接口方式(现在的主流接口..._java查询paypal订阅是否续费
文章浏览阅读1.3k次。区别一:在Java中字符串使用String类进行表示,但是String类表示字符串有一个最大的问题:“字符串常量一旦声明则不可改变,而字符串对象可以改变,但是改变的是其内存地址的指向。”所以String类不适合于频繁修改的字符串操作上,所以在这种情况下,往往可以使用StringBuffer类,即StringBuffer类方便用户进行内容修改,区别二:在String类中使用“+”作为数据的连接操作,而在StringBuffer类中使用append()方法(方法定义:public StringBuffer a_string与stringbuffer最大的区别
文章浏览阅读1.5w次,点赞2次,收藏5次。页面初始化DataTable的时候,请求参数往往是封装在dataTable里面的,刷新表格数据的时候重新注入参数会很麻烦。版主碰到这个问题后,试过了很多方法,最终还是在官方API例找到了例子,即draw()方法。具体代码如下:draw()var table = $('#example').DataTable();_dtable刷新
文章浏览阅读803次。Description You are given two integers: n and k, your task is to find the most significant three digits, and least significant three digits of nk.Input Input starts with an integer T (≤ 1000), denoti_求前三位pow(10,)
文章浏览阅读140次。I'm having a strange time dealing with selecting from a table with about 30,000 rows.It seems my script is using an outrageous amount of memory for what is a simple, forward only walk over a query res..._pdo_mysql.cache_size
文章浏览阅读1.0k次。最近在使用layui的过程中,遇到了表格合并单元格,设置不同底色的问https://www.hixiaoe.com/题。在此总结,大家一起学习。效果如下:同一组新闻的底色相同实现代码:<script> layui.config({ base: '/static/' //静态资源所在路径 }).extend({ index: 'admin/lib/index' //主入口模块 .._layui tablemerge 合并背景
文章浏览阅读3.8k次,点赞10次,收藏18次。Kali Linux系统作为白帽、黑帽最受欢迎的渗透测试系统,你如果是一个安全渗透专家或者网络安全管理员,必须要学会慎重并且合理地利用这个系统,因为对目标系统造成的实质伤害会带来法律的约束以及制裁!1、Kali Linux下载官网下载镜像:下载链接:https://www.kali.org2、安装配置我这里使用的虚拟机软件是 VMware 15,名字随便看需要,也可按默认配置建议选4G,也可以选2G内存默认,下一步默认,下一步默认,下一步默认,下一步这里建议将磁_kali2020.3安装