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));
}
收!