项目背景:
在系统初期为了实现业务的快速增长、对系统没有很好的设计、后期想实现缓存,提升应用系统的性能。
面临问题:
- 缓存配置混乱、系统微服务化基本上需要实现缓存、序列化方式不统一
- 研发效率低下、每次需要对缓存配置逐一配置且繁琐
- 通用能力不支持、例如、分布式锁、通用方法
方案设计:
- Cache Aside 侵入式缓存设计方案
- 应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。
- 更新是先更新数据库,成功后,让缓存失效
- 为什么不是写完数据库后更新缓存?
- 主要是怕两个并发的写操作导致脏数据。
- Cache as SoR(System of Record) 非侵入式缓存设计方案
- Read Through 当应用系统向缓存系统请求数据时;如果缓存中并没有对应的数据存在,缓存系统将向底层数据源的读取数据。如果数据在缓存中存在,则直接返回缓存中存在的数据。
- Write Through 当应用系统对缓存中的数据进行更新时缓存系统会同步更新缓存数据和底层数据源。
- Write Behind 当应用系统对缓存中的数据进行更新时,缓存系统会在指定的时间后向底层数据源更新数据,直接操作的缓存,异步操作数据库
Codefocus-Cache 非侵入式缓存设计、基于YML动态配置Redis,集成Spring Cache,增加二级缓存caffeine,优化keys及scan命令,过期时间扩展,使用方式和Spring Cache完全一样,支持其它更多的功能 。
实现方案:
基于CacheProvide扩展自个的Codefocus-Cache,支持一级和二级缓存
背后实现逻辑:
基于Spring @Cacheable注解 通过扩展自定义的CodeFocusCacheManager实现自定义缓存
可支持自定义缓存单位、时间、key
通过CodeFocusCache实现对缓存的put、get、evict、clear操作。
通过Spring caffeine Cache 缓存、为什么没有使用Guava Cache,因为spring已经不更新了换句话说不支持了
使用 Redis 消息队列 进行一级缓存和二级缓存的通过。pub 和sub
过程中踩的坑:
坑1:在批量更新时Spring Cacheable 是通过keys命令获取所有的key进行更新、Redis集群环境是不支持的,怎么搞?
解决方案:
通过key转换器,对缓存进行拆分、存储两种数据结构,hash和list。
hash存储真实的数据、list存储对应hash的key,list的key是拆分出的公共key
在更新时通过公共key获取list中keys进行update
坑2:坑1进行拆分之后,那么每次操作是需要操作hash一次和list一次,如何保持原子性呢?
解决方案:
通过lua脚本保证原子性、减少网络开销
坑3:在通过lua脚本保证原子性操作时,因为每次操作的key不一样导致、线上Redis集群缓存、导致会遇到hash槽的问题
解决方案:
Hash Tag 通过key转换器在每次操作是使用公共key进行hash保证两次操作映射到同一hash槽中
扩展功能支持点:
通过注解形式进行自定义API限流
背后实现逻辑:
通过实现扩展HandlerInterceptor实现RequestLimitInterceptor对请求进行拦截
通过Annotation进行对API方法的解决判断是否有限流注解类,如果有解析对应的limit、period、unit、api method
通过request解析到对应的parameter、header
通过上述解析传递参数、API 自定义实现AccessSpeedLimit实现限流、限流策略:令牌桶
扩展限流策略:
计数器:在一段时间间隔内(时间窗),处理请求的最大数量固定,超过部分不做处理。
漏桶:漏桶大小固定,处理速度固定,但请求进入速度不固定(在突发情况请求过多时,会丢弃过多的请求)
令牌桶:令牌桶的大小固定,令牌的产生速度固定,但是消耗令牌(即请求)速度不固定(可以应对一些某些时间请求过多的情 况);每个请求都会从令牌桶中取出令牌,如果没有令牌则丢弃该次请求。
代码案列:
(step 1)使用教程:
<dependency>
<groupId>club.codefocus.framework</groupId>
<artifactId>codefocus-cache</artifactId>
<version>1.0.3-SNAPSHOT</version>
</dependency>
(step 2)YML配置详解
spring:
redis:
port: 6379
database: 0
password:
host: 127.0.0.1
lettuce:
pool:
max-active: 8 #连接池最大连接数(使用负值表示没有限制) 默认为8
max-idle: 8 # 连接池中的最大空闲连接 默认为8
min-idle: 0 #连接池中的最小空闲连接 默认为 0
timeout: 2000
code-focus:
global-limit-count: 200 #次数
global-limit-period-time: 1 #秒 单位时间内的次数
global-limit-open: true #是否开启服务限流
cache-config: #缓存配置
cache-null-values: true #是否允许为Null
cache-base-name: demo #项目名称,缓存key的前缀
split-code: '#' #拆分符合
caffeine:
initial-capacity: 1 #初始值
maximum-size: 2 #最大值
##Spring Cacheable 缓存使用教程、过期时间
#拆分符 支持自定义
@Cacheable(value = "UserInfoList#30s" ,key = "#p0")
UserInfoList:
30s
s:秒
m:分钟
h:小时
d:天
#扩展功能-自定义限流 使用教程
/**
* @author jackl
* @Date: 2019/10/25 10:26
* @Description:分布式锁开关
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DistributedLock {
/**
* 分布式锁开关:true=开启;false=关闭
*/
boolean open() default true;
LockType lock() default LockType.IP;
String field() default "";
int expire() default 5000;
int timeOut() default 3000;
}
使用列子:
@DistributedLock(lock = LockType.UNIQUEID,field="a")
/**
* @Title: RequestLimit
* @Description: 限制方法调用次数(可以注解在方法或类)
* @ProjectName:
* @author: jackl
* @date: 2019/10/25 10:26
* @version: V1.0.0
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestLimit {
/**
* 限制上限
*/
int limit() default 5;
/**
* 单位时间内
*/
int period() default 1;
/**
* 时间单位
*/
TimeUnit unit() default TimeUnit.SECONDS;
/**
* 用户id
* @return
*/
String userId() default "";
/**
* 判断用户id具体的位置
* @return
*/
RequestLimitType userIdRequestLimitType() default RequestLimitType.REQUEST;
/**
* 客户唯一id
* @return
*/
String clientId() default "";
/**
* 客户唯一ID的类型
* @return
*/
RequestLimitType clientIdRequestLimitType() default RequestLimitType.REQUEST;
}
使用列子:
@RequestLimit(limit = 1,period =1 ,unit = TimeUnit.SECONDS)
(step 3)Bean实例详解 【扩展通用能力支持】:
@Resource
RedisHandler redisHandler;
/**
* 获取锁
* @param key 锁的key
* @param time 锁的过期时间
* @return
*/
boolean lock(String key, int time);
/**
* 持续获取锁
* @param key 锁的key
* @param time 锁的过期时间
* @return
*/
boolean getLockWhile(String key, int time) #不推荐使用
#以上都会自动释放锁,也可以通过下面方式手动释放
/**
* 释放锁
* @param key
*/
public void unlock(String key)
源代码库 请移步:https://github.com/codefocus-club/codefocus-project/
目前CodeFocus-Project 支持 自定义分布式链路追踪traceId、基于YML动态配置创建Rabbit、基于YML动态配置Redis,集成Spring Cache,增加二级缓存caffeine,优化keys及scan命令, 过期时间扩展,使用方式和Spring Cache完全一样,支持其它更多的功能。