目录
1.StringRedisTemplate
2.RedisTemplate
2. redis的使用场景
2.1 作为缓存
2.2.分布式锁
2.2.1.idea搭建集群项目
3.redis的解决分布式锁的bug
4. redis中常见的面试题
4.1. 什么是缓存穿透?怎么解决?
4.2. 什么是缓存击穿?如何解决?
4.3. 什么是缓存雪崩?如何解决?
4.4. Redis 淘汰策略有哪些?
springboot对redis的操作封装了两个模板工具类, StringRedisTemplate和RedisTemplate类,StringRedisTemplate是RedisTemplate的子类,StringRedisTemplate它只能存储字符串类型,无法存储对象类型。要想用StringRedisTemplate存储对象必须把对象转为json字符串。
(1)引入相关依赖
org.springframework.boot
spring-boot-starter-data-redis
(2)配置文件 前提记得开redis服务
#redis的配置
spring.redis.host=192.168.19.151
spring.redis.port=7777
spring.redis.jedis.pool.max-active=20
spring.redis.jedis.pool.max-wait=20000
spring.redis.jedis.pool.max-idle=10
spring.redis.jedis.pool.min-idle=5
(3)注入StringRedisTemplate该类对象
@Autowired
private StringRedisTemplate redisTemplate;
(4)使用StringRedisTemplate
该类把对每种数据类型的操作,单独封了相应的内部类。
Spring内部存在一个类可以把java对象转化json字符串 ObjectMapper类2.2
package com.wzh;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sun.scenario.effect.impl.sw.sse.SSEBlend_SRC_OUTPeer;
import com.wzh.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.HashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import redis.clients.jedis.*;
import javax.lang.model.element.VariableElement;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.TimeUnit;
@SpringBootTest
class RedisJedisSpringbootApplicationTests02 {
//里面所有的key还是value field它的类型必须都是string类型
//因为key和value获取field他们使用的都是String的序列化方式
@Autowired
private StringRedisTemplate redisTemplate;
@Test
public void test01(){
//对hash类型的操作
HashOperations forHash = redisTemplate.opsForHash();
forHash.put("k1","name","王振华");
forHash.put("k1","age","15");
Map 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
当你存储的value类型为对象类型使用redisTemplate
存储的value类型为字符串。使用StringRedisTemplate 比如验证码
package com.wzh;
import com.wzh.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.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 RedisJedisSpringbootApplicationTests03 {
//当你存储的value类型为对象类型使用redisTemplate
//存储的value类型为字符串。StringRedisTemplate 比如验证码
@Autowired
public RedisTemplate redisTemplate;
@Test
public void test01(){
//必须人为指定序列化方式
redisTemplate.setKeySerializer(new StringRedisSerializer());
//redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer(Object.class));
//这两个序列化比较常用 Jackson2JsonRedisSerializer占用的内存小一点
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
//对String类型操作类
ValueOperations forValue = redisTemplate.opsForValue();
//redis中key和value都变成乱码了
//key和value都没有指定序列化方式,默认采用jdk的序列化方式
forValue.set("k1","v1");
//value默认采用jdk,类必须实现序列化接口
forValue.set("k2",new User(15,"张三",22));
}
}
当RedisTemplate的value值是对象类型时,类需要实现Serializable接口,不然会乱码,因为默认会采用jdk序列化,上面存储String类型时没有乱码的原因是因为String类实现序列化接口了。
上面的RedisTemplate需要每次都指定key value以及field的序列化方式,能不能搞一个配置类,已经为RedisTemplate指定好序列化。以后再用就无需指定。
package com.wzh.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;
/**
* @ProjectName: redis-jedis-springboot
* @Package: com.wzh.config
* @ClassName: RedisConfig
* @Author: 王振华
* @Description:
* @Date: 2022/8/2 20:06
* @Version: 1.0
*/
@Configuration //标记该类为配置类
public class RedisConfig {
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
RedisTemplate template = new RedisTemplate<>();
RedisSerializer 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;
}
}
这样上面的就不需要设置序列化方式了
(1)数据存储在内存中,数据查询速度快。可以分摊数据库压力。
(2)什么样的数据适合放入缓存
查询频率比较高,修改频率比较低。
安全系数低的数据
(3)使用redis作为缓存
这里我们用到了mybatis-plus所以需要引入依赖
mysql
mysql-connector-java
com.baomidou
mybatis-plus-boot-starter
3.5.1
controller层:
这里有个知识点,我们可以通过 @GetMapping(value = "getById/{id}") 传参,public CommonResult getById(@PathVariable("id") Integer id) 获取获取请求映射中{}中的值。
这样的好处是安全 ,这样他们就不知道参数 1 是什么 ,如果有id = 1 聪明的人一眼就知道了,所以这种方式现在比较流行
package com.wzh.controller;
import com.wzh.entity.User;
import com.wzh.service.UserService;
import com.wzh.utils.CommonResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* @ProjectName: redis-jedis-springboot
* @Package: com.wzh.controller
* @ClassName: UserController
* @Author: 王振华
* @Description:
* @Date: 2022/8/2 20:36
* @Version: 1.0
*/
@RestController
@RequestMapping("order")
public class UserController {
@Autowired
private UserService userService;
//order/getById/1
//@PathVariable:获取请求映射中{}中的值
@GetMapping(value = "getById/{id}")
public CommonResult getById(@PathVariable("id") Integer id){
return userService.findById(id);
}
@GetMapping("deleteById/{id}")
public CommonResult deletebyId(@PathVariable("id") Integer id){
return userService.deleteById(id);
}
@PostMapping("update")
public CommonResult update(@RequestBody User user){
return userService.update(user);
}
@PostMapping("insert")
public CommonResult insert(@RequestBody User user){
return userService.insert(user);
}
}
service:
package com.wzh.service;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.wzh.entity.User;
import com.wzh.utils.CommonResult;
/**
* @ProjectName: redis-jedis-springboot
* @Package: com.wzh.service
* @ClassName: UserService
* @Author: 王振华
* @Description:
* @Date: 2022/8/2 20:41
* @Version: 1.0
*/
public interface UserService{
CommonResult findById(Integer id);
CommonResult deleteById(Integer id);
CommonResult insert(User user);
CommonResult update(User user);
}
serviceImpl:
/*
package com.wzh.service.impl;
*/
import com.wzh.entity.User;
import com.wzh.mapper.UserMapper;
import com.wzh.service.UserService;
import com.wzh.utils.CommonResult;
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;
/**
* @ProjectName: redis-jedis-springboot
* @Package: com.wzh.service.impl
* @ClassName: UserServiceImpl
* @Author: 王振华
* @Description:
* @Date: 2022/8/2 20:41
* @Version: 1.0
*//*
*/
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private RedisTemplate redisTemplate;
//业务代码
@Override
public CommonResult findById(Integer id){
ValueOperations forValue = redisTemplate.opsForValue();
//查询缓存
Object o = forValue.get("user::" + id);
//缓存命中
if(o!=null){
return (CommonResult) o;
}
//未命中 查询数据库
User user = userMapper.selectById(id);
if(user!=null){
//存入缓存
forValue.set("user::"+id,new CommonResult(2000,"查询成功",user),30,TimeUnit.MINUTES);
}else{
return new CommonResult(5000,"查询失败",null);
}
return new CommonResult(2000,"查询成功",user);
}
@Override
public CommonResult deleteById(Integer id){
redisTemplate.delete("user::"+id);
int i = userMapper.deleteById(id);
return new CommonResult(2000,"删除成功",i);
}
//@Transactional
@Override
public CommonResult update(User user){
//ValueOperations forValue = redisTemplate.opsForValue();
//forValue.set("user::"+user.getId(),user,2, TimeUnit.HOURS);
redisTemplate.delete("user::"+user.getId());
int i = userMapper.updateById(user);
return new CommonResult(2000,"修改成功",i);
}
@Override
public CommonResult insert(User user){
int insert = userMapper.insert(user);
return new CommonResult(2000,"添加成功",insert);
}
}
/*
mapper:
package com.wzh.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.wzh.entity.User;
import org.apache.ibatis.annotations.Mapper;
/**
* @ProjectName: redis-jedis-springboot
* @Package: com.wzh.mapper
* @ClassName: UserMapper
* @Author: 王振华
* @Description:
* @Date: 2022/8/2 20:44
* @Version: 1.0
*/
@Mapper
public interface UserMapper extends BaseMapper {
}
replication.properties:
#redis的配置
spring.redis.host=192.168.19.151
spring.redis.port=7777
spring.redis.jedis.pool.max-active=20
spring.redis.jedis.pool.max-wait=20000
spring.redis.jedis.pool.max-idle=10
spring.redis.jedis.pool.min-idle=5
server.port=8080
#mysql配置
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/mydb?serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=123456
#日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
查看的缓存: 前部分代码相同@before通知,后部分代码也相同后置通知。 我们可以AOP完成缓存代码和业务代码分离。
spring框架它应该也能想到。--使用注解即可完成。解析该注解。
(1)把缓存的配置类加入
package com.wzh.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;
/**
* @ProjectName: redis-jedis-springboot
* @Package: com.wzh.config
* @ClassName: RedisConfig
* @Author: 王振华
* @Description:
* @Date: 2022/8/2 20:06
* @Version: 1.0
*/
@Configuration //标记该类为配置类
public class RedisConfig {
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer 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;
}
}
上面不是已经配置过一次了吗,为什么还需要再配置,是因为上面的都是手动创建的RedisTeplate类对象redisTemplate.opsForValue(),可是这里我们使用注解的话,都是spring容器帮我们创建的,所以需要重新配置。他的缺点就是灵活性差,缓存过期时间都是统一的,不能根据客户的需要设置,可以使用AOP自己写也能实现。
(2)主启动类开启缓存注解
(3)使用注解
package com.wzh.service.impl;
import com.wzh.entity.User;
import com.wzh.mapper.UserMapper;
import com.wzh.service.UserService;
import com.wzh.utils.CommonResult;
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.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.concurrent.TimeUnit;
/**
* @ProjectName: redis-jedis-springboot
* @Package: com.wzh.service.impl
* @ClassName: UserServiceImpl
* @Author: 王振华
* @Description:
* @Date: 2022/8/2 20:41
* @Version: 1.0
*/
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private RedisTemplate redisTemplate;
//业务代码
//使用查询注解:cacheNames表示缓存的名称 key:唯一标志---user::key
//先从缓存中查看key为(cacheNames::key)是否存在,如果存在则不会执行方法体,如果不存在则执行方法体并把方法的返回值存入缓存中
@Override
@Cacheable(cacheNames = {"user"},key = "#id")
public CommonResult findById(Integer id){
User user = userMapper.selectById(id);
return new CommonResult(2000,"查询成功",user);
}
@Override
//先删除缓存在执行方法体。
@CacheEvict(cacheNames = {"user"},key = "#id")
public CommonResult deleteById(Integer id){
int i = userMapper.deleteById(id);
return new CommonResult(2000,"删除成功",i);
}
//@Transactional
@Override
//这个注释可以确保方法被执行,同时方法的返回值也被记录到缓存中,实现缓存与数据库的同步更新。
@CachePut(cacheNames = {"user"},key = "#user.id")
public CommonResult update(User user){
int i = userMapper.updateById(user);
if(i!=0) {
return new CommonResult(2000, "成功", user);
}else {
return new CommonResult(5000, "失败", i);
}
}
@Override
public CommonResult insert(User user){
int insert = userMapper.insert(user);
return new CommonResult(2000,"添加成功",insert);
}
}
我们用这个项目来示范多线程并发带来的问题:
controller:
package com.ykq.distrinctlock.controller;
import com.ykq.distrinctlock.service.ProductStockService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpRequest;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
@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:
package com.ykq.distrinctlock.service;
public interface ProductStockService {
//减少库存
public String decreaseStock( Integer productId);
}
serviceImpl:
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) {
//查看该商品的库存数量
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";
}
}
}
dao:
package com.ykq.distrinctlock.dao;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ProductStockDao {
public Integer findStockByProductId(Integer id);
public void updateStockByProductId(Integer id);
}
ProductStockMapper.xml:
update tbl_stock set num=num-1 where productId=#{productId}
application.properties:
server.port=8001
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/distrinct_lock?serverTimezone=Asia/Shanghai
mybatis.mapper-locations=classpath:/mapper/*.xml
spring.redis.host=192.168.19.151
spring.redis.port=7777
pom.xml:
org.redisson
redisson
3.13.4
org.springframework.boot
spring-boot-starter-data-redis
org.springframework.boot
spring-boot-starter-web
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.1.3
org.springframework.boot
spring-boot-devtools
runtime
true
mysql
mysql-connector-java
runtime
org.springframework.boot
spring-boot-configuration-processor
true
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
org.junit.vintage
junit-vintage-engine
使用压测工具jmeter测试高并发下带来线程安全问题
我们看到同一个库存被使用了n次。以及数据库中库存为负数。 线程安全问题导致。
解决方案: 使用 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开集群项目
(1)创建另外一个tomcat
(2)配置nginx.conf并开启nginx(这里我们下载了window版的nginx) 建议不要下载到中文路径下
记得修改测压的端口号跟上边保持一致
(3)开启两个项目
再次压测,发现又出现: 重复数字以及库存为负数。
我们可以使用 redis作为锁 ,来获取锁和释放锁
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.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 forValue = redisTemplate.opsForValue();
//必须保证一开始没有该key 也就是说flag必须为true
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 "服务器正忙,请稍后在试......";
}
}
记得开启redis服务,不然无法存储锁
这里我们发现没有重复和负数,我们的问题就解决了!!!!
Redis分布式锁不能解决超时的问题,分布式锁有一个超时时间,程序的执行如果超出了锁的超时时间就会出现问题。
可以使用:redission依赖,redission解决redis超时问题的原理。
为持有锁的线程开启一个守护线程,守护线程会每隔10秒检查当前线程是否还持有锁,如果持有则延迟生存时间。
使用:
(1)引入redisson依赖
org.redisson
redisson
3.13.4
(2)配置redission对象并交于spring容器管理
@Bean
public Redisson redisson(){
Config config =new Config();
config.useSingleServer().
setAddress("redis://localhost:6379").
//redis默认有16个数据库
setDatabase(0);
return (Redisson) Redisson.create(config);
}
这里我们因为方便,就不用linux的redis了,我们在window上下载了个redis,跟linux上的使用是一样的
测试:
package com.ykq.distrinctlock.service.impl;
import com.ykq.distrinctlock.dao.ProductStockDao;
import com.ykq.distrinctlock.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_redisson implements ProductStockService {
@Autowired
private ProductStockDao productStockDao;
@Autowired
private Redisson redisson;
@Override
public String decreaseStock(Integer productId) {
//获取锁对象
RLock lock = redisson.getLock("aaa::" + 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();
}
}
}
1. 数据库中没有该记录,缓存中也没有该记录,这时由人恶意大量访问这样的数据。这样就会导致该请求绕过缓存,直接访问数据,从而造成数据库压力过大。
2.解决办法:
[1]在controller加数据校验。
[2]我们可以在redis中存入一个空对象,而且要设置过期时间不能太长。超过5分钟
[3]我们使用布隆过滤器。底层:有一个bitmap数组,里面存储了该表的所有id.
建议三种都使用:
Controller层 判断数据是否有意义
这里只对查找方法进行了优化
@Override
public CommonResult findById(Integer id){
//创建布隆过滤器对象
BloomFilter bloomFilter = BloomFilter.create(Funnels.integerFunnel(), 50,0.05);
//存放1-50的id号
for (int i = 1; i <= 50; i++) {
bloomFilter.put(i);
}
ValueOperations forValue = redisTemplate.opsForValue();
//查询缓存
Object o = forValue.get("user::" + id);
//缓存命中
if(o!=null){
// 判断对象是否属于该类型
if(!(o instanceof NullObject)) {
return (CommonResult) o;
}
return null;
}
//未命中
if(bloomFilter.mightContain(id)){ //查看布隆过滤器是否存在
User user = userMapper.selectById(id);
if(user!=null){
//存入缓存
forValue.set("user::"+id,new CommonResult(2000,"查询成功",user),30, TimeUnit.MINUTES);
return new CommonResult(2000,"查询成功",user);
}else{
//在redis中存入一个空对象,小于5分钟 避免缓存穿透
forValue.set("user::"+id,new NullObject(),5, TimeUnit.MINUTES);
return new CommonResult(5000,"查询失败",null);
}
}
return null;
}
//伪代码
String get(String key) { //布隆过滤器钟存储的是数据库表钟对应的id
String value = redis.get(key); //先从缓存获取。
if (value == null) { //缓存没有命中
if(!bloomfilter.mightContain(key)){//查看布隆过滤器中是否存在
return null;q
}else{
value = db.get(key); //查询数据库
redis.set(key, value);
}
}
return value;
}
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。
缓存击穿解决方案:
1.设置永久不过期。【这种只适合内存】
2.使用互斥锁(mutex key)业界比较常用的做法。
//伪代码
public String get(key) {
String value = redis.get(key);
if (value == null) { //代表缓存值过期
//设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db
if (redis.setnx(key_mutex, 1, 3 * 60) == 1) { //代表设置成功
value = db.get(key);
redis.set(key, value, expire_secs);
redis.del(key_mutex);
} else { //这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可
sleep(50);
get(key); //重试
}
} else {
return value;
}
}
缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是, 缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
1.什么下会发生缓存雪崩:
[1]项目刚上线,缓存中没有任何数据
[2]缓存出现大量过期。
[3]redis宕机
2.解决办法:
1.上线前预先把一些热点数据放入缓存。
2.设置过期时间为散列值
3.搭建redis集群
默认是从已设置过期时间的数据集中挑选最近最少使用的数据淘汰
在redis.conf中配置