Day115 SpringBoot整合Redis,RedisTemplate和注解两种方式的使用
Day115 SpringBoot整合Redis,注解方式和RedisTemplate方式
1.注解方式使用 Redis 缓存
使用缓存有两个前置步骤
1.在 pom.xml 引入依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 整合redis -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- springboot测试 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope>
</dependency>
2.在启动类上加注解 @EnableCaching
@SpringBootApplication
@EnableCaching
public class RedisApplication {public static void main(String[] args) {SpringApplication.run(RedisApplication.class, args);}
}
常用的注解有以下几个@Cacheable、@CachePut、@CacheEvict
1.1 添加缓存
缓存的key就是配置在注解里面的值product::123,值是你方法的返回值,如果没有返回值,值就是NullValue
在需要加缓存的方法上添加注解
@Cacheable(cacheNames = "product", key = "123")
cacheNames
和key
都必须填,如果不填key
,默认的key
是当前的方法名,更新缓存时会因为方法名不同而更新失败
eg:在订单列表上加缓存
@RequestMapping(value = "/list", method = RequestMethod.GET)@Cacheable(cacheNames = "product", key = "123")public List<Product> list() {List<Product> products=new ArrayList<>();for (int i = 0; i < 3; i++) {Product product=new Product();product.setId(i+1);product.setName("第"+(i+1)+"件商品");product.setPrice((double) ((i+1)*100));products.add(product);}return products;}
可能会报错,原因是对象未序列化。让对象实现
Serializable
方法即可
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Product implements Serializable {private Integer id;private String name;private Double price;
}
重启项目访问订单列表,在 rdm 里查看 Redis 缓存,有
product::123
说明缓存成功
1.2 更新缓存
在需要更新缓存的方法上加注解:
@CachePut(cacheNames = "product", key = "123")
注意
1.
cacheNames
和key
要跟@Cacheable()
里的一致,才会正确更新2.
@CachePut()
和@Cacheable()
注解的方法返回值要一致
1.3 删除缓存
在需要删除缓存的方法上加注解:
@CacheEvict(cacheNames = "product", key = "123")
,执行完这个方法之后会将 Redis 中对应的记录删除
1.4 其他常用功能
1.
cacheNames
也可以统一写在类上面,@CacheConfig(cacheNames = "product")
,具体的方法上就不用写啦
@RestController
@CacheConfig(cacheNames = "product")
public class ProdectController {}
2.Key 也可以动态设置为方法的参数
@GetMapping("/detail")@Cacheable(cacheNames = "product", key = "#id")public Product detail(@RequestParam("id") Integer id){if (id==1){return new Product(1,"电冰箱",20d);}else if (id==2){return new Product(2,"洗衣机",30d);}else {return new Product(3,"彩电",40d);}}
如果参数是个对象,也可以设置对象的某个属性为 key。比如其中一个参数是 user 对象,key 可以写成
key="#user.id"
3.缓存还可以设置条件
eg:设置当 openid 的长度大于2时才缓存
@GetMapping("/detailOnCondition")@Cacheable(cacheNames = "product", key = "#id", condition = "#id > 2")public void detail(@RequestParam("id") String id){System.out.println("id是"+id);}
还可以指定
unless
即条件不成立时缓存。#result
代表返回值,意思是当返回码不等于 0 时不缓存,也就是等于 0 时才缓存
@GetMapping("/detailOnConditionAndUnless")@Cacheable(cacheNames = "product", key = "#id", condition = "#id > 2", unless = "#result!= 0")public Integer detailOnConditionAndUnless(@RequestParam("id") Integer id){if (id==3){return 0;}else {return 1;}}
2.RedisTemplate 使用 Redis 缓存
与使用注解方式不同,注解方式可以零配置,只需引入依赖并在启动类上加上
@EnableCaching
注解就可以使用;而使用 RedisTemplate 方式麻烦些,需要做一些配置
2.1 Redis 配置
第一步还是引入依赖和在启动类上加上
@EnableCaching
注解然后在
application.yml
文件中配置 Redis
spring:redis:port: 6379database: 0host: 127.0.0.1password:jedis:pool:max-active: 8max-wait: -1msmax-idle: 8min-idle: 0timeout: 5000ms
然后写个
RedisConfig.java
配置类
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
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.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;import java.net.UnknownHostException;@Configuration
public class RedisConfig {@Bean@ConditionalOnMissingBean(name = "redisTemplate")public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)throws UnknownHostException {Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);ObjectMapper om = new ObjectMapper();om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);jackson2JsonRedisSerializer.setObjectMapper(om);RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();template.setConnectionFactory(redisConnectionFactory);template.setKeySerializer(jackson2JsonRedisSerializer);template.setValueSerializer(jackson2JsonRedisSerializer);template.setHashKeySerializer(jackson2JsonRedisSerializer);template.setHashValueSerializer(jackson2JsonRedisSerializer);template.afterPropertiesSet();return template;}@Bean@ConditionalOnMissingBean(StringRedisTemplate.class)public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory)throws UnknownHostException {StringRedisTemplate template = new StringRedisTemplate();template.setConnectionFactory(redisConnectionFactory);return template;}
}
Redis 的配置就完成了
2.2 Redis 的数据结构类型
Redis 可以存储键与5种不同数据结构类型之间的映射,这5种数据结构类型分别为String(字符串)、List(列表)、Set(集合)、Hash(哈希)和 Zset(有序集合)
结构类型 | 结构存储的值 | 结构的读写能力 |
---|---|---|
字符串(String) | 可以是字符串、整数或者浮点数 | 对整个字符串或者字符串的其中一部分执行操作;对象和浮点数执行自增或者自减 |
列表(List) | 一个链表,链表上的每个节点都包含了一个字符串,有序,可重复 | 从链表的两端推入或者弹出元素;根据偏移量对链表进行修剪;读取单个或者多个元素;根据值来查找或者移除元素 |
集合(Set) | 被包含的每个字符串都是无序,不可重复 | 添加、获取、移除单个元素;检查一个元素是否存在于某个集合中;计算交集、并集、差集;从集合里卖弄随机获取元素 |
哈希(Hash) | 包含键值对的无序散列表 | 添加、获取、移除单个键值对;获取所有键值对 |
有序集合(ZSet) | 字符串成员与浮点数分值之间的有序映射,元素的排列顺序由分值的大小决定 | 添加、获取、删除单个元素;根据分值范围(range)或者成员来获取元素 |
2.3 操作
RedisTemplate 对五种数据结构分别定义了操作
操作字符串:redisTemplate.opsForValue();
操作hash:redisTemplate.opsForHash();
操作list:redisTemplate.opsForList();
操作set:redisTemplate.opsForSet();
操作有序set:redisTemplate.opsForZSet();
如果操作字符串的话,建议用
StringRedisTemplate
StringRedisTemplate 与 RedisTemplate 的区别
1.StringRedisTemplate 继承了 RedisTemplate
2.RedisTemplate 是一个泛型类,而 StringRedisTemplate 则不是
3.StringRedisTemplate 只能对 key=String,value=String 的键值对进行操作,RedisTemplate 可以对任何类型的 key-value 键值对操作
4.两者的数据是不共通的,StringRedisTemplate 只能管理 StringRedisTemplate 里面的数据,RedisTemplate 只能管理 RedisTemplate中 的数据
2.4 项目中使用
在需要使用 Redis 的地方,用
@Autowired
注入进来
@Autowired
RedisTemplate redisTemplate;@Autowired
StringRedisTemplate stringRedisTemplate;
用 RedisTemplate 操作 Hash
package io.redis.demo;import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.test.context.junit4.SpringRunner;import java.util.List;@SpringBootTest
public class RedisTemplateTest {@Autowiredprivate RedisTemplate redisTemplate;@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Testpublic void test1(){redisTemplate.opsForValue().set("name","zhangsan");String name = (String)redisTemplate.opsForValue().get("name");System.out.println(name);}@Testpublic void test2(){stringRedisTemplate.opsForValue().set("name","zhangsan");String name = stringRedisTemplate.opsForValue().get("name");System.out.println(name);}@Testpublic void test3(){redisTemplate.opsForHash().put("produce","1","电视机");redisTemplate.opsForHash().put("produce","2","冰箱");redisTemplate.opsForHash().put("produce","3","彩电");redisTemplate.opsForHash().put("produce","4","自行车");String name = (String) redisTemplate.opsForHash().get("produce", "4");System.out.println(name);}@Testpublic void test4(){redisTemplate.opsForList().leftPush("name","zhangfei");redisTemplate.opsForList().leftPush("name","liubei");redisTemplate.opsForList().leftPush("name","guanyu");List names = redisTemplate.opsForList().range("name", 0, -1);for (Object name : names) {System.out.println(name);}}
}
3.Redis 实现分布式锁
实现分布式锁之前先看两个 Redis 命令:
SETNX:将
key
设置值为value
,如果key
不存在,这种情况下等同SET命令。 当key
存在时,什么也不做返回值, 特定值:
1:如果key被设置了
0:如果key没有被设置
eg:
redis> SETNX mykey "Hello"
(integer) 1
redis> SETNX mykey "World"
(integer) 0
redis> GET mykey
"Hello"
redis>
GETSET:自动将key对应到value并且返回原来key对应的value。如果key存在但是对应的value不是字符串,就返回错误
设计:
GETSET可以和INCR一起使用实现支持重置的计数功能。举个例子:每当有事件发生的时候,一段程序都会调用INCR给key mycounter加1,但是有时我们需要获取计数器的值,并且自动将其重置为0
这可以通过GETSET mycounter “0”来实现:
INCR mycounter
GETSET mycounter "0"
GET mycounter
返回值:返回之前的旧值,如果之前
Key
不存在将返回null
eg:
redis> INCR mycounter
(integer) 1
redis> GETSET mycounter "0"
"1"
redis> GET mycounter
"0"
redis>
这两个命令在 java 中对应为
setIfAbsent
和getAndSet
分布式锁的实现:
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;@Component
@Slf4j
public class RedisLock {@AutowiredStringRedisTemplate redisTemplate;/*** 加锁* @param key* @param value 当前时间 + 超时时间*/public boolean lock(String key, String value){if (redisTemplate.opsForValue().setIfAbsent(key, value)){return true;}//解决死锁,且当多个线程同时来时,只会让一个线程拿到锁String currentValue = redisTemplate.opsForValue().get(key);//如果过期if (!StringUtils.isEmpty(currentValue) &&Long.parseLong(currentValue) < System.currentTimeMillis()){//获取上一个锁的时间String oldValue = redisTemplate.opsForValue().getAndSet(key, value);if (StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue)){return true;}}return false;}/*** 解锁*/public void unlock(String key, String value){try {String currentValue = redisTemplate.opsForValue().get(key);redisTemplate.opsForValue().getOperations().delete(key); }catch (Exception e){log.error("【redis锁】解锁失败, {}", e);}}
}
使用:
@SpringBootTest
@RunWith(SpringRunner.class)
public class LockTest {/*** 模拟秒杀*/@AutowiredRedisLock redisLock;//超时时间10sprivate static final int TIMEOUT = 10 * 1000;@Testpublic void secKill(){String productId="1";long time = System.currentTimeMillis() + TIMEOUT;//加锁if (!redisLock.lock(productId, String.valueOf(time))){throw new RuntimeException("人太多了,等会儿再试吧~");}//具体的秒杀逻辑System.out.println("秒杀的业务逻辑");//解锁redisLock.unlock(productId, String.valueOf(time));}}