2. springboot整合redis。
springboot整合redis时提供了两个模板工具类,StringRedisTemplate和RedisTemplate.
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.ykq.qy151redisspringboot; 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 Qy151RedisSpringbootApplicationTests { //里面所有的key还是value field它的类型必须都是String类型。 //因为key和value获取field他们使用的都是String的序列化方式 @Autowired private StringRedisTemplate redisTemplate; @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"); } }
RedisTemplate
package com.ykq.qy151redisspringboot; 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.GenericJackson2JsonRedisSerializer; 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 Qy151RedisSpringbootApplicationTests02 { //当你存储的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.ykq.qy151redisspringboot.config; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; 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.RedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; /** * @program: qy151-redis-springboot * @description: * @author: 闫克起2 * @create: 2022-08-02 15:16 **/ @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.ykq.qy151redisspringboot.service; import com.ykq.qy151redisspringboot.dao.DeptMapper; import com.ykq.qy151redisspringboot.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 org.springframework.transaction.annotation.Transactional; import java.util.concurrent.TimeUnit; /** * @program: qy151-redis-springboot * @description: * @author: 闫克起2 * @create: 2022-08-02 15:55 **/ @Service public class DeptService { @Autowired private DeptMapper deptMapper; @Autowired private RedisTemplate redisTemplate; //业务代码 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 row = deptMapper.deleteById(id); return row; } 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 insert = deptMapper.updateById(dept); return dept; } }
查看的缓存: 前部分代码相同@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)使用注解
//业务代码 //使用查询注解: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 row = deptMapper.deleteById(id); return row; } //这个注释可以确保方法被执行,同时方法的返回值也被记录到缓存中,实现缓存与数据库的同步更新。 @CachePut(cacheNames = "dept",key="#dept.id") public Dept update(Dept dept){ int insert = deptMapper.updateById(dept); return dept; }
3.2 分布式锁
使用压测工具测试高并发下带来线程安全问题
我们看到同一个库存被使用了n次。以及数据库中库存为负数。 线程安全问题导致。
1. 解决方案: 使用 synchronized 或者lock锁
package com.ykq.distrinctlock.service.impl; import com.ykq.distrinctlock.dao.ProductStockDao; import com.ykq.distrinctlock.service.ProductStockService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @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开集群项目
发现又出现: 重复数字以及库存为负数。