方圆的秒杀系统优化方案实战,(六)分布式缓存

本文介绍了如何在秒杀系统中引入分布式缓存,重点使用Redis和Redisson实现。详细讨论了RedisConfig和RedissonConfig的配置,封装了RedisCacheService并介绍了AbstractCacheService的改动,以确保并发更新缓存的安全性。同时,文章提出了遵循的缓存原则和系统应对策略,如热点数据缓存、优先读取本地缓存、设置过期时间等。
摘要由CSDN通过智能技术生成

1. 写在前头

大家好,我是方圆。这篇我们将在本地缓存的基础上引入分布式缓存,开发分支对应increase_distributed_cache,它是在increase_local_cache分支基础上进行开发和拓展的。在此处不再赘述分布式缓存的生效流程图,请以第五章开头整体流程图为准。

2. 分布式缓存的实现

分布式缓存使用的是Redis,它也是程序员中间件必须要掌握的工具之一,另在此推荐《Redeis设计与实现》这本书,很好读、很流畅。

分布式锁我借助了Redisson实现,之前实习的时候,导师也给我推荐过这个工具。我们需要在infrastructure层pom文件中引入它的依赖。

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.16.8</version>
</dependency>

在使用前需要安装Redis,并在yaml配置文件中添加如下Redis相关配置

spring:
  application:
    # 应用名称
    name: flash-sale
  # mysql
  datasource:
    type: org.apache.commons.dbcp2.BasicDataSource
    url: jdbc:mysql://xxx:3306/flash_sale?useUnicode=true&characterEncoding=UTF-8&allowPublicKeyRetrieval=true
    username: xxx
    password: xxx
    driver-class-name: com.mysql.cj.jdbc.Driver
  # redis
  redis:
    host: 127.0.0.1
    password: xxx
    address: redis://127.0.0.1:6379

2.1 分析

  • 在缓存服务实现上使用了模板方法模式,分布式缓存不过是在本地缓存的基础上加了一层而已,它的代码一定是能够通用的,也仅有查询数据库的操作需要具体的实现,所以我们在AbstractCacheService抽象类上进行开发新增分布式缓存的代码即可。

2.2 RedissonConfig

  • 为Redisson添加必要的配置信息
@Configuration
public class RedissonConfig {

    @Value("${spring.redis.address}")
    private String address;
    @Value("${spring.redis.password}")
    private String password;

    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer().setAddress(address).setPassword(password);

        return Redisson.create(config);
    }
}

2.3 RedisConfig

  • RedisConfig,缓存Key添加了StringRedisSerializer解析
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(factory);

        // 缓存的key值用StringRedisSerializer解析
        StringRedisSerializer keySerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(keySerializer);
        redisTemplate.setHashKeySerializer(keySerializer);

        return redisTemplate;
    }
}
  • 这里我没有对缓存value值指定解析器,而是使用了默认的JdkSerializationRedisSerializer。因为如果不使用它的话,而是使用网上大家说的比较多的JSON相关的解析器会导致EntityCache对象字段dataList内类型都为JSONObject,这样我们就不得不在具体的业务层代码中添加一层转换,将JSONObject转换成FlashActivity或FlashItem,以免发生转型失败的异常。如果看不懂这里的解释的话,大家可以写上如下代码试一试。
        FastJsonRedisSerializer<Object> valueSerializer = new FastJsonRedisSerializer<>(Object.class);
        redisTemplate.setValueSerializer(valueSerializer);
        redisTemplate.setHashValueSerializer(valueSerializer);

2.4 封装RedisCacheService

  • 使用了泛型T,代表缓存的对象类型。注入RedisTemplate,封装了一下get和set值的方法,方便之后调用
@Service
public class RedisCacheService<T> {

    private static final Long ONE_WEEK_SECONDS = 607800L;

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    @SuppressWarnings("unchecked")
    public EntityCache<T> getValue(String key) {
        return (EntityCache<T>) redisTemplate.opsForValue().get(key);
    }

    /**
     * 保存长期的key,目前长期定的是1周
     */
    public void setValue(String key, Object value) {
        setValue(key, value, ONE_WEEK_SECONDS);
    }

    /**
     * 保存有时间限制的key
     *
     * @param second 秒数
     */
    public void setValue(String key, Object value, Long second) {
        redisTemplate.opsForValue().set(key, value, second, TimeUnit.SECONDS);
    }
}

2.5 AbstractCacheService

  • 在AbstractCacheService注入如下Bean
    @Resource
    private RedissonClient redissonClient;
    @Resource
    private RedisCacheService<T> redisCacheService;
  • 新增一些静态变量。其中在获取分布式锁时,为key定义了前缀,其中%s占位符表示的是缓存的key值,把分布式锁的key定义成动态的想法是:我期待能够实现并发的更新不同的缓存,对于同一条缓存大家争抢一把分布式锁,对于不同的缓存,那么就不让它争抢锁了吧
    /**
     * 分布式锁的key前缀, key: 前缀 + 查询条件toString
     */
    private static final String UPDATE_LOCK_PREFIX = "UPDATE_LOCK_PREFIX_%s";

    /**
     * 分布式获取锁的等待时间
     */
    private static final long WAIT_TIME = 1L;

    /**
     * 分布式锁最大的持有时间,超过自动释放锁
     */
    private static final long LEASE_TIME = 5L;

    /**
     * 分布式缓存的持续时间
     */
    private static final long DISTRIBUTED_CACHE_LIVE_TIME = 60;
  • 代码主流程上的改动,以getCache方法举例说明,请关注下文中的代码注释,最终仍以分支代码为准
    @Override
    public T getCache(BaseQueryCondition queryCondition) {
        String key = queryCondition.toString();

        EntityCache<T> flashActivityCaches = flashLocalCache.getIfPresent(key);

        if (flashActivityCaches != null) {
            return hitLocalCache(flashActivityCaches).get(0);
        } else {
            // 再未命中本地缓存时,查询分布式缓存并更新本地缓存
            return getDataFromDistributedCacheAndSaveLocalCache(queryCondition, key);
        }
    }
  • 从分布式缓存中取单个对象,取出后进行本地缓存
    private T getDataFromDistributedCacheAndSaveLocalCache(BaseQueryCondition queryCondition, String key) {
        // 从分布式缓存中取单条数据
        T data = getDataFromDistributedCache(queryCondition, key);

        if (data == null) {
            saveLocalCache(Collections.emptyList(), key);
        } else {
            saveLocalCache(Collections.singletonList(data), key);
        }

        return data;
    }
  • 从Redis分布式缓存获取单条数据,两种情况,命中缓存和未命中缓存
    private T getDataFromDistributedCache(BaseQueryCondition queryCondition, String key) {
        EntityCache<T> distributedCache = redisCacheService.getValue(key);

        if (distributedCache != null) {
            return hitDistributedCache(distributedCache, key).get(0);
        } else {
            return getDataFromDataBaseAndSaveDistributedCache(queryCondition, key);
        }
    }
  • 命中缓存,在数据存在的情况下直接返回,注意在数据不存在时,这里也更新了一下本地缓存,否则该请求将重复的命中分布式缓存
    private List<T> hitDistributedCache(EntityCache<T> distributedCache, String key) {
        log.info("命中分布式缓存, {}", JSONObject.toJSONString(distributedCache));

        if (distributedCache.isExist()) {
            return distributedCache.getDataList();
        } else {
            saveLocalCache(Collections.emptyList(), key);

            throw new RepositoryException(DATA_NOT_FOUND);
        }
    }
  • 未命中缓存,这里涉及到了分布式锁,代码逻辑也很清晰,先尝试获取锁,获取到了查库,更新分布式缓存,返回数据,注意对于查询不到的数据,仍然进行了缓存,避免缓存击穿
    private T getDataFromDataBaseAndSaveDistributedCache(BaseQueryCondition queryCondition, String key) {
        RLock lock = redissonClient.getLock(String.format(UPDATE_LOCK_PREFIX, key));

        T data = null;
        try {
            if (lock.tryLock(WAIT_TIME, LEASE_TIME, TimeUnit.SECONDS)) {
                try {
                    data = getSingleDataFromDataBase(queryCondition);

                    saveDistributedCache(Collections.singletonList(data), key);
                } catch (DomainException e) {
                    // 查询不存在的数据,同样加入缓存中,防止缓存穿透
                    saveDistributedCache(Collections.emptyList(), key);
                } finally {
                    lock.unlock();
                }
            } else {
                throw new RepositoryException(TRY_LATTER);
            }
        } catch (InterruptedException e) {
            log.error("分布式锁获取失败", e);
        }

        return data;
    }

getCache方法加入分布式锁的代码已经讲解完了,很简单,getCaches方法与之大同小异,参考源码即可。

3. 缓存的原则和系统的应对

《高并发秒杀的设计精要与实现》提到过一些缓存原则,对照这些原则,我想做一个总结,也是系统对这些原则的应对措施。

  • 热点数据一律进缓存:系统中采用的是本地缓存和分布式缓存的解决方案,并为查询的操作做了缓存处理,其中对于数据库中不存在的数据也进行了缓存,避免缓存击穿
  • 优先读取本地缓存,以本地缓存为主,远端分布式缓存为辅
  • 所有缓存设置过期时间,本地缓存过期时间控制在秒级:本地缓存设置的是10s,分布式缓存为60s
  • 本地缓存务必同时设置容量驱逐和时间驱逐两种方式:时间驱逐为10s,容量限制为33(暂定)
  • 缓存KEY具有业务可读性,杜绝不同场景出现相同KEY:系统中的key采用的是查询条件的toString字符串,因不同业务可不同
  • 缓存列表数据时,仅缓存第一页,缓存数量不超过20:缓存列表这里系统实现的不够好,而且之后更新列表缓存仍然是个问题,打算放弃列表的缓存更新,要么就要将条件查询方法重写或者改变列表缓存key在之后在更新缓存时能构造出key值
  • 杜绝并发更新缓存,防止缓存击穿:在分布式缓存处增加了分布式锁
  • 空数据进缓存,防止缓存击穿
  • 读数据时,先读缓存,再读数据库
  • 写数据时,先写数据库,再写缓存:这一点还未涉及,之后更新缓存时,将遵守!

加油儿

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

方圆想当图灵

嘿嘿,小赏就行,不赏俺也不争你

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值