Redis学习

SpringBoot+Redis(一) 基本操作

2020-04-29  本文已影响0人  岁月如歌2020
目录

0. 版本说明

1. 环境准备

1.1 Spring Boot项目建立

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.ys</groupId>
    <artifactId>redis</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>redis</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <!-- 引入Redis支持 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- 引入Web项目支持 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- 引入devtools支持热部署, 让修改立马生效 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <!-- 用来支持.properties和.xml配置文件解析 -->
        <dependency>
            <groupId> org.springframework.boot </groupId>
            <artifactId> spring-boot-configuration-processor </artifactId>
            <optional> true </optional>
        </dependency>
        <!-- 用来支持单元测试, 包含了JUnit5 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <!-- 用来为JUnit4过渡到JUnit5, 这里无需过渡 -->
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <!-- 表示使用Maven来执行build -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

1.2 RedisConfig配置

# Redis数据库HOST
spring.redis.host=127.0.0.1
# Redis数据库PORT
spring.redis.port=6379
# Redis数据库索引
spring.redis.database=0
# Redis数据库密码
spring.redis.password=
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.jedis.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.jedis.pool.max-wait=-1
# 连接池中的最大空闲连接
spring.redis.jedis.pool.max-idle=8
# 连接池中的最小空闲连接
spring.redis.jedis.pool.min-idle=0
# 连接超时时间(毫秒)
spring.redis.timeout=300
@Configuration
public class RedisConfig {
    @Bean
    @ConditionalOnMissingBean(name = "redisTemplate")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        jackson2JsonRedisSerializer.setObjectMapper(om);

        StringRedisSerializer keySerializer = new StringRedisSerializer();

        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.setKeySerializer(keySerializer);
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashKeySerializer(keySerializer);
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    @Bean
    @ConditionalOnMissingBean(StringRedisTemplate.class)
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        StringRedisTemplate template = new StringRedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

}

这样, StringRedisTemplate和RedisTemplate就交由BeanFactory来创建, 可以保证全局唯一

1.3 SpringBootTest准备

@SpringBootTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class RedisStringTest {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Test
    @Order(1)
    public void testSet() {
        stringRedisTemplate.opsForValue().set("test-string-value", "Hello World");
    }
}

重点是

@TestMethodOrder(MethodOrderer.OrderAnnotation.class)

@Order(1)

这两个注解, 规定了@Test注解方法的执行顺序

2. 基本操作

2.1 字符串(string)

@Test
@Order(1)
public void testSet() {
    stringRedisTemplate.opsForValue().set("test-string-value", "Hello World");
}
@Test
@Order(2)
public void testGet() {
    String result = stringRedisTemplate.opsForValue().get("test-string-value");
    System.out.println(result);
}

打印结果

Hello World
@Test
@Order(3)
public void testSetTimeout() {
    stringRedisTemplate.opsForValue().set("test-string-key-timeout", "Hello Again", 15, TimeUnit.SECONDS);
}

15秒后, 再在Redis命令行中查询该key, 发现已不存在

@Test
@Order(4)
public void testDelete() {
    stringRedisTemplate.delete("test-string-value");
}

2.2 列表(list)

@Test
@Order(1)
public void lpush() {
    redisTemplate.opsForList().leftPush("TestList", "TestLeftPush");
}
@Test
@Order(2)
public void rpush() {
    redisTemplate.opsForList().rightPush("TestList", "TestRightPush");
}
@Test
@Order(3)
public void lpop() {
    Object result = redisTemplate.opsForList().leftPop("TestList");
    System.out.println("lpop的结果: " + result);
}

输出结果

lpop的结果: TestLeftPush
@Test
@Order(4)
public void rpop() {
    Object result = redisTemplate.opsForList().rightPop("TestList");
    System.out.println("rpop第1次的结果为: " + result);

    result = redisTemplate.opsForList().rightPop("TestList");
    System.out.println("rpop第2次的结果为: " + result);
}

输出结果

rpop第1次的结果为: TestRightPush
rpop第2次的结果为: null

2.3 哈希(hash)

@Test
@Order(1)
public void testPut() {
    redisTemplate.opsForHash().put("TestHash", "FirstElement", "Hello, Redis hash.");
    Assert.isTrue(redisTemplate.opsForHash().hasKey("TestHash", "FirstElement"),
            "HashKey: key=TestHash, field=FirstElement不存在!");
}
@Test
@Order(2)
public void testGet() {
    Object element = redisTemplate.opsForHash().get("TestHash", "FirstElement");
    Assert.isTrue(element.equals("Hello, Redis hash."), "Hash value不匹配!");
}
@Test
@Order(3)
public void testDel() {
    redisTemplate.opsForHash().delete("TestHash", "FirstElement");
    Assert.isTrue(!redisTemplate.opsForHash().hasKey("TestHash", "FirstElement"),
            "HashKey: key=TestHash, field=FirstElement依然存在!");
}

2.4 集合(set)

@Test
@Order(1)
public void testAdd() {
    redisTemplate.opsForSet().add("TestSet", "e1", "e2", "e3");
    long size = redisTemplate.opsForSet().size("TestSet");
    System.out.println("TestSet's size is: " + size);
}

输出结果

TestSet's size is: 3
@Test
@Order(2)
public void testGet() {
    Set<String> testSet = redisTemplate.opsForSet().members("TestSet");
    System.out.println(testSet);
}

输出结果

[e1, e2, e3]
@Test
@Order(3)
public void testDel() {
    redisTemplate.opsForSet().remove("TestSet", "e1", "e2");
    Set<String> testSet = redisTemplate.opsForSet().members("TestSet");
    System.out.println("删除操作后, 当前集合元素为: " + testSet);
}

输出结果

删除操作后, 当前集合元素为: [e3]

2.5 有序集合(zset)

@Test
@Order(1)
public void testAdd() {
    redisTemplate.opsForZSet().add("TestZset", "e1", 1);

    Set<ZSetOperations.TypedTuple<String>> zset = new HashSet();
    zset.add(new DefaultTypedTuple("e2", 20.0));
    zset.add(new DefaultTypedTuple("e3", 30.0));
    redisTemplate.opsForZSet().add("TestZset", zset);
}
@Test
@Order(2)
public void testRange() {
    Set<String> results = redisTemplate.opsForZSet().range("TestZset", 0, 1);
    System.out.println("分数最低的2个成员 range(0, 1): " + results);

    results = redisTemplate.opsForZSet().rangeByScore("TestZset", 0.0, 100.0);
    System.out.println("分数处于指定区间的成员 rangeByScore(0.0, 100.0): " + results);

    Set<ZSetOperations.TypedTuple<String>> zset = redisTemplate.opsForZSet().rangeByScoreWithScores("TestZset", 0.0, 100.0);
    System.out.print("分数处于指定区间的成员 rangeByScoreWithScores(0.0, 100.0): [");
    zset.stream().forEach(x -> System.out.print("<value=" + x.getValue() + ",score=" + x.getScore() + ">,"));
    System.out.println("]");

    Set<ZSetOperations.TypedTuple<String>> topScorer = redisTemplate.opsForZSet().reverseRangeWithScores("TestZset", 0, 0);
    System.out.print("分数最高的1个成员 reverseRangeWithScores(0, 0): [");
    topScorer.stream().forEach(x -> System.out.print("<value=" + x.getValue() + ",score=" + x.getScore() + ">,"));
    System.out.println("]");
}

输出结果

分数最低的2个成员 range(0, 1): [e1, e2]
分数处于指定区间的成员 rangeByScore(0.0, 100.0): [e1, e2, e3]
分数处于指定区间的成员 rangeByScoreWithScores(0.0, 100.0): [<value=e1,score=1.0>,<value=e2,score=20.0>,<value=e3,score=30.0>,]
分数最高的1个成员 reverseRangeWithScores(0, 0): [<value=e3,score=30.0>,]
@Test
@Order(3)
public void testSize() {
    long size = redisTemplate.opsForZSet().size("TestZset");
    System.out.println("key=TestZset的有序集合的成员数: " + size);
}

输出结果

key=TestZset的有序集合的成员数: 3
@Test
@Order(4)
public void testScore() {
    double score = redisTemplate.opsForZSet().score("TestZset", "e2");
    System.out.println("成员e2的分数为: " + score);
}

输出结果

成员e2的分数为: 20.0
@Test
@Order(5)
public void testRank() {
    Set<ZSetOperations.TypedTuple<String>> zset = redisTemplate.opsForZSet().rangeWithScores("TestZset", 0, -1);
    zset.stream().forEach(x -> System.out.printf("成员%s的分数为:%f, 名次为:%d\n",
            x.getValue(),
            x.getScore(),
            redisTemplate.opsForZSet().rank("TestZset", x.getValue())));
}

输出结果

成员e1的分数为:1.000000, 名次为:0
成员e2的分数为:20.000000, 名次为:1
成员e3的分数为:30.000000, 名次为:2
@Test
@Order(6)
public void testChangeScore() {
    redisTemplate.opsForZSet().add("TestZset", "e1", 50.0);
    double score = redisTemplate.opsForZSet().score("TestZset", "e1");
    System.out.println("通过zadd后, e1的分数被覆盖成: " + score);

    score = redisTemplate.opsForZSet().incrementScore("TestZset", "e1", 10.0);
    System.out.println("通过incrementScore(10.0)后, e1的分数变成: " + score);
}

输出结果

通过zadd后, e1的分数被覆盖成: 50.0
通过incrementScore(10.0)后, e1的分数变成: 60.0
@Test
@Order(7)
public void testDel() {
    redisTemplate.opsForZSet().remove("TestZset", "e1");
    Set<String> zset = redisTemplate.opsForZSet().range("TestZset", 0, -1);
    System.out.println("剩余成员为: " + zset);
}

输出结果

剩余成员为: [e2, e3]

3. 坑

3.1 key值带额外双引号

情况:

  1. 利用RedisTemplate来向Redis增加一个key值为TestList的列表, 然后通过lpushrpush向该列表添加两个值TestLeftPushTestRightPush;
  2. Redis命令行中手动运行lpush mylist l1
  3. 然后在Redis命令行中运行keys *, 发现key值为
"\"TestList\""
"mylist"

为何会不一样?

原因:

RedisConfig类中通过

redisTemplate.setKeySerializer(jackson2JsonRedisSerializer)

修改key值解析方式为JSON格式了, 于是key值就多了双引号

解决办法:

修改RedisConfig类中代码

StringRedisSerializer keySerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(keySerializer);
redisTemplate.setHashKeySerializer(keySerializer);

参考链接

3.2 Redis命令超时

报错:

Command timed out after no timeout

原因:

未设置redis超时时间

解决办法:

application.properties修改配置项

# 连接超时时间(毫秒)
spring.redis.timeout=300

参考链接

3.3 SpringBootTest中Test方法的执行顺序

情况:

在@SpringBootTest注解的类里, 四个@Test注解的方法, 执行顺序并不是按照定义顺序, 且每次执行都一样; 那么如何控制这些@Test注解方法的执行顺序?

原因:

使用的是JUnit5, 但未明确定义@Test执行顺序

解决办法:

参考链接

4. 拓展思考

4.1 适用场景

分布式缓存:在分布式的系统架构中,将缓存存储在内存中显然不当,因为缓存需要与其他机器共享,这时 Redis 便挺身而出了,缓存也是 Redis 使用最多的场景。

分布式锁:在高并发的情况下,我们需要一个锁来防止并发带来的脏数据,Java 自带的锁机制显然对进程间的并发并不好使,此时可以利用 Redis 单线程的特性来实现我们的分布式锁。

Session 存储/共享:Redis 可以将 Session 持久化到存储中,这样可以避免由于机器宕机而丢失用户会话信息。

发布/订阅:Redis 还有一个发布/订阅的功能,您可以设定对某一个 key 值进行消息发布及消息订阅,当一个 key 值上进行了消息发布后,所有订阅它的客户端都会收到相应的消息。这一功能最明显的用法就是用作实时消息系统。

任务队列:Redis 的 lpush+brpop 命令组合即可实现阻塞队列,生产者客户端使用 lrpush 从列表左侧插入元素,多个消费者客户端使用 brpop 命令阻塞式的"抢"列表尾部的元素,多个客户端保证了消费的负载均衡和高可用性。

限速,接口访问频率限制:比如发送短信验证码的接口,通常为了防止别人恶意频刷,会限制用户每分钟获取验证码的频率,例如一分钟不能超过 5 次。

4.2 缓存与数据库一致性

  1. 先写数据库, 再写缓存
  2. 先写缓存, 再写数据库

大部分情况下,我们的缓存理论上都是需要可以从数据库恢复出来的,所以基本上采取第一种顺序都是不会有问题的。针对那些必须保证数据库和缓存一致的情况,通常是不建议使用缓存的,如果必须使用的话

4.3 缓存击穿

用户故意查询数据库中不存在(意味着缓存肯定也没有)的内容,导致每次查询都会去库里查一次

策略:

4.4 缓存雪崩

缓存down了,所有查询都落到数据库

策略: 让缓存不会真正的down,具体来说

4.5 缓存并发

这里的并发指的是多个 Redis 的客户端同时 set 值引起的并发问题。比较有效的解决方案就是把 set 操作放在队列中使其串行化,必须得一个一个执行。

5. 参考链接:

了解 Redis 并在 Spring Boot 项目中使用 Redis

SpringBoot高级篇Redis之ZSet数据结构使用姿势

SpringBoot Guide-与Redis通讯

解决redis redistemplate KEY为字符串是多双引号的问题

报错Command timed out after no timeout

JUnit5.5的TestMethodOrder API说明

上一篇下一篇

猜你喜欢

热点阅读