1.热点数据的缓存
1.1缓存的作用
把经常需要访问的数据储存到redis中,以后再查询该数据时,优先从redis中查询,如果redis没有中没有,则才会查询数据。并把查询到的结果放到redis中以便下次能从redis中获取,提高查询效率,减少数据库的压力
1.2什么样的数据适合放入缓存
-查询频率高的数据
-修改频率低的数据
-数据安全性低要求不高的数据
1.3如何使用redis作为缓存
案例:创建springboot项目使用2.3.2.RELEASE版本
1.3.1依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
1.3.2配置文件
#mysql
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/qy168?serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=root
#redis
spring.redis.host=192.168.61.223
spring.redis.port=6379
#mybatis-plus
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
1.3.3实体类
@Data
@TableName("tbl_dept")
public class Dept {
@TableId(type = IdType.AUTO)
private Integer id;
@TableField(value = "d_name")
private String dname;
private String loc;
}
1.3.4dao层
public interface DeptDao extends BaseMapper<Dept> {
}
1.3.5server层
public interface DeptService {
public Dept findById(Integer id);
}
@Service
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptDao deptDao;
@Override
public Dept findById(Integer id) {
Dept dept = deptDao.selectById(id);
return dept;
}
}
1.3.6controller层
@RestController
@RequestMapping("/dept")
public class DeptController {
@Autowired
private DeptService deptService;
@GetMapping("/getById/{id}")
public Dept getById(@PathVariable Integer id ){
Dept dept = deptService.findById(id);
return dept;
}
}
测试:可以看到每次查询都会查询数据库,下面使用redis缓存来解决这个问题
修改业务层添加redis缓存
@Service
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptDao deptDao;
@Autowired
private RedisTemplate redisTemplate;
@Override
public Dept findById(Integer id) {
//先查询缓存中有没有该数据
ValueOperations forValue = redisTemplate.opsForValue();
Object o = forValue.get("dept::" + id);
if (o!=null){
//如果有则直接返回
return (Dept) o;
}
//如果没有再查询数据库
Dept dept = deptDao.selectById(id);
if (dept!=null){
//不为空则添加到缓存
forValue.set("dept::"+id,dept);
}
return dept;
}
}
运行测试发现只有第一次访问查询了数据库后来的都使用了redis中的缓存数据
修改和删除时也应该修改缓存中的数据,添加则不需要
@Service
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptDao deptDao;
@Autowired
private RedisTemplate redisTemplate;
@Override
public Dept findById(Integer id) {
//先查询缓存中有没有该数据
ValueOperations forValue = redisTemplate.opsForValue();
Object o = forValue.get("dept::" + id);
if (o!=null){
//如果有则直接返回
return (Dept) o;
}
//如果没有再查询数据库
Dept dept = deptDao.selectById(id);
if (dept!=null){
//不为空则添加到缓存
forValue.set("dept::"+id,dept);
}
return dept;
}
@Override
public Dept insert(Dept dept) {
int insert = deptDao.insert(dept);
return dept;
}
@Override
public int update(Dept dept) {
int i = deptDao.updateById(dept);
if (i>0){
//如果修改成功则清除对应的缓存或者修改对应的缓存,这里采用了删除
redisTemplate.delete("dept::"+dept.getId());
}
return i;
}
@Override
public int deleteById(Integer id) {
int i = deptDao.deleteById(id);
if (i>0){
//如果删除成功则清除对应的缓存
redisTemplate.delete("dept::"+id);
}
return i;
}
}
思考:我们再使用redis作为缓存时,每次都需要自己添加缓存代码[非业务代码]。未来维护时还需要维护redis非业务代码。
解决:可是使用AOP解决
spring框架也会想到使用AOP解决业务代码和缓存的非业务代码的重合。--使用缓存的注解
1.3.7如何使用spring缓存注解的方式实现redis缓存功能
必须让redistemplate支持缓存
@Configuration
public class RedisConfig {
@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;
}
}
使用spring提供的缓存注解
@Service
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptDao deptDao;
@Autowired
private RedisTemplate redisTemplate;
@Override
@Cacheable(cacheNames = "dept",key = "#id")//查询
public Dept findById(Integer id) {
Dept dept = deptDao.selectById(id);
return dept;
}
@Override
public Dept insert(Dept dept) {
int insert = deptDao.insert(dept);
return dept;
}
@Override
@CachePut(cacheNames = "dept",key = "#dept.id")//修改
public int update(Dept dept) {
int i = deptDao.updateById(dept);
return i;
}
@Override
@CacheEvict(cacheNames = "dept",key = "#id")//删除
public int deleteById(Integer id) {
int i = deptDao.deleteById(id);
return i;
}
}
要使用缓存注解必须在主启动类上添加@EnableCaching 用来开启缓存注解
@RestController
@RequestMapping("/dept")
public class DeptController {
@Autowired
private DeptService deptService;
@GetMapping("/getById/{id}")
public Dept getById(@PathVariable Integer id ){
Dept dept = deptService.findById(id);
return dept;
}
@GetMapping("/delete/{id}")
public int delete(@PathVariable Integer id){
return deptService.deleteById(id);
}
@GetMapping("/insert")
public Dept insert(Dept dept){
return deptService.insert(dept);
}
@GetMapping("/update")
public int update(Dept dept){
return deptService.update(dept);
}
}
测试:效果和上面一样
2.分布式锁
1.为什么使用锁
可以确保多个线程之间共享资源的互斥访问,从而避免出现数据竞争和线程安全问题。接下来通过案例来说明为什么使用锁
dao层
@Mapper
public interface StockDao {
@Select("select num from tbl_stock where productid=#{productid}")
int findById(Integer productid);
@Update("update tbl_stock set num=num-1 where productid=#{productid} ")
void update(Integer productid);
}
业务层
@Service
public class StockService02 {
@Autowired
private StockDao stockDao;
public String decrement(Integer productid) {
int num = stockDao.findById(productid);
if (num > 0) {
stockDao.update(productid);
System.out.println("商品编号为:" + productid + "的商品库存剩余:" + (num - 1) + "个");
return "商品编号为:" + productid + "的商品库存剩余:" + (num - 1) + "个";
} else {
System.out.println("商品编号为:" + productid + "的商品库存不足。");
return "商品编号为:" + productid + "的商品库存不足。";
}
}
}
测试:修改商品数量为10,使用测压工具0.1秒执行200次任务,模拟高并发发现商品出现了负数:
2.单机如何使用锁
可以使用lock或synchronized
接下来我们使用自动锁来解决这一问题:
@Service
public class StockService02 {
@Autowired
private StockDao stockDao;
public String decrement(Integer productid) {
//自动锁
synchronized (this) {
int num = stockDao.findById(productid);
if (num > 0) {
stockDao.update(productid);
System.out.println("商品编号为:" + productid + "的商品库存剩余:" + (num - 1) + "个");
return "商品编号为:" + productid + "的商品库存剩余:" + (num - 1) + "个";
} else {
System.out.println("商品编号为:" + productid + "的商品库存不足。");
return "商品编号为:" + productid + "的商品库存不足。";
}
}
}
}
测试发现这次商品并没有出现负数:
但随着我们项目的集群搭建,由代理服务器分配到具体的项目上,而项目与项目之间的锁互不影响,所以还是会导致超卖的现象
模拟多台效果:使用nginx代理服务模拟集群,启动两台项目
nginx.conf:
upstream qy168{
server localhost:8088;
server localhost:8089;
}
server {
listen 81;
server_name localhost;
location \ {
proxy_pass http://qy168;
}
}
测试修改库存数量为10,使用测压工具访问81端口0.1秒访问200次
发现还是会导致有超卖的现象,因为两台项目之间的锁互不影响,都拿到了自己的锁,从而导致-1
接下来我们使用redis来完成分布式锁
3.怎么使用分布式锁
修改业务层
@Service
public class StockService02 {
@Autowired
private StockDao stockDao;
@Autowired
private RedisTemplate redisTemplate;
public String decrement(Integer productid) {
ValueOperations forValue = redisTemplate.opsForValue();
//setIfAbsent如果指定的key不存在存入,存在则不存入
//如果返回true表示获取锁成功,返回的是false表示获取锁失败
Boolean flag = forValue.setIfAbsent("product::" + productid, "xxxx", 30, TimeUnit.SECONDS);
if (flag){
try {
int num = stockDao.findById(productid);
if (num > 0) {
stockDao.update(productid);
System.out.println("商品编号为:" + productid + "的商品库存剩余:" + (num - 1) + "个");
return "商品编号为:" + productid + "的商品库存剩余:" + (num - 1) + "个";
} else {
System.out.println("商品编号为:" + productid + "的商品库存不足。");
return "商品编号为:" + productid + "的商品库存不足。";
}
}finally {
//执行完释放锁资源
redisTemplate.delete("product::" + productid);
}
}else {
try {
//如果没获取到锁睡眠100毫秒重新执行
Thread.sleep(100);
}catch (InterruptedException e){
e.printStackTrace();
}
}
return decrement(productid);
}
}
测试发现问题解决:
但这里还是有缺陷,当我们的程序执行时间超过redis锁的时间时,会出现bug。
如何解决该问题:使用第三方插件redisson【底层是基于redis完成的--提供了一个看门狗机制】
4.如何使用redisson
依赖:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.23.4</version>
</dependency>
创建一个RedissonClient对象--交于spring容器管理 :
@Configuration
public class MyRedissonConfiguration {
@Bean //返回的对象交于spring容器来管理
public RedissonClient redissonClient(){
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.61.223:6379");
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
}
使用RedissonClient对象获取锁
@Service
public class StockService02 {
@Autowired
private StockDao stockDao;
@Autowired
private RedissonClient redissonClient;
public String decrement(Integer productid) {
//获取指定锁对象
RLock lock = redissonClient.getLock("product::" + productid);
//加锁
lock.lock(30,TimeUnit.SECONDS);
try {
int num = stockDao.findById(productid);
if (num > 0) {
stockDao.update(productid);
System.out.println("商品编号为:" + productid + "的商品库存剩余:" + (num - 1) + "个");
return "商品编号为:" + productid + "的商品库存剩余:" + (num - 1) + "个";
} else {
System.out.println("商品编号为:" + productid + "的商品库存不足。");
return "商品编号为:" + productid + "的商品库存不足。";
}
}finally {
//释放锁
lock.unlock();
}
}
}
3.redis常见的面试题
1.redis工作中的使用场景。
-可以作为热点数据的缓存
-可以解决分布式锁
-作为限时任务的操作
-商品的排行榜
2.redis支持的数据类
-redis支持很多数据类型,而我们在工作中主要使用的是String、Hash、List、Set、SortedSet
3.redis持久化方式
-RDB:快照存储,每一段时间对redis内存中的数据进行快照存储
-AOF:日志追加,当执行写操作时会通过write函数记录到日志文件中
4.redis缓存穿透是什么?以及如何解决缓存穿透
-什么是缓存穿透:数据库中没有该数据,缓存中也没有该数据,这时有人恶意访问这种数据。
-解决方案:
1、在控制层对一些不合法的数据进行校验。
2、使用布隆过滤器。把数据库中存在的id记录到一个大的bitmap数组中,当查询一个不存在的id是就会被该过滤器过滤掉。
3、可以把查询到的空数据也存入到缓存中。但是这个对象的储存时间不能太长,一般不超过5分钟
5.redis缓存雪崩?以及如何解决缓存雪崩?
1.什么是缓存雪崩:缓存雪崩就是缓存中出现大量数据过期的现象,而就在这时有大量的请求访问这些数据。压力顶到数据库。从而造成数据库压力过大。
-项目刚上线时可能会遇到些问题。解决办法:先把数据存放到缓存中
-缓存中的数据在某个时间段内出现大量过期,解决办法:可以设置散列的过期时间,也就是让这些缓存分批过期
-redis宕机时,解决办法搭建redis集群
6.如何保证缓存数据和数据库数据一致
1.合理设置缓存的过期时间。2.在添加修改删除时同步修改缓存数据。
7.redis内存淘汰策略--修改redis.conf配置文件可以改变淘汰策略
1.volatile-lru:从已设置过期时间的数据集中挑选最近使用次数最少的数据淘汰
2.volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰
3.volatile-random:从已设置过期时间的数据集中随机挑选数据淘汰
4.allkeys-lru:从数据集中挑选最近使用次数最少的数据淘汰
5.allkeys-random:从数据集中随机选择数据淘汰
6.no-enviction:禁止驱逐数据