📖 本文目录
📖 Redis 实战 —— 实战篇

课程介绍
heima 点评Redis —— 项目
1️⃣ 短信登录 —— Redis 的共享 session 应用
2️⃣ 商户查询缓存 —— 企业的缓存使用技巧| 缓存雪崩、穿透等问题解决
3️⃣ 达人探店 —— 基于 List 点赞链表|基于 SortedSet 的点赞排行榜
4️⃣ 优惠券秒杀 —— Redis 的计数器| Lua 脚本 Redis | 分布式锁 | Redis 的三种消息队列 ⭐
5️⃣ 好友关注 —— 基于 Set 集合的关注|取关|共同关注|消息推送的功能
6️⃣ 附近的商户 —— Redis 的 GeoHash 的应用
7️⃣ 用户签到 —— Redis 的 BitMap 数据统计功能
8️⃣ UV 统计 —— Redis 的 HyperLogLog 的统计功能
📑 Business Search Cache —— 商业查询缓存

🔖 什么是缓存
缓存就是数据交换的缓冲区(称之为 Cache),是存储数据的临时地方,一般读写性能较高。

缓存的作用
1️⃣ 降低后端负载
2️⃣ 提高读写效率,降低响应时间
缓存的成本
1️⃣ 数据的一致性成本问题
2️⃣ 代码维护成本
3️⃣ 运维成本
🔖 添加 Redis 缓存
查询商户的业务流程

根据请求查询对应的数据
@RestController
@RequestMapping("/shop")
public class ShopController {
@Resource
public IShopService shopService;
/**
* 根据id查询商铺信息
* @param id 商铺id
* @return 商铺详情数据
*/
@GetMapping("/{id}")
public Result queryShopById(@PathVariable("id") Long id) {
return Result.ok(shopService.getById(id));
}
}
缓存作用模型

添加缓存的业务逻辑思路

1️⃣ 当用户进行提交查询商铺时的请求时 会携带商铺的 id 信息
2️⃣ 尝试 通过 商铺 id 信息 到缓存中查看是否存在对应缓存信息
- 1.如果未命中缓存,则根据 商铺 id 到数据库中查询对应的数据
- 存在商铺数据——将其商铺数据写入 Redis 当中,然后返回商铺信息
- 不存在商铺数据——返回 404 状态码
- 2.如果命中缓存,则直接返回商铺对应的信息。
3️⃣ 结束
❓ 为什么 使用BeanUtil做beanToMap时,转换字段(可能为null)的属性为String类型
这里小付采用 Redis Hash 将数据存入 redis中
【参考资料】 【运行报错】使用BeanUtil做beanToMap时,转换字段(可能为null)的属性为String类型
1️⃣ 报错信息
- 使用Redis代替session做缓存时,需要将redis中未命中的数据,从数据库查出再存入redis缓存
- 首先得将对象转为hashmap(这里使用得是hutool的BeanUtil)
- 且使用
StringRedisTemplateredis需要转换成的map的各个字段都是String类型!而bean的各个字段类型各不相同 - 如何在不遍历map(繁琐)的情况下,在转换为map时就直接将字段类型也转换为String?
Map<String, Object> map = BeanUtil.beanToMap(shop,new HashMap<>(),
CopyOptions.create().
setIgnoreNullValue(true)
.setFieldValueEditor((fieldName,fieldValue) -> fieldValue.toString()));
stringRedisTemplate.opsForHash().putAll(RedisConstants.CACHE_SHOP_KEY + id,map);
- 但是执行报错了
java.lang.NullPointerException: null
at com.hmdp.service.impl.ShopServiceImpl.lambda$queryById$0(ShopServiceImpl.java:81) ~[classes/:na]
at cn.hutool.core.bean.copier.CopyOptions.editFieldValue(CopyOptions.java:258) ~[hutool-all-5.7.17.jar:na]
at cn.hutool.core.bean.copier.BeanCopier.lambda$beanToMap$1(BeanCopier.java:233) ~[hutool-all-5.7.17.jar:na]
at java.util.LinkedHashMap$LinkedValues.forEach(LinkedHashMap.java:608) ~[na:1.8.0_181]
at cn.hutool.core.bean.BeanUtil.descForEach(BeanUtil.java:182) ~[hutool-all-5.7.17.jar:na]
at cn.hutool.core.bean.copier.BeanCopier.beanToMap(BeanCopier.java:195) ~[hutool-all-5.7.17.jar:na]
at cn.hutool.core.bean.copier.BeanCopier.copy(BeanCopier.java:106) ~[hutool-all-5.7.17.jar:na]
at cn.hutool.core.bean.BeanUtil.beanToMap(BeanUtil.java:690) ~[hutool-all-5.7.17.jar:na]
- 空指针异常
2️⃣ 原因分析
- 发现需要转成map得bean中有字段是null
- 而null不能toString()
- 故将代码修改为:
Map<String, Object> map = BeanUtil.beanToMap(shop,new HashMap<>(),
CopyOptions.create().
setIgnoreNullValue(true)
.setFieldValueEditor((fieldName,fieldValue) -> fieldValue + ""));
stringRedisTemplate.opsForHash().putAll(RedisConstants.CACHE_SHOP_KEY + id,map);
-
但还是存在问题:
-
现在空值是以字符串 “null” 存在redis中的
-
当再次点击此商铺,此时缓存命中,故将redis中的hash类型数据转为 shop 对象时,“null” 又无法将string类型的value解析成一个数字类型
-
此时注意到,我代码中明明将 空值忽略掉了
setIgnoreNullValue(true) -
但debug却发现
setIgnoreNullValue(true)并没有 生效
3️⃣ 解决方法
- setFieldValueEditor 优先级要高于 ignoreNullValue导致前者首先被触发,因此出现空指针问题。你在setFieldValueEditor中也需要判空
- 这么设计的原因主要是,如果原值确实是null,但是你想给一个默认值,在此前过滤掉就不合理了,而你的值编辑后转换为null,后置的判断就会过滤掉
- 修改代码:
// todo 3.2 判断商铺缓存是否存在 => 存在 将商铺数据写入到 Redis 中 => 返回 商铺信息
Map<String, Object> redisCache = BeanUtil.beanToMap(shop,new HashMap<>()
,CopyOptions.create()
.setIgnoreNullValue(true)
.setFieldValueEditor((k,v)->{
if (k == null){
v = "0";
}else {
v = v + "";
}
return v;
}));
stringRedisTemplate.opsForHash().putAll(shopKey , redisCache);
return Result.ok(redisCache);
- 问题解决
⭐ 使用 Redis Hash 进行存储数据
步骤一:改写 Service 层业务逻辑
ShopController
/**
* 根据id查询商铺信息
* @param id 商铺id
* @return 商铺详情数据
*/
@GetMapping("/{id}")
public Result queryShopById(@PathVariable("id") Long id) {
return shopService.queryById(id);
}
IShopService
public interface IShopService extends IService<Shop> {
Result queryById(Long id);
}
ShopServiceImpl
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
@Autowired
StringRedisTemplate stringRedisTemplate ;
@Override
public Result queryById(Long id) {
// todo 1. 根据商铺 id 到 Redis 中查询商铺缓存
String shopKey = RedisConstants.CACHE_SHOP_KEY + id ;
Map<Object, Object> shopMap = stringRedisTemplate.opsForHash().entries(shopKey);
// todo 2. 缓存命中 => 返回商铺信息
if (shopMap.isEmpty()){
// todo 3. 缓存未命中 => 根据 id 查询数据库
Shop shop = getById(id);
// todo 3.1 判断商铺缓存是否存在 => 不存在 返回404状态码 提示错误信息
if (shop == null){
return Result.fail("404 您访问的商户不存在!");
}
// todo 3.2 判断商铺缓存是否存在 => 存在 将商铺数据写入到 Redis 中 => 返回 商铺信息
Map<String, Object> redisCache = BeanUtil.beanToMap(shop,new HashMap<>()
,CopyOptions.create()
.setIgnoreNullValue(true)
.setFieldValueEditor((k,v)->{
if (k == null){
v = "0";
}else {
v = v + "";
}
return v;
}));
stringRedisTemplate.opsForHash().putAll(shopKey , redisCache);
return Result.ok(redisCache);
}
// todo 4 结束
return Result.ok(shopMap);
}
}
步骤二:启动应用程序进行业务逻辑测试

1️⃣ 第一次查询还是走了数据库的查询 所以 时间为 153 ms
查看对应 Redis 中是否已经缓存好了对应的数据

📚 此时已经缓存到了数据库
此时我们再次去进行查询之前的数据查看请求时间

此时就发现我们之前缓存好的数据只需要 16 ms 就可以完成请求了 快了 将近 10倍
反观IDEA控制台中并没有对应的 SQL 查询日志,故我们的缓存生效 极大提高了效率。
⭐ 使用 Redis String 进行存储数据
步骤一:这里只提供对应的Controller 层代码的编写对应的业务逻辑要求
/**
* 功能描述
* 商业缓存的具体实现 以 String 类型传入
* @date 2022/7/10
* @author Alascanfu
*/
@Override
public Result cacheStringQueryById(Long id) {
// todo 1. 根据商铺 id 到 Redis 中查询商铺缓存
String shopKey = RedisConstants.CACHE_SHOP_KEY + id ;
// todo 2. 缓存命中 => 返回商铺信息
String shopJson = stringRedisTemplate.opsForValue().get(shopKey);
if (StrUtil.isNotBlank(shopJson)){
Shop shop = JSONUtil.toBean(shopJson , Shop.class);
return Result.ok(shop) ;
}
// todo 3. 缓存未命中 => 根据 id 查询数据库
Shop shop = getById(id);
if (shop == null){
// todo 3.1 判断商铺缓存是否存在 => 不存在 返回404状态码 提示错误信息
return Result.fail("404 您访问的商户不存在!");
}
// todo 3.2 判断商铺缓存是否存在 => 存在 将商铺数据写入到 Redis 中 => 返回 商铺信息
stringRedisTemplate.opsForValue().set(shopKey,JSONUtil.toJsonStr(shopJson));
return Result.ok(shop);
}
1万+

被折叠的 条评论
为什么被折叠?



