springboot整合redis时提供了两个模板工具类,StringRedisTemplate和RedisTemplate.
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
该类把对每种数据类型的操作,单独封了相应的内部类
package com.lpt;
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.HashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@SpringBootTest
class SpringbootRedis0802ApplicationTests {
@Autowired
private StringRedisTemplate redisTemplate;
@Test
void contextLoads() {
//对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","15");
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 contextLoads01(){
//删除指定的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);
//等价yusetnx 存入成功返回true失败返回false
Boolean aBoolean = forValue.setIfAbsent("k2", "李四", 30, TimeUnit.SECONDS);
System.out.println(aBoolean);
Integer append = forValue.append("k2", "单身");
String k2 = forValue.get("k2");
System.out.println(k2);
}
}
2.RedisTemplate
package com.lpt;
import com.lpt.eneity.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.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@SpringBootTest
class SpringbootRedis0802ApplicationTests02 {
//当你存储的value类型为对象类型使用redisTemplate
//存储的value类型为字符串。StringRedisTemplate 验证码
@Autowired
private RedisTemplate redisTemplate;
@Test
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","张三");
forValue.set("k2",new User(1,"彭于晏","城管西"));
}
上面的RedisTemplate需要每次都指定key value以及field的序列化方式,能不能搞一个配置类,已经为RedisTemplate指定好序列化。以后再用就无需指定。
package com.lpt.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;
@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;
}
3. redis的使用场景
3.1作为缓存
(1)数据存储在内存中,数据查询速度快。可以分摊数据库压力。
(2)什么样的数据适合放入缓存
查询频率比较高,修改频率比较低。
安全系数低的数据
(3)使用redis作为缓存
package com.lpt.service;
import com.lpt.dao.UserMapper;
import com.lpt.eneity.User;
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;
@Service
public class UserService {
// @Autowired
// private UserMapper userMapper;
//
// @Autowired
// private RedisTemplate redisTemplate;
//
// //业务代码
// public User findById(Integer id){
// ValueOperations forValue = redisTemplate.opsForValue();
// //查询缓存
// Object o = forValue.get("user::" + id);
// //缓存命中
// if(o!=null){
// return (User) o;
// }
// User user = userMapper.selectById(id);
// if (user!=null){
// //存入缓存中
// //forValue.set(TimeUnit.HOURS, "user::"+id, 3);
// forValue.set("user::"+id,user,3,TimeUnit.HOURS);
// }
// return user;
// }
// public int deleteById(Integer id){
// redisTemplate.delete("user::"+id);
// int i = userMapper.deleteById(id);
// return i;
// }
// public User insert(User user){
// int insert = userMapper.insert(user);
// return user;
// }
// public User update(User user){
// ValueOperations forValue = redisTemplate.opsForValue();
// forValue.set("user::"+user.getId(),user,3, TimeUnit.HOURS);
// int insert = userMapper.updateById(user);
// return user;
// }
}
查看的缓存: 前部分代码相同@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)使用注解
@Service
public class UserService02 {
@Autowired
private UserMapper userMapper;
//业务代码
//使用查询注解:cacheNames表示缓存的名称 key:唯一标志---dept::key
//先从缓存中查看key为(cacheNames::key)是否存在,如果存在则不会执行方法体,如果不存在则执行方法体并把方法的返回值存入缓存中
@Cacheable(cacheNames = {"user"},key="#id")
public User findById(Integer id){
User user = userMapper.selectById(id);
return user;
}
//先删除缓存在执行方法体。
@CacheEvict(cacheNames = {"user"},key = "#id")
public int deleteById(Integer id){
int row = userMapper.deleteById(id);
return row;
}
//这个注释可以确保方法被执行,同时方法的返回值也被记录到缓存中,实现缓存与数据库的同步更新。
@CachePut(cacheNames = "user",key="#user.id")
public User update(User user){
int insert = userMapper.updateById(user);
return user;
}
public User insert(User user){
int insert = userMapper.insert(user);
return user;
}
}
3.2 分布式锁
使用压测工具测试高并发下带来线程安全问题
我们看到同一个库存被使用了n次。以及数据库中库存为负数。 线程安全问题导致。
1. 解决方案: 使用 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开集群项目
发现又出现: 重复数字以及库存为负数
解决办法
//package com.lpt.service.impl;
//
//import com.lpt.dao.ProductStockDao;
//import com.lpt.service.ProductStockService;
//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;
//
//@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 "服务器正忙,请稍后在试......";
// }
//}
数据库数据
测试工具
结果如下:
端口8080:
端口8081: