【商城实战】04-进阶篇-缓存与分布式锁
【商城实战】04-进阶篇-缓存与分布式锁
Last edited 2022-9-27
date
Jul 1, 2022
type
Post
status
Published
slug
msb-mac-mall-cache-lock
summary
概要:1 缓存的概念;2 高并发环境中缓存的穿透、雪崩、击穿问题及解决方案;2 分布式环境中分布式锁的应用;
tags
微服务
spring-cloud-alibaba
商城
category
商城实战
password
Property
Sep 27, 2022 02:57 AM
icon

缓存与分布式锁

一、本地缓存和分布式缓存介绍

二、整合redis做分布式缓存

SpringBoot项目中添加Redis的maven依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
修改redis配置信息
spring: application: name: demo redis: host: 192.168.33.11 port: 6379
添加Redis配置类(这个是为了序列化配置,生产环境要加)
@Configuration public class RedisAutoConfiguration { /** * redisTemplate configuration * * @param factory factory * @return redisTemplate */ @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(factory); redisTemplate.setKeySerializer(RedisSerializer.string()); redisTemplate.setHashKeySerializer(RedisSerializer.string()); redisTemplate.setValueSerializer(RedisSerializer.json()); redisTemplate.setHashValueSerializer(RedisSerializer.json()); redisTemplate.afterPropertiesSet(); return redisTemplate; } }
测试Redis
@SpringBootTest class DemoApplicationTests { @Autowired private RedisTemplate<String, String> redisTemplate; @Test void contextLoads() { } @Test public void testStringRedisTemplate() { //往redis里写数据 redisTemplate.opsForValue().set("desc", "我是中国人,我爱我的祖国! " + UUID.randomUUID()); //读redis里的数据 System.out.println(redisTemplate.opsForValue().get("desc")); } }
使用工具查看Redis服务器数据(Redis Desktop Manager)
notion image

三、高并发下缓存问题

1. 缓存穿透

  • 指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,我们没有将这次查询的null写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。
  • 利用不存在的数据进行攻击,数据库瞬时压力增大,最终导致崩溃
解决方案也比较简单,直接把null结果缓存,并加入短暂的过期时间
public Map<String, List<Product>> getProducts(String pid) { String key = CACHE_KEY_PREFIX + pid; String products = redisTemplate.opsForValue().get(key); if (StringUtils.hasText(products)) { //空缓存 if (CACHE_NULL_VALUE.equals(products)) { return null; } List<Product> list = JSON.parseArray(products, Product.class); return list.stream().collect(Collectors.groupingBy(Product::getSpu)); } //缓存没有数据 List<Product> list = getUsersFromDb(pid); if (CollectionUtils.isEmpty(list)) { //表示数据库里也没有数据,添加空数据缓存,防止穿透 redisTemplate.opsForValue().set(key, CACHE_NULL_VALUE, 5, TimeUnit.SECONDS); return null; } else { //数据库查出的数据,存到缓存一份,添加随机过期时间,防止缓存雪崩 String json = JSON.toJSONString(list); redisTemplate.opsForValue().set(key, json, new Random().nextInt(10), TimeUnit.HOURS); } return list.stream().collect(Collectors.groupingBy(Product::getSpu)); }

2. 缓存雪崩

  • 缓存雪崩是指在我们设置缓存时key采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。
  • 解决方案:原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
notion image

3. 缓存击穿

  • 对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。如果这个key在大量请求同时进来前正好失效,那么所有对这个key的数据查询都落到db,我们称为缓存击穿。
  • 解决方案:加锁,大量并发只让一个去查,其他人等待,查到以后释放锁,其他人获取到锁,先查缓存,就会有数据,不用去db。
notion image

4. 本地锁的局限性

本地锁在分布式环境下,是没有办法锁住其他节点的操作的,这种情况肯定是有问题的
notion image
针对本地锁的问题,我们需要通过分布式锁来解决,那么是不是意味着本身锁在分布式场景下就不需要了呢?
notion image
显然不是这样的,因为如果分布式环境下的每个节点不控制请求的数量,那么分布式锁的压力会非常大,这时我们需要本地锁来控制每个节点的同步,来降低分布式锁的压力,所以实际开发中我们都是本地锁和分布式锁结合使用的。

四、分布式锁

、SpringCache

SpringCache的不足:

1).读模式

  • 缓存穿透:查询一个null的数据。可以解决 cache-null-values=true
  • 缓存击穿:大量并发进来同时查询一个正好过期的数据。解决方案:分布式锁 sync=true 本地锁
  • 缓存雪崩:大量的key同一个时间点失效。解决方案:添加过期时间 time-to-live=60000 指定过期时间

2).写模式

  • 读写锁
  • 引入canal,监控binlog日志文件来同步更新数据
  • 读多写多,直接去数据库中读取数据即可

总结:

  • 常规数据(读多写少):而且对及时性和数据的一致性要求不高的情况,我们完全可以使用SpringCache
  • 特殊情况:特殊情况特殊处理。
  • 微服务
  • spring-cloud-alibaba
  • 商城
  • 【商城实战】06-进阶篇-异步处理【商城实战】10-ElasticSearch