1. 緩存管理器和配置
Spring在使用緩存注解前耳鸯,需要配置緩存管理器,緩存管理器將提供一些重要的信息膀曾,如緩存配型县爬,超時(shí)時(shí)間等。Spring可以支持多種緩存的使用添谊,因此它存在多種緩存處理器财喳,并提供了緩存處理器的接口CacheManage和相關(guān)類:
這是緩存相關(guān)的全部配置
spring:
cache:
# 緩存相關(guān)配置
cache-names: test_cache
# 如果有底層的緩存配置管理器支持創(chuàng)建
caffeine:
# caffeine 緩存
spec: x
# 配置細(xì)節(jié)
couchbase:
# couchbase 緩存
expiration: 0ms
# 超時(shí)時(shí)間,默認(rèn)永不超時(shí)
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
# 緩存超時(shí)時(shí)間,配置為0則表示永不超時(shí)
use-key-prefix: true
# 是否啟用Redis的鍵前綴
type: redis
# 緩存類型斩狱,默認(rèn)情況下耳高,Spring會(huì)自動(dòng)根據(jù)上下文探測
Spring支持的緩存類型有這些
完整的配置如下:
spring:
# redis 服務(wù)器配置
redis:
host: 10.0.228.117
port: 6379
password: ""
timeout: 60000
database: 0
# 連接池屬性配置
lettuce:
pool:
# 最小空閑連接數(shù)
min-idle: 5
# 最大空閑連接數(shù)
max-idle: 10
# 最大活動(dòng)的連接數(shù)
max-active: 10
# 連接最大等待數(shù)
max-wait: 3000
cache:
# 緩存相關(guān)配置
cache-names: test_cache
# # 如果有底層的緩存配置管理器支持創(chuàng)建
# caffeine:
# # caffeine 緩存
# spec: x
# # 配置細(xì)節(jié)
# couchbase:
# # couchbase 緩存
# expiration: 0ms
# # 超時(shí)時(shí)間,默認(rèn)永不超時(shí)
# 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
# 緩存超時(shí)時(shí)間,配置為0則表示永不超時(shí)
use-key-prefix: true
# 是否啟用Redis的鍵前綴
type: redis
# 緩存類型所踊,默認(rèn)情況下泌枪,Spring會(huì)自動(dòng)根據(jù)上下文探測
除了在配置文件中配置,還需要啟用緩存機(jī)制
2. 實(shí)例程序框架搭建
實(shí)例集成了oracle數(shù)據(jù)庫秕岛,MyBatis,Redis,緩存和日志碌燕。
增加依賴:
3. 配置
配置:
spring:
# redis 服務(wù)器配置
redis:
host: 10.0.228.117
port: 6379
password: ""
timeout: 60000
database: 0
# 連接池屬性配置
lettuce:
pool:
# 最小空閑連接數(shù)
min-idle: 5
# 最大空閑連接數(shù)
max-idle: 10
# 最大活動(dòng)的連接數(shù)
max-active: 10
# 連接最大等待數(shù)
max-wait: 3000
cache:
# 緩存相關(guān)配置
cache-names: redisCache
redis:
# redis緩存
cache-null-values: true
# 是否允許緩存空值
key-prefix: x
# redis的鍵的前綴
time-to-live: 0ms
# 緩存超時(shí)時(shí)間,配置為0則表示永不超時(shí)
use-key-prefix: true
# 是否啟用Redis的鍵前綴
type: redis
# 緩存類型继薛,默認(rèn)情況下修壕,Spring會(huì)自動(dòng)根據(jù)上下文探測
datasource:
# 配置數(shù)據(jù)庫
driver-class-name: oracle.jdbc.OracleDriver
# 數(shù)據(jù)庫驅(qū)動(dòng)
url: jdbc:oracle:thin:@//10.0.250.19:1521/starbass
# 數(shù)據(jù)庫連接
username: system
# 用戶名
password: system
# 密碼
tomcat:
# 數(shù)據(jù)庫相關(guān)配置
max-idle: 10
# 最大閑置的連接數(shù)量
max-active: 10
# 最大活躍連接數(shù)量
min-idle: 5
# 最小閑置數(shù)量
max-wait: 2000
# 最大等待時(shí)間
mybatis:
# mybatis配置
mapper-locations: classpath:com/study/redishello/mapper/*.xml
# mybatis的文件目錄
type-aliases-package: com.study.redishello.pojo
# mybatis實(shí)體的包路徑
logging:
# 日志配置
level:
root: debug
# 日志級(jí)別:調(diào)試
org:
springframework: debug
org:
mybatis: debug
4. 創(chuàng)建實(shí)體
@Alias("user")
@Data
public class User implements Serializable {
private Long id;
private String userName;
private String note;
}
@Data是lombok的注解,表示自動(dòng)生成getter,setter,構(gòu)造,toString,equals方法遏考。在調(diào)用toString和equals方法時(shí)慈鸠,會(huì)調(diào)用父類的方法。
@Alias定義了別名诈皿,這個(gè)別名在mybatis的xml文件中用到林束。
同時(shí)像棘,User類實(shí)現(xiàn)了Serializable接口稽亏,表明User類可以被序列化(redis緩存需要用到,redis存儲(chǔ)對(duì)象缕题,需要借助序列化和反序列化)
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. 創(chuàng)建dao
首先創(chuàng)建dao接口
@Repository
public interface UserDao {
User getUser(Long id);
int insertUser(User user);
int updateUser(User user);
List<User> findUsers(@Param("userName") String userName, @Param("note") String note);
int deleteUser(Long id);
}
==使用了@Repository注解截歉,標(biāo)識(shí)這是個(gè)Dao。==
==在mybatis體系中烟零,這里應(yīng)該是@Mapper瘪松,但是使用@Mapper,后續(xù)在自動(dòng)注入dao時(shí)锨阿,IDE會(huì)提示宵睦,實(shí)際運(yùn)行正確。==
==我們?cè)赼pplication中使用了@MapperScan來指明我們的Mapper上面使用的是什么注解墅诡。==
接著創(chuàng)建mybatis的xml文件
<?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.study.redishello.dao.UserDao">
<select id="getUser" parameterType="long" resultType="user">
select id,user_name as userName, note from t_user
where id = #{id}
</select>
<insert id="insertUser" parameterType="user">
<selectKey resultType="long" keyProperty="id" order="BEFORE">
select seq_t_user.nextVal from dual
</selectKey>
insert into t_user(id,user_name,note) values(#{id},#{userName},#{note})
</insert>
<update id="updateUser">
update t_user
<set>
<if test="userName != null">
user_name = #{userName},
</if>
<if test="note != null">
note=#{note},
</if>
</set>
where id=#{id}
</update>
<select id="findUsers" resultType="user">
select identity,user_name as userName,note from t_user
<where>
<if test="userName != null">
and user_name=#{userName}
</if>
<if test="note != null">
and note=#{note}
</if>
</where>
</select>
<delete id="deleteUser" parameterType="long">
delete from t_user where id = #{id}
</delete>
</mapper>
在insert中壳嚎,我們需要先查詢序列,然后將序列的值設(shè)置到user的id中,然后在執(zhí)行插入操作烟馅。order指明了在insert前執(zhí)行说庭。
xml的namespace就是指明了dao接口的路徑。
6. 創(chuàng)建Service
接口
public interface UserService {
User getUser(Long id);
User insertUser(User user);
User updateUserName(Long id,String userName);
List<User> findUsers(String userName, String note);
int deleteUser(Long id);
}
實(shí)現(xiàn)
@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<User> 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 : 將方法結(jié)果返回放到緩存中郑趁。
- @Cacheable: 先從緩存中通過定義的鍵查詢刊驴,如果可以查詢到數(shù)據(jù),則返回寡润,否則執(zhí)行該方法捆憎,返回?cái)?shù)據(jù),并且將返回結(jié)果保存到緩存中悦穿。
- @CacheEvict:通過定能夠以的鍵移除緩存攻礼,它有一個(gè)Boolean類型的配置項(xiàng):beforeInvocation,表示在方法之前或者方法之后移除緩存。默認(rèn)false,默認(rèn)在方法之后移除緩存
6.2 緩存使用參數(shù)
上述緩存注解中都配置了value="redisCache",因?yàn)槲覀冊(cè)谂渲梦募欣跗猓渲玫膔edis的名字就是redisCache:
這樣就能夠引用到對(duì)應(yīng)的緩存了礁扮,而鍵配置規(guī)則是一個(gè)Spring EL,很多時(shí)候可以看到配置為'redis_user_'+#id
其中#id代表參數(shù)瞬沦,通過名稱匹配太伊。所以參數(shù)中必須存在一個(gè)參數(shù)的名字是id。除此之外逛钻,還可以使用序號(hào)引用參數(shù)僚焦,比如#a[0]或者#p[1]表示第一個(gè)或者第二個(gè)。 ==在一次配置中曙痘,名字應(yīng)該一致芳悲。==
6.3 緩存返回值
當(dāng)希望使用返回結(jié)果的一些屬性緩存數(shù)據(jù),比如insertUser方法边坤。在插入數(shù)據(jù)庫前名扛,此時(shí)user還沒有id。而這個(gè)id將會(huì)在寫入數(shù)據(jù)庫時(shí)茧痒,由selectKey標(biāo)簽寫入肮韧。所以,需要使用返回結(jié)果的user的id旺订,這樣使用#result就代表了返回的user對(duì)象弄企。因?yàn)閕d是user的一個(gè)屬性,所以使用#result.id取出id区拳。
6.4 緩存條件
在updateUserName方法中拘领,可能存在返回null的情況。如果返回null樱调,則不需要進(jìn)行緩存约素。所以在注解@CachePut中加入了condition條件洽瞬,它也是一個(gè)Spring EL表達(dá)式。表達(dá)式要求返回Boolean類型的值业汰,如果尾true伙窃,則使用緩存操作;否則就不使用样漆。==同樣的@Cacheable和@CacheEvict也可以使用为障。==
6.5 緩存不可靠
在updateUserName的方法中,我們首先調(diào)用了getUser從數(shù)據(jù)庫中查詢user放祟,然后更新user鳍怨。因?yàn)榫彺嬷锌赡艽嬖谶^時(shí)的數(shù)據(jù),也就是臟數(shù)據(jù)跪妥⌒客戶端從緩存中獲取到臟數(shù)據(jù),然后在臟數(shù)據(jù)的基礎(chǔ)上進(jìn)行修改眉撵,最后在進(jìn)行更新侦香。此時(shí)就可能存在因緩存過時(shí)問題,造成==使用不可靠的緩存數(shù)據(jù)去更新數(shù)據(jù)庫數(shù)據(jù)纽疟。==
這是不可取的罐韩,非常危險(xiǎn)的操作。
不過污朽,在updateUserName的方法中散吵,調(diào)用getUser方法,每次都會(huì)從數(shù)據(jù)庫中讀取蟆肆。這里的==緩存失效==了矾睦。
6.6 緩存命中率低
對(duì)于方法findUsers,因?yàn)槊看蝹魅氲臈l件都不相同炎功,所以枚冗,就會(huì)導(dǎo)致緩存的命中率非常低,那么我們不使用緩存亡问。使用了緩存官紫,性能反而變慢.
7. 驗(yàn)證
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
可以發(fā)現(xiàn)肛宋,userService中的dao是空的州藕。
7.3 update
7.4 delete
8. 緩存失效
在前面我們就提到了緩存失效,那么酝陈,為什么呢床玻?
因?yàn)镾pring的緩存機(jī)制也是基于Spring Aop的原理,而在Spring中AOP是通過動(dòng)態(tài)代理技術(shù)實(shí)現(xiàn)的沉帮,updateUserName調(diào)用getUser方法是類內(nèi)部的自調(diào)用锈死,并不存在代理對(duì)象的調(diào)用贫堰,這樣便不會(huì)出現(xiàn)AOP,也就不會(huì)使用到標(biāo)注在getUser上的緩存注解去獲取緩存的值了待牵。
如何避免緩存失效其屏?
答案是每次調(diào)用都產(chǎn)生一個(gè)新事務(wù),這樣就可以克服自調(diào)用緩存缨该、事務(wù)失效的問題了偎行。
9. 緩存數(shù)據(jù)不可靠--臟數(shù)據(jù)
因?yàn)槲覀兊臄?shù)據(jù)是唯一的,但是使用的用途卻不唯一贰拿。每一種使用的用途都有可能需要做緩存蛤袒。
如果多個(gè)鍵保存了同一個(gè)數(shù)據(jù),那么在修改的時(shí)候膨更,只有修改的調(diào)用者知道妙真,這個(gè)緩存不是最新的,需要將數(shù)據(jù)庫中的數(shù)據(jù)更新到緩存荚守。而其余的調(diào)用者珍德,是不知道緩存的數(shù)據(jù)已經(jīng)被更新了,還在使用舊的緩存數(shù)據(jù)矗漾,就可能存在問題菱阵。這里就是使用了臟數(shù)據(jù)。
臟數(shù)據(jù)無法避免缩功,但是可以減小臟數(shù)據(jù)被使用的可能性晴及,比如給緩存加過期時(shí)間,或者每隔指定的時(shí)間嫡锌,統(tǒng)一隨機(jī)刷新緩存等等虑稼。
==所以,用到了緩存势木,在寫入數(shù)據(jù)的時(shí)候需要非常的小心蛛倦。==
10. 自定義緩存管理器
我們前面使用的緩存管理器,是通過配置啦桌,定制實(shí)現(xiàn)的緩存管理器溯壶,過期時(shí)間是0,表示永不過期甫男,自動(dòng)生產(chǎn)的鍵有x
前綴
如果通過配置定制的緩存管理器且改,無法滿足需求,還可以自定義實(shí)現(xiàn)緩存管理器
此時(shí)需要去除緩存管理器的配置板驳,然后使用java 代碼又跛,自定義緩存管理器
然后自定義緩存管理器
@Bean
public RedisCacheManager initRedisCacheManager(){
// Redis加鎖寫入器
RedisCacheWriter writer = RedisCacheWriter.lockingRedisCacheWriter(redisTemplate.getConnectionFactory());
// 啟動(dòng)Redis緩存的默認(rèn)設(shè)置
RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
// 設(shè)置jdk序列化器
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new JdkSerializationRedisSerializer()))
// 禁用前綴
.disableKeyPrefix()
// 設(shè)置超時(shí)時(shí)間 1分鐘
.entryTtl(Duration.ofMinutes(1));
// 創(chuàng)建Redis緩存管理器
RedisCacheManager redisCacheManager = new RedisCacheManager(writer, cacheConfiguration);
return redisCacheManager;
}
驗(yàn)證