跨境電商網(wǎng)站建設(shè)方案網(wǎng)站優(yōu)化排名易下拉穩(wěn)定
Caffeine+Redis兩級緩存架構(gòu)
在高性能的服務(wù)項(xiàng)目中,我們一般會(huì)將一些熱點(diǎn)數(shù)據(jù)存儲(chǔ)到 Redis這類緩存中間件中,只有當(dāng)緩存的訪問沒有命中時(shí)再查詢數(shù)據(jù)庫。在提升訪問速度的同時(shí),也能降低數(shù)據(jù)庫的壓力。
但是在一些場景下單純使用 Redis 的分布式緩存不能滿足高性能的要求,所以還需要加入使用本地緩存Caffeine,從而再次提升程序的響應(yīng)速度與服務(wù)性能。于是,就產(chǎn)生了使用本地緩存(Caffeine)作為一級緩存,再加上分布式緩存(Redis)作為二級緩存的兩級緩存架構(gòu)。
兩級緩存架構(gòu)優(yōu)缺點(diǎn)
優(yōu)點(diǎn):
- 一級緩存基于應(yīng)用的內(nèi)存,訪問速度非???#xff0c;對于一些變更頻率低、實(shí)時(shí)性要求低的數(shù)據(jù),可以放在本地緩存中,提升訪問速度;
- 使用一級緩存能夠減少和 Redis 的二級緩存的遠(yuǎn)程數(shù)據(jù)交互,減少網(wǎng)絡(luò) I/O 開銷,降低這一過程中在網(wǎng)絡(luò)通信上的耗時(shí)。
缺點(diǎn):
- 數(shù)據(jù)一致性問題:兩級緩存與數(shù)據(jù)庫的數(shù)據(jù)要保持一致,一旦數(shù)據(jù)發(fā)生了修改,在修改數(shù)據(jù)庫的同時(shí),一級緩存、二級緩存應(yīng)該同步更新。
- 分布式多應(yīng)用情況下:一級緩存之間也會(huì)存在一致性問題,當(dāng)一個(gè)節(jié)點(diǎn)下的本地緩存修改后,需要通知其他節(jié)點(diǎn)也刷新本地一級緩存中的數(shù)據(jù),否則會(huì)出現(xiàn)讀取到過期數(shù)據(jù)的情況。
- 緩存的過期時(shí)間、過期策略以及多線程的問題
Caffeine+Redis兩級緩存架構(gòu)實(shí)戰(zhàn)
1、準(zhǔn)備表結(jié)構(gòu)和數(shù)據(jù)
準(zhǔn)備如下的表結(jié)構(gòu)和相關(guān)數(shù)據(jù)
DROP TABLE IF EXISTS user;CREATE TABLE user
(id BIGINT(20) NOT NULL COMMENT '主鍵ID',name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',age INT(11) NULL DEFAULT NULL COMMENT '年齡',email VARCHAR(50) NULL DEFAULT NULL COMMENT '郵箱',PRIMARY KEY (id)
);
插入對應(yīng)的相關(guān)數(shù)據(jù)
DELETE FROM user;INSERT INTO user (id, name, age, email) VALUES
(1, 'Jone', 18, 'test1@baomidou.com'),
(2, 'Jack', 20, 'test2@baomidou.com'),
(3, 'Tom', 28, 'test3@baomidou.com'),
(4, 'Sandy', 21, 'test4@baomidou.com'),
(5, 'Billie', 24, 'test5@baomidou.com');
2、創(chuàng)建項(xiàng)目
創(chuàng)建一個(gè)SpringBoot項(xiàng)目,然后引入相關(guān)的依賴,首先是父依賴
<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.6.6</version><relativePath/> <!-- lookup parent from repository --></parent>
具體的其他的依賴
<!-- spring-boot-starter-web 的依賴 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><!-- 引入MyBatisPlus的依賴 --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.1</version></dependency><!-- 數(shù)據(jù)庫使用MySQL數(shù)據(jù)庫 --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><!-- 數(shù)據(jù)庫連接池 Druid --><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.1.14</version></dependency><!-- lombok依賴 --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency>
3、配置信息
然后我們需要在application.properties中配置數(shù)據(jù)源的相關(guān)信息
spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/mp?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
spring.datasource.username=root
spring.datasource.password=123456spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
然后我們需要在SpringBoot項(xiàng)目的啟動(dòng)類上配置Mapper接口的掃描路徑
4、添加User實(shí)體
添加user的實(shí)體類
@ToString
@Data
public class User {private Long id;private String name;private Integer age;private String email;
}
5、創(chuàng)建Mapper接口
在MyBatisPlus中的Mapper接口需要繼承BaseMapper.
/*** MyBatisPlus中的Mapper接口繼承自BaseMapper*/
public interface UserMapper extends BaseMapper<User> {
}
6、測試操作
然后來完成對User表中數(shù)據(jù)的查詢操作
@SpringBootTest
class MpDemo01ApplicationTests {@Autowiredprivate UserMapper userMapper;@Testvoid queryUser() {List<User> users = userMapper.selectList(null);for (User user : users) {System.out.println(user);}}}
7、日志輸出
為了便于學(xué)習(xí)我們可以指定日志的實(shí)現(xiàn)StdOutImpl來處理
# 指定日志輸出
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
然后操作數(shù)據(jù)庫的時(shí)候就可以看到對應(yīng)的日志信息了:
手動(dòng)兩級緩存架構(gòu)實(shí)戰(zhàn)
@Configuration
public class CaffeineConfig {@Beanpublic Cache<String,Object> caffeineCache(){return Caffeine.newBuilder().initialCapacity(128)//初始大小.maximumSize(1024)//最大數(shù)量.expireAfterWrite(15, TimeUnit.SECONDS)//過期時(shí)間 15S.build();}
}
//Caffeine+Redis兩級緩存查詢public User query1_2(long userId){String key = "user-"+userId;User user = (User) cache.get(key,k -> {//先查詢 Redis (2級緩存)Object obj = redisTemplate.opsForValue().get(key);if (Objects.nonNull(obj)) {log.info("get data from redis:"+key);return obj;}// Redis沒有則查詢 DB(MySQL)User user2 = userMapper.selectById(userId);log.info("get data from database:"+userId);redisTemplate.opsForValue().set(key, user2, 30, TimeUnit.SECONDS);return user2;});return user;}
在 Cache 的 get 方法中,會(huì)先從Caffeine緩存中進(jìn)行查找,如果找到緩存的值那么直接返回。沒有的話查找 Redis,Redis 再不命中則查詢數(shù)據(jù)庫,最后都同步到Caffeine的緩存中。
通過案例演示也可以達(dá)到對應(yīng)的效果。
另外修改、刪除的代碼可以看代碼案例!
注解方式兩級緩存架構(gòu)實(shí)戰(zhàn)
在 spring中,提供了 CacheManager 接口和對應(yīng)的注解
- @Cacheable:根據(jù)鍵從緩存中取值,如果緩存存在,那么獲取緩存成功之后,直接返回這個(gè)緩存的結(jié)果。如果緩存不存在,那么執(zhí)行方法,并將結(jié)果放入緩存中。
- @CachePut:不管之前的鍵對應(yīng)的緩存是否存在,都執(zhí)行方法,并將結(jié)果強(qiáng)制放入緩存。
- @CacheEvict:執(zhí)行完方法后,會(huì)移除掉緩存中的數(shù)據(jù)。
使用注解,就需要配置 spring 中的 CacheManager ,在這個(gè)CaffeineConfig類中
@Beanpublic CacheManager cacheManager(){CaffeineCacheManager cacheManager=new CaffeineCacheManager();cacheManager.setCaffeine(Caffeine.newBuilder().initialCapacity(128).maximumSize(1024).expireAfterWrite(15, TimeUnit.SECONDS));return cacheManager;}
EnableCaching
在啟動(dòng)類上再添加上 @EnableCaching 注解
在UserService類對應(yīng)的方法上添加 @Cacheable 注解
//Caffeine+Redis兩級緩存查詢-- 使用注解@Cacheable(value = "user", key = "#userId")public User query2_2(long userId){String key = "user-"+userId;//先查詢 Redis (2級緩存)Object obj = redisTemplate.opsForValue().get(key);if (Objects.nonNull(obj)) {log.info("get data from redis:"+key);return (User)obj;}// Redis沒有則查詢 DB(MySQL)User user = userMapper.selectById(userId);log.info("get data from database:"+userId);redisTemplate.opsForValue().set(key, user, 30, TimeUnit.SECONDS);return user;}
然后就可以達(dá)到類似的效果。
@Cacheable 注解的屬性:
參數(shù) | 解釋 | col3 |
---|---|---|
key | 緩存的key,可以為空,如果指定要按照SpEL表達(dá)式編寫,如不指定,則按照方法所有參數(shù)組合 | @Cacheable(value=”testcache”, key=”#userName”) |
value | 緩存的名稱,在 spring 配置文件中定義,必須指定至少一個(gè) | 例如:@Cacheable(value=”mycache”) |
condition | 緩存的條件,可以為空,使用 SpEL 編寫,返回 true 或者 false,只有為 true 才進(jìn)行緩存 | @Cacheable(value=”testcache”, condition=”#userName.length()>2”) |
methodName | 當(dāng)前方法名 | #root.methodName |
method | 當(dāng)前方法 | #root.method.name |
target | 當(dāng)前被調(diào)用的對象 | #root.target |
targetClass | 當(dāng)前被調(diào)用的對象的class | #root.targetClass |
args | 當(dāng)前方法參數(shù)組成的數(shù)組 | #root.args[0] |
caches | 當(dāng)前被調(diào)用的方法使用的Cache | #root.caches[0].name |
這里有一個(gè)condition屬性指定發(fā)生的條件
示例表示只有當(dāng)userId為偶數(shù)時(shí)才會(huì)進(jìn)行緩存
//只有當(dāng)userId為偶數(shù)時(shí)才會(huì)進(jìn)行緩存@Cacheable(value = "user", key = "#userId", condition="#userId%2==0")public User query2_3(long userId){String key = "user-"+userId;//先查詢 Redis (2級緩存)Object obj = redisTemplate.opsForValue().get(key);if (Objects.nonNull(obj)) {log.info("get data from redis:"+key);return (User)obj;}// Redis沒有則查詢 DB(MySQL)User user = userMapper.selectById(userId);log.info("get data from database:"+userId);redisTemplate.opsForValue().set(key, user, 30, TimeUnit.SECONDS);return user;}
CacheEvict
@CacheEvict是用來標(biāo)注在需要清除緩存元素的方法或類上的。
當(dāng)標(biāo)記在一個(gè)類上時(shí)表示其中所有的方法的執(zhí)行都會(huì)觸發(fā)緩存的清除操作。
@CacheEvict可以指定的屬性有value、key、condition、allEntries和beforeInvocation。其中value、key和condition的語義與@Cacheable對應(yīng)的屬性類似。即value表示清除操作是發(fā)生在哪些Cache上的(對應(yīng)Cache的名稱);key表示需要清除的是哪個(gè)key,如未指定則會(huì)使用默認(rèn)策略生成的key;condition表示清除操作發(fā)生的條件。下面我們來介紹一下新出現(xiàn)的兩個(gè)屬性allEntries和beforeInvocation。
//清除緩存(所有的元素)@CacheEvict(value="user", key = "#userId",allEntries=true)public void deleteAll(long userId) {System.out.println(userId);}//beforeInvocation=true:在調(diào)用該方法之前清除緩存中的指定元素@CacheEvict(value="user", key = "#userId",beforeInvocation=true)public void delete(long userId) {System.out.println(userId);}
自定義注解實(shí)現(xiàn)兩級緩存架構(gòu)實(shí)戰(zhàn)
首先定義一個(gè)注解,用于添加在需要操作緩存的方法上:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DoubleCache {String cacheName();String key(); //支持springEl表達(dá)式long l2TimeOut() default 120;CacheType type() default CacheType.FULL;
}
l2TimeOut 為可以設(shè)置的二級緩存 Redis 的過期時(shí)間
CacheType 是一個(gè)枚舉類型的變量,表示操作緩存的類型
public enum CacheType {FULL, //存取PUT, //只存DELETE //刪除
}
從前面我們知道,key要支持 springEl 表達(dá)式,寫一個(gè)ElParser的方法,使用表達(dá)式解析器解析參數(shù):
public class ElParser {public static String parse(String elString, TreeMap<String,Object> map){elString=String.format("#{%s}",elString);//創(chuàng)建表達(dá)式解析器ExpressionParser parser = new SpelExpressionParser();//通過evaluationContext.setVariable可以在上下文中設(shè)定變量。EvaluationContext context = new StandardEvaluationContext();map.entrySet().forEach(entry->context.setVariable(entry.getKey(),entry.getValue()));//解析表達(dá)式Expression expression = parser.parseExpression(elString, new TemplateParserContext());//使用Expression.getValue()獲取表達(dá)式的值,這里傳入了Evaluation上下文String value = expression.getValue(context, String.class);return value;}
}
package com.msb.caffeine.cache;import com.github.benmanes.caffeine.cache.Cache;
import lombok.AllArgsConstructor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.data.redis.core.RedisTemplate;import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;import java.lang.reflect.Method;
import java.util.Objects;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;@Slf4j
@Component
@Aspect
@AllArgsConstructor
public class CacheAspect {private final Cache cache;private final RedisTemplate redisTemplate;@Pointcut("@annotation(com.msb.caffeine.cache.DoubleCache)")public void cacheAspect() {}@Around("cacheAspect()")public Object doAround(ProceedingJoinPoint point) throws Throwable {MethodSignature signature = (MethodSignature) point.getSignature();Method method = signature.getMethod();//拼接解析springEl表達(dá)式的mapString[] paramNames = signature.getParameterNames();Object[] args = point.getArgs();TreeMap<String, Object> treeMap = new TreeMap<>();for (int i = 0; i < paramNames.length; i++) {treeMap.put(paramNames[i],args[i]);}DoubleCache annotation = method.getAnnotation(DoubleCache.class);String elResult = ElParser.parse(annotation.key(), treeMap);String realKey = annotation.cacheName() + ":" + elResult;//強(qiáng)制更新if (annotation.type()== CacheType.PUT){Object object = point.proceed();redisTemplate.opsForValue().set(realKey, object,annotation.l2TimeOut(), TimeUnit.SECONDS);cache.put(realKey, object);return object;}//刪除else if (annotation.type()== CacheType.DELETE){redisTemplate.delete(realKey);cache.invalidate(realKey);return point.proceed();}//讀寫,查詢CaffeineObject caffeineCache = cache.getIfPresent(realKey);if (Objects.nonNull(caffeineCache)) {log.info("get data from caffeine");return caffeineCache;}//查詢RedisObject redisCache = redisTemplate.opsForValue().get(realKey);if (Objects.nonNull(redisCache)) {log.info("get data from redis");cache.put(realKey, redisCache);return redisCache;}log.info("get data from database");Object object = point.proceed();if (Objects.nonNull(object)){//寫入RedisredisTemplate.opsForValue().set(realKey, object,annotation.l2TimeOut(), TimeUnit.SECONDS);//寫入Caffeinecache.put(realKey, object);}return object;}
}
切面中主要做了下面幾件工作:
- 通過方法的參數(shù),解析注解中 key 的 springEl 表達(dá)式,組裝真正緩存的 key。
- 根據(jù)操作緩存的類型,分別處理存取、只存、刪除緩存操作。
- 刪除和強(qiáng)制更新緩存的操作,都需要執(zhí)行原方法,并進(jìn)行相應(yīng)的緩存刪除或更新操作。
- 存取操作前,先檢查緩存中是否有數(shù)據(jù),如果有則直接返回,沒有則執(zhí)行原方法,并將結(jié)果存入緩存。
然后使用的話就非常方便了,代碼中只保留原有業(yè)務(wù)代碼,再添加上我們自定義的注解就可以了:
@DoubleCache(cacheName = "user", key = "#userId",type = CacheType.FULL)public User query3(Long userId) {User user = userMapper.selectById(userId);return user;}@DoubleCache(cacheName = "user",key = "#user.userId",type = CacheType.PUT)public int update3(User user) {return userMapper.updateById(user);}@DoubleCache(cacheName = "user",key = "#user.userId",type = CacheType.DELETE)public void deleteOrder(User user) {userMapper.deleteById(user);}
兩級緩存架構(gòu)的緩存一致性問題
就是如果一個(gè)應(yīng)用修改了緩存,另外一個(gè)應(yīng)用的caffeine緩存是沒有辦法感知的,所以這里就會(huì)有緩存的一致性問題
解決方案也很簡單,就是在Redis中做一個(gè)發(fā)布和訂閱。
遇到修改緩存的處理,需要向?qū)?yīng)的頻道發(fā)布一條消息,然后應(yīng)用同步監(jiān)聽這條消息,有消息則需要?jiǎng)h除本地的Caffeine緩存。
核心代碼如下: