1. 缓存管理器和配置
Spring在使用缓存注解前,需要配置缓存管理器,缓存管理器将提供一些重要的信息,如缓存配型,超时时间等。Spring可以支持多种缓存的使用,因此它存在多种缓存处理器,并提供了缓存处理器的接口CacheManage和相关类:
这是缓存相关的全部配置
spring:
cache:
# 缓存相关配置
cache-names: test_cache
# 如果有底层的缓存配置管理器支持创建
caffeine:
# caffeine 缓存
spec: x
# 配置细节
couchbase:
# couchbase 缓存
expiration: 0ms
# 超时时间,默认永不超时
ehcache:
# ehcache 缓存
config: x
# 配置ehcache缓存初始化文件路径
infinispan:
# infinispan 缓存
config: x
# 配置infinispan缓存配置文件
jcache:
# jcache 缓存
config: x
# jcache缓存配置文件
provider: ""
# jcache缓存提供者配置
redis:
# redis缓存
cache-null-values: true
# 是否允许缓存空值
key-prefix: x
# redis的键的前缀
time-to-live: 0ms
# 缓存超时时间,配置为0则表示永不超时
use-key-prefix: true
# 是否启用Redis的键前缀
type: redis
# 缓存类型,默认情况下,Spring会自动根据上下文探测
Spring支持的缓存类型有这些
完整的配置如下:
spring:
# redis 服务器配置
redis:
host: 10.0.228.117
port: 6379
password: ""
timeout: 60000
database: 0
# 连接池属性配置
lettuce:
pool:
# 最小空闲连接数
min-idle: 5
# 最大空闲连接数
max-idle: 10
# 最大活动的连接数
max-active: 10
# 连接最大等待数
max-wait: 3000
cache:
# 缓存相关配置
cache-names: test_cache
# # 如果有底层的缓存配置管理器支持创建
# caffeine:
# # caffeine 缓存
# spec: x
# # 配置细节
# couchbase:
# # couchbase 缓存
# expiration: 0ms
# # 超时时间,默认永不超时
# ehcache:
# # ehcache 缓存
# config: x
# # 配置ehcache缓存初始化文件路径
# infinispan:
# # infinispan 缓存
# config: x
# # 配置infinispan缓存配置文件
# jcache:
# # jcache 缓存
# config: x
# # jcache缓存配置文件
# provider: ""
# jcache缓存提供者配置
redis:
# redis缓存
cache-null-values: true
# 是否允许缓存空值
key-prefix: x
# redis的键的前缀
time-to-live: 0ms
# 缓存超时时间,配置为0则表示永不超时
use-key-prefix: true
# 是否启用Redis的键前缀
type: redis
# 缓存类型,默认情况下,Spring会自动根据上下文探测
除了在配置文件中配置,还需要启用缓存机制
2. 实例程序框架搭建
实例集成了oracle数据库,MyBatis,Redis,缓存和日志。
增加依赖:
3. 配置
配置:
spring:
# redis 服务器配置
redis:
host: 10.0.228.117
port: 6379
password: ""
timeout: 60000
database: 0
# 连接池属性配置
lettuce:
pool:
# 最小空闲连接数
min-idle: 5
# 最大空闲连接数
max-idle: 10
# 最大活动的连接数
max-active: 10
# 连接最大等待数
max-wait: 3000
cache:
# 缓存相关配置
cache-names: redisCache
redis:
# redis缓存
cache-null-values: true
# 是否允许缓存空值
key-prefix: x
# redis的键的前缀
time-to-live: 0ms
# 缓存超时时间,配置为0则表示永不超时
use-key-prefix: true
# 是否启用Redis的键前缀
type: redis
# 缓存类型,默认情况下,Spring会自动根据上下文探测
datasource:
# 配置数据库
driver-class-name: oracle.jdbc.OracleDriver
# 数据库驱动
url: jdbc:oracle:thin:@//10.0.250.19:1521/starbass
# 数据库连接
username: system
# 用户名
password: system
# 密码
tomcat:
# 数据库相关配置
max-idle: 10
# 最大闲置的连接数量
max-active: 10
# 最大活跃连接数量
min-idle: 5
# 最小闲置数量
max-wait: 2000
# 最大等待时间
mybatis:
# mybatis配置
mapper-locations: classpath:com/study/redishello/mapper/*.xml
# mybatis的文件目录
type-aliases-package: com.study.redishello.pojo
# mybatis实体的包路径
logging:
# 日志配置
level:
root: debug
# 日志级别:调试
org:
springframework: debug
org:
mybatis: debug
4. 创建实体
@Alias("user")
@Data
public class User implements Serializable {
private Long id;
private String userName;
private String note;
}
@Data是lombok的注解,表示自动生成getter,setter,构造,toString,equals方法。在调用toString和equals方法时,会调用父类的方法。
@Alias定义了别名,这个别名在mybatis的xml文件中用到。
同时,User类实现了Serializable接口,表明User类可以被序列化(redis缓存需要用到,redis存储对象,需要借助序列化和反序列化)
5. oracle
建表
CREATE TABLE t_user(
ID NUMBER NOT NULL,
user_name VARCHAR2(50) NOT NULL,
note VARCHAR2(200));
ALTER
主键
ALTER TABLE t_user ADD CONSTRAINT t_user_pk
PRIMARY KEY (ID);
序列
CREATE SEQUENCE seq_t_user
MINVALUE 1
MAXVALUE 99999
START WITH 1
INCREMENT BY 1
CACHE 20;
6. 创建dao
首先创建dao接口
@Repository
public interface UserDao {
User getUser(Long id);
int insertUser(User user);
int updateUser(User user);
List findUsers(@Param("userName") String userName, @Param("note") String note);
int deleteUser(Long id);
}
==使用了@Repository注解,标识这是个Dao。==
==在mybatis体系中,这里应该是@Mapper,但是使用@Mapper,后续在自动注入dao时,IDE会提示,实际运行正确。==
==我们在application中使用了@MapperScan来指明我们的Mapper上面使用的是什么注解。==
接着创建mybatis的xml文件
select seq_t_user.nextVal from dual
insert into t_user(id,user_name,note) values(#{id},#{userName},#{note})
update t_user
user_name = #{userName},
note=#{note},
where id=#{id}
delete from t_user where id = #{id}
在insert中,我们需要先查询序列,然后将序列的值设置到user的id中,然后在执行插入操作。order指明了在insert前执行。
xml的namespace就是指明了dao接口的路径。
6. 创建Service
接口
public interface UserService {
User getUser(Long id);
User insertUser(User user);
User updateUserName(Long id,String userName);
List findUsers(String userName, String note);
int deleteUser(Long id);
}
实现
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Override
@Transactional
@Cacheable(value = "redisCache", key = "'redis_user_'+#id")
public User getUser(Long id) {
return userDao.getUser(id);
}
@Override
@Transactional
@CachePut(value = "redisCache", key = "'redis_user_'+#result.id")
public User insertUser(User user) {
userDao.insertUser(user);
return user;
}
@Override
@Transactional
@CachePut(value = "redisCache", condition = "#result != 'null' ", key = "'redis_user_'+#result.id")
public User updateUserName(Long id, String userName) {
User user = getUser(id);
if (user == null) {
return null;
}
user.setUserName(userName);
userDao.updateUser(user);
return user;
}
@Override
@Transactional
public List findUsers(String userName, String note) {
return userDao.findUsers(userName, note);
}
@Override
@Transactional
@CacheEvict(value = "redisCache", key = "'redis_user_'+#id", beforeInvocation = false)
public int deleteUser(Long id) {
return userDao.deleteUser(id);
}
}
6.1 缓存注解
- @CachePut : 将方法结果返回放到缓存中。
- @Cacheable: 先从缓存中通过定义的键查询,如果可以查询到数据,则返回,否则执行该方法,返回数据,并且将返回结果保存到缓存中。
- @CacheEvict:通过定能够以的键移除缓存,它有一个Boolean类型的配置项:beforeInvocation,表示在方法之前或者方法之后移除缓存。默认false,默认在方法之后移除缓存
6.2 缓存使用参数
上述缓存注解中都配置了value="redisCache",因为我们在配置文件中,配置的redis的名字就是redisCache:
这样就能够引用到对应的缓存了,而键配置规则是一个Spring EL,很多时候可以看到配置为'redis_user_'+#id
其中#id代表参数,通过名称匹配。所以参数中必须存在一个参数的名字是id。除此之外,还可以使用序号引用参数,比如#a[0]或者#p[1]表示第一个或者第二个。 ==在一次配置中,名字应该一致。==
6.3 缓存返回值
当希望使用返回结果的一些属性缓存数据,比如insertUser方法。在插入数据库前,此时user还没有id。而这个id将会在写入数据库时,由selectKey标签写入。所以,需要使用返回结果的user的id,这样使用#result就代表了返回的user对象。因为id是user的一个属性,所以使用#result.id取出id。
6.4 缓存条件
在updateUserName方法中,可能存在返回null的情况。如果返回null,则不需要进行缓存。所以在注解@CachePut中加入了condition条件,它也是一个Spring EL表达式。表达式要求返回Boolean类型的值,如果尾true,则使用缓存操作;否则就不使用。==同样的@Cacheable和@CacheEvict也可以使用。==
6.5 缓存不可靠
在updateUserName的方法中,我们首先调用了getUser从数据库中查询user,然后更新user。因为缓存中可能存在过时的数据,也就是脏数据。客户端从缓存中获取到脏数据,然后在脏数据的基础上进行修改,最后在进行更新。此时就可能存在因缓存过时问题,造成==使用不可靠的缓存数据去更新数据库数据。==
这是不可取的,非常危险的操作。
不过,在updateUserName的方法中,调用getUser方法,每次都会从数据库中读取。这里的==缓存失效==了。
6.6 缓存命中率低
对于方法findUsers,因为每次传入的条件都不相同,所以,就会导致缓存的命中率非常低,那么我们不使用缓存。使用了缓存,性能反而变慢.
7. 验证
7.1 insert
@SpringBootTest
public class UserTests {
@Autowired
private UserService userService;
@Test
public void testAddUser(){
User user = new User();
user.setUserName(UUID.randomUUID().toString());
user.setNote(UUID.randomUUID().toString());
System.out.println(user);
userService.insertUser(user);
}
}
7.2 query
可以发现,userService中的dao是空的。
7.3 update
7.4 delete
8. 缓存失效
在前面我们就提到了缓存失效,那么,为什么呢?
因为Spring的缓存机制也是基于Spring Aop的原理,而在Spring中AOP是通过动态代理技术实现的,updateUserName调用getUser方法是类内部的自调用,并不存在代理对象的调用,这样便不会出现AOP,也就不会使用到标注在getUser上的缓存注解去获取缓存的值了。
如何避免缓存失效?
答案是每次调用都产生一个新事务,这样就可以克服自调用缓存、事务失效的问题了。
9. 缓存数据不可靠--脏数据
因为我们的数据是唯一的,但是使用的用途却不唯一。每一种使用的用途都有可能需要做缓存。
如果多个键保存了同一个数据,那么在修改的时候,只有修改的调用者知道,这个缓存不是最新的,需要将数据库中的数据更新到缓存。而其余的调用者,是不知道缓存的数据已经被更新了,还在使用旧的缓存数据,就可能存在问题。这里就是使用了脏数据。
脏数据无法避免,但是可以减小脏数据被使用的可能性,比如给缓存加过期时间,或者每隔指定的时间,统一随机刷新缓存等等。
==所以,用到了缓存,在写入数据的时候需要非常的小心。==
10. 自定义缓存管理器
我们前面使用的缓存管理器,是通过配置,定制实现的缓存管理器,过期时间是0,表示永不过期,自动生产的键有x
前缀
如果通过配置定制的缓存管理器,无法满足需求,还可以自定义实现缓存管理器
此时需要去除缓存管理器的配置,然后使用java 代码,自定义缓存管理器
然后自定义缓存管理器
@Bean
public RedisCacheManager initRedisCacheManager(){
// Redis加锁写入器
RedisCacheWriter writer = RedisCacheWriter.lockingRedisCacheWriter(redisTemplate.getConnectionFactory());
// 启动Redis缓存的默认设置
RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
// 设置jdk序列化器
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new JdkSerializationRedisSerializer()))
// 禁用前缀
.disableKeyPrefix()
// 设置超时时间 1分钟
.entryTtl(Duration.ofMinutes(1));
// 创建Redis缓存管理器
RedisCacheManager redisCacheManager = new RedisCacheManager(writer, cacheConfiguration);
return redisCacheManager;
}
验证