掘金 后端 ( ) • 2024-04-02 15:31

一文涵盖所有工作中遇到的redis操作,让你从此学会redis

本文会从基础篇到进阶篇,逐步来讲解redis和springboot的整合,如何去操作,以及他的作用。让你学会使用redis,爱上使用redis。

介绍redis

首先我们来介绍一下redis

NoSQL 数据库

redis是一个

key - value 存储系统(区别于 MySQL,他存储的是键值对)

之后我们来看

redis的数据结构

String 字符串类型: name: "xiaou"

List 列表:names: ["xiaou", "dogxiaou", "xiaou"]

Set 集合:names: ["xiaou", "dogxiaou"](值不能重复)

Hash 哈希:nameAge: { "xiaou": 1, "dogxiaou": 2 }

Zset 集合:names: { xiaou - 9, dogxiaou - 12 }(适合做排行榜)

bloomfilter(布隆过滤器,主要从大量的数据中快速过滤值,比如邮件黑名单拦截)

geo(计算地理位置)

hyperloglog(pv / uv)

pub / sub(发布订阅,类似消息队列)

BitMap (1001010101010101010101010101)

如何与java整合?

Spring Data Redis(推荐)

Spring Data:通用的数据访问框架,定义了一组 增删改查 的接口

mysql、redis、jpa

spring-data-redis

Jedis

独立于 Spring 操作 Redis 的 Java 客户端

要配合 Jedis Pool 使用

Lettuce

高阶 的操作 Redis 的 Java 客户端

异步、连接池

Redisson

分布式操作 Redis 的 Java 客户端,让你像在使用本地的集合一样操作 Redis(分布式 Redis 数据网格)

JetCache

对比

  1. 如果你用的是 Spring,并且没有过多的定制化要求,可以用 Spring Data Redis,最方便
  2. 如果你用的不是 SPring,并且追求简单,并且没有过高的性能要求,可以用 Jedis + Jedis Pool
  3. 如果你的项目不是 Spring,并且追求高性能、高定制化,可以用 Lettuce,支持异步、连接池

  • 如果你的项目是分布式的,需要用到一些分布式的特性(比如分布式锁、分布式集合),推荐用 redisson

之后我们来介绍一下redis的基础操作

redis基础操作

这里我们选用的是spring-boot-starter-data-redis

首先我们在本机上开启redis双击运行server这个就可以

image-20240402135038894

看到这个就代表着redis已经成功运行了

image-20240402135102613

之后来看maven的依赖

<?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">
  <!-- @author <a href="https://github.com/liyupi">程序员鱼皮</a> -->
  <!-- @from <a href="https://yupi.icu">编程导航知识星球</a> -->
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.6.4</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>
  <groupId>com.xiaou</groupId>
  <artifactId>xiaou-backend</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>xiaou-backend</name>
  <description>xiaou-backend</description>
  <properties>
    <java.version>1.8</java.version>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
<!--    <dependency>-->
<!--      <groupId>org.mybatis.spring.boot</groupId>-->
<!--      <artifactId>mybatis-spring-boot-starter</artifactId>-->
<!--      <version>2.2.2</version>-->
<!--    </dependency>-->
<!--    <dependency>-->
<!--      <groupId>com.baomidou</groupId>-->
<!--      <artifactId>mybatis-plus-boot-starter</artifactId>-->
<!--      <version>3.5.1</version>-->
<!--    </dependency>-->
    <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-lang3</artifactId>
      <version>3.12.0</version>
    </dependency>
    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-collections4</artifactId>
      <version>4.4</version>
    </dependency>
    <dependency>
      <groupId>com.google.code.gson</groupId>
      <artifactId>gson</artifactId>
      <version>2.8.9</version>
    </dependency>
​
<!--    <dependency>-->
<!--      <groupId>mysql</groupId>-->
<!--      <artifactId>mysql-connector-java</artifactId>-->
<!--      <scope>runtime</scope>-->
<!--    </dependency>-->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-configuration-processor</artifactId>
      <optional>true</optional>
    </dependency>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <optional>true</optional>
    </dependency>
    <!--        swagger-->
    <dependency>
      <groupId>com.github.xiaoymin</groupId>
      <artifactId>knife4j-openapi2-spring-boot-starter</artifactId>
      <version>4.4.0</version>
    </dependency>
​
    <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-redis</artifactId>
      <version>2.6.4</version>
    </dependency>
​
<!--    &lt;!&ndash; https://mvnrepository.com/artifact/org.springframework.session/spring-session-data-redis &ndash;&gt;-->
    <dependency>
      <groupId>org.springframework.session</groupId>
      <artifactId>spring-session-data-redis</artifactId>
      <version>2.6.3</version>
    </dependency>
​
<!--    <dependency>-->
<!--      <groupId>org.redisson</groupId>-->
<!--      <artifactId>redisson</artifactId>-->
<!--      <version>3.27.1</version>-->
<!--    </dependency>-->
​
​
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
    <!-- https://mvnrepository.com/artifact/junit/junit -->
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.13.2</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
​
  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <configuration>
          <excludes>
            <exclude>
              <groupId>org.projectlombok</groupId>
              <artifactId>lombok</artifactId>
            </exclude>
          </excludes>
        </configuration>
      </plugin>
    </plugins>
  </build>
​
</project>
​

之后我们对yml进行一个配置

server:
  port: 8081
#spring:
#  datasource:
#    driver-class-name: com.mysql.jdbc.Driver
#    url: jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=UTC&&allowPublicKeyRetrieval=true #url
#    username: root
#    password: 1234
spring:
  redis:
    database: 4
    host: localhost
    port: 6379

之后我们要做的是对redis编写一个配置类

@Configuration
@EnableCaching // 启用缓存功能
public class RedisConfig {
​
    // 定义 RedisTemplate Bean,用于操作 Redis 数据
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(factory);
​
        // 设置 RedisTemplate 的键和值的序列化方式
        redisTemplate.setKeySerializer(new StringRedisSerializer()); // 设置键的序列化器为 StringRedisSerializer
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); // 设置值的序列化器为 GenericJackson2JsonRedisSerializer
​
        // 设置 RedisTemplate 的哈希键和哈希值的序列化方式
        redisTemplate.setHashKeySerializer(new StringRedisSerializer()); // 设置哈希键的序列化器为 StringRedisSerializer
        redisTemplate.setHashValueSerializer(new Jackson2JsonRedisSerializer<Object>(Object.class)); // 设置哈希值的序列化器为 Jackson2JsonRedisSerializer
​
        return redisTemplate;
    }
​
    // 定义 RedisCacheManager Bean,用于管理缓存
    @Bean
    public RedisCacheManager redisCacheManager(RedisTemplate redisTemplate) {
        // 创建 RedisCacheWriter 实例,用于向 Redis 写入缓存,非锁定方式
        RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisTemplate.getConnectionFactory());
        
        // 创建 RedisCacheConfiguration 实例,配置缓存的默认行为
        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisTemplate.getValueSerializer())); // 设置缓存值的序列化方式
​
        // 返回 RedisCacheManager 实例,用给定的 RedisCacheWriter 和 RedisCacheConfiguration 初始化
        return new RedisCacheManager(redisCacheWriter, redisCacheConfiguration);
    }
}
​

之后我们就可以使用redisTemplate进行一个操作

使用RedisTemplate进行常见的Redis操作,如存储、检索和删除数据

首先要做的第一步就是引入RedisTemplate

这里推荐一个redis的可视化工具 quickredis

QuickOfficial - QuickRedis (quick123.net)

@Autowired
private RedisTemplate redisTemplate;

String

首先来看第一个

// 测试设置字符串类型数据到 Redis
@Test
public void testString() {
    String key = "user:token:0001";
    redisTemplate.opsForValue().set(key, UUID.randomUUID().toString(), 30, TimeUnit.MINUTES);
    System.out.println(redisTemplate.opsForValue().get(key));
}

这个就是像redis添加了一个key

image-20240402141034447

之后来看这个

// 测试字符串类型数据自增操作
@Test
public void testString2() {
    String key = "article:A00001:viewsCount";
    redisTemplate.opsForValue().increment(key);
    System.out.println(redisTemplate.opsForValue().get(key));
}

increment实现了对这个key的一个计数功能。比如我们第一次执行,就是1 第二次就是2 以此类推

第三个是测试设置Map类型到redis中

// 测试设置 Map 类型数据到 Redis
@Test
public void testString3() {
    Map<String, Object> user = new HashMap<>();
    user.put("id", "0001");
    user.put("name", "张三疯");
    user.put("age", 28);
    user.put("birthday", new Date(2005 - 1900, 10, 03));
    String key = "user:0001";
    redisTemplate.opsForValue().set(key, user);
    System.out.println(redisTemplate.opsForValue().get(key));
}

image-20240402141418044

Hash

 // 测试设置哈希类型数据到 Redis
    @Test
    public void testHash() {
        String key = "user:0001:cart";
        Map<String, Object> shoppingCart = new HashMap<>();
        shoppingCart.put("cartId", "123456789");
        shoppingCart.put("userId", "987654321");
        List<Map<String, Object>> items = List.of(
                Map.of("itemId", "1", "itemName", "手机", "price", 999.99, "quantity", 1),
                Map.of("itemId", "2", "itemName", "笔记本电脑", "price", 1499.99, "quantity",
                        2),
                Map.of("itemId", "3", "itemName", "耳机", "price", 49.99, "quantity", 3)
        );
        shoppingCart.put("items", items);
        shoppingCart.put("totalAmount", 3149.92);
        shoppingCart.put("creationTime", "2046-03-07T10:00:00");
        shoppingCart.put("lastUpdateTime", "2046-03-07T12:30:00");
        shoppingCart.put("status", "未结账");
        redisTemplate.opsForHash().putAll(key, shoppingCart);
        System.out.println(redisTemplate.opsForHash().get(key, "items"));
    }

image-20240402141734052

set

// 测试设置集合类型数据到 Redis
@Test
public void testSet() {
    String key = "author:0001:fans";
    redisTemplate.opsForSet().add(key, "张三", "李四", "王五");
    System.out.println("粉丝量:" + redisTemplate.opsForSet().size(key));
}

image-20240402141809859

zset

// 测试设置有序集合类型数据到 Redis
@Test
public void testZset() {
    String key = "user:0001:friends";
    redisTemplate.opsForZSet().add(key, "张三", System.currentTimeMillis());
    redisTemplate.opsForZSet().add(key,"李四", System.currentTimeMillis());
    redisTemplate.opsForZSet().add(key,"王五", System.currentTimeMillis());
​
    Set set = redisTemplate.opsForZSet().reverseRange(key, 0, -1);
    System.out.println(set);
}

image-20240402141927751

list

@Test
public void testList() {
    String key = "order:queue";
    Map<String, Object> order1 = new HashMap<>();
    order1.put("orderId", "1001");
    order1.put("userId", "2001");
    order1.put("status", "已完成");
    order1.put("amount", 500.75);
    order1.put("creationTime", "2024-03-07T09:30:00");
    order1.put("lastUpdateTime", "2024-03-07T10:45:00");
    order1.put("paymentMethod", "在线支付");
    order1.put("shippingMethod", "自提");
    order1.put("remarks", "尽快处理");
    Map<String, Object> order2 = new HashMap<>();
    order2.put("orderId", "1002");
    order2.put("userId", "2002");
    order2.put("status", "待处理");
    order2.put("amount", 280.99);
    order2.put("creationTime", "2024-03-07T11:00:00");
    order2.put("lastUpdateTime", "2024-03-07T11:00:00");
    order2.put("paymentMethod", "货到付款");
    order2.put("shippingMethod", "快递配送");
    order2.put("remarks", "注意保鲜");
    // A程序接收订单请求并将其加入队列
    redisTemplate.opsForList().leftPush(key,order1);
    redisTemplate.opsForList().leftPush(key,order2);
    // B程序从订单队列中获取订单数据并处理
    System.out.println("处理订单:" + redisTemplate.opsForList().rightPop(key));
}

image-20240402142133783

这是一些简单的操作,除了这些,我们来介绍一些别的东西

redis进阶操作

@RedisHash注解

用于将Java对象映射到Redis的Hash数据结构中,使得对象的存储和检索变得更加简单

首先我们创建和实体类一样的数据库表

这个是集体类

​
@Data
@RedisHash
public class User {
    @Id
    private Integer id;
    private String name;
    private Integer age;
    private String phone;
}

2.创建接口,注意需要继承CrudRepository,这样改接口就具备对应实体的redis中的增删改查操作

​
public interface UserRedisMapper extends CrudRepository<User,Integer> {
}

之后进行一个测试

// 测试 Redis 与 Spring Data Redis 集成的操作
@Test
public void testRedisHash(){
    User user = new User();
    user.setId(100);
    user.setName("张三疯");
    user.setAge(18);
    user.setPhone("19988889999");
    // 保存
    userRedisMapper.save(user);
    // 读取
    User redisUser = userRedisMapper.findById(100).get();
    System.out.println("redisUser:" + redisUser);
    // 更新
    user.setPhone("18899998888");
    userRedisMapper.save(user);
    // 删除
    //userRedisMapper.deleteById(100);
    // 判断存在
    boolean exists = userRedisMapper.existsById(100);
    System.out.println("exists: " + exists);
}

这样就可以用redis来对数据库进行一个操作。

redis工作场景分析

session存储信息

用来做数据共享分布式

这个需要引入

<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
    <version>2.6.3</version>
</dependency>

之后设置一下配置

# session 失效时间
session:
  timeout: 86400
  store-type: redis

之后我们例如在用户登陆的时候

/**
 * 用户登录
 */
@Override
public User userLogin(String userAccount, String userPassword, HttpServletRequest request) {
    //登陆逻辑....
    //获得了登陆的用户信息
    //模拟
    User safetyUser=null;
    //记录session
    HttpSession session = request.getSession();
    session.setAttribute(USER_LOGIN_STATE, safetyUser);
    return safetyUser;
}

可以设置这个session

这个可以用来做,例如获得当前用户

/**
 * 获取当前用户
 *
 */
@GetMapping("/current")
public BaseResponse<User> getCurrentUser(HttpServletRequest request) {
    Object userObj = request.getSession().getAttribute(USER_LOGIN_STATE);
    return ResultUtils.success(userObj);
}

等等操作,你可以理解为,她在redis里面存放着一个用户信息。你需要的时候可以随时的去取出来这个信息

分布式锁

这个推荐用的是redisson

Redisson 是一个 java 操作 Redis 的客户端,提供了大量的分布式数据集来简化对 Redis 的操作和使用,可以让开发者像使用本地集合一样使用 Redis,完全感知不到 Redis 的存在。

可以看下面的俩个事例

// list,数据存在本地 JVM 内存中
List<String> list = new ArrayList<>();
list.add("yupi");
System.out.println("list:" + list.get(0));
​
list.remove(0);
​
// 数据存在 redis 的内存中
RList<String> rList = redissonClient.getList("test-list");
rList.add("xiaou");
System.out.println("rlist:" + rList.get(0));
rList.remove(0);
void testWatchDog() {
    RLock lock = redissonClient.getLock("test:precachejob:docache:lock");
    try {
        // 只有一个线程能获取到锁
        if (lock.tryLock(0, -1, TimeUnit.MILLISECONDS)) {
            // todo 实际要执行的方法
            doSomeThings();
            System.out.println("getLock: " + Thread.currentThread().getId());
        }
    } catch (InterruptedException e) {
        System.out.println(e.getMessage());
    } finally {
        // 只能释放自己的锁
        if (lock.isHeldByCurrentThread()) {
            System.out.println("unLock: " + Thread.currentThread().getId());
            lock.unlock();
        }
    }
}

限流

这个也推荐使用redisson

public void doRateLimit(String key) {
    RRateLimiter rRateLimiter = redissonClient.getRateLimiter(key);
    //rate是每个时间单位允许访问几次 rateInterval就是时间单位
    rRateLimiter.trySetRate(RateType.OVERALL, 2, 1, RateIntervalUnit.SECONDS);
    //每当一个操作来了之后,去请求一个令牌
    boolean canOp = rRateLimiter.tryAcquire(1);
    if (!canOp) {
        throw new BusinessException(ErrorCode.SYSTEM_ERROR, "当前访问过于频繁");
    }
}

抽奖

/**
 * 抽奖
 * @return
 */
@GetMapping("/lucky-draw")
public String luckyDraw() {
    try {
        // 获取奖池和参与者列表
        RList<String> prizePool = redissonClient.getList("prize_pool");
        RList<String> participants = redissonClient.getList("participants");
​
        // 进行抽奖
        String winner = drawWinner(prizePool, participants);
​
        // 返回抽奖结果
        return " The winner is: " + winner;
    } finally {
        // 关闭 Redisson 客户端连接
        redissonClient.shutdown();
    }
}

总结

当然除了这些,还有很多使用redis的场景,欢迎各位来补充。

文中所有的源码在如下仓库xiaou61/xiaou-easy-code: 前后端通用解决方案 springboot vue react 原生js (github.com)

文章也会在这里同步发送Xiaou-EasyCode-Docs (xiaou61.top)

这个项目是我自己开发的一个前后端通用解决方案的一个项目,致力于各种工作中常用的代码的demo展示。目前github已有400+star 如果你有兴趣,可以去里面提出你的issue或者是pr 共同来完善这个项目。