1.springboot整合redis
springboot对redis的操作封装了两个StringRedisTemplate和RedisTemplate类,StringRedisTemplate是RedisTemplate的子类,StringRedisTemplate它只能存储字符串类型,无法存储对象类型。要想用StringRedisTemplate存储对象必须把对象转为json字符串。
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
该类把对每种数据类型的操作,单独封了相应的内部类。
对String类型的数据操作
//对String类型的操作
@Test
void test01() {
ValueOperations<String, String> forValue = stringRedisTemplate.opsForValue();
//时间存储 时间结束后销毁
forValue.set("k1","李四",30l, TimeUnit.SECONDS);
//获取k1的value值
String s = forValue.get("k1");
System.out.println(s);
//若存在,不存入,不存在则存入 返回布尔值
Boolean aBoolean = forValue.setIfAbsent("k1", "张三", 30l, TimeUnit.SECONDS);
System.out.println(aBoolean);
//追加
Integer i = forValue.append("k1", "是个人");
System.out.println(i);
}
对Hash类型的数据操作
//对Hash类型的操作
@Test
void test02(){
HashOperations<String, Object, Object> forHash = stringRedisTemplate.opsForHash();
forHash.put("k1","name","张三");
//必须都是字符串类型,虽然上面泛型是Object 但使用的是spring的序列化 Integer 无法转为String
forHash.put("k1","age","18");
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> s= forHash.keys("k1");
System.out.println(s);
List<Object> l = forHash.values("k1");
System.out.println(l);
//获取k1对于的所有的field和value
Map<Object, Object> k12 = forHash.entries("k1");
System.out.println(k12);
}
注意:
1.2.RedisTemplate
使用RedisTemplate 必须要指定序列化方式,默认使用jdk序列化方式。但会引起乱码,而且占用内存大。
@Autowired
private RedisTemplate redisTemplate;
@Test
void test01(){
//指定key的序列化方式
redisTemplate.setKeySerializer(new StringRedisSerializer());
//指定value的序列化方式 GenericJackson2JsonRedisSerializer()/Jackson2JsonRedisSerializer<Object>(Object.class)
redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<Object>(Object.class));
ValueOperations forValue = redisTemplate.opsForValue();
forValue.set("k1","张三",60l,TimeUnit.SECONDS);
//value默认采用jdk,类必须实现序列化接口
forValue.set("k2",new User(1,"李四","123456"));
}
上面的RedisTemplate需要每次都指定key value以及field的序列化方式,可以创建一个配置类,为RedisTemplate指定好序列化。以后再用就无需指定。
@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;
}
}
2.redis的使用场景
2.1.作为缓存
(1)数据存储在内存中,数据查询速度快。可以分摊数据库压力。
(2)什么样的数据适合放入缓存
查询频率比较高,修改频率比较低。
安全系数低的数据
(3)使用redis作为缓存
@Autowired
private UserMapper userMapper;
@Autowired
private RedisTemplate redisTemplate;
//根据id查询
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("user::"+id,user,2, TimeUnit.HOURS);
}
return user;
}
//根据id删除
public int delete(Integer id){
//先删除缓存再删除数据库中的数据
redisTemplate.delete("user::"+id);
int i = userMapper.deleteById(id);
return i;
}
//添加
public User insert(User user){
int i = userMapper.insert(user);
return user;
}
//根据id修改
//先删掉缓存,再修改数据库
public User update(User user){
Integer id = user.getId();
redisTemplate.delete("user::"+id);
int i = 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.2.分布式锁
使用压测工具测试高并发下带来线程安全问题
同一个库存被使用了n次。以及数据库中库存为负数。 线程安全问题导致。
1. 解决方案: 使用 synchronized 或者lock锁
2.使用redis作为锁
nginx(windows系统下)
配置nginx文件 nginx.conf
开启nginx
注意nginx包的目录必须没有中文,否则无法开启
准备数据库文件
开启idea集群
测试代码:
controller层
@RestController
@RequestMapping("productStock")
public class ProductStockController {
@Autowired
private ProductStockService productStockService;
//减库存
@RequestMapping("decreaseStock/{productId}")
public String decreaseStock(@PathVariable("productId") Integer productId){
return productStockService.decreaseStock(productId);
}
}
service层
@Service
public class ProductStockServiceImpl2 implements ProductStockService {
@Autowired
private ProductStockDao productStockDao;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public String decreaseStock(Integer productId) {
ValueOperations<String,String> forValue = stringRedisTemplate.opsForValue();
Boolean flag = forValue.setIfAbsent("dis::" + 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 {
stringRedisTemplate.delete("dis::" + productId);
}
}
return "服务忙,请稍后..........";
}
}
sql语句
<select id="findStockByProductId" resultType="integer">
select num from tbl_stock where productId=#{productId}
</select>
<update id="updateStockByProductId">
update tbl_stock set num=num-1 where productId=#{productId}
</update>
3.解决redis分布式锁的bug
可以使用:redission依赖,redission解决redis超时问题的原理。
为持有锁的线程开启一个守护线程,守护线程会每隔10秒检查当前线程是否还持有锁,如果持有则延迟生存时间。
使用redis破解版(可以在windows中使用的),开启redis
<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://192.168.226.234:6379").
//redis默认有16个数据库
setDatabase(0);
return (Redisson) Redisson.create(config);
}
测试代码
@Service
public class ProductStockServiceImpl2 implements ProductStockService {
@Autowired
private ProductStockDao productStockDao;
@Autowired
private Redisson redisson;
@Override
public String decreaseStock(Integer productId) {
//获取锁对象
RLock rlock = redisson.getLock("dis::"+productId);
try{
rlock.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 {
rlock.unlock();
}
}
}