解决不同设备间速度不匹配问题。
互联网分层架构:降低数据库压力,提升系统整体性能,缩短访问时间。
高并发问题
利用 Spring Cache 处理 Redis 缓存数据
Spring Cache 注解@Cacheable携带的sync()属性可支持互斥锁限制单个线程处理,可避免缓存击穿
注意开启 Spring Cache 需要在配置类(或启动类)上增加@EnableCaching
cacheNames() / value() 与时长对应可根据 配置或代码写死 指定不同 缓存空间 缓存时长
此方式以 缓存空间名 为标识区分时长,未配置的缓存空间走全局设定
使用示例:
@GetMapping("/spel2")
@Cacheable(cacheNames = "prefix")
public ResponseResult<String> spel2(Long id) {
id += RandomUtil.randomLong();
log.info("hello:spel2:{}", id);
return ResponseResult.success("hello:spel2:" + id);
}
# yml 配置
expand-cache-config:
ttl-map: '{"yml-ttl":1000,"hello":2000}'
// 引入配置
@Value("#{${expand-cache-config.ttl-map:null}}")
private Map<String, Long> ttlMap;
/**
* 注入缓存管理器及处理配置中的缓存时长
*/
@Bean(BEAN_REDIS_CACHE_MANAGER)
public RedisCacheManager expandRedisCacheManager(RedisConnectionFactory factory) {
/*
使用 Jackson 作为值序列化处理器
FastJson 存在部分转换问题如:Set 存储后因为没有对应的类型保存无法转换为 JSONArray(实现 List ) 导致失败
*/
ObjectMapper om = JsonUtil.createJacksonObjectMapper();
GenericJackson2JsonRedisSerializer valueSerializer = new GenericJackson2JsonRedisSerializer(om);
// 配置key、value 序列化(解决乱码的问题)
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
// key 使用 string 序列化方式
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(StringRedisSerializer.UTF_8))
// value 使用 jackson 序列化方式
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(valueSerializer))
// 配置缓存空间名称前缀
.prefixCacheNameWith("spring:cache:")
// 配置全局缓存过期时间
.entryTtl(Duration.ofMinutes(30L));
// 专门指定某些缓存空间的配置,如果过期时间,这里的 key 为缓存空间名称
Map<String, RedisCacheConfiguration> configMap = new HashMap<>();
// 代码写死示例
configMap.put("world", config.entryTtl(Duration.ofSeconds(60)));
Set<Map.Entry<String, Long>> entrySet =
Optional.ofNullable(ttlMap).map(Map::entrySet).orElse(Collections.emptySet());
for (Map.Entry<String, Long> entry : entrySet) {
// 指定特定缓存空间对应的过期时间
configMap.put(entry.getKey(), config.entryTtl(Duration.ofSeconds(entry.getValue())));
}
RedisCacheWriter redisCacheWriter = RedisCacheWriter.lockingRedisCacheWriter(factory);
// 使用自定义缓存管理器附带自定义参数随机时间,注意此处为全局设定,5-最小随机秒,30-最大随机秒
return new ExpandRedisCacheManager(redisCacheWriter, config, configMap, 5, 30);
}
cacheNames() / value() 中附带时间字符串自定义缓存管理器继承
RedisCacheManager,重写创建缓存处理器方法,拿到缓存空间名与缓存配置进行更新缓存时长处理
此方式以 缓存空间名中非指定时间部分 为标识区分时长,缓存空间名不指定时间走全局设定
使用示例:
@GetMapping("/spel2")
@Cacheable(cacheNames = "prefix#5m", cacheManager = ExpandRedisConfig.BEAN_REDIS_CACHE_MANAGER)
public ResponseResult<String> spel2(Long id) {
id += RandomUtil.randomLong();
log.info("hello:spel2:{}", id);
return ResponseResult.success("hello:spel2:" + id);
}
@Override
protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) {
String theName = name;
if (name.contains(NAME_SPLIT_SYMBOL)) {
// 名称中存在#标记,修改实际名称,替换默认配置的缓存时长为指定缓存时长
String[] nameArr = name.split(NAME_SPLIT_SYMBOL);
theName = nameArr[0];
Duration duration = TimeUtil.parseDuration(nameArr[1]);
if (duration != null) {
cacheConfig = cacheConfig.entryTtl(duration);
}
}
// 使用自定义缓存处理器附带自定义参数随机时间,将注入的随机时间传递
return new ExpandRedisCache(theName, cacheWriter, cacheConfig, this.minRandomSecond,
this.maxRandomSecond);
}
@Cacheable 标识,可支持 Spring Cache 处理timeout() + unit() ,需要与注入注解的初始化配置方生效cacheNames() / value() 中非时间部分为标识区分时长使用示例:
@GetMapping("/spel3")
@ExpandCacheable(cacheNames = "prefix", spelKey = "hello-spel3-${#id}", timeout = 100,
unit = TimeUnit.SECONDS)
public ResponseResult<String> spel3(Long id) {
id += RandomUtil.randomLong();
log.info("hello:spel3:{}", id);
return ResponseResult.success("hello:spel3:" + id);
}
/**
* Spring Bean 加载后处理
* 获取所有 @Component 注解的 Bean 判断类中方法是否存在 @SpringCacheable 注解,存在进行过期时间设置
*/
@PostConstruct
public void init() {
Map<String, Object> beanMap = beanFactory.getBeansWithAnnotation(Component.class);
if (MapUtil.isEmpty(beanMap)) {
return;
}
beanMap.values().forEach(item ->
ReflectionUtils.doWithMethods(item.getClass(), method -> {
ReflectionUtils.makeAccessible(method);
putConfigTtl(method);
})
);
expandRedisCacheManager.initializeCaches();
}
/**
* 利用反射设置方法注解上配置的过期时间
* @param method 注解了自定义缓存的方法
*/
private void putConfigTtl(Method method) {
ExpandCacheable annotation = method.getAnnotation(ExpandCacheable.class);
if (annotation == null) {
return;
}
String[] cacheNames = annotation.cacheNames();
if (ArrayUtil.isEmpty(cacheNames)) {
cacheNames = annotation.value();
}
// 反射获取缓存管理器初始化配置并设值
Map<String, RedisCacheConfiguration> initialCacheConfiguration =
(Map<String, RedisCacheConfiguration>)
ReflectUtil.getFieldValue(expandRedisCacheManager, "initialCacheConfiguration");
RedisCacheConfiguration defaultCacheConfig =
(RedisCacheConfiguration)
ReflectUtil.getFieldValue(expandRedisCacheManager, "defaultCacheConfig");
Duration ttl = Duration.ofSeconds(annotation.unit().toSeconds(annotation.timeout()));
for (String cacheName : cacheNames) {
initialCacheConfiguration.put(cacheName, defaultCacheConfig.entryTtl(ttl));
}
}
继承 Spring 缓存处理器
RedisCache,重写设置缓存方法
可针对 null 进行短时间存储避免缓存穿透、增加随机时长避免缓存雪崩
@Override
public void put(Object key, @Nullable Object value) {
Object cacheValue = preProcessCacheValue(value);
// 替换父类设置缓存时长处理
Duration duration = getDynamicDuration(cacheValue);
cacheWriter.put(name, createAndConvertCacheKey(key),
serializeCacheValue(cacheValue), duration);
}
/**
* 获取动态时长
*/
private Duration getDynamicDuration(Object cacheValue) {
// 如果缓存值为 null,固定返回时长为 30s 避免缓存穿透
if (NullValue.INSTANCE.equals(cacheValue)) {
return Duration.ofSeconds(30);
}
int randomInt = RandomUtil.randomInt(this.minRandomSecond, this.maxRandomSecond);
return this.cacheConfig.getTtl().plus(Duration.ofSeconds(randomInt));
}
参考:
使用 Spring AOP 切面对注解拦截以支持方法调用结果缓存。
@GetMapping("/default/all")
@MethodCacheable(key = "hello-all", keyType = AspectKeyTypeEnum.DEFAULT, unless = "${#id<0}",
timeout = 300, unit = TimeUnit.SECONDS, addRandomDuration = false, useLocal = true,
localTimeout = 60)
public ResponseResult<String> exactMatchAll(Long id) {
id += RandomUtil.randomLong();
log.info("custom:all:{}", id);
return ResponseResult.success("custom:all:" + id);
}
key() + keyType()unless()timeout() + unit()addRandomDuration() ,注意固定了随机范围,可避免缓存雪崩useLocal() + localTimeout() ,注意本地缓存存在全局最大时长限制/**
* 利用 AOP 环绕通知对注解方法返回进行缓存处理
*/
@Around("@annotation(cn.eastx.practice.demo.cache.config.custom.MethodCacheable)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
MethodCacheableOperation operation = MethodCacheableOperation.convert(joinPoint);
if (Objects.isNull(operation)) {
return joinPoint.proceed();
}
Object result = getCacheData(operation);
if (Objects.nonNull(result)) {
return convertCacheData(result);
}
// 加锁处理同步执行
synchronized (operation.getKey().intern()) {
result = getCacheData(operation);
if (Objects.nonNull(result)) {
return convertCacheData(result);
}
result = joinPoint.proceed();
setDataCache(operation, result);
}
return result;
}
/**
* 设置数据缓存
* 特殊值缓存需要转换,特殊值包括 null
* @param operation 操作对象
* @param data 数据
*/
private void setDataCache(MethodCacheableOperation operation, Object data) {
// null缓存处理,固定存储时长,防止缓存穿透
if (Objects.isNull(data)) {
redisUtil.setEx(operation.getKey(), NULL_VALUE, SPECIAL_VALUE_DURATION);
return;
}
// 存在实际数据缓存处理
redisUtil.setEx(operation.getKey(), data, operation.getDuration());
if (Boolean.TRUE.equals(operation.getUseLocal())) {
LocalCacheUtil.put(operation.getKey(), data, operation.getLocalDuration());
}
}
本文 demo 地址:https://github.com/EastX/java-practice-demos/tree/main/demo-cache
推荐阅读:
我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div
总的来说,我对ruby还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用
类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc
我正在尝试设置一个puppet节点,但rubygems似乎不正常。如果我通过它自己的二进制文件(/usr/lib/ruby/gems/1.8/gems/facter-1.5.8/bin/facter)在cli上运行facter,它工作正常,但如果我通过由rubygems(/usr/bin/facter)安装的二进制文件,它抛出:/usr/lib/ruby/1.8/facter/uptime.rb:11:undefinedmethod`get_uptime'forFacter::Util::Uptime:Module(NoMethodError)from/usr/lib/ruby
我想了解Ruby方法methods()是如何工作的。我尝试使用“ruby方法”在Google上搜索,但这不是我需要的。我也看过ruby-doc.org,但我没有找到这种方法。你能详细解释一下它是如何工作的或者给我一个链接吗?更新我用methods()方法做了实验,得到了这样的结果:'labrat'代码classFirstdeffirst_instance_mymethodenddefself.first_class_mymethodendendclassSecond使用类#returnsavailablemethodslistforclassandancestorsputsSeco
我在我的项目中添加了一个系统来重置用户密码并通过电子邮件将密码发送给他,以防他忘记密码。昨天它运行良好(当我实现它时)。当我今天尝试启动服务器时,出现以下错误。=>BootingWEBrick=>Rails3.2.1applicationstartingindevelopmentonhttp://0.0.0.0:3000=>Callwith-dtodetach=>Ctrl-CtoshutdownserverExiting/Users/vinayshenoy/.rvm/gems/ruby-1.9.3-p0/gems/actionmailer-3.2.1/lib/action_mailer
设置:狂欢ruby1.9.2高线(1.6.13)描述:我已经相当习惯在其他一些项目中使用highline,但已经有几个月没有使用它了。现在,在Ruby1.9.2上全新安装时,它似乎不允许在同一行回答提示。所以以前我会看到类似的东西:require"highline/import"ask"Whatisyourfavoritecolor?"并得到:Whatisyourfavoritecolor?|现在我看到类似的东西:Whatisyourfavoritecolor?|竖线(|)符号是我的终端光标。知道为什么会发生这种变化吗? 最佳答案
我已经从我的命令行中获得了一切,所以我可以运行rubymyfile并且它可以正常工作。但是当我尝试从sublime中运行它时,我得到了undefinedmethod`require_relative'formain:Object有人知道我的sublime设置中缺少什么吗?我正在使用OSX并安装了rvm。 最佳答案 或者,您可以只使用“require”,它应该可以正常工作。我认为“require_relative”仅适用于ruby1.9+ 关于ruby-主要:Objectwhenrun
我有一个具有一些属性的模型:attr1、attr2和attr3。我需要在不执行回调和验证的情况下更新此属性。我找到了update_column方法,但我想同时更新三个属性。我需要这样的东西:update_columns({attr1:val1,attr2:val2,attr3:val3})代替update_column(attr1,val1)update_column(attr2,val2)update_column(attr3,val3) 最佳答案 您可以使用update_columns(attr1:val1,attr2:val2
我不确定传递给方法的对象的类型是否正确。我可能会将一个字符串传递给一个只能处理整数的函数。某种运行时保证怎么样?我看不到比以下更好的选择:defsomeFixNumMangler(input)raise"wrongtype:integerrequired"unlessinput.class==FixNumother_stuffend有更好的选择吗? 最佳答案 使用Kernel#Integer在使用之前转换输入的方法。当无法以任何合理的方式将输入转换为整数时,它将引发ArgumentError。defmy_method(number)