1 springboot整合redis
springboot整合redis时提供了两个模板工具类,StringRedisTemplate和RedisTemplate.
1.1 StringRedisTemplate
(1) 引入相关的依赖
? <dependency> ??????????? <groupId>org.springframework.boot</groupId> ??????????? <artifactId>spring-boot-starter-data-redis</artifactId> ??????? </dependency>
(2)注入StringRedisTemplate该类对象 ?
?@Autowired ?private StringRedisTemplate redisTemplate;
?(3)使用StringRedisTemplate
该类把对每种数据类型的操作,单独封了相应的内部类
//里面所有的key还是value field它的类型必须都是String类型。
//因为key和value获取field他们使用的都是String的序列化方式
@Autowired
private StringRedisTemplate redisTemplate;
@Test
public void Test01(){
//key操作
Boolean aBoolean = redisTemplate.hasKey("k1");
System.out.println(aBoolean);
Boolean k1 = redisTemplate.delete("k1");
System.out.println(k1);
//String操作
ValueOperations<String, String> forValue = redisTemplate.opsForValue();
forValue.set("k1","value1",30, TimeUnit.SECONDS);
String k11 = forValue.get("k1");
System.out.println(k11);
Boolean absent = forValue.setIfAbsent("k2", "xzj", 30, TimeUnit.SECONDS);
System.out.println(absent);
// Hash操作
HashOperations<String, Object, Object> forHash = redisTemplate.opsForHash();
forHash.put("people","name","张三");
forHash.put("people","age","22");
Map<Object, Object> people = forHash.entries("people");
System.out.println(people);
}
@Test
public void test02(){
//对hash类型的操作。
HashOperations<String, Object, Object> forHash = redisTemplate.opsForHash();
forHash.put("k1","name","张三");
forHash.put("k1","age","15");
Map<String,String> map=new HashMap<>();
map.put("name","李四");
map.put("age","25");
forHash.putAll("k2",map);
Object o = forHash.get("k1", "name");
System.out.println(o);
Set<Object> k1 = forHash.keys("k1");
System.out.println(k1);
List<Object> k11 = forHash.values("k1");
System.out.println(k11);
//获取k1对于的所有的field和value
Map<Object, Object> k12 = forHash.entries("k1");
System.out.println(k12);
}
@Test
void contextLoads() {
//删除指定的key
// redisTemplate.delete();
//查看所有的key
// redisTemplate.keys()
//是否存在指定的key
// redisTemplate.hasKey()
//对字符串数据类型的操作ValueOperations
ValueOperations<String, String> forValue = redisTemplate.opsForValue();
//存储字符串类型--key value long unit setex();
forValue.set("k1","张三",30, TimeUnit.SECONDS);
//等价于setnx 存入成功返回true,失败返回false
Boolean absent = forValue.setIfAbsent("k11", "李四", 30, TimeUnit.SECONDS);
System.out.println(absent);
Integer append = forValue.append("k11", "真帅");
String key = forValue.get("k11");
}
}
1.2 RedisTemplate
//当你存储的value类型为对象类型使用redisTemplate
//存储的value类型为字符串。StringRedisTemplate 验证码
@Autowired
private RedisTemplate redisTemplate;
@Test
public void test01(){
//必须认为指定序列化方式
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<Object>(Object.class));
//对String类型操作类
ValueOperations forValue = redisTemplate.opsForValue();
//redis中key和value都变成乱码了。
//key和value都没有指定序列化方式,默认采用jdk的序列化方式。
forValue.set("k1","张三");
//value默认采用jdk,类必须实现序列化接口
forValue.set("k2",new User(1,"刘德华",22));
}
}
上面的RedisTemplate需要每次都指定key value以及field的序列化方式,能不能搞一个配置类,已经为RedisTemplate指定好序列化。以后再用就无需指定。
package com.xzj.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
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.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
/**
* @author xuan
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setConnectionFactory(factory);
//key序列化方式
template.setKeySerializer(redisSerializer);
//value序列化
template.setValueSerializer(jackson2JsonRedisSerializer);
//value hashmap序列化 filed value
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.setHashKeySerializer(redisSerializer);
return template;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
//解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置序列化(解决乱码的问题),过期时间600秒
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(600)) //缓存过期10分钟 ---- 业务需求。
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))//设置key的序列化方式
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)) //设置value的序列化
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}
}
package com.xzj;
import com.xzj.entity.User;
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.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
@SpringBootTest
class Day0802springbootRedisApplicationTests02 {
@Test
void contextLoads() {
}
@Autowired
private RedisTemplate redisTemplate;
@Test
public void Test01(){
ValueOperations forValue = redisTemplate.opsForValue();
forValue.set("k1","张三");
forValue.set("k2",new User(1,"小杨",23));
}
}
2 redis的使用场景
2.1 作为缓存
(1)数据存储在内存中,数据查询速度快。可以分摊数据库压力。
(2)什么样的数据适合放入缓存
查询频率比较高,修改频率比较低。
安全系数低的数据
(3)使用redis作为缓存 ?
package com.xzj.service;
import com.xzj.dao.DeptMapper;
import com.xzj.entity.Dept;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
/**
* @author xuan
*/
@Service
public class DeptService {
@Autowired
private DeptMapper deptMapper;
@Autowired
private RedisTemplate redisTemplate;
//@Cacheable(cacheNames = {"dept"},key="#id")
public Dept findById(Integer id){
ValueOperations forValue = redisTemplate.opsForValue();
Object o = forValue.get("dept::" + id);
if(o!=null){
return (Dept) o;
}
Dept dept = deptMapper.selectById(id);
if(dept!=null){
forValue.set("dept::"+id,dept,2, TimeUnit.HOURS);
}
return dept;
}
public int deleteById(Integer id){
redisTemplate.delete("dept::"+id);
int id1 = deptMapper.deleteById(id);
return id1;
}
public Dept insert(Dept dept){
int insert = deptMapper.insert(dept);
return dept;
}
public Dept update(Dept dept){
ValueOperations forValue = redisTemplate.opsForValue();
forValue.set("dept::"+dept.getId(),dept,2, TimeUnit.HOURS);
int update = deptMapper.updateById(dept);
return dept;
}
}
controller层
package com.xzj.controller;
import com.xzj.entity.Dept;
import com.xzj.service.DeptService02;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
/**
* @author xuan
*/
@RestController
public class DeptController {
@Autowired
private DeptService02 deptService;
@GetMapping("findById/{id}")
public Dept finsById(@PathVariable Integer id){
Dept byId = deptService.findById(id);
return byId;
}
@GetMapping("insert/{id}/{deptname}/{address}")
public Dept findInsert(Dept dept){
Dept insert = deptService.insert(dept);
return insert;
}
@GetMapping("deleteById/{id}")
public String deleteById(@PathVariable Integer id){
int row = deptService.deleteById(id);
return row>0?"删除成功":"删除失败";
}
}
查看的缓存: 前部分代码相同@before通知,后部分代码也相同后置通知。 我们可以AOP完成缓存代码和业务代码分离。
spring框架它应该也能想到。--使用注解即可完成。解析该注解。
(1)把缓存的配置类加入
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
//解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置序列化(解决乱码的问题),过期时间600秒
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(600)) //缓存过期10分钟 ---- 业务需求。
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))//设置key的序列化方式
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)) //设置value的序列化
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}
(2)使用开启缓存注解
?(3)使用注解
package com.xzj.service;
import com.xzj.dao.DeptMapper;
import com.xzj.entity.Dept;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
/**
* @author xuan
*/
@Service
public class DeptService02 {
@Autowired
private DeptMapper deptMapper;
@Autowired
private RedisTemplate redisTemplate;
//业务代码
//使用查询注解:cacheNames表示缓存的名称 key:唯一标志---dept::key
//先从缓存中查看key为(cacheNames::key)是否存在,如果存在则不会执行方法体,如果不存在则执行方法体并把方法的返回值存入缓存中
@Cacheable(cacheNames = {"dept"},key="#id")
public Dept findById(Integer id){
Dept dept = deptMapper.selectById(id);
return dept;
}
//先删除缓存在执行方法体。
@CacheEvict(cacheNames = {"dept"},key = "#id")
public int deleteById(Integer id){
int id1 = deptMapper.deleteById(id);
return id1;
}
public Dept insert(Dept dept){
int insert = deptMapper.insert(dept);
return dept;
}
//这个注释可以确保方法被执行,同时方法的返回值也被记录到缓存中,实现缓存与数据库的同步更新。
@CachePut(cacheNames = "dept",key="#dept.id")
public Dept update(Dept dept){
int update = deptMapper.updateById(dept);
return dept;
}
}
2.2 分布式锁
使用压测工具测试高并发下带来线程安全问题
?我们看到同一个库存被使用了n次。以及数据库中库存为负数。 线程安全问题导致。
?解决方案: 使用 synchronized 或者lock锁
@Service
public class ProductStockServiceImpl2 implements ProductStockService {
@Autowired
private ProductStockDao productStockDao;
@Override
public String decreaseStock(Integer productId) {
synchronized (this) {
//查看该商品的库存数量
Integer stock = productStockDao.findStockByProductId(productId);
if (stock > 0) {
//修改库存每次-1
productStockDao.updateStockByProductId(productId);
System.out.println("扣减成功!剩余库存数:" + (stock - 1));
return "success";
} else {
System.out.println("扣减失败!库存不足!");
return "fail";
}
}
}
}
使用synchronized 或者lock锁 如果我们搭建了项目集群,那么该锁无效
?使用idea开集群项目
?
?发现又出现: 重复数字以及库存为负数。
@Service
public class ProductStockServiceImpl_redis implements ProductStockService {
@Autowired
private ProductStockDao productStockDao;
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public String decreaseStock(Integer productId) {
ValueOperations<String, String> forValue = redisTemplate.opsForValue();
Boolean flag = forValue.setIfAbsent("aaa::" + productId, "~~~~~~~~~~~~~~~~~~~~~~");
if(flag) {
try {
//查看该商品的库存数量
Integer stock = productStockDao.findStockByProductId(productId);
if (stock > 0) {
//修改库存每次-1
productStockDao.updateStockByProductId(productId);
System.out.println("扣减成功!剩余库存数:" + (stock - 1));
return "success";
} else {
System.out.println("扣减失败!库存不足!");
return "fail";
}
}finally {
redisTemplate.delete("aaa::" + productId);
}
}
return "服务器正忙,请稍后在试......";
}
}
3. redis的解决分布式锁的bug
?
可以使用:redission依赖,redission解决redis超时问题的原理
?
为持有锁的线程开启一个守护线程,守护线程会每隔10秒检查当前线程是否还持有锁,如果持有则延迟生存时间。
使用:
?? <dependency> ??????????? <groupId>org.redisson</groupId> ??????????? <artifactId>redisson</artifactId> ??????????? <version>3.13.4</version> ??????? </dependency> ?????? ? ???????? //获取redisson对象并交于spring容器管理 ??? @Bean ??? public Redisson redisson(){ ??????? Config config =new Config(); ??????? config.useSingleServer(). ??????????????? setAddress("redis://localhost:6379"). ??????????????? //redis默认有16个数据库 ??????????????? setDatabase(0); ??????? return (Redisson) Redisson.create(config); ??? }
?
package com.xzj.service.impl;
import com.xzj.dao.ProductStockDao;
import com.xzj.service.ProductStockService;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class ProductStockServiceImpl_redis implements ProductStockService {
@Autowired
private ProductStockDao productStockDao;
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private Redisson redisson;
@Override
public String decreaseStock(Integer productId) {
RLock lock = redisson.getLock("xxx::" + productId);
try {
lock.lock(30, TimeUnit.SECONDS);
//查看该商品的库存数量
Integer stock = productStockDao.findStockByProductId(productId);
if (stock > 0) {
//修改库存每次-1
productStockDao.updateStockByProductId(productId);
System.out.println("扣减成功!剩余库存数:" + (stock - 1));
return "success";
} else {
System.out.println("扣减失败!库存不足!");
return "fail";
}
} finally {
lock.unlock();
}
}
}
4. redis中常见的面试题
4.1 什么是缓存穿透?怎么解决?
数据库中没有该记录,缓存中也没有该记录,这时由人恶意大量访问这样的数据。这样就会导致该请求绕过缓存,直接访问数据,从而造成数据库压力过大。
2.解决办法: ?? [1]在controller加数据校验。 ?? [2]我们可以在redis中存入一个空对象,而且要设置过期时间不能太长。超过5分钟 ?? [3]我们使用布隆过滤器。底层:有一个bitmap数组,里面存储了该表的所有id.
//伪代码 String?get(String?key)?{ //布隆过滤器钟存储的是数据库表钟对应的id ??? String?value?=?redis.get(key);??//先从缓存获取。? ??? if?(value??==?null)?{ //缓存没有命中 ??????? if(!bloomfilter.mightContain(key)){//查看布隆过滤器钟是否存在 ??????????? return?null; ??????? }else{ ??????????? value?=?db.get(key);?//查询数据库 ??????????? redis.set(key,?value); ??????? }??? ??? } ??? return?value; }
?
4.2 什么是缓存雪崩?如何解决?
缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是, 缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。 1.什么下会发生缓存雪崩: ? [1]项目刚上线,缓存中没有任何数据 ? [2]缓存出现大量过期。 ? [3]redis宕机 ? 2.解决办法: ?? 1.上线前预先把一些热点数据放入缓存。 ?? 2.设置过期时间为散列值 ?? 3.搭建redis集群
4.4 什么是缓存击穿?如何解决?
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。
缓存击穿解决方案: 1.设置永久不过期。【这种只适合内存】 2.使用互斥锁(mutex key)业界比较常用的做法。
?
4.5 Redis 淘汰策略有哪些?
?
|