方圆的秒杀系统优化方案实战,(七)操作对缓存的更新

本文介绍了如何在秒杀系统中优化缓存更新,包括添加缓存常量、更改获取缓存方法参数、定义事件处理流程。通过使用事件处理器,实现了在发布、上线和下线操作时对缓存的精确更新,同时讨论了缓存更新的延迟问题和可能的改进方案,如使用消息中间件。
摘要由CSDN通过智能技术生成

1. 写在前头

大家好,我是方圆。这篇写的是秒杀活动、商品在执行发布、上线和下线操作时对缓存的更新,对应分支是increase_refresh_cache,源码以该分支为准。

本篇将对获取缓存的方法参数进行更改,并且对单条和列表缓存的key添加前缀,以上两处改动都是为了方便缓存的更新。

  • 之前单条缓存和列表缓存key值都采用的是QueryCondition的toString字符串,改动之后单条缓存为前缀 + ID的格式,列表缓存为前缀 + QueryCondition的toString字符串格式

为什么要这么改?

  • 添加不同的前缀是为了将单条明细缓存和列表缓存区分开,并且列表缓存有了前缀之后,可以进行模糊匹配删除。因为列表缓存的更新是上述修改操作发生之后全部进行清除,我没有想到更好的对列表缓存的更新措施,因为目前无法确定在操作某一条数据时,列表缓存中是否存在该条数据,所以只能进行全部清除,除非,能想到更好的方法…

缓存更新流程图如下,以秒杀活动为例(秒杀商品缓存的更新与此相似)

注意这里我们采用的是借助本地事件处理器,alibaba.cola.event包下的事件处理器,同《高并发秒杀的设计精要与实现》中介绍的方法一致,发生如图上事件时对缓存进行更新。

在分布式缓存刷新的过程中,并不会主动刷新所有服务器上的本地缓存,本地缓存将遵循主机的刷新策略。换句话说,本地缓存可能会有秒级的滞后,对于数据一致性非绝对敏感的场景,这种短时间的延迟下的脏数据是可以接收的,它只是会对用户侧的展示有所影响,而不会影响到服务端的数据状态 ——《高并发秒杀的设计精要与实现》

后续可以将这一部分改成采用消息中间件处理的办法:通过广播更新所有服务器的缓存,但是目前我们是单台服务器开发,这个就之后再改吧。

2. 更新缓存的实现

2.1 添加缓存通用常量CacheConstants

  • 目前该类中,保存的是缓存的前缀,如下
public class CacheConstants {
    /**
     * 秒杀活动单个缓存key前缀
     */
    public static final String FLASH_ACTIVITY_SINGLE_CACHE_PREFIX = "FLASH_ACTIVITY_SINGLE_CACHE_%d";

    /**
     * 秒杀商品单个缓存key前缀
     */
    public static final String FLASH_ITEM_SINGLE_CACHE_PREFIX = "FLASH_ITEM_SINGLE_CACHE_%d";

    /**
     * 秒杀活动缓存列表key前缀
     */
    public static final String FLASH_ACTIVITY_CACHE_LIST_PREFIX = "FLASH_ACTIVITY_CACHE_LIST_%s";

    /**
     * 秒杀商品缓存列表key前缀
     */
    public static final String FLASH_ITEM_CACHE_LIST_PREFIX = "FLASH_ITEM_CACHE_LIST_%s";
}

2.2 更改获取缓存的方法参数

  • CacheService
    /**
     * 查询单条本地缓存数据
     *
     * @param keyPrefix 单条缓存key前缀
     * @param id 秒杀商品or秒杀活动 ID
     */
    T getCache(String keyPrefix, Long id);

    /**
     * 根据查询条件读取本地缓存
     *
     * @param keyPrefix 列表缓存key前缀
     */
    List<T> getCaches(String keyPrefix, BaseQueryCondition queryCondition);
  • AbstractCacheService
    @Override
    public T getCache(String keyPrefix, Long id) {
        String key = String.format(keyPrefix, id);

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

        if (flashActivityCaches != null) {
            return hitLocalCache(flashActivityCaches).get(0);
        } else {
            return getDataFromDistributedCacheAndSaveLocalCache(id, key);
        }
    }

    @Override
    public List<T> getCaches(String keyPrefix, BaseQueryCondition queryCondition) {
        String key = String.format(keyPrefix, queryCondition.toString());

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

        if (flashActivityCaches != null) {
            return hitLocalCache(flashActivityCaches);
        } else {
            return getDataListFromDistributedCacheAndSaveLocalCache(queryCondition, key);
        }
    }

传入参数都添加了前缀参数,获取单条缓存的一系列的方法参数都要进行修改,以代码源码为准,比较简单,不再赘述。

2.3 为CacheService添加更新缓存的方法

    /**
     * 更新单条缓存
     *
     * @param keyPrefix 单个缓存key前缀
     * @param id 秒杀商品or秒杀活动 ID
     */
    void refreshCache(String keyPrefix, Long id);

    /**
     * 更新列表查询缓存 (将所有列表查询的缓存清除)
     *
     * @param keyPrefix 列表缓存key前缀
     */
    void refreshCaches(String keyPrefix);
  • 在这里,我想的是,刷新单条缓存时直接根据 key前缀 + ID 拼接出key进行缓存新增or覆盖刷新列表缓存是根据前缀模糊匹配删除

具体实现如下

    @Override
    public void refreshCache(String keyPrefix, Long id) {
        String key = String.format(keyPrefix, id);

        getDataFromDataBaseAndSaveDistributedCache(id, key);
    }

    @Override
    public void refreshCaches(String keyPrefix) {
        redisCacheService.deleteByPrefix(keyPrefix);
    }
  • 我还是把它实现写在了AbstractCacheService模板抽象类中,因为觉得有现成的更新分布式缓存的方法可以复用,而且它们的代码必定都会是重复

另外列表模糊匹配删除的方法是在RedisCacheService封装了如下方法

    /**
     * 根据前缀模糊删除所有相关的缓存
     */
    public void deleteByPrefix(String keyPrefix) {
        String key = String.format(keyPrefix, "*");
        Set<String> keys = redisTemplate.keys(key);

        if (keys != null) {
            redisTemplate.delete(keys);
        }
    }

2.4 添加ColaConfig配置

  • 在flash-sale-infrastructure层event包下添加
@Configuration
public class ColaConfig {

    @Bean(initMethod = "init")
    public SpringBootstrap bootstrap() {
        return new SpringBootstrap();
    }
}
  • 这个照抄即可,添加它是为了帮助我们注册标记有@EventHandler注解的事件处理器,否则事件处理器没法用

2.5 定义事件

  • 以秒杀活动为例,领域事件,都在flash-sale-domain层event包
  • 首先定义了BaseDomainEvent类,因为我觉得秒杀活动和秒杀商品都需要在事件类中保存ID信息,所以我就把它抽了出来
@Getter
public class BaseDomainEvent implements DomainEventI {

    /**
     * id值
     */
    protected Long id;
}
  • 秒杀活动事件FlashActivityEvent,没什么好说的,其中保存了枚举ActivityEventType,写了两个参数的构造器,以及重写了toString方法,因为我是想在打印日志的时候查看是什么事件枚举类型
public class FlashActivityEvent extends BaseDomainEvent {

    private final ActivityEventType eventType;

    public FlashActivityEvent(Long id, ActivityEventType eventType) {
        this.id = id;
        this.eventType = eventType;
    }

    @Override
    public String toString() {
        return "FlashActivityEvent{" + "id=" + id + ", eventType=" + eventType.getDesc() + '}';
    }
}
  • 秒杀活动事件枚举
public enum ActivityEventType {

    PUBLISH("发布秒杀活动"),
    ONLINE("上线秒杀活动"),
    OFFLINE("下线秒杀活动");

    private final String desc;

    ActivityEventType(String desc) {
        this.desc = desc;
    }

    public String getDesc() {
        return desc;
    }
}

2.6 定义事件发布者

/**
 * 领域事件发布接口
 *
 * @author fangyuan
 */
public interface DomainEventPublisher {

    /**
     * 发布事件
     */
    void publish(DomainEventI domainEvent);
}
  • 对应发布者实现,注入了EventBus,之后我们定义事件处理器即可对领域事件进行消费
/**
 * 本地领域事件发布器
 */
@Slf4j
@Component
public class LocalDomainEventPublisher implements DomainEventPublisher {

    @Resource
    private EventBusI eventBus;

    @Override
    public void publish(DomainEventI domainEvent) {
        log.info("发布事件, {}", domainEvent.toString());

        eventBus.fire(domainEvent);
    }
}

2.7 定义事件处理器

  • 事件处理器我写在了flash-sale-infrastructure层event文件夹下
@Slf4j
@EventHandler
public class FlashActivityEventHandler implements EventHandlerI<Response, FlashActivityEvent> {

    @Resource
    private CacheService<FlashActivity> cacheService;

    @Override
    public Response execute(FlashActivityEvent flashActivityEvent) {
        log.info("开始处理秒杀活动事件, {}", flashActivityEvent.toString());

        if (flashActivityEvent.getId() == null) {
            log.error("秒杀活动事件参数错误");
            return Response.buildFailure("500", "秒杀活动事件参数错误");
        }

        // 刷新单条缓存和清除列表缓存
        cacheService.refreshCache(CacheConstants.FLASH_ACTIVITY_SINGLE_CACHE_PREFIX, flashActivityEvent.getId());
        cacheService.refreshCaches(CacheConstants.FLASH_ACTIVITY_CACHE_LIST_PREFIX);

        return Response.buildSuccess();
    }
}
  • 标注@EventHandler注解,实现EventHandlerI接口,重写execute方法,注入CacheService,调用更新缓存的方法,如此,便可

2.8 业务中的实现

  • 注入事件发布者,在需要更新缓存的业务中调用方法即可,以秒杀活动发布为例,如下
    @Resource
    private DomainEventPublisher domainEventPublisher;

    @Override
    public void publishActivity(FlashActivity flashActivity) {
        if (flashActivity == null || !flashActivity.validateParamsForCreate()) {
            throw new DomainException(PUBLISH_FLASH_ACTIVITY_PARAMS_INVALID);
        }

        // 状态为已发布
        flashActivityRepository.save(flashActivity.setStatus(PUBLISHED.getCode()));
        log.info("activityPublish|活动已发布|{}", JSON.toJSONString(flashActivity));

        // 秒杀活动发布事件,更新缓存
        domainEventPublisher.publish(new FlashActivityEvent(flashActivity.getId(), ActivityEventType.PUBLISH));
    }

收!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

方圆想当图灵

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

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

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

打赏作者

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

抵扣说明:

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

余额充值