0121 spring-boot-redis的使用

redis是什么呢?redis,属于NoSQL的一种,在互联网时代,起到加速系统的作用。

redis是一种内存数据库,支持7种数据类型的存储,性能1S 10w次读写;
redis提供的简单的事务保证了高并发场景下数的一致性。
redis在2.6版本之后增加了lua支持,命令是原子性的;

本篇文章主要基于springboot的redis-starter。<br />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;<br />

当然也支持自定义序列化器,比如效率比较高的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);
    }

在同一条连接中进行多次操作

  1. SessionCallback 高级操作对象
  2. RedisCallback 低级操作对象<br />
    代码中直接使用的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;
    }

}

测试代码

  1. publish myChannel helloworld
  2. 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);
    }

小结

通过本篇文章,你可以学会:

  1. 学会使用spring-boot-redis-starter熟练的进行各种数据类型的操作;
  2. 学会了使用注解的方式使用redis缓存;
  3. redis的特殊用法,事务,消息队列,批量操作,lua脚本支持;

代码点我获取!

美女还是要给看的。<br />
image.png

原创不易,转载请注明出处。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,701评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,649评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,037评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,994评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,018评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,796评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,481评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,370评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,868评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,014评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,153评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,832评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,494评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,039评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,156评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,437评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,131评论 2 356

推荐阅读更多精彩内容

  • 1安装 Redis下载地址: https://redis.io/download 首先要在本地安装一个redis程...
    浪漫前行阅读 349评论 0 2
  • 姓名:王玉刚 学号:17021211232 【嵌牛导读】:利用Mybatis-Plus这个第三方的ORM框架进...
    秋夜慢懂阅读 1,933评论 1 0
  • 如果这个问题一定要有一个答案的话,那只能是生命毫无意义。 既然生命是无意义的,那么“生命有什么意义?”这个问题自然...
    aiintel阅读 566评论 0 0
  • 经常裸体行走 却没有把最隐私的东西暴露 爱你只适合藏在心头
    零更一阅读 162评论 0 1
  • 非深圳户籍的适龄儿童申请在我市接受义务教育需核验哪些就读证明材料? (一)父母双方或一方具有使用功能的深圳经济特区...
    赵雪奎阅读 194评论 0 0