0121 spring-boot-redis的使用
redis是什么呢?redis,属于NoSQL的一种,在互联网时代,起到加速系统的作用。
redis是一种内存数据库,支持7种数据类型的存储,性能1S 10w次读写;
redis提供的简单的事务保证了高并发场景下数的一致性。
redis在2.6版本之后增加了lua支持,命令是原子性的;
本篇文章主要基于springboot的redis-starter。
HELLO, 性能利器Redis.
spring-boot-starter-redis
这个是springboot提供的redis操作工具包,底层的redis驱动使用的是lettus,而不是jedis;
依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
序列化
主要通过RedisTemplate来操作redis;
当然也支持自定义序列化器,比如效率比较高的kyto序列化器;
StringRedisTemplate:key,value都是按照字符串存储的。
TypedTuple 保存集合中的有序元素;
可以查看一下StringRedisTemplate的源码:
public StringRedisTemplate() { setKeySerializer(RedisSerializer.string()); setValueSerializer(RedisSerializer.string()); setHashKeySerializer(RedisSerializer.string()); setHashValueSerializer(RedisSerializer.string()); }
数据类型操作接口
功能 | 单个操作接口 | 批量操作接口 |
---|---|---|
有序集合 | ZSetOperations | BoundZsetOperations |
字符串 | ValueOperations | BoundValueOpetations |
集合 | SetOperations | BoundSetOperations |
列表 | ListOperations | BoundListOperations |
散列 | HashOperations | BoundHashOperations |
基数 | HyperLogLogOperations | BoundHyperLogLogOperaions |
地理位置 | GeoOperations | BoundGeoOperations |
使用代码
@Autowired private RedisTemplate redisTemplate; @Test void stringRedisTest() { final ValueOperations valueOperations = redisTemplate.opsForValue(); valueOperations .set("key1", "value1", Duration.ofMinutes(1)); final Object value = valueOperations.get("key1"); Assert.isTrue(Objects.equals("value1", value), "set失败"); final HashOperations hashOperations = redisTemplate.opsForHash(); hashOperations.put("hash1", "f1", "v1"); hashOperations.put("hash1", "f2", "v2"); hashOperations.values("hash1").forEach(System.out::println); }
在同一条连接中进行多次操作
- SessionCallback 高级操作对象
- RedisCallback 低级操作对象
代码中直接使用的java8的lambda表达式。
使用代码
@Test void redisCallbackTest() { redisTemplate.execute((RedisCallback) connection -> { connection.set("rkey1".getBytes(), "rv1".getBytes()); connection.set("rkey2".getBytes(), "rv2".getBytes()); return null; }); } @Test void sessionCallbackTest() { redisTemplate.execute(new SessionCallback() { @Override public Object execute(RedisOperations operations) throws DataAccessException { final ListOperations listOperations = operations.opsForList(); listOperations.leftPush("sk1", "sv1"); listOperations.leftPush("sk1", "sv2"); listOperations.getOperations().expire("sk1", 1, TimeUnit.MINUTES); listOperations.range("sk1", 0, 2).forEach(System.out::println); return 1; } }); }
字符串操作
最为常用的数据类型
实际情况使用的不多,现实的场景一般是放一个对象或者对象列表 转换为字符串 进行存储,取出的时候再转换为对象;
代码:
@Test void stringTest() { redisTemplate.opsForValue().set("stringKey1", "value1", 5, TimeUnit.MINUTES); //字符串类型的整数,不能进行数字运算; redisTemplate.opsForValue().set("stringKey2", "1", 5, TimeUnit.MINUTES); //进行数字运算,增加,减少 redisTemplate.opsForValue().set("stringKey3", 1, 5, TimeUnit.MINUTES); redisTemplate.opsForValue().increment("stringKey3",1); redisTemplate.opsForValue().decrement("stringKey3",1); //其它操作方法 final Long keySize = redisTemplate.opsForValue().size("stringKey1"); System.out.println(keySize); //批量设置 Map<String,Long> map = new HashMap<>(4); map.put("sk1",1L); map.put("sk2",2L); map.put("sk3",3L); map.put("sk4",4L); redisTemplate.opsForValue().multiSet(map); redisTemplate.opsForValue().multiSetIfAbsent(map); //批量获取 redisTemplate.opsForValue().multiGet(map.keySet()).forEach(System.out::println); //getAndSet final Object sk5Value = redisTemplate.opsForValue().getAndSet("sk5", 100); System.out.println("sk5Value:"+sk5Value); redisTemplate.opsForValue().append("sk5","hello redis"); System.out.println("sk5Value2:"+redisTemplate.opsForValue().get("sk5")); //按照情况设置,可以省去了之前查询出来之后判断是否存在再操作的代码; redisTemplate.opsForValue().setIfAbsent("sk6",1000,5,TimeUnit.MINUTES); redisTemplate.opsForValue().setIfPresent("sk6",100,5,TimeUnit.MINUTES); }
其它方法:
更多提供的方法需要在业务场景中多使用
列表操作
@Test void listTest() { stringRedisTemplate.opsForList().leftPush("lk1","lkv1"); stringRedisTemplate.opsForList().leftPushAll("lk2","lk2v1","lk2v2"); stringRedisTemplate.opsForList().leftPushAll("lk2",Arrays.asList("lk2v3","lk2v4")); stringRedisTemplate.opsForList().leftPushIfPresent("lk3","lk3v1"); final List<String> lk2ValuesList = stringRedisTemplate.opsForList().range("lk2", 0, 3); System.out.println(lk2ValuesList); }
集合操作
@Test void setTest() { stringRedisTemplate.opsForSet().add("sk1","sk1v1","sk1v2","sk1v3"); stringRedisTemplate.opsForSet().add("sk2","sk1v1","sk2v2","sk2v3"); final Set<String> sk1 = stringRedisTemplate.opsForSet().members("sk1"); final Set<String> sk2 = stringRedisTemplate.opsForSet().members("sk2"); System.out.println("sk1: "+sk1); System.out.println("sk2: "+sk2); final Set<String> intersect = stringRedisTemplate.opsForSet().intersect("sk1", "sk2"); System.out.println("交集是:" + intersect); final Set<String> union = stringRedisTemplate.opsForSet().union("sk1", "sk2"); System.out.println("并集:" + union); final Set<String> difference = stringRedisTemplate.opsForSet().difference("sk1", "sk2"); System.out.println("差集:"+ difference); final Long size = stringRedisTemplate.opsForSet().size("sk1"); System.out.println("size for sk1 : " + size); stringRedisTemplate.delete("sk1"); stringRedisTemplate.delete("sk2"); }
有序集合操作
@Test void zsetTest() { IntStream.rangeClosed(1,100).forEach(i->{ stringRedisTemplate.opsForZSet().add("zsk1",String.valueOf(i),i*10); }); final Set<ZSetOperations.TypedTuple<String>> typedTupleSet = IntStream.rangeClosed(1, 100).mapToObj(i -> new DefaultTypedTuple<String>(String.valueOf(i), (double) i * 11)).collect(Collectors.toSet()); stringRedisTemplate.opsForZSet().add("zsk2",typedTupleSet); final Set<String> zsk1 = stringRedisTemplate.opsForZSet().rangeByLex("zsk1", RedisZSetCommands.Range.range().gte(20).lte(100)); System.out.println("范围内的集合:" + zsk1); }
散列表操作
@Test void hashTest() { stringRedisTemplate.opsForHash().put("hashk1","k1","v1"); stringRedisTemplate.opsForHash().put("hashk1","k2","v1"); stringRedisTemplate.opsForHash().put("hashk1","k3","v1"); stringRedisTemplate.opsForHash().putIfAbsent("hashk1","k4","new V1"); final List<Object> multiGet = stringRedisTemplate.opsForHash().multiGet("hashk1", Arrays.asList("k1", "k2")); System.out.println("一次获取多个:" + multiGet); }
springboot中redis的配置
配置分两个部分,连接池和连接信息;下表列出必须给出的配置:
spring.redis.port=6379 spring.redis.host=localhost spring.redis.password= spring.redis.timeout=1000 #最小空闲连接数 spring.redis.lettuce.pool.min-idle=2 #最大空闲连接数 spring.redis.lettuce.pool.max-idle=4 #最大活跃连接数 spring.redis.lettuce.pool.max-active=8 #连接最长分配等待时间 spring.redis.lettuce.pool.max-wait=2000 #回收线程间隔毫秒数 spring.redis.lettuce.pool.time-between-eviction-runs=100
注解操作redis
配置CacheManager
spring.redis.cache.type=redis
spring.redis.cache.name=redisCache
通过注解@EnableCaching启用;
@CachePut 更新缓存
@CacheEvicat 清除缓存
@CacheAble 使用查询缓存
缓存在一个类中互相调用失效 : 基于AOP的动态代理,没有生成代理类;
package com.springbootpractice.demo.redis.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.cache.RedisCacheWriter; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializationContext; import java.time.Duration; /** * 说明:代码方式配置缓存管理器 * @author carter * 创建时间: 2020年01月21日 7:00 下午 **/ @Configuration public class RedisConfig { @Autowired private RedisTemplate redisTemplate; @Bean public RedisCacheManager redisCacheManager(){ RedisCacheWriter redisWrite = RedisCacheWriter.lockingRedisCacheWriter(redisTemplate.getConnectionFactory()); RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig(); configuration.prefixKeysWith("_demo_redis_"); configuration.entryTtl(Duration.ofMinutes(10)); configuration.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new JdkSerializationRedisSerializer())); RedisCacheManager redisCacheManager = new RedisCacheManager(redisWrite,configuration); return redisCacheManager; } }
用法
package com.springbootpractice.demo.redis.biz; import com.springbootpractice.demo.redis.dao.entity.UserLoginExtEntity; import com.springbootpractice.demo.redis.dao.mapper.UserLoginExtEntityMapper; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.CachePut; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; /** * 说明:操作user的数据增强层 * @author carter * 创建时间: 2020年01月21日 6:40 下午 **/ @Service public class UserLoginExtBiz { private final UserLoginExtEntityMapper userLoginExtEntityMapper; public UserLoginExtBiz(UserLoginExtEntityMapper userLoginExtEntityMapper) { this.userLoginExtEntityMapper = userLoginExtEntityMapper; } @Cacheable(value = "redisCache",key = "'getById:'+#id") public UserLoginExtEntity getById(Integer id){ return userLoginExtEntityMapper.selectByPrimaryKey(id); } @CachePut(value = "redisCache",key = "'getById:'+#param.id") public UserLoginExtEntity updateUserLoginExt(UserLoginExtEntity param){ userLoginExtEntityMapper.updateByPrimaryKeySelective(param); return param; } @CacheEvict(value = "redisCache",key = "'getById:'+#id") public int deleteUserLoginExt(Integer id){ return userLoginExtEntityMapper.logicalDeleteByPrimaryKey(id); } }
redis的特殊用法
redis中事务的用法
利用的是SessionCallback的RedisOperations 的 watch-multi-exec 连环操作;
watch: 监控某些key;
multi:开始事务;
exec: 执行事务
如果watch的key对应的值发生变化(设置为原值也是发生了变化),则会回滚事务;否则,正常的执行事务 ;
redis在执行事务的时候,要么全部执行,要么全部失败,不会被其它的redis客户端打断,保证了redis事务下数据的一致性;
@Test void transactionTest() { final String ttk1 = "ttk1"; stringRedisTemplate.opsForValue().set(ttk1,"ttk1v1"); final List list = stringRedisTemplate.execute(new SessionCallback<List>() { @Override public List execute(RedisOperations operations) throws DataAccessException { System.out.println("监听"+ttk1); //如果ttk1的值发生了变化,重新set一样的值也是发生了变化,则回滚事务,否则正常执行 operations.watch(ttk1); //开启事务 System.out.println("开启事务"); operations.multi(); operations.opsForList().leftPushAll("xxx_lk1", "v1", "v2", "v3"); final List xxx_lk1 = operations.opsForList().range("xxx_lk1", 0, 2); System.out.println(xxx_lk1); operations.opsForSet().add("xxx_sk1", "v1", "v2", "v3"); final Set xxx_sk1 = operations.opsForSet().members("xxx_sk1"); System.out.println(xxx_sk1); //提交事务 final List list = operations.exec(); System.out.println("提交事务"); return list; } }); System.out.println("执行结果:"+list); }
批量执行redis操作
redisTemplate.executePipelined();
@Test void pipelineTest() { StopWatch stopWatch = new StopWatch("pipelineTest"); stopWatch.start(); final List<Object> objectList = stringRedisTemplate.executePipelined(new SessionCallback<Object>() { @Override public Object execute(RedisOperations operations) throws DataAccessException { for (int i = 1; i <= 10000; i++) { operations.opsForValue().set("pk" + i, "pkv" + i, 5, TimeUnit.MINUTES); } return null; } }); stopWatch.stop(); System.out.println(stopWatch.prettyPrint()); }
消息队列
需要定义一个一个RedisMessageListenerContainer,配置topic和监听器; 作为消费者;
通过redisTemplate.convertAndSend方法发送消息;
定义监听器
package com.springbootpractice.demo.redis.listener; import org.springframework.data.redis.connection.Message; import org.springframework.stereotype.Component; /** * 说明:redis的监听器 * @author carter * 创建时间: 2020年01月21日 5:51 下午 **/ @Component public class MyRedisMessageListener implements org.springframework.data.redis.connection.MessageListener { @Override public void onMessage(Message message, byte[] pattern) { System.out.println("MyRedisMessageListener topic:"+new String(pattern) +" 消息:"+ new String(message.getBody())); } }
注册监听器容器
package com.springbootpractice.demo.redis.listener; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.listener.ChannelTopic; import org.springframework.data.redis.listener.RedisMessageListenerContainer; import org.springframework.data.redis.listener.Topic; import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; /** * 说明:配置队列监听器,对应的主题 * @author carter * 创建时间: 2020年01月21日 5:55 下午 **/ @Configuration public class RedisListenerConfig { public static final String MY_CHANNEL = "myChannel"; private final MyRedisMessageListener myRedisMessageListener; private final MyRedisMessageListener2 myRedisMessageListener2; private final RedisTemplate redisTemplate; public RedisListenerConfig(MyRedisMessageListener myRedisMessageListener, MyRedisMessageListener2 myRedisMessageListener2, RedisTemplate redisTemplate) { this.myRedisMessageListener = myRedisMessageListener; this.myRedisMessageListener2 = myRedisMessageListener2; this.redisTemplate = redisTemplate; } @Bean public RedisMessageListenerContainer redisMessageListenerContainer() { RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer(); redisMessageListenerContainer.setConnectionFactory(redisTemplate.getConnectionFactory()); final ExecutorService taskExecutor = new ThreadPoolExecutor(1, 2, 30, TimeUnit.SECONDS, new LinkedBlockingDeque<>(2000)); redisMessageListenerContainer.setTaskExecutor(taskExecutor); final Topic myChannel = new ChannelTopic(MY_CHANNEL); redisMessageListenerContainer.addMessageListener(myRedisMessageListener, myChannel); redisMessageListenerContainer.addMessageListener(myRedisMessageListener2, myChannel); System.out.println("注册redis的消息队列成功!"); return redisMessageListenerContainer; } }
测试代码
publish myChannel helloworld
- redisTemplate.convertAndSend(channel,message);
package com.springbootpractice.demo.redis.web; import com.springbootpractice.demo.redis.listener.RedisListenerConfig; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; /** * 说明:TODO * @author carter * 创建时间: 2020年01月21日 6:22 下午 **/ @RestController public class TestController { private final StringRedisTemplate stringRedisTemplate; public TestController(StringRedisTemplate stringRedisTemplate) { this.stringRedisTemplate = stringRedisTemplate; } @GetMapping(path = "/send/{message}") public void publishMessage(@PathVariable("message") String message){ stringRedisTemplate.convertAndSend(RedisListenerConfig.MY_CHANNEL,message); } }
使用lua脚本
使用的redisTemplate.execute(RedisScript,List,List);
@GetMapping(path = "/lua/{k1}/{v1}/{k2}/{v2}") public Long publishMessage(@PathVariable("k1") String k1,@PathVariable("k2") String k2,@PathVariable("v1") String v1,@PathVariable("v2") String v2){ DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(); redisScript.setScriptText(LuaScript.lua1); redisScript.setResultType(Long.class); return stringRedisTemplate.execute(redisScript, Arrays.asList(k1, k2), v1, v2); }
小结
通过本篇文章,你可以学会:
- 学会使用spring-boot-redis-starter熟练的进行各种数据类型的操作;
- 学会了使用注解的方式使用redis缓存;
- redis的特殊用法,事务,消息队列,批量操作,lua脚本支持;
美女还是要给看的。
原创不易,转载请注明出处。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架