北京国家建设部网站首页,网站上的动态图怎么做的,wordpress 加载进度条,借个网站备案号商户查询缓存
缓存的定义
缓存就是数据交换的缓冲区#xff08;Cache#xff09;#xff0c;是存储数据的临时地方#xff0c;一般读写性能较高。
比如计算机的CPU计算速度非常快#xff0c;但是需要先从内存中读取数据再放入CPU的寄存器中进行运算#xff0c;这样会限…商户查询缓存
缓存的定义
缓存就是数据交换的缓冲区Cache是存储数据的临时地方一般读写性能较高。
比如计算机的CPU计算速度非常快但是需要先从内存中读取数据再放入CPU的寄存器中进行运算这样会限制CPU的运算速度所以CPU中也会设计一个缓存存入经常需要用到的数据提升了运算效率。CPU缓存也是衡量CPU性能好坏的重要标准之一。再比如浏览器缓存会缓存一些页面静态资源js、css浏览器缓存未命中的一些数据就会去Tomcat中的Java应用请求而Java应用也有应用层缓存一般用Redis去做。如果缓存再没有命中就可以去数据库查询数据库也有缓存mysql中如索引数据。最后还会去查询CPU缓存磁盘缓存。
缓存的优缺点
优点
降低了后端的负载实际开发的过程中企业的数据量少则几十万多则几千万如果没有缓存来作为避震器这么大的用户并发量服务器是扛不住的。缓存的读写效率非常高响应时间短
缺点
数据一致性成本高代码维护成本高解决一致性问题需要复杂的业务编码也有可能出现缓存穿透、缓存雪崩等问题运维成本缓存需要大规模集群模式需要人力成本
给店铺查询任务添加缓存
整体的业务逻辑如下图所示
先从redis中通过店铺id查询缓存数据登录模块是用map存的这里我们使用String来存就需要将对象先转为JSON格式。如果redis中存在就返回店铺信息。如果redis中不存在就继续向数据库中查询。如果数据库不存在返回“店铺不存在”如果数据库存在将店铺信息写入redis返回店铺信息 代码如下
package com.hmdp.service.impl;import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;import static com.hmdp.utils.RedisConstants.CACHE_SHOP_KEY;/*** p* 服务实现类* /p** author 虎哥* since 2021-12-22*/
Service
public class ShopServiceImpl extends ServiceImplShopMapper, Shop implements IShopService {AutowiredStringRedisTemplate stringRedisTemplate;Overridepublic Result queryById(Long id) {String key CACHE_SHOP_KEY id;//1.从redis中查询String shopJson stringRedisTemplate.opsForValue().get(key);//2.存在返回店铺信息if (!StringUtils.isBlank(shopJson)) {return Result.ok(JSONUtil.toBean(shopJson, Shop.class));}//3.不存在用id在数据库查询Shop shop getById(id);//4.不存在返回“店铺不存在”if (shop null) {return Result.ok(店铺不存在);}//5.存在缓存到redisstringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop));//6.返回店铺信息return Result.ok(shop);}
}
然后再去第二次查询某一个美食的数据发现速度由2ms变成了1ms。 在resp中也发现了cache:shop:id的缓存。
拓展练习
将首页的店铺种类信息缓存到redis中 因为店铺种类有十种可以通过LIst的数据结构存储但是需要将List中的ShopType对象先转为JSON取出的时候再由JSON转为ShopType对象。具体代码如下
package com.hmdp.service.impl;import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.dto.Result;
import com.hmdp.entity.ShopType;
import com.hmdp.mapper.ShopTypeMapper;
import com.hmdp.service.IShopTypeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;import java.util.ArrayList;
import java.util.List;import static com.hmdp.utils.RedisConstants.CACHE_SHOP_TYPE_KEY;/*** p* 服务实现类* /p** author 虎哥* since 2021-12-22*/
Service
public class ShopTypeServiceImpl extends ServiceImplShopTypeMapper, ShopType implements IShopTypeService {AutowiredStringRedisTemplate stringRedisTemplate;public Result queryTypeList() {//1.从redis中查找店铺类型数据ListString shopTypesByRedis stringRedisTemplate.opsForList().range(CACHE_SHOP_TYPE_KEY, 0, 9);//2.存在返回店铺信息,最终需要返回ListShopType形式的list因此需要将JSON转换为ShopType类型ListShopType shopTypes new ArrayList();if(shopTypesByRedis.size() ! 0){for(String s:shopTypesByRedis){//转为JSONShopType shoptype JSONUtil.toBean(s, ShopType.class);shopTypes.add(shoptype);}return Result.ok(shopTypes);}//3.不存在去数据库中寻找,并根据sort排序ListShopType shopTypesByMysql query().orderByAsc(sort).list();//4.数据库不存在返回店铺信息不存在if(shopTypesByMysql.size() 0){return Result.ok(店铺信息不存在);}//5.店铺信息存在存入redis中for(ShopType shop:shopTypesByMysql){stringRedisTemplate.opsForList().leftPush(CACHE_SHOP_TYPE_KEY, JSONUtil.toJsonStr(shop));}//6.返回店铺信息return Result.ok(shopTypesByMysql);}
}
缓存更新策略
在业务中如果我们对数据库数据做了一些修改但是缓存中的数据没有保持同步更新用户查询时会查到缓存中的旧数据这在很多场景下是不允许的。缓存更新的几种策略有三种
内存淘汰该机制默认存在 缓存设定一定的上限当达到这个上限就会自动淘汰部分数据。一致性保持较差因为淘汰的这一部分数据才可以更新维护成本为0.超时剔除 通过redis中的expire关键字添加TTL时间到期后自动删除缓存。 一致性强弱取决于TTL的时间一致性一般好于内存淘汰机制。维护成本也不是很高。 主动更新 \font 自己编写业务逻辑在修改数据库的同时更新缓存。 一致性好但是维护成本较高。
业务场景选择更新策略的原则
低一致性需求使用内存淘汰机制例如店铺类型的查询缓存高一致性需求主动更新并以超时剔除作为兜底方案。如店铺详情查询。 一般采用01的方式主动更新缓存 主动更新的方法可以采用当数据库发生改变的时候删除缓存当查询数据库的时候更新缓存。 这里有两种操作顺序的选择
先删除缓存再操作数据库但是有可能发生如下图左图的安全问题。先操作数据库再删除缓存。有可能发生如下图右图的安全问题。但是因为数据库读写时间远远大于缓存读写时间因此右图发生的概率更低。万一发生超时时间可以兜底。
业务修改
根据id查询商铺信息如果未能在缓存命中从数据库查询并写入缓存设置超时时间 //5.存在缓存到redis,加入有效时间限制stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);根据id修改商铺信息先修改数据库再删除缓存。这两个动作需要绑定所以该方法用事务控制其原子性。 OverrideTransactionalpublic Result update(Shop shop) {Long id shop.getId();if(id null){return Result.fail(店铺id不为空);}//1.更新数据库updateById(shop);//2.删除缓存stringRedisTemplate.delete(CACHE_SHOP_KEYshop.getId());return Result.ok();}测试
首先测试当访问某一家店铺信息的时候未命中是否会缓存到redis中再测试修改店铺信息是否会删除redis缓存因为修改的功能只能在商家界面做所以这里用http-client对业务逻辑进行测试。发送请求数据库修改redis缓存也被删除。说明业务修改成功。这样可以有效解决一致性问题。
PUT http://localhost:8081/shop
Content-Type: application/json{area:大关,openHours: 10:00-22:00,sold: 4215,address: china,comments:3035,avgPrice: 80,score: 37,name: 110茶餐厅,typeId: 1,id: 1
}缓存穿透
客户端请求的数据在缓存和数据库中都不存在那么根据我们的缓存更新策略最终都会向数据库索取数据那么如果有不怀好意的人用并发的线程用虚假的id向数据库请求数据就会搞垮数据库。 两种解决方案
缓存空对象如果redis和数据库中都未能命中最终数据库会向redis写入一个null这样在下一次向redis请求的时候就不会再到达数据库。 优点实现简单、维护方便 缺点 有额外的内存消耗但是也可以给null设置一个TTL可能造成短期的不一致可以控制TTL的时长 布隆过滤器 布隆过滤器的原理 定义布隆过滤器Bloom Filter是一种空间效率非常高的概率型数据结构用于判断一个元素是否属于某个集合。构成 1.布隆过滤器使用一个固定长度的位数组所有位初始都设置为0。 2.一组独立的哈希函数用于将输入元素映射到位数组中的某个位置。判断原理向布隆过滤器中添加元素时通过k个哈希函数计算出k个位置并将这些位置上的位设置为1。查询元素时使用同样的哈希函数计算出k个位置并检查这些位置上的位是否全为1。如果所有位置都为1则元素可能在集合中如果有一个位置为0则元素肯定不在集合中。 优点内存占用小没有多余的key缺点实现复杂、存在误判可能 采用缓存空对象解决缓存穿透问题
我们应该做如下修改 我们需要修改queryById方法注意字符串判断内容是否相等用equalsshopJson.equals(“”) Overridepublic Result queryById(Long id) {String key CACHE_SHOP_KEY id;//1.从redis中查询String shopJson stringRedisTemplate.opsForValue().get(key);//2.命中返回店铺信息if (!StringUtils.isBlank(shopJson)) {return Result.ok(JSONUtil.toBean(shopJson, Shop.class));}//如果命中的是就返回店铺信息不存在if(shopJson ! null){return Result.fail(店铺信息不存在);}//3.不存在用id在数据库查询Shop shop getById(id);//4.不存在返回“店铺不存在”if (shop null) {//如果数据库不存在该id的商铺就向redis中存入空字符串并返回店铺信息不存在stringRedisTemplate.opsForValue().set(key, );return Result.ok(店铺信息不存在);}//5.存在缓存到redis,加入有效时间限制stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);//6.返回店铺信息return Result.ok(shop);}然后进行测试发送请求http://localhost:8080/api/shop/1111id1111并不存在但是该数据会被缓存到redis中 再次发送这个请求不会到达数据库而是访问redis之后直接就返回。控制台也没有任何数据库调用的日志打印出来。 其他解决方案
增加id的复杂度让攻击者无发猜测到id格式。对id做一些基础的格式校验加强用户权限的管理做好热点参数的限流
缓存雪崩
缓存雪崩指的是大量可以在同一时段同时失效或者Redis服务宕机导致大量请求到达数据库带来巨大压力。 解决方案
给不同的key的TTL添加随机值这样key就不会在同一时间宕机提高Redis集群Redis哨兵模式服务的可用性。当一个Redis挂了会被监控到立马启动另外一个Redis提供服务。也可以使用主从结构构成集群防止主节点的数据丢失。给缓存业务添加降级限流策略比如快速失败、拒绝服务给业务添加多级缓存Nginx缓存–JVM缓存–Redis缓存–数据库缓存。
缓存击穿
缓存击穿也叫热点Key问题就是一个被高并发访问比如正在做活动的某一件商品并且缓存建立业务较为复杂的key失效了突然大量的请求会在瞬间给数据库带来巨大的冲击。 两种解决方案
互斥锁解决缓存击穿
让多线程只有一个线程能获取锁来创建缓存 我们可以手动地设定一个锁来实现这样的功能redis中的setnx表示只有当一个key不存在的时候才可以写入那么这样就可以达到互斥的效果。那么
获取锁的操作就是setnx lock 1通常还会给锁加一个TTL如果超过这个时间就自动删除锁。防止获取到锁的线在这里插入代码片程出问题。释放锁的操作就是:del lock 代码实现首先定义获取锁和释放锁的方法 获取锁 private boolean tryLock(String key){//获取锁Boolean flag stringRedisTemplate.opsForValue().setIfAbsent(key, 1, 10L, TimeUnit.SECONDS);return BooleanUtil.isTrue(flag);}释放锁 private void unLock(String key){stringRedisTemplate.delete(key);}将缓存穿透的业务逻辑封装最终返回Shop对象 //解决缓存穿透的代码public Shop queryWithPassThroough(Long id){String key CACHE_SHOP_KEY id;//1.从redis中查询String shopJson stringRedisTemplate.opsForValue().get(key);//2.命中返回店铺信息if (!StringUtils.isBlank(shopJson)) {return JSONUtil.toBean(shopJson, Shop.class);}//如果命中的是就返回店铺信息不存在if(shopJson ! null){return null;}//3.不存在用id在数据库查询Shop shop getById(id);//4.不存在返回“店铺不存在”if (shop null) {//如果数据库不存在该id的商铺就向redis中存入空字符串并返回店铺信息不存在stringRedisTemplate.opsForValue().set(key, );return shop;}//5.存在缓存到redis,加入有效时间限制stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);//6.返回店铺信息return shop;}用互斥锁解决缓存击穿的问题 //用互斥锁解决缓存击穿的问题public Shop queryWithMutex(Long id){String key CACHE_SHOP_KEY id;//1.从redis中查询String shopJson stringRedisTemplate.opsForValue().get(key);//2.命中返回店铺信息if (!StringUtils.isBlank(shopJson)) {return JSONUtil.toBean(shopJson, Shop.class);}//3.如果命中的是就返回店铺信息不存在if(shopJson ! null){return null;}Shop shop null;//4.实现缓存重建//4.1 获取互斥锁String lockKey lock:shop: id;boolean isLock tryLock(lockKey);try {//4.2 判断是否获取成功//4.3 失败则休眠并重试if(!isLock){Thread.sleep(50);return queryWithMutex(id);}//4.4 成功用id在数据库查询shop getById(id);//5.不存在返回“店铺不存在”if (shop null) {//如果数据库不存在该id的商铺就向redis中存入空字符串并返回店铺信息不存在stringRedisTemplate.opsForValue().set(key, );return shop;}//6.存在缓存到redis,加入有效时间限制stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);//7.释放互斥锁} catch (InterruptedException e) {throw new RuntimeException(e);}finally{unLock(lockKey);}//8.返回店铺信息return shop;}通过自动化测试工具jmeter对进行压力测试 执行完发现没有报错 并且数据库只调用了一次select操作说明互斥锁成功实现了
设置逻辑过期时间解决缓存击穿 缓存工具封装对象 先定义一个类用来保存以及超时时间对原来代码没有侵入性。
package com.hmdp.entity;import lombok.Data;import java.time.LocalDateTime;/*** author Zonda* version 1.0* description TODO* 2024/7/4 16:21*/
Data
public class RedisData {private LocalDateTime expireTime;private Object data;}
在ShopServiceImpl 新增此方法利用单元测试进行缓存预热 public void saveShop2Redis(Long id,Long expireSeconds){Shop shop getById(id);RedisData redisData new RedisData();redisData.setData(shop);redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY id, JSONUtil.toJsonStr(redisData));}