手把手搭建springboot项目05-springboot整合Redis及其业务场景
目录
- 前言
- 一、食用步骤
- 1.1 安装步骤
- 1.1.1 客户端安装
- 1.2 添加依赖
- 1.3 修改配置
- 1.4 项目使用
- 1.5 序列化
- 二、应用场景
- 2.1 缓存
- 2.2.分布式锁
- 2.2.1 redis实现
- 2.2.2 使用Redisson 作为分布式锁
- 2.3 全局ID、计数器、限流
- 2.4 购物车
- 2.5 消息队列 (List)
- 2.6 点赞、签到、打卡 (Set)
- 2.7 筛选(Set)
- 2.8 排行榜
前言
在日常的Java开发中,Redis是使用频次非常高的一个nosql数据库,数据以key-value键值对的形式存储在内存中,可以做缓存,分布式锁等。
spring-data-redis属于spring-data,提供给spring项目对于redis的操作,里边主要封装了jedis和lettuce两个客户端。
一、食用步骤
1.1 安装步骤
1.1.1 客户端安装
https://redis.io/docs/getting-started/installation/
1.2 添加依赖
<!-- redis依赖 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
默认使用lettuce客户端
1.3 修改配置
spring:redis:host: localhostport: 6379password: database: 0
redis客户端配置连接池
<!-- redis依赖commons-pool 这个依赖一定要添加 -->
<dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId>
</dependency>
1.4 项目使用
通过spring-data-redis中为我们提供的 RedisTemplate 这个类操作redis服务器
@RestController
public class RedisController {@Autowiredprivate RedisTemplate redisTemplate;@GetMapping("save")public void save(String key, String value){redisTemplate.opsForValue().set(key, value);}
}
1.5 序列化
运行后,发现redis里的值不直观,不利于排查问题
原因是RedisTemplate默认的序列化方式导致的,需要重新配置序列化方式
经常使用的对象序列化方式是: Jackson2JsonRedisSerializer
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;@Configuration
public class RedisConfig {@Bean(name = "redisTemplate")public RedisTemplate<String, Object> getRedisTemplate(RedisConnectionFactory factory) {RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();redisTemplate.setConnectionFactory(factory);StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();// key的序列化类型redisTemplate.setKeySerializer(stringRedisSerializer); Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);ObjectMapper objectMapper = new ObjectMapper();objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance ,ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);jackson2JsonRedisSerializer.setObjectMapper(objectMapper);jackson2JsonRedisSerializer.setObjectMapper(objectMapper);// value的序列化类型redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); redisTemplate.setHashKeySerializer(stringRedisSerializer);redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);redisTemplate.afterPropertiesSet();return redisTemplate;}
}
二、应用场景
2.1 缓存
- String字符串类型: 动态变长字符串
- List列表类型:类似LinkedList前后插入和删除速度快
- Hash:类似Hashmap数组+链表的数据结构
- Set集合类型:类似HashSet,键值是无序唯一
- Zset有序集合:Set和Map的结合体,既能保证key唯一、又能根据value做排序
2.2.分布式锁
2.2.1 redis实现
在分布式环境下我们需要保证某一方法同一时刻只能被一个线程执行,或者多个服务的定时任务只能执行一次。
原理:SETNX
redis> SETNX mykey "Hello"
(integer) 1
redis> SETNX mykey "World"
(integer) 0
redis> GET mykey
"Hello"
NX :表示key不存在的时候,才能set成功,也即保证只有第一个客户端请求才能获得锁,而其他客户端请求只能等其释放锁,才能获取。
分布式锁具备的特征:
- 互斥:任意时刻只能有一个客户端持有锁
- 锁超时释放:锁超时要释放,防止资源浪费和死锁
- 可重入性:一个线程如果获取了锁之后,可以再次对其请求加锁。(锁续期)
- 高性能和高可用:加锁和解锁需要开销尽可能低,同时也要保证高可用,避免分布式锁失效。
- 安全性:锁只能被持有的客户端删除,不能被其他客户端删除
场景伪代码:
//获取锁
if(SETNX key "lock" == 1){ //设置过期时间expire(key,100);try {业务;}catch(){}finally {//释放锁del(key);}
}
以上代码,有以下问题:
- setnx和expire不是原子操作,会导致key永久存在
- 当线程a锁过期释放了,业务还没执行完,b抢到锁执行方法,此时线程a方法执行完后,把b抢到的锁给释放了…达不到锁的标准
代码如下(示例):
public String testLock() {//uuid保证只能释放当前线程的锁String uuid = UUID.randomUUID().toString();//推荐使用setIfAbsent这样redis在set的时候是单线程的,原子的,不会存在短时间重复set的问题。Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);if (Boolean.TRUE.equals(lock)) {System.out.println("获取分布式锁成功...");try {System.out.println("加锁成功...执行业务");} finally {// lua 脚本解锁String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";// 删除redis.call('get', "lock") 的值和uuid相等的锁,成功返回1 失败返回 0redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Collections.singletonList("lock"), uuid);}return "执行完成";} return "获取分布式锁失败";}
2.2.2 使用Redisson 作为分布式锁
官方文档:https://github.com/redisson/redisson/wiki
引入依赖
<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.11.1</version>
</dependency>
配置redission
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class MyRedissonConfig {//所有对 Redisson 的使用都是通过 RedissonClient@Bean(destroyMethod = "shutdown")public RedissonClient redisson() {// 1、创建配置Config config = new Config();// 路径要加上 redis:// 或者 rediss:// //单节点模式config.useSingleServer().setAddress("redis://192.168.3.25:6379");// 2、根据 Config 创建出 RedissonClient 实例return Redisson.create(config);}
}
使用
@Autowired
RedissonClient redisson;public List<User> getUser() {// 1. 获取一把锁RLock lock = redisson.getLock("lock");// 2. 加锁, 阻塞式等待 默认30s 会自动续期// 指定过期时间就不会自动续期lock.lock();try {Thread.sleep(10000);System.out.println("加锁成功,执行业务...");} catch (Exception e) {System.out.println("报错啦...");} finally {// 3. 解锁 假设解锁代码没有运行,Redisson也不会出现死锁lock.unlock();}return userServiceImpl.queryAll();
}
特点:
- redission继承了juc的锁
- 调用lock方法后,会调用renewExpiration() 开启后台线程『看门狗』,30 / 3 = 10s后自动续期到30s。
- 锁的自动续期,如果业务耗时长,运行期间自动给锁续期 ,所以不用担心业务时间过长,锁自动过期被删掉;
//读写锁 锁粒度细,运行越快
RReadWriteLock readWriteLock = redisson.getReadWriteLock("lock");
//读锁 非阻塞
RLock rLock = readWriteLock.readLock();
rLock.lock();
rLock.unlock();
//写锁 阻塞,数据被写锁占有,读锁会被阻塞
RLock wLock = readWriteLock.writeLock();
wLock.lock();
wLock.unlock();
2.3 全局ID、计数器、限流
利用incrby的原子性
- 计数器:阅读量、点赞数
- 限流:访问者的ip和其他信息作为key,访问一次增加一次计数,超过次数则返回false
Long incr = redisTemplate.opsForValue().increment("incrlock");
2.4 购物车
public void testStringCart() {//添加购物车 +1Car.CartItem cartItem = new Car.CartItem(1L, "iphone13 256G 蓝色",new BigDecimal(100), 1);Car carSave = new Car(Collections.singletonList(cartItem));redisTemplate.opsForValue().set("user_001", JSONObject.toJSONString(carSave));//删除 -1 skuId = 1Car car = JSON.parseObject((String) redisTemplate.opsForValue().get("user_001"), Car.class);//记得判空car.getItems().forEach(i -> {if(Objects.equals(i.getSkuId(), 1L)){i.setCount(i.getCount() - 1);}} );//删除后保存新的购物车redisTemplate.opsForValue().set("user_001", JSONObject.toJSONString(car));
}
2.5 消息队列 (List)
//左推
redisTemplate.opsForList().leftPushAll("list", "西瓜1","西瓜2","西瓜3");
//右取 西瓜1 =》 "西瓜2" =》 "西瓜3"
redisTemplate.opsForList().rightPop("list");
2.6 点赞、签到、打卡 (Set)
//user_001给新闻01点赞
redisTemplate.opsForSet().add("like:news01","user_001");
redisTemplate.opsForSet().add("like:news01","user_001");
redisTemplate.opsForSet().add("like:news01","user_002");
//set大小为2
redisTemplate.opsForSet().size("like:news01");
//点赞的所有用户 [user_002, user_001]
redisTemplate.opsForSet().members("like:news01");
2.7 筛选(Set)
- (用户关注)我关注的人也关注了他
- 可能认识的人
- 商品筛选
//user1给新闻点赞
redisTemplate.opsForSet().add("like:news01","user_001");
redisTemplate.opsForSet().add("like:news01","user_002");
redisTemplate.opsForSet().add("like:news02","user_001");
redisTemplate.opsForSet().add("like:news02","user_003");
//获取新闻点赞的用户差集 => [user_002]
System.out.println(redisTemplate.opsForSet().difference("like:news01", "like:news02"));
//获取新闻点赞的用户交集 => [user_001]
System.out.println(redisTemplate.opsForSet().intersect("like:news01", "like:news02"));
//获取新闻点赞的用户并集 => [user_002, user_001, user_003]
System.out.println(redisTemplate.opsForSet().union("like:news01", "like:news02"));
2.8 排行榜
点击率排行榜
示例
public void testZset() {//用户点击新闻自增1Double news_01 = redisTemplate.opsForZSet().incrementScore("hot:20230222", "news_01", 1);//初始化ZSetredisTemplate.opsForZSet().add("hot:20230222","news_01", 700);redisTemplate.opsForZSet().add("hot:20230222","news_02", 1700);redisTemplate.opsForZSet().add("hot:20230222","news_03", 3700);redisTemplate.opsForZSet().add("hot:20230222","news_04", 500);//获取新闻点击最多的3条//[DefaultTypedTuple [score=3700.0, value=news_03],// DefaultTypedTuple [score=1700.0, value=news_02], // DefaultTypedTuple [score=700.0, value=news_01]]//reverseRangeWithScores 分数倒序从下标0取到2 System.out.println(redisTemplate.opsForZSet().reverseRangeWithScores("hot:20230222", 0,2));
}