您当前的位置:首页 > 电脑百科 > 程序开发 > 框架

springboot 缓存一致性常用解决方案

时间:2022-09-04 14:53:05  来源:今日头条  作者:小心程序猿QAQ

多级缓存在微服务的架构设计中可谓随处可见,多级缓存作为提升系统高并发的常规手段,在各类大中小型的系统设计中都有体现;

下图是一张简单的服务端多级缓存设计示意图,多级缓存的常用解决方案,像ehcache + redis,或caffeine + springcache等,即利用JVM内存缓存 + redis缓存配合;


 

一、缓存一致性问题

多级缓存带来的好处是显著的,一定程度上可以应对较高的并发,但随之带来了一个比较大的问题就是缓存一致性问题;

我们知道,JVM缓存属于进程级的缓存,和当前服务实例是绑定的,而redis缓存可以作为分布式缓存,通常JVM缓存的是那些生命周期较短的热点查询数据,即过期时间不会太久,而redis缓存相对来说,过期时间相对长一点,JVM缓存通常作为服务端扛压的第一道屏障,如果设置的过期时间太长,将会对JVM内存的开销非常大,所以一般作为短频使用;

设想这么一个场景,服务A采用多实例部署,这里假设部署了两个节点,首次根据ID查询一个用户信息的对象数据将会同时被JVM缓存,同时也会被redis缓存,下一次过来同样参数的请求时,首先走JVM缓存,查到了直接返回,否则走redis缓存;

上面是一个正常的关于缓存存取的过程,问题是,JVM缓存是同进程绑定的,如果第一个节点的数据发生了变更,比如删除了,对于redis缓存来说,可以做到动态刷缓存的效果,但是redis缓存和本地缓存之间并没有一种强同步的机制确保两者的缓存保持一致;

甚至来说,第一个节点与第二个节点之间,两者是无状态的,当第一个节点上面的数据被删除时,假如此刻并发的查询请求到达第二个节点,JVM缓存查询到必然是上一次缓存的数据;

于是,我们的问题就是,在多级缓存模式下,如何解决缓存一致性的问题呢?

二、一个简单的案例

基于之前的一篇springcache 详细使用和spring boot 二级缓存案例基础上我们进行案例演示和改造;

在案例中,我们提供了几个核心的接口:

 

  • 根据用户ID查询用户,并缓存到redis;
  • 根据用户ID查询用户,并缓存到JVM,这里采用caffeine;
  • 根据用户ID删除用户;
1、缓存核心配置类import com.fasterxml.jackson.annotation.JsonAutoDetect;import com.fasterxml.jackson.annotation.JsonInclude;import com.fasterxml.jackson.annotation.JsonTypeInfo;import com.fasterxml.jackson.annotation.PropertyAccessor;import com.fasterxml.jackson.databind.MApperFeature;import com.fasterxml.jackson.databind.ObjectMapper;import com.fasterxml.jackson.databind.SerializationFeature;import com.fasterxml.jackson.databind.jsontype.impl.LAIssezFaireSubTypeValidator;import com.fasterxml.jackson.datatype.jsr310.JAVATimeModule;import com.Github.benmanes.caffeine.cache.Caffeine;import org.springframework.cache.CacheManager;import org.springframework.cache.annotation.CachingConfigurerSupport;import org.springframework.cache.caffeine.CaffeineCacheManager;import org.springframework.cache.interceptor.KeyGenerator;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.Primary;import org.springframework.data.redis.cache.RedisCacheConfiguration;import org.springframework.data.redis.cache.RedisCacheManager;import org.springframework.data.redis.connection.RedisConnectionFactory;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;import org.springframework.data.redis.serializer.RedisSerializationContext;import org.springframework.data.redis.serializer.StringRedisSerializer;import org.springframework.util.StringUtils;import java.lang.reflect.Method;import java.time.Duration;import java.util.concurrent.TimeUnit;@Configurationpublic class RedisConfig extends CachingConfigurerSupport {@Beanpublic RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) {RedisTemplate template = new RedisTemplate<>();template.setConnectionFactory(connectionFactory);//使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);ObjectMapper mapper = new ObjectMapper();mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);jackson2JsonRedisSerializer.setObjectMapper(mapper);template.setValueSerializer(jackson2JsonRedisSerializer);//使用StringRedisSerializer来序列化和反序列化redis的key值template.setKeySerializer(new StringRedisSerializer());template.afterPropertiesSet();return template;* 分钟级别* @param connectionFactory* @return@Bean("cacheManagerMinutes")public RedisCacheManager cacheManagerMinutes(RedisConnectionFactory connectionFactory){RedisCacheConfiguration configuration = instanceConfig(3 * 60L);return RedisCacheManager.builder(connectionFactory).cacheDefaults(configuration).transactionAware().build();* 小时级别* @param connectionFactory* @return@Bean("cacheManagerHour")@Primarypublic RedisCacheManager cacheManagerHour(RedisConnectionFactory connectionFactory){RedisCacheConfiguration configuration = instanceConfig(3600L);return RedisCacheManager.builder(connectionFactory).cacheDefaults(configuration).transactionAware().build();* 天级别* @param connectionFactory* @return@Bean("cacheManagerDay")public RedisCacheManager cacheManagerDay(RedisConnectionFactory connectionFactory){RedisCacheConfiguration configuration = instanceConfig(3600 * 24L);;return RedisCacheManager.builder(connectionFactory).cacheDefaults(configuration).transactionAware().build();* 正常时间的本地缓存@Bean("caffeineCacheManager")public CacheManager caffeineCacheManager() {CaffeineCacheManager cacheManager = new CaffeineCacheManager();cacheManager.setCaffeine(Caffeine.newBuilder().expireAfterWrite(50, TimeUnit.SECONDS).initialCapacity(256).maximumSize(10000));return cacheManager;private RedisCacheConfiguration instanceConfig(long ttl){Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);ObjectMapper objectMapper = new ObjectMapper();objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);objectMapper.registerModule(new JavaTimeModule());objectMapper.configure(MapperFeature.USE_ANNOTATIONS,false);//只针对非空的值进行序列化objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);//将类型序列化到属性的json字符串objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,ObjectMapper.DefaultTyping.NON_FINAL,JsonTypeInfo.As.PROPERTY);jackson2JsonRedisSerializer.setObjectMapper(objectMapper);return RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(ttl)).disableCachingNullValues().serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer));* 自定义key生成策略* @return@Bean("defaultSpringKeyGenerator")public KeyGenerator defaultSpringKeyGenerator(){return new KeyGenerator() {@Overridepublic Object generate(Object o, Method method, Object... objects) {String key = o.getClass().getSimpleName() + "_"+ method.getName() +"_"+ StringUtils.arrayToDelimitedString(objects,"_");System.out.println("key :" + key);return key;}2、配置文件开启使用 springcachespring:redis:host: localhostport: 6379cache:type: redis3、几个核心接口1)根据用户ID获取用户@GetMapping("/getById")public DbUser getById(String id){return dbUserService.getById(id);@Override@Cacheable(value = {"dbUser"},key = "#root.args[0]",cacheManager = "cacheManagerMinutes")public DbUser getById(String id) {System.out.println("首次查询走数据库");DbUser dbUser = dbUserMapper.getByUserId(id);return dbUser;2)根据用户ID查询用户,并缓存到JVM;@GetMapping("/getByIdFromCaffeine")public DbUser getByIdFromCaffeine(String id){return dbUserService.getByIdFromCaffeine(id);@Override@Cacheable(value = {"dbUser"},key = "#root.args[0]",cacheManager = "caffeineCacheManager")public DbUser getByIdFromCaffeine(String id) {System.out.println("查询数据库");DbUser dbUser = dbUserMapper.getByUserId(id);System.out.println("第一次走缓存");return dbUser;3)根据用户ID删除用户;@GetMapping("/deleteById")public String deleteById(String id){return dbUserService.deleteById(id);@Override@CacheEvict(value = {"dbUser"},key = "#root.args[0]",cacheManager = "cacheManagerMinutes")public String deleteById(String id) {dbUserMapper.deleteByUserId(id);return "delete success";4、功能测试

 

首先在数据库的 db_user 表准备一条测试数据

分别调用查询用户接口

1、 http://localhost:8083/getById?id=1 ;

多次刷新接口,sql只输出了一次


 

2、 http://localhost:8083/getByIdFromCaffeine?id=1


 

多次刷新接口,sql只输出了一次


 

从上面的结果可以看到,我们模拟了查询数据分别缓存到了JVM内存和redis的效果,接下来,删除当前这条数据,执行下面的接口

3、 http://localhost:8083/deleteById?id=1


 


 

再次调用第一个查询用户的接口,无返回数据,表明redis中缓存的结果被清理了,这是我们使用了springcache后,通过 CacheEvict 这个注解,会自动帮我们管理redis中的缓存;

但这时,再次调用查询JVM缓存的接口,发现仍然可以从本地缓存中得到数据

4、 http://localhost:8083/getByIdFromCaffeine?id=1


 

基于上面的测试结果,可以看到,缓存一致性的问题就产生了,这里我故意将本地缓存的时间调整的长了一点,实际开发过程中,建议本地缓存的时间一般不要超过1分钟;

三、解决方案一:清理redis缓存,同步清理本地缓存1、增加一个本地缓存操作的工具类import com.congge.config.SpringContextHolder;import org.springframework.cache.Cache;import org.springframework.cache.CacheManager;import java.util.Objects;public class CaffeineCacheUtils {private static CacheManager cm;static {cm = SpringContextHolder.getBean("caffeineCacheManager");* 添加缓存* @param cacheName 缓存名称* @param key 缓存key* @param value 缓存值public static void put(String cacheName, String key, Object value) {Cache cache = cm.getCache(cacheName);cache.put(key, value);* 获取缓存* @param cacheName 缓存名称* @param key 缓存key* @returnpublic static Object get(String cacheName, String key) {Cache cache = cm.getCache(cacheName);if (cache == null) {return null;return Objects.requireNonNull(cache.get(key)).get();* 获取缓存(字符串)* @param cacheName 缓存名称* @param key 缓存key* @returnpublic static String getString(String cacheName, String key) {Cache cache = cm.getCache(cacheName);if (cache == null) {return null;Cache.ValueWrapper wrapper = cache.get(key);if (wrapper == null) {return null;return Objects.requireNonNull(wrapper.get()).toString();* 获取缓存(泛型)* @param cacheName 缓存名称* @param key 缓存key* @param clazz 缓存类* @param 返回值泛型* @returnpublic static T get(String cacheName, String key, Class clazz) {Cache cache = cm.getCache(cacheName);if (cache == null) {return null;Cache.ValueWrapper wrapper = cache.get(key);if (wrapper == null) {return null;return (T) wrapper.get();* 清理缓存* @param cacheName 缓存名称* @param key 缓存keypublic static void evict(String cacheName, String key) {Cache cache = cm.getCache(cacheName);System.out.println(cache.getName());if (cache != null) {cache.evict(key);2、删除用户接口中同步清理本地缓存

只需改造下删除接口的服务实现方法即可

private CaffeineCacheUtils caffeineCacheUtils = new CaffeineCacheUtils();* 删除,同时需要删除相关的key* @param id* @return@Override@CacheEvict(value = {"dbUser"},key = "#root.args[0]",cacheManager = "cacheManagerMinutes")public String deleteById(String id) {dbUserMapper.deleteByUserId(id);caffeineCacheUtils.evict("dbUser",id);return "delete success";3、方案优缺点优点

  • 操作简便;
  • 只要参数传入正确,就可以确保缓存一致性;
  • 适合单机模式下使用
缺点
  • 代码产生了一定的耦合性;
  • 不适合分布式环境使用;
  • 需要手动管理key的相关参数;
四、解决方案二:使用zookeeper 实现缓存同步

 

对zookeeper有所了解和使用的同学,应该对zk的节点管理不陌生,zk作为一款分布式协调中间件,在很多分布式场景都有着广泛的使用,比如实现集群选举,分布式锁,节点管理等等,利用zk的节点属性,可以很好的解决这个问题;

使用zk的解决思路

  • 查询用户接口中,注册一个节点,节点命名最好和缓存的key保持一致;
  • 删除接口中,手动触zk的节点删除;
  • zk监听到删除节点的事件变化时,同步清理本地缓存;
1、zk客户端依赖com.101teczkclient0.10org.slf4jslf4j-log4j122、提供一个zk节点操作工具类import org.I0Itec.zkclient.IZkDataListener;import org.I0Itec.zkclient.ZkClient;import org.I0Itec.zkclient.serialize.SerializableSerializer;import org.Apache.zookeeper.CreateMode;public class ZkUtils {private ZkClient zkClient = null;private String node;/*public static void main(String[] args) {ZkUtils zkUtils = new ZkUtils();zkUtils.createNode(node);zkUtils.nodeExist(node);zkUtils.deleteNode(node);public ZkUtils(String node) {zkClient = new ZkClient("localhost:2181", 60000 * 30, 60000, new SerializableSerializer());//监听节点变化//需要通过java修改zookeeper数据,才能监听到zkClient.subscribeDataChanges("/" + node, new IZkDataListener() {//节点数据变化时触发@Overridepublic void handleDataChange(String s, Object o) throws Exception {System.out.println("change Node: " + s);System.out.println("change data: " + o);//节点数据删除时触发@Overridepublic void handleDataDeleted(String s) throws Exception {System.out.println("delete Node: " + s);* 创建zk节点* @param nodepublic void createNode(String node) {//创建持久节点String node1 = zkClient.create("/" + node, node, CreateMode.PERSISTENT);System.out.println(node1);* 修改zk节点数据* @param node* @param datapublic void writeNodeData(String node, String data) {zkClient.writeData("/" + node, 233);* 查询zk节点* @param nodepublic boolean nodeExist(String node) {boolean exists = zkClient.exists("/" + node);return exists;* 查询节点数据* @param node* @returnpublic String findNodeData(String node) {Object data = zkClient.readData("/" + node);System.out.println(data);return data.toString();* 删除节点* @param nodepublic void deleteNode(String node) {boolean b2 = zkClient.deleteRecursive("/" + node);System.out.println(b2);3、查询用户接口,注册缓存的key对应的z-node节点@Override@Cacheable(value = {"dbUser"},key = "#root.args[0]",cacheManager = "cacheManagerMinutes")public DbUser getById(String id) {System.out.println("首次查询走数据库");DbUser dbUser = dbUserMapper.getByUserId(id);//FIXME 将缓存注册到节点registerCacheNode(id);return dbUser;public void registerCacheNode(String id){String node = "user:" + id;ZkUtils zkUtils = new ZkUtils(node);zkUtils.createNode(node);4、删除用户接口添加删除zk节点逻辑@Override@CacheEvict(value = {"dbUser"},key = "#root.args[0]",cacheManager = "cacheManagerMinutes")public String deleteById(String id) {dbUserMapper.deleteByUserId(id);//删除 z-node 节点String node = "user:" + id;ZkUtils zkUtils = new ZkUtils(node);zkUtils.deleteNode(node);return "delete success";5、改造zk监听逻辑,同步移除本地缓存private CaffeineCacheUtils caffeineCacheUtils = new CaffeineCacheUtils();public ZkUtils(String node) {zkClient = new ZkClient("localhost:2181", 60000 * 30, 60000, new SerializableSerializer());//监听节点变化//需要通过java修改zookeeper数据,才能监听到zkClient.subscribeDataChanges("/" + node, new IZkDataListener() {//节点数据变化时触发@Overridepublic void handleDataChange(String s, Object o) throws Exception {System.out.println("change Node: " + s);System.out.println("change data: " + o);//节点数据删除时触发@Overridepublic void handleDataDeleted(String s) throws Exception {System.out.println("delete Node: " + s);caffeineCacheUtils.evict("dbUser","1");6、测试1、启动服务后,按照上面的测试步骤,分别调用2个查询接口

 

通过控制台输出结果,可以看到节点数据注册到zk中


 


 

2、调用删除接口

此时zk的监听逻辑中监听到了节点数据变更的事件,在变更的逻辑中,我们将同步删除本地缓存的数据;


 

再次调用时发现缓存已经被清理


 

通过上面的操作演示,实现了基于zk的节点注册与事件监听机制实现缓存一致性的问题处理;

五、解决方案三:使用redis事件订阅与发布机制实现缓存同步

Redis 发布订阅 (pub/sub) 是一种消息通信模式:发送者 (pub) 发送消息,订阅者 (sub) 接收消息。

这种模式很像消息队列的实现机制,服务端发布消息到topic,客户端监听topic的消息,并做自身的业务处理;

只不过在redis这里,不叫topic,而是channel,下面来看一个简单的redis实现的发布订阅使用

1、导入依赖org.springframework.bootspring-boot-starter-data-redis2、自定义 RedisMessageListener

该类的功能和消息中间件中的监听逻辑很相似

import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.redis.connection.Message;import org.springframework.data.redis.connection.MessageListener;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.stereotype.Component;@Componentpublic class RedisMessageListener implements MessageListener {@Autowiredprivate RedisTemplate redisTemplate;@Overridepublic void onMessage(Message message, byte[] pattern) {// 获取消息byte[] messageBody = message.getBody();// 使用值序列化器转换Object msg = redisTemplate.getValueSerializer().deserialize(messageBody);// 获取监听的频道byte[] channelByte = message.getChannel();// 使用字符串序列化器转换Object channel = redisTemplate.getStringSerializer().deserialize(channelByte);// 渠道名称转换String patternStr = new String(pattern);System.out.println(patternStr);System.out.println("---频道---: " + channel);System.out.println("---消息内容---: " + msg);3、自定义 RedisSubConfig

该类用于配置特定的channel,即监听来自哪些channel的消息

import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.data.redis.connection.RedisConnectionFactory;import org.springframework.data.redis.listener.ChannelTopic;import org.springframework.data.redis.listener.RedisMessageListenerContainer;@Configurationpublic class RedisSubConfig {@Beanpublic RedisMessageListenerContainer container(RedisConnectionFactory factory, RedisMessageListener listener) {RedisMessageListenerContainer container = new RedisMessageListenerContainer();container.setConnectionFactory(factory);//订阅频道redis.news 和 redis.life 这个container 可以添加多个 messageListenercontainer.addMessageListener(listener, new ChannelTopic("redis.life"));container.addMessageListener(listener, new ChannelTopic("redis.news"));return container;4、最后编写一个接口做测试@GetMapping("/testPublish")public void testPublish(){dbUserService.testPublish();@Autowiredprivate RedisTemplate redisTemplate;@Overridepublic void testPublish() {redisTemplate.convertAndSend("redis.life", "aaa");redisTemplate.convertAndSend("redis.news", "bbb");

调用下接口,可以看到控制台输出如下信息


 

通过上面的演示,快速了解了一下redis的这种发布订阅模式的功能使用,下面就来使用这种方式来解决缓存一致性问题;

5、删除用户接口中向redis channel 推送消息@Override@CacheEvict(value = {"dbUser"},key = "#root.args[0]",cacheManager = "cacheManagerMinutes")public String deleteById(String id) {dbUserMapper.deleteByUserId(id);redisTemplate.convertAndSend("redis.user", id);return "delete success";6、RedisMessageListener 改造

添加删除本地缓存逻辑

@Overridepublic void onMessage(Message message, byte[] pattern) {CaffeineCacheUtils caffeineCacheUtils = new CaffeineCacheUtils();// 获取消息byte[] messageBody = message.getBody();// 使用值序列化器转换Object msg = redisTemplate.getValueSerializer().deserialize(messageBody);// 获取监听的频道byte[] channelByte = message.getChannel();// 使用字符串序列化器转换Object channel = redisTemplate.getStringSerializer().deserialize(channelByte);// 渠道名称转换String patternStr = new String(pattern);System.out.println(patternStr);System.out.println("---频道---: " + channel);System.out.println("---消息内容---: " + msg);caffeineCacheUtils.evict("dbUser",patternStr);7、模拟测试

启动服务后,直接调用删除用户接口,可以看到,监听逻辑中收到了一条消息,然后调用本地缓存工具类删除本地缓存即可


 


 

六、解决方案四:使用消息队列实现缓存同步

了解了redis发布订阅这种方式实现原理后,如果再更换为消息中间件来实现就不难理解了,其实现的大致思路如下:

 

  1. 调用删除接口删除用户;
  2. 向特定的队列推送一条删除消息;
  3. 在消息监听逻辑中接收消息,并清理本地缓存

 

以rabbitmq为例,其核心实现如下:

@RabbitHandlerpublic void process(String msg) {System.out.println("topicMessageReceiver 接收到了消息 : " +msg);//执行本地缓存的删除操作

关于rabbitmq的相关实现感兴趣的同学可以参考:rabbbitmq 技术全解

七、总结

关于后3三种的实现,不仅可以解决缓存一致性问题,同时适用于分布式应用的场景,算是比较通用的解决方案,但这样一来,引入了第三方组件,也增加了系统整体的复杂性,这一点需要在架构设计中进行综合考量,结合小编本人的一些实践经验,比较推荐使用redis的发布订阅模式,这种方式简单高效,同时兼顾了避免引入更多的外部组件,可酌情参考。



Tags:springboot   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
详解基于SpringBoot的WebSocket应用开发
在现代Web应用中,实时交互和数据推送的需求日益增长。WebSocket协议作为一种全双工通信协议,允许服务端与客户端之间建立持久性的连接,实现实时、双向的数据传输,极大地提升了用...【详细内容】
2024-01-30  Search: springboot  点击:(18)  评论:(0)  加入收藏
SpringBoot如何实现缓存预热?
缓存预热是指在 Spring Boot 项目启动时,预先将数据加载到缓存系统(如 Redis)中的一种机制。那么问题来了,在 Spring Boot 项目启动之后,在什么时候?在哪里可以将数据加载到缓存系...【详细内容】
2024-01-19  Search: springboot  点击:(86)  评论:(0)  加入收藏
SpringBoot3+Vue3 开发高并发秒杀抢购系统
开发高并发秒杀抢购系统:使用SpringBoot3+Vue3的实践之旅随着互联网技术的发展,电商行业对秒杀抢购系统的需求越来越高。为了满足这种高并发、高流量的场景,我们决定使用Spring...【详细内容】
2024-01-14  Search: springboot  点击:(91)  评论:(0)  加入收藏
公司用了六年的 SpringBoot 项目部署方案,稳得一批!
本篇和大家分享的是springboot打包并结合shell脚本命令部署,重点在分享一个shell程序启动工具,希望能便利工作。 profiles指定不同环境的配置 maven-assembly-plugin打发布压...【详细内容】
2024-01-10  Search: springboot  点击:(165)  评论:(0)  加入收藏
简易版的SpringBoot是如何实现的!!!
SpringBoot作为目前最流行的框架之一,同时是每个程序员必须掌握的知识,其提供了丰富的功能模块和开箱即用的特性,极大地提高了开发效率和降低了学习成本,使得开发人员能够更专注...【详细内容】
2023-12-29  Search: springboot  点击:(135)  评论:(0)  加入收藏
用 SpringBoot+Redis 解决海量重复提交问题
前言 一:搭建redis的服务Api 二:自定义注解AutoIdempotent 三:token创建和检验 四:拦截器的配置 五:测试用例 六:总结前言:在实际的开发项目中,一个对外暴露的接口往往会面临很多...【详细内容】
2023-12-20  Search: springboot  点击:(53)  评论:(0)  加入收藏
SpringBoot中如何优雅地个性化定制Jackson
当使用 JSON 格式时,Spring Boot 将使用ObjectMapper实例来序列化响应和反序列化请求。在本教程中,我们将了解配置序列化和反序列化选项的最常用方法。一、默认配置默认情况下...【详细内容】
2023-12-20  Search: springboot  点击:(132)  评论:(0)  加入收藏
springboot-如何集成Validation进行参数校验
一、步骤概览 二、步骤说明1.引入依赖包在 pom.xml 文件中引入 validation 组件,它提供了在 Spring Boot 应用程序中进行参数校验的支持。<!-- WEB 程序依赖包 --><dependen...【详细内容】
2023-12-13  Search: springboot  点击:(157)  评论:(0)  加入收藏
优雅的springboot参数校验,你学会了吗?
前言在后端的接口开发过程,实际上每一个接口都或多或少有不同规则的参数校验,有一些是基础校验,如非空校验、长度校验、大小校验、格式校验;也有一些校验是业务校验,如学号不能重...【详细内容】
2023-11-29  Search: springboot  点击:(200)  评论:(0)  加入收藏
Springboot扩展点之BeanDefinitionRegistryPostProcessor,你学会了吗?
前言通过这篇文章来大家分享一下,另外一个Springboot的扩展点BeanDefinitionRegistryPostProcessor,一般称这类扩展点为容器级后置处理器,另外一类是Bean级的后置处理器;容器级...【详细内容】
2023-11-27  Search: springboot  点击:(175)  评论:(0)  加入收藏
▌简易百科推荐
Qt与Flutter:在跨平台UI框架中哪个更受欢迎?
在跨平台UI框架领域,Qt和Flutter是两个备受瞩目的选择。它们各自具有独特的优势,也各自有着广泛的应用场景。本文将对Qt和Flutter进行详细的比较,以探讨在跨平台UI框架中哪个更...【详细内容】
2024-04-12  刘长伟    Tags:UI框架   点击:(1)  评论:(0)  加入收藏
Web Components实践:如何搭建一个框架无关的AI组件库
一、让人又爱又恨的Web ComponentsWeb Components是一种用于构建可重用的Web元素的技术。它允许开发者创建自定义的HTML元素,这些元素可以在不同的Web应用程序中重复使用,并且...【详细内容】
2024-04-03  京东云开发者    Tags:Web Components   点击:(8)  评论:(0)  加入收藏
Kubernetes 集群 CPU 使用率只有 13% :这下大家该知道如何省钱了
作者 | THE STACK译者 | 刘雅梦策划 | Tina根据 CAST AI 对 4000 个 Kubernetes 集群的分析,Kubernetes 集群通常只使用 13% 的 CPU 和平均 20% 的内存,这表明存在严重的过度...【详细内容】
2024-03-08  InfoQ    Tags:Kubernetes   点击:(17)  评论:(0)  加入收藏
Spring Security:保障应用安全的利器
SpringSecurity作为一个功能强大的安全框架,为Java应用程序提供了全面的安全保障,包括认证、授权、防护和集成等方面。本文将介绍SpringSecurity在这些方面的特性和优势,以及它...【详细内容】
2024-02-27  风舞凋零叶    Tags:Spring Security   点击:(54)  评论:(0)  加入收藏
五大跨平台桌面应用开发框架:Electron、Tauri、Flutter等
一、什么是跨平台桌面应用开发框架跨平台桌面应用开发框架是一种工具或框架,它允许开发者使用一种统一的代码库或语言来创建能够在多个操作系统上运行的桌面应用程序。传统上...【详细内容】
2024-02-26  贝格前端工场    Tags:框架   点击:(47)  评论:(0)  加入收藏
Spring Security权限控制框架使用指南
在常用的后台管理系统中,通常都会有访问权限控制的需求,用于限制不同人员对于接口的访问能力,如果用户不具备指定的权限,则不能访问某些接口。本文将用 waynboot-mall 项目举例...【详细内容】
2024-02-19  程序员wayn  微信公众号  Tags:Spring   点击:(39)  评论:(0)  加入收藏
开发者的Kubernetes懒人指南
你可以将本文作为开发者快速了解 Kubernetes 的指南。从基础知识到更高级的主题,如 Helm Chart,以及所有这些如何影响你作为开发者。译自Kubernetes for Lazy Developers。作...【详细内容】
2024-02-01  云云众生s  微信公众号  Tags:Kubernetes   点击:(51)  评论:(0)  加入收藏
链世界:一种简单而有效的人类行为Agent模型强化学习框架
强化学习是一种机器学习的方法,它通过让智能体(Agent)与环境交互,从而学习如何选择最优的行动来最大化累积的奖励。强化学习在许多领域都有广泛的应用,例如游戏、机器人、自动驾...【详细内容】
2024-01-30  大噬元兽  微信公众号  Tags:框架   点击:(68)  评论:(0)  加入收藏
Spring实现Kafka重试Topic,真的太香了
概述Kafka的强大功能之一是每个分区都有一个Consumer的偏移值。该偏移值是消费者将读取的下一条消息的值。可以自动或手动增加该值。如果我们由于错误而无法处理消息并想重...【详细内容】
2024-01-26  HELLO程序员  微信公众号  Tags:Spring   点击:(88)  评论:(0)  加入收藏
SpringBoot如何实现缓存预热?
缓存预热是指在 Spring Boot 项目启动时,预先将数据加载到缓存系统(如 Redis)中的一种机制。那么问题来了,在 Spring Boot 项目启动之后,在什么时候?在哪里可以将数据加载到缓存系...【详细内容】
2024-01-19   Java中文社群  微信公众号  Tags:SpringBoot   点击:(86)  评论:(0)  加入收藏
站内最新
站内热门
站内头条