14.Spring Boot与缓存
1.JSR107
Java Caching:定义了5个核心接口,分别是CachingProvider,CacheManager,Cache,Entry 和 Expiry。
CachingProvider:定义了创建、配置、获取、管理和控制多个CacheManager。一个应用可以在运行期访问多个CachingProvider。
CacheManager:定义了创建、配置、获取、管理和控制多个唯一命名的Cache,这些Cache存在于CacheManager的上下文中。一个CacheManager仅被一个CachingProvider所拥有。
Cache:是一个类似Map的数据结构并临时存储以Key为索引的值。一个Cache仅被一个CacheManager所拥有。
Entry:是一个存储在Cache中的key-value对。
Expiry:每一个存储在Cache中的条目有一个定义的有效期。一旦超过这个时间,条目为过期的状态。一旦过期,条目将不可访问、更新和删除。缓存有效期可以通过ExpiryPolicy设置。
依赖:
<dependency> <groupId>javax.cache</groupId> <artifactId>cache-api</artifactId> </dependency>
jsr107使用较少
2.spring缓存抽象
Spring从3.1开始定义了org.springframework.cache.Cache
和org.springframework.cache.CacheManager
接口来统一不同的缓存技术;并支持使用JCache(JSR-107)注解简化我们开发;
Cache接口为缓存的组件规范定义,包含缓存的各种操作集合;Cache接口下Spring提供了各种xxxCache的实现;如RedisCache,EhCacheCache,ConcurrentMapCache等;
每次调用需要缓存功能的方法时,Spring会检查指定参数的指定的目标方法是否已经被调用过;如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户。下次调用直接从缓存中获取。
使用Spring缓存抽象时我们需要关注以下两点;
1、确定方法需要被缓存以及他们的缓存策略
2、从缓存中读取之前缓存存储的数据
- 几个重要概念
名称 | 作用 |
---|---|
Cache | 缓存接口,定义缓存操作。实现有:RedisCache、EhCacheCache、ConcurrentMapCache 等 |
CacheManager | 缓存管理器,管理各种缓存(Cache)组件 |
keyGenerator | 缓存数据时key生成策略 |
serialize | 缓存数据时value序列化策略 |
- 缓存注解
注解名称 | 作用 |
---|---|
@Cacheable | 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存(作用在方法上) |
@CacheEvict | 清空缓存(作用在方法上) |
@CachePut | 保证方法被调用,又希望结果被缓存。 (缓存更新)(作用在方法上) |
@EnableCaching | 开启基于注解的缓存 |
- 注解中
key
属性可以使用SqEL表达式
开启基于注解的缓存(启动类) @EnableCaching
@MapperScan("com.hguo.cache.mapper") @SpringBootApplication @EnableCaching // 开启缓存 public class Springboot01CacheApplication { public static void main(String[] args) { SpringApplication.run(Springboot01CacheApplication.class, args); } }
使用注解缓存
标注缓存注解即可@Cacheable
、@CacheEvict
、@CachePut
,默认使用的是ConcurrentMapCacheManager创建ConcurrentMapCache;将数据保存在ConcurrentMap<Object, Object>
中,开发中使用缓存中间件;redis、memcached、ehcache。
/* @CacheConfig注解: 抽取缓存的公共配置 cacheNames="emp"配置公共的缓存组件名,以下方法上注解就不需要定义缓存组件名 还可以定义其他一些公共配置:cacheManager... */ @CacheConfig(cacheNames="emp", cacheManager = "employeeCacheManager") @Service public class EmployeeService { @Autowired EmployeeMapper employeeMapper; /** * 将方法的运行结果进行缓存;以后再要相同的数据,直接从缓存中获取,不用调用方法; * CacheManager管理多个Cache组件的,对缓存的真正CRUD操作在Cache组件中,每一个缓存组件有自己唯一一个名字; * * 原理: * 1、自动配置类;CacheAutoConfiguration * 2、缓存的配置类(从上到下有顺序的,上面的配置类创建缓存处理器后,下面的就不能再创建,不起作用了) * org.springframework.boot.autoconfigure.cache.GenericCacheConfiguration * org.springframework.boot.autoconfigure.cache.JCacheCacheConfiguration * org.springframework.boot.autoconfigure.cache.EhCacheCacheConfiguration * org.springframework.boot.autoconfigure.cache.HazelcastCacheConfiguration * org.springframework.boot.autoconfigure.cache.InfinispanCacheConfiguration * org.springframework.boot.autoconfigure.cache.CouchbaseCacheConfiguration * org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration * org.springframework.boot.autoconfigure.cache.CaffeineCacheConfiguration * org.springframework.boot.autoconfigure.cache.GuavaCacheConfiguration * org.springframework.boot.autoconfigure.cache.SimpleCacheConfiguration【默认】 * org.springframework.boot.autoconfigure.cache.NoOpCacheConfiguration * * 3、哪个配置类默认生效:SimpleCacheConfiguration; * * 4、给容器中注册了一个CacheManager:ConcurrentMapCacheManager * 5、可以获取和创建ConcurrentMapCache类型的缓存组件;他的作用将数据保存在ConcurrentMap中; * * 运行流程: * @Cacheable: * 1、方法运行之前,先去查询Cache(缓存组件),按照cacheNames指定的名字获取; * (CacheManager先获取相应的缓存),第一次获取缓存如果没有Cache组件会自动创建。 * 2、去Cache中查找缓存的内容,使用一个key,默认就是方法的参数; * key是按照某种策略生成的;默认是使用keyGenerator生成的,默认使用SimpleKeyGenerator生成key; * SimpleKeyGenerator生成key的默认策略; * 如果没有参数;key=new SimpleKey(); * 如果有一个参数:key=参数的值 * 如果有多个参数:key=new SimpleKey(params); * 3、没有查到缓存就调用目标方法; * 4、将目标方法返回的结果,放进缓存中 * * @Cacheable标注的方法执行之前先来检查缓存中有没有这个数据,默认按照参数的值作为key去查询缓存, * 如果没有就运行方法并将结果放入缓存;以后再来调用就可以直接使用缓存中的数据; * * 核心: * 1)、使用CacheManager【ConcurrentMapCacheManager】按照名字得到Cache【ConcurrentMapCache】组件 * 2)、key使用keyGenerator生成的,默认是SimpleKeyGenerator * * * 几个属性: * cacheNames/value:指定缓存组件的名字;将方法的返回结果放在哪个缓存中,是数组的方式,可以指定多个缓存; * * key:缓存数据使用的key;可以用它来指定。默认是使用方法参数的值如:参数值-方法的返回值 * 编写SpEL:#id(参数id的值)、#a0、#p0、#root.args[0]、getEmp[2] * * keyGenerator:key的生成器;可以自己指定key的生成器的组件id * (key/keyGenerator:连个属性二选一使用) * * * cacheManager:指定缓存管理器;或者cacheResolver指定获取解析器 * * condition:指定符合条件的情况下才缓存;如: condition = "#id > 0" * condition = "#a0 > 1" // 第一个参数的值>1的时候才进行缓存 * * unless:否定缓存;当unless指定的条件为true,方法的返回值就不会被缓存,可以对获取到结果进行判断 * unless = "#result == null" * unless = "#a0==2" // 如果第一个参数的值是2,结果不缓存; * sync:是否使用异步模式(问题:异步模式下,unless不支持) * */ @Cacheable(value={"emp"},keyGenerator="myKeyGenerator",condition="#a0>1",unless="#a0==2") public Employee getEmp(Integer id){ System.out.println("查询"+id+"号员工"); Employee emp = employeeMapper.getEmpById(id); return emp; } /** * @CachePut:既调用方法,又更新缓存数据;同步更新缓存 * 修改了数据库的某个数据,同时更新缓存; * 运行时机: * 1、先调用目标方法 * 2、将目标方法的结果缓存起来 * * 测试步骤: * 1、查询1号员工;调用上面getEmp()方法,查到的结果会放在缓存中;此时缓存的key是参数值: * key:1 value:lastName:张三 * 2、以后查询还是之前的结果 * 3、更新1号员工数据:lastName:zhangsan;gender:0 将方法的返回值也放进缓存了;更新后存入缓存的key还是方法的参数值,但是此时参数值是Employee类实例: * key:传入的employee对象 value:返回的employee对象; * 4、查询1号员工?应该是更新后的员工;但是查询的key还是旧key,所以新存入缓存的数据查不到。此时我们自定更新的key是哪个: * key = "#employee.id":使用传入的参数的员工id * key = "#result.id":使用返回后的id * * @Cacheable的key是不能用#result,因为Cacheable是在调用目标方法之前查看缓存是否有这个key, * @CachePut是在调用目标方法之后,此时是有结果可以查询的。 * * 重点:@CachePut注解需要指定哪个key的缓存值需要更新,如果不指定则key是方法参数值,查询缓存的key不一样容易查不到更新的缓存结果 */ @CachePut(value = "emp", key = "#result.id") public Employee updateEmp(Employee employee){ System.out.println("updateEmp:"+employee); employeeMapper.updateEmp(employee); return employee; } /** * @CacheEvict:缓存清除 * key:指定要清除的数据 * allEntries = true:指定清除emp这个缓存中所有的数据 * * beforeInvocation = false(默认):缓存的清除是否在方法之前执行 * 默认代表缓存清除操作是在方法执行之后执行;如果出现异常缓存就不会清除 * * beforeInvocation = true: * 代表清除缓存操作是在方法运行之前执行,无论方法是否出现异常,缓存都清除 * * */ @CacheEvict(value="emp",beforeInvocation = true,key = "#id") public void deleteEmp(Integer id){ System.out.println("deleteEmp:"+id); //employeeMapper.deleteEmpById(id); int i = 10/0; } // @Caching 定义复杂的缓存规则 @Caching( cacheable = { @Cacheable(value="emp",key = "#lastName") }, put = { // 可以根据Employee属性ip查也可以根据属性email查,查到的结果是缓存中的数据 @CachePut(value="emp",key = "#result.id"), @CachePut(value="emp",key = "#result.email") } ) public Employee getEmpByLastName(String lastName){ return employeeMapper.getEmpByLastName(lastName); } }
自定义keyGenerator
@Configuration public class MyCacheConfig { @Bean("myKeyGenerator") public KeyGenerator keyGenerator(){ return new KeyGenerator(){ @Override public Object generate(Object target, Method method, Object... params) { return method.getName()+"["+ Arrays.asList(params).toString()+"]"; } }; } }
3.整合redis实现缓存
redis简介
Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,redis作为一种非关系型数据库,读写非常快,应用十分广泛,它采用key-value的形式存储数据,value常用的五大数据类型有string(字符串),list(链表),set(集合),zset(有序集合)和hash(哈希表)。它可以用作数据库、缓存和消息中间件。
redis的特性决定了它的功能,它的应用场景如下:
1.排行榜,利用zset可以方便的实现排序功能
2.计数器,利用redis中原子性的自增操作,可以统计到阅读量,点赞量等功能
3.简单消息队列,list存储结构,满足先进先出的原则,可以使用lpush/rpop或rpush/lpop实现简单消息队列
4.session共享,分布式系统中,可以利用redis实现session共享。spring官方提供的分布式解决方案Spring Session就是利用redis 实现的。
Spring Boot对redis也实现自动化装配,使用非常方便。
引入依赖
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency> </dependencies>
配置redis相关信息
spring: redis: # redis库 database: 0 # redis 服务器地址 host: localhost # redis 端口号 port: 6379 # redis 密码 password: # 连接超时时间(毫秒) timeout: 1000 lettuce: pool: # 连接池最大链接数(负数表示没有限制) max-active: 8 # 连接池最大阻塞等待时间(负数表示没有限制) max-wait: -1 # 连接池最大空闲连接数 max-idle: 8 # 连接池最小空闲连接数 min-idle: 0
操作redis
SpringBoot提供了两个bean来操作redis,分别是RedisTemplate
和 StringRedisTemplate
,这两者的主要区别如下。
RedisTemplate
使用的是JdkSerializationRedisSerializer
存入数据会将数据先序列化成字节数组然后在存入Redis数据库。
StringRedisTemplate
使用的是StringRedisSerializer。
@RunWith(SpringRunner.class) @SpringBootTest public class Springboot01CacheApplicationTests { @Autowired StringRedisTemplate stringRedisTemplate; //操作k-v都是字符串的 @Autowired RedisTemplate redisTemplate; //k-v都是对象的 @Autowired // 自定义序列化器 RedisTemplate<Object, Employee> empRedisTemplate; @Test public void test01(){ stringRedisTemplate.opsForValue().set("desc","坚持分享java技术栈"); //给redis中保存数据 stringRedisTemplate.opsForValue().append("msg","hello"); String msg = stringRedisTemplate.opsForValue().get("msg"); System.out.println(msg); stringRedisTemplate.opsForList().leftPush("mylist","1"); stringRedisTemplate.opsForList().leftPush("mylist","2"); } //测试保存对象 @Test public void test02(){ Employee empById = employeeMapper.getEmpById(1); // 默认如果保存对象,使用jdk序列化机制,序列化后的数据保存到redis中 redisTemplate.opsForValue().set("emp-01", empById); /* 使用自定义序列化器empRedisTemplate,将数据以json的方式保存 方式一:自己将对象转为json 方式二:改变redisTemplate默认的序列化规则 */ empRedisTemplate.opsForValue().set("emp-01",empById); } }
test01方法存入的数据如下图:
test02方法没有自定义序列化存入的数据如下图:
由于RedisTemplate是序列化成字节数组存储的,因此在redis客户端的可读性并不好。
自定义序列化器
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; 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 java.net.UnknownHostException; /** * 自定义缓存管理序列化 */ @Configuration public class MyCacheConfig { @Bean public RedisTemplate<Object, Object> customRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException { RedisTemplate<Object, Object> template = new RedisTemplate<Object, Object>(); template.setConnectionFactory(redisConnectionFactory); Jackson2JsonRedisSerializer<Object> ser = new Jackson2JsonRedisSerializer<Object>(Object.class); template.setDefaultSerializer(ser); return template; } @Bean public RedisCacheManager customCacheManager(RedisTemplate<Object, Object> customRedisTemplate) { RedisCacheManager cacheManager = new RedisCacheManager(customRedisTemplate); //key多了一个前缀,因为下面设置了使用前缀,会将方法参数值添加到key中 //使用前缀,默认会将CacheName作为key的前缀 cacheManager.setUsePrefix(true); return cacheManager; } }
上面结果可以看出,我们自定义序列化器的效果,其中我们设置的key是@Cacheable(value = "student:key")
但是在redis中key是student:key:1
,多出的1
是因为上面我们配置了使用前缀,将我们方法参数值也加入到key中。
自动缓存
@Cacheable
可以标记在一个方法上,也可以标记在一个类上。当标记在一个方法上时表示该方法是支持缓存的,当标记在一个类上时则表示该类所有的方法都是支持缓存的。
如果添加了@Cacheable
注解,那么方法被调用后,值会被存入redis,下次再调用的时候会直接从redis中取值返回。
@Service public class StudentServiceImpl implements StudentService { @Autowired private StudentMapper studentMapper; @Override @Cacheable(value = "student:key") public String getNameById(int id) { return studentMapper.getNameById(id); } }
记得要开启缓存,在启动类加上@EnableCaching
注解
访问上面的方法,如果不打印日志,则是从缓存中获取的值。
配置原理
CacheManager === Cache 缓存组件来实际给缓存中存取数据。
1)、引入redis的starter,容器中保存的是 RedisCacheManager;
2)、RedisCacheManager 帮我们创建 RedisCache 来作为缓存组件;RedisCache通过操作redis缓存数据的
3)、默认保存数据 k-v 都是Object;利用序列化保存;
- 如何保存为json格式?
1、引入了redis的starter,cacheManager变为 RedisCacheManager;根据上面spring缓存抽象说明可以知道缓存的配置类是从上到下加载的:
// 缓存的配置类(从上到下有顺序的,上面的配置类创建缓存处理器后,下面的就不能再创建,不起作用了) org.springframework.boot.autoconfigure.cache.GenericCacheConfiguration org.springframework.boot.autoconfigure.cache.JCacheCacheConfiguration org.springframework.boot.autoconfigure.cache.EhCacheCacheConfiguration org.springframework.boot.autoconfigure.cache.HazelcastCacheConfiguration org.springframework.boot.autoconfigure.cache.InfinispanCacheConfiguration org.springframework.boot.autoconfigure.cache.CouchbaseCacheConfiguration org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration org.springframework.boot.autoconfigure.cache.CaffeineCacheConfiguration org.springframework.boot.autoconfigure.cache.GuavaCacheConfiguration org.springframework.boot.autoconfigure.cache.SimpleCacheConfiguration【默认】 org.springframework.boot.autoconfigure.cache.NoOpCacheConfiguration
RedisCacheConfiguration源码:
@Configuration @AutoConfigureAfter({RedisAutoConfiguration.class}) @ConditionalOnBean({RedisTemplate.class}) @ConditionalOnMissingBean({CacheManager.class}) // 判断当前容器中没有加载CacheManager,则配置类生效 @Conditional({CacheCondition.class}) class RedisCacheConfiguration { @Bean public RedisCacheManager cacheManager(RedisTemplate<Object, Object> redisTemplate) { RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate); cacheManager.setUsePrefix(true); List<String> cacheNames = this.cacheProperties.getCacheNames(); if (!cacheNames.isEmpty()) { cacheManager.setCacheNames(cacheNames); } return (RedisCacheManager)this.customizerInvoker.customize(cacheManager); } }
RedisCacheConfiguration优先于SimpleCacheConfiguration(默认)加载的,所以我们引入redest启动器后,自动使用redis的缓存配置。
从上面RedisCacheConfiguration源码可以看到,默认创建的 RedisCacheManager 操作redis的时候使用的是 RedisTemplate<Object, Object>
,而RedisTemplate<Object, Object>
是默认使用JDK的序列化机制。
若想让RedisTemplate使用其他序列化的格式则需要自定义CacheManager。(具体见上面操作redis下的自定义序列化器章节)
封装redisUtils
RedisTemplate提供了很多方法来操作redis,但是找起来比较费事,为了更好的操作redis,一般会封装redisUtils来满足业务开发。这里简单封装几个做个示例,如果开发中有需求可以自己封装。
public class RedisUtils { @Autowired private RedisTemplate redisTemplate; /** * 普通存入 * @param key * @param value * @return */ public boolean set(String key,Object value){ try { redisTemplate.opsForValue().set(key,value); return true; }catch (Exception e){ e.printStackTrace(); return false; } } /** * 普通获取key * @param key * @return */ public Object get(String key){ return key == null ? null : redisTemplate.opsForValue().get(key); } /** * 存入key,设置过期时长 * @param key * @param value * @param time * @return */ public boolean set(String key,Object value,long time){ try { if(time > 0){ redisTemplate.opsForValue().set(key,value,time, TimeUnit.SECONDS); }else{ redisTemplate.opsForValue().set(key,value); } return true; }catch (Exception e){ e.printStackTrace(); return false; } } /** * 判断key是否存在 * @param key * @return */ public boolean exists(String key){ try { return redisTemplate.hasKey(key); }catch (Exception e){ e.printStackTrace(); return false; } } /** * 删除key * @param key */ public void del(String key){ try { if(key != null && key.length() > 0){ redisTemplate.delete(key); } }catch (Exception e){ e.printStackTrace(); } } }
4.StringRedisTemplate和RedisTemplate的区别及使用方法
StringRedisTemplate继承了RedisTemplate,所以两者对Redis的操作方法具有相同之处。
StringRedisTemplate源码:
public class StringRedisTemplate extends RedisTemplate<String, String> { public StringRedisTemplate() { RedisSerializer<String> stringSerializer = new StringRedisSerializer(); this.setKeySerializer(stringSerializer); this.setValueSerializer(stringSerializer); this.setHashKeySerializer(stringSerializer); this.setHashValueSerializer(stringSerializer); } public StringRedisTemplate(RedisConnectionFactory connectionFactory) { this(); this.setConnectionFactory(connectionFactory); this.afterPropertiesSet(); } protected RedisConnection preProcessConnection(RedisConnection connection, boolean existingConnection) { return new DefaultStringRedisConnection(connection); } }
区别
两者的数据是不共通的;也就是说StringRedisTemplate只能管理StringRedisTemplate里面的数据,RedisTemplate只能管理RedisTemplate中的数据。
其实他们两者之间的区别主要在于他们使用的序列化类:
RedisTemplate使用的是JdkSerializationRedisSerializer 存入数据会将数据先序列化成字节数组然后在存入Redis数据库。
StringRedisTemplate使用的是StringRedisSerializer
使用时注意事项
当你的redis数据库里面本来存的是字符串数据或者你要存取的数据就是字符串类型数据的时候,那么你就使用StringRedisTemplate即可。
但是如果你的数据是复杂的对象类型,而取出的时候又不想做任何的数据转换,直接从Redis里面取出一个对象,那么使用RedisTemplate是更好的选择。
RedisTemplate使用时常见问题
redisTemplate 中存取数据都是字节数组。当redis中存入的数据是可读形式而非字节数组时,使用redisTemplate取值的时候会无法获取导出数据,获得的值为null。可以使用 StringRedisTemplate 试试。
StringRedisTemplate操作方法
Redis常见的五大数据类型
String(字符串)、List(列表)、Set(集合)、Hash(散列)、ZSet(有序集合)
- 操作String字符串类型
StringRedisTemplate.opsForValue().*
- 根据key/keys删除
StringRedisTemplate.delete(key/collection)
- 操作List类型
StringRedisTemplate.opsForList().*
- 操作Hash类型
StringRedisTemplate.opsForHash().*
- 操作set类型
StringRedisTemplate.opsForSet().*
- 操作有序set
StringRedisTemplate.opsForZSet().*
🌰:
@Service public class RedisTestServiceImpl implements RedisTestService { private static final Logger log = LoggerFactory.getLogger(RedisTestService.class); @Autowired private StringRedisTemplate redisTemplate; /** * @Description 获取String类型的value * @param name * @return */ @Override public String findName(String name) { if (name==null){ log.error("===============key为null================"); } return redisTemplate.opsForValue().get(name); } /** * @Description 添加String类型的key-value * @param name * @param value * @return */ @Override public String setNameValue(String name, String value) { log.info("==================添加String类型的key-value================="); redisTemplate.opsForValue().set(name,value); return name; } /** * @Description 根据key删除redis的数据 * @param name * @return */ @Override public String delNameValue(String name) { redisTemplate.delete(name); return name; } /** * @Description 根据key获取list类型的value(范围) * @param key * @return */ @Override public List<String> findList(String key,int start,int end) { log.info("================按照范围查询redis中List类型================="); return redisTemplate.opsForList().range(key,start,end); } /** * @Description 插入多条数据 * @param key * @param value * @return */ @Override public long setList(String key, List<String> value) { log.info("===========redis List type insert =========="); return redisTemplate.opsForList().rightPushAll(key, value); } /** * @Description 获取list最新记录(右侧) * @param key * @return */ @Override public String findLatest(String key) { log.info("=================rides List latest rigth================"); return redisTemplate.opsForList().index(key,redisTemplate.opsForList().size(key)-1); } /** * @Description 查询hash * @param key * @return */ @Override public Map<Object, Object> findHash(String key) { log.info("================redis hash ================"); return redisTemplate.opsForHash().entries(key); } /** * @Description 查询hash中所有的key * @param key * @return */ @Override public Set<Object> findHashKeys(String key) { log.info("================ All keys of hash ==============="); return redisTemplate.opsForHash().keys(key); } /** * @Description 查询hash中所有的value * @param key * @return */ @Override public List<Object> findHashValues(String key) { log.info("=============== All values of hash =============="); return redisTemplate.opsForHash().values(key); } /** * @Desscription 插入hash数据 * @param key * @param map * @return */ @Override public long insertHash(String key, Map<String, Object> map) { log.info("================ insert hashes into redis ==============="); redisTemplate.opsForHash().putAll(key,map); return map.size(); } }
本文来自博客园,作者:Lz_蚂蚱,转载请注明原文链接:https://www.cnblogs.com/leizia/p/17156372.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步