文章目录
一、SpringBoot整合Redis
1、引入springboot整合redis的starter
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</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
<!-- Jedis 依赖(版本由springboot自动仲裁) -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</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
- redisTemplate 默认使用的是 JDK 序列化,但是可以主动设置
- redisTemplate 执行两条命令其实是在两个连接里完成的,因为 redisTemplate 执行完一个命令就会对其关闭。但是 redisTemplate 提供了 RedisCallback 和 SessionCallBack 两个接口
- StringRedisTemplate 继承 RedisTemplate,只是提供字符串的操作,复杂的 Java 对象还要自行处理
2.2 RedisCallback 和 SessionCallBack
- 作用: 让 RedisTemplate 进行回调,通过他们可以在同一条连接中执行多个 redis 命令
- SessionCalback 提供了良好的封装,优先使用它,redisCallback 太复杂还是不要使用为好
- SessionCallBack 源码如下:
public interface SessionCallback<T> {
@Nullable
<K, V> T execute(RedisOperations<K, V> operations) throws DataAccessException;
}
- 该接口只有一个方法,但该方法是泛型方法,所以无法使用Lambda表达式
为什么Lambda表达式没办法使用泛型方法,其实很简单,Lambda表达式是一种函数式编程,作用就是重写接口的唯一方法,也可以看做是声明这个方法,而泛型方法中的类型参数,是在声明方法的时候使用的,而只有在调用方法的时候才确定具体的类型,目前Lambda语法是没办法兼有泛型方法的类型参数
- 该方法在调用的时候会传入一个参数
RedisOperations<K, V> operations
,传入的值其实就是redisTemplate
,因此我们可以在该方法内直接使用 redisTemplate 操作 Redis,也能保证所有的 Redis 命令在同一个连接里
2.3 乐观锁实现超卖问题
Redis 通过 watch
来监测数据,在执行 exec 前,监测的数据被其他人更改会抛出错误,取消执行。而 exec 执行时,redis保证不会插入其他人语句来实现隔离。(可以预见到此机制如果事务中包裹过多的执行长指令,可能导致长时间阻塞其他人)
注意: watch 监测数据要写在所有 Redis 命令之前