SpringBoot整合Redis及秒杀案例分析

文章目录

  • 一、SpringBoot整合Redis
    • 1、引入springboot整合redis的starter
    • 2、yaml文件配置
    • 3、测试
  • 二、秒杀案例
    • 1、代码
    • 2、案例分析
      • 2.1 RedisTemplate 和 StringRedisTemplate
      • 2.2 RedisCallback 和 `SessionCallBack`
      • 2.3 乐观锁实现超卖问题


一、SpringBoot整合Redis

1、引入springboot整合redis的starter

<dependency>
	<groupId>org.springframework.bootgroupId>
	<artifactId>spring-boot-starter-data-redisartifactId>
dependency>

2、yaml文件配置

spring:
  redis:
#    host: CentOS7     # Redis服务器地址
#    port: 6379        # Redis服务器连接端口
#    password: xxxxxx  # Redis服务器连接密码
    url: redis://xxxxxx@CentOS7:6379   # 连接Redis数据库的URL,包括host, port, password, user可忽略
    database: 0          # Redis数据库索引(默认为0)
    timeout: 1800000     # 连接超时时间(毫秒)
#    client-type: jedis  # 配置客户端类型
    lettuce:
      pool:
        max-active: 20  # 连接池最大连接数(使用负值表示没有限制)
        max-wait: -1    # 最大阻塞等待时间(负数表示没限制)
        max-idle: 5     # 连接池中的最大空闲连接
        min-idle: 0     # 连接池中的最小空闲连接

默认操作Redis的客户端类型是Lettuce,是一个并发性能较好的连接池技术,如果想换为Jedis,需要引入Jedis的相关依赖,并且配置client-type: jedis


<dependency>
	<groupId>redis.clientsgroupId>
	<artifactId>jedisartifactId>
dependency>

3、测试

package com.freedom.redis;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;

@SpringBootTest
class RedisSpringBootApplicationTests {

    /**
     * 底层只要使用 StringRedisTemplate 或 RedisTemplate 就可以操作redis
     */
    @Autowired
    private StringRedisTemplate redisTemplate;

    @Test
    void redisTest() {
        // 获取对Redis字符串数据键值的操作对象
        ValueOperations<String, String> operations = redisTemplate.opsForValue();

        operations.set("message", "hello, redis-springboot");
        String value = operations.get("message");
        System.out.println(value);
    }
}

更多 RedisTemplate 和 StringRedisTemplate知识可参考链接地址:https://blog.csdn.net/weixin_42140580/article/details/85211887

二、秒杀案例

1、代码

package com.freedom.seckill.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.*;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.util.List;

@Service
public class SeckillService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 秒杀过程
     *
     * @param userId    用户Id
     * @param productId 商品Id
     * @return 结果
     */
    public String seckill(String userId, String productId) {
        // 1. userId 和 productId 非空判断
        if (!(StringUtils.hasText(userId) && StringUtils.hasText(productId))) {
            System.out.println("参数有误");
            return "参数有误";
        }

        // 使用 sessionCallback,使得操作Redis的命令在同一个连接里执行
        return redisTemplate.execute(new SessionCallback<String>() {
            @Override
            public String execute(RedisOperations operations) throws DataAccessException {

                // 2. 获取对Redis数据键值的操作对象
                ValueOperations<String, String> opsForValue = redisTemplate.opsForValue();
                SetOperations<String, String> opsForSet = redisTemplate.opsForSet();

                // 3. 拼接 库存key 和 秒杀成功用户key
                String stockKey = "sk:" + productId + ":stock";
                String userKey = "sk:" + productId + ":user";

                // 监控库存
                redisTemplate.watch(stockKey);

                // 4. 获取库存,如果为null,则秒杀还没开始;如果库存小于等于0,则秒杀已结束
                String stock = opsForValue.get(stockKey);
                if (stock == null) {
                    System.out.println("秒杀还没有开始,请等待");
                    return "秒杀还没有开始,请等待";
                } else if (Integer.parseInt(stock) <= 0) {
                    System.out.println("秒杀已经结束了");
                    return "秒杀已经结束了";
                }

                // 5. 判断用户是否重复秒杀操作
                if (Boolean.TRUE.equals(opsForSet.isMember(userKey, userId))) {
                    System.out.println("已经秒杀成功了,不能重复秒杀");
                    return "已经秒杀成功了,不能重复秒杀";
                }

                // 6. 进行秒杀(使用乐观锁实现事务)
                // 开启事务
                redisTemplate.multi();

                // 6.1 库存 -1
                opsForValue.decrement(stockKey);
                // 6.2 将秒杀成功的用户Id添加到Set集合中
                opsForSet.add(userKey, userId);

                // 执行事务
                List<Object> results = redisTemplate.exec();

                if (results.size() == 0) {
                    System.out.println("秒杀失败了...");
                    return "秒杀失败了...";
                } else {
                    System.out.println("秒杀成功了...");
                    return "秒杀成功了...";
                }
            }
        });
    }
}

2、案例分析

2.1 RedisTemplate 和 StringRedisTemplate

  1. redisTemplate 默认使用的是 JDK 序列化,但是可以主动设置
  2. redisTemplate 执行两条命令其实是在两个连接里完成的,因为 redisTemplate 执行完一个命令就会对其关闭。但是 redisTemplate 提供了 RedisCallback 和 SessionCallBack 两个接口
  3. StringRedisTemplate 继承 RedisTemplate,只是提供字符串的操作,复杂的 Java 对象还要自行处理

2.2 RedisCallback 和 SessionCallBack

  1. 作用: 让 RedisTemplate 进行回调,通过他们可以在同一条连接中执行多个 redis 命令
  2. SessionCalback 提供了良好的封装,优先使用它,redisCallback 太复杂还是不要使用为好
  3. SessionCallBack 源码如下:
public interface SessionCallback<T> {
    @Nullable
    <K, V> T execute(RedisOperations<K, V> operations) throws DataAccessException;
}
  • 该接口只有一个方法,但该方法是泛型方法,所以无法使用Lambda表达式

为什么Lambda表达式没办法使用泛型方法,其实很简单,Lambda表达式是一种函数式编程,作用就是重写接口的唯一方法,也可以看做是声明这个方法,而泛型方法中的类型参数,是在声明方法的时候使用的,而只有在调用方法的时候才确定具体的类型,目前Lambda语法是没办法兼有泛型方法的类型参数

  • 该方法在调用的时候会传入一个参数RedisOperations operations,传入的值其实就是redisTemplate,因此我们可以在该方法内直接使用 redisTemplate 操作 Redis,也能保证所有的 Redis 命令在同一个连接里

2.3 乐观锁实现超卖问题

Redis 通过 watch 来监测数据,在执行 exec 前,监测的数据被其他人更改会抛出错误,取消执行。而 exec 执行时,redis保证不会插入其他人语句来实现隔离。(可以预见到此机制如果事务中包裹过多的执行长指令,可能导致长时间阻塞其他人)

注意: watch 监测数据要写在所有 Redis 命令之前

你可能感兴趣的:(Redis学习,redis,spring,boot,java)