1 用AOP的形式 实现Redis缓存的服务
1.1 当前版本的代码的分析
1.在serviceImpl中的findItemCatch方法中,实现了redis缓存。但要是别的方法中(eg:saveItem( )等)也需要redis缓存,就还要在方法中再写一遍redis的实现过程。代码不具有复用性。
2.serviceImpl中本身的作用就是写业务代码,但现在在serviceImpl中又写入了更偏向于redis的代码,“serviceImpl中的代码变得不纯洁”。代码的耦合性高。
需求:
1.实现代码的复用。
2.降低代码的耦合性。
1.2 AOP
1.2.1 AOP的作用
名称:面向切面编程。
一句话总结:在不改变原有代码的条件下,对原有代码的功能进行扩展。
公式: AOP = 切入点表达式 + 通知方法
专业术语:
1.连接点 在执行正常的业务过程中,满足了切入点表达式时,进入切面的点。(可以有多个)
2.通知 在切面中执行的具体的业务(扩展)(方法)
3.切入点 能够进入切面的一个判断 (只能是一个)
4.目标方法 将要按部就班执行的原本的业务
1.2.2 通知的类型(5种)
1.@Before
2.@AfterReturning
3.@AfterThrowing
4.@After
上述的4大通知类型 不能 控制目标方法 是否执行。一般用这4大通知类型,用来记录目标程序的执行状态。
5.@Around 在目标方法执行之前,执行之后,都要执行的通知方法。
作用:控制目标方法的执行。
功能最强大。
1.2.3 切入点表达式的类型(4种)
粗粒度
1.bean(bean的id) 交给spring管理的 对象 叫做 bean。bean的id就是首字母小写的类名(匹配确切的1个类)
配置文件中,<bean id="itemCatServiceImpl" class="com.jt.controller.ItemCatServiceImpl">
eg: @Pointcut(value = "bean(itemCatServiceImpl)")
2.within(包名.类名) 按包路径匹配包下的所有符合条件的类 (匹配多个类)因为可以用通配符*
eg: @Pointcut("within(com.jt.service..*)")
..*
表示还包含子包 .*
表示只包含当前级别下的包
细粒度
3.execution(返回值类型 包名.类名.方法名(参数列表))
4.@annotation(包名.注解名) 按注解进行拦截,从而执行通知方法
1.2.4 AOP的Demo练习(复习切入点表达式和通知方法)
在jt-common中的aop的CacheAop.java中
/**
* AOP = 切入点表达式 + 通知方法
* 拦截需求:
* 1.拦截itemCatServiceImpl的bean
* 2.拦截com.jt.service下的所有的类
* 3.拦截com.jt.service下的所有类及方法
* 3.1 拦截com.jt.service下的所有的类,返回值为int类型,并且以add开头的方法,并且参数为1个(String类型)
*/
//用bean表达式 定义一个切入点表达式
//@Pointcut(value = "bean(itemCatServiceImpl)")
//用within表达式 定义一个切入点表达式
//@Pointcut("within(com.jt.service..*)")
//execution表达式:拦截com.jt.service下的所有类的所有方法的任意参数类型。
@Pointcut("execution(* com.jt.service..*.*(..))")
public void pointcut(){}
//定义一个前置通知before
@Before("pointcut()")
public void before(){
System.out.println("我是@Before");
}
1.3 京淘项目–商品分类信息展示—通过AOP实现Redis缓存
1.3.1 需求分析
1.自定义注解@CacheFind ,只要是遇到被这个注解标识的方法,就开启redis缓存
2.为了将来区分业务,需要在注解中标识key属性,由使用者自行填写
3.为用户提供数据超时功能
1.3.2 自定义注解
注意自定义注解@CacheFind,在新建时有专门的annotation选项
新建完是这样:
这个自定义的注解会被很多业务所调用,所以要写在jt-common中
@Retention(RetentionPolicy.RUNTIME) //该注解什么时候有效
@Target({ElementType.METHOD}) //对方法有效
// (如果想不仅对方法有效,还想对属性等别的有效就直接写在后面,形成个数组。注解中的数据是{A,B,C,D}的形式,而不是[A,B,C,D]的形式)
//@Target({ElementType.METHOD,ElementType.FIELD})
//当然 如果只有一个的话,{}也可以省略不写。@Target(ElementType.METHOD)
public @interface CacheFind {
//由于业务需求 要给这个CacheFind指定一个Key,用来区分这个@CacheFind注解标识的方法在执行后,将数据写入到redis缓存中时,
// (redis缓存中的数据都是以K-V的形式存在的),key写的是啥。
//比如,是将商品分类信息ItemCat写入redis,那么value一定是(itemCatValue=[{"id":559,"text":"手机通讯","state":"closed"},{"id":562,"text":"运营商","state":"closed"},{"id":567,"text":"手机配件","state":"closed"}])
//那么为了日后一眼能看出来这个数据属于那个业务的(商品分类ItemCat),就把key写得具体点儿。
// 比如,在执行目标方法时,客户端会传过来parentId(比如是99),那么key就可以写成ITEM_CAT_PARENTID::99,后面再接上value,就是下面这种形式
// ITEM_CAT_PARENTID::99 : {"id":559,"text":"手机通讯","state":"closed"}
//redis缓存中的数据 就长上面这个样子
//要是不加这个key 那redis缓存中存储的数据成了 99 : {"id":559,"text":"手机通讯","state":"closed"}
//这谁能知道,这条数据是商品分类ItemCat的数据,还是商品订单的数据,还是别的什么数据呢,非常不清楚。
String key(); //该属性为必须添加
//这条redis数据的超时时间,由于客户对这个也没做具体要求,就可以设置为不超时 ,如果想设置就把0改掉
int seconds() default 0; //设定超时时间 default 0表示 默认的是不超时
}
1.3.3 编辑CacheAop(切面方法),用以增强目标方法
整体思路:
1.拐弯抹角第得到redis数据中的key
2.根据key去看redis中是否已经有这条数据了
3.要是有了,就直接从redis中查。
4.要是还没有,就去数据库中查,并把这条数据保存报redis中一份。
@Component //将切面中的对象交给Spring去管理
@Aspect //标识 我是一个切面(里面有很多通知方法)
public class CacheAOP {
//1.注入缓存redis对象
@Autowired
private Jedis jedis;
/**
* 拦截ItemCatServiceImpl中由@CacheFind这个注解 标识的方法
* 通知选择:缓存的实现应该选用 @Around
* 步骤:
* 1.动态生成key 用户填写的写死的key(比如:ITEM_CAT_PARENTID)+用户提交的参数parentId (比如99)
*/
@Around("@annotation(cacheFind)") //这个ProceedingJoinPoint joinPoint参数一定要写在参数中的第1位的位置!!
//因为joinPoint的值是Spring为其赋值的,Spring是通过arg[0]的方式找到joinPoint的。
public Object around(ProceedingJoinPoint joinPoint,CacheFind cacheFind){
//1.如何获取用户在ItemCatServiceImpl中@CacheFind这个注解中填写的内容
//即如何获取这个注解对象
String key = cacheFind.key();
//System.out.println("===key==="+key); //ITEM_CAT_PARENTID
//2.如何获取目标方法(eg:findItemCatList(Long parentId)方法)的参数?
Object[] array = joinPoint.getArgs();//getArgs()会得把目标方法的所有参数都得到,官方规定得用一个Object类型的数组来接收
//得到最终key,为第3步做准备
key = key + "::" + Arrays.toString(array); //Arrays.toString(array) 的结果就是[0]这种,[55]这种,[68]这种。0,55,68都是传过来的parentId
//System.out.println("==key=="+key); //ITEM_CAT_PARENTID::[0]
//假如目标方法有多个参数,eg:findItemCatList(Long parentId,String name)
//那这个参数数组就成了[0,家用电器],[55,五金家装],[66,男表]等等
//这时redis中的key就成了:ITEM_CAT_PARENTID::[0,家用电器],ITEM_CAT_PARENTID::[55,五金家装],ITEM_CAT_PARENTID::[66,男表]这个样子
//3.从redis中获取数据
Object result = null;
//判断key是否存在(即redis中是否已经有这条数据的记录了?)
if(jedis.exists(key)){
//如果redis中有这条数据的key了,那就直接把这条数据的value(json类型的)转化会对象类型返回给客户端,就可以了
String json = jedis.get(key);
//如何获取返回值的类型(通过joinPoint获取方法签名)
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
//targetClass: findItemCatList()方法的返回值的类型
Class targetClass = methodSignature.getReturnType(); //interface java.util.List 说明findItemCatList()方法的返回值类型是List集合
result = ObjectMapperUtil.toObject(json,targetClass);
//System.out.println("这次是AOP从redis缓存中查的数据~~");
}else {
//如果key不存在,就去查询数据库
try {
result = joinPoint.proceed(); //执行目标方法,从数据库中查询数据,获取返回值结果
//把这个查询到的结果result放进redis缓存数据库中
//由于查询到的结果的类型不是json类型,需要用我自己写的工具API把它转化成json类型
String json = ObjectMapperUtil.toJSON(result);
//如果用户规定了,这条数据信息有存活时间限制,那么这条数据就要用setex()保存到数据库中。
if(cacheFind.seconds()>0){
jedis.setex(key,cacheFind.seconds(),json);
}else{
//如果用户说这条信息没有存活时间限制,那么在存这条数据时就存key和value就好了。
jedis.set(key,json);
}
System.out.println("AOP执行去数据库查询的操作了!!!");
} catch (Throwable throwable) {
throwable.printStackTrace();//这个是将异常打印出来
throw new RuntimeException(throwable);//这个是将通知用户,系统出现了异常
}
}
//System.out.println("环绕通知生效~~");
return result;
}
}
1.3.4 关于around方法参数的说明
**注意1:**这个ProceedingJoinPoint joinPoint参数一定要写在参数中的第1位的位置!!
因为joinPoint的值是Spring为其赋值的,Spring是通过arg[0]的方式找到joinPoint的。
否则报错信息如下:
**注意2:**其他四大通知类型是否可以添加ProceedingJoinPoint对象?
答案: ProceedingJoinPoint 只能添加到环绕通知中.
否则报错如下:
1.3.5 JoinPoint对象能调用的那些方法
通过连接点joinPoint去调用各种方法,就能得到不同的信息。
@Before("@annotation(com.jt.annotation.CacheFind)")
public void before(JoinPoint joinPoint){
//1.获取目标对象本身
Object target = joinPoint.getTarget();
System.out.println("目标对象target:"+target); //目标对象target:com.jt.service.ItemCatServiceImpl@4299cff8
//2.获取目标方法的参数
Object[] args = joinPoint.getArgs();
System.out.println("目标方法的参数args:"+args); //目标方法的参数args:[Ljava.lang.Object;@58e1c3ef
System.out.println("目标方法的参数args(toString后):"+Arrays.toString(args)); //目标方法的参数args(toString后):[0]
//====以下的方法 得先获得目标方法的签名 getSignature()
//3.获取目标对象的名称
String targetName = joinPoint.getSignature().getDeclaringTypeName();
System.out.println("目标对象的名称targetName:"+targetName); //目标对象的名称targetName:com.jt.service.ItemCatServiceImpl
//4.获取目标对象的类型
Class targetClass = joinPoint.getSignature().getDeclaringType();
System.out.println("目标对象的类型targetClass:"+targetClass); //目标对象的类型targetClass:class com.jt.service.ItemCatServiceImpl
//4.5 获取目标对象的返回值的类型(getReturnType())------methodSignature乱入~~~~
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Class targetClass1 = methodSignature.getReturnType(); //interface java.util.List
System.out.println("目标方法对象的返回值类型:"+targetClass1);
//5.获取目标方法的名称
String methodName = joinPoint.getSignature().getName();
System.out.println("目标方法的名称methodName:"+methodName); //目标方法的名称methodName:findItemCatList
}
1.4 商品查询列表–叶子类目数据–实现redis缓存功能
说明:在jt-manage中的ItemCatController中 的findItemCatName() 方法上添加那个我自定义的注解@CacheFind,注解中的key定义为“ITEM_CAT_NAME”。
就能实现redis缓存功能。
这个用不用写在业务层,是因为,controller中这个方法的返回值,与业务层中对应方法的返回值相同。
以后客户端再通过这个方法查数据时,直接在controller中设定的redis缓存中查就可以了,不用再去业务层中跑一遍了,更高效。
/**
* 分析业务:通过itemCatId获取商品分类的名称
* 1.url地址:url:"/item/cat/queryItemName"
* 2.参数:data:{itemCatId:val}
* 3.返回值:商品分类的名称 String
*
*/
@RequestMapping("/queryItemName")
@CacheFind(key = "ITEM_CAT_NAME")
public String findItemCatName(Long itemCatId){
return itemCatService.findItemCatNameById(itemCatId);
}
2 Redis的一些特性的说明(持久化,内存优化 的策略)
3 Redis的分片机制 (为Redis的集群做铺垫)
3.1 问题引出
如果需要在redis中进行海量的数据存储,如果只有一台redis显然不能实现该功能.
如果单纯地通过扩大内存的方式也不能达到要求.
因为时间都浪费在数据地址的查询中.
如何有效的存储海量的数据呢???
3.2 Redis分片说明
答:
通过配置多个redis缓存数据库,让它们同时工作。缓存数据按照某种算法存储到不同的redis数据库中。
到时候再通过某种算法从redis数据库中取数据就行了。
这就是Redis的分片机制。
对于用户而言:用户不在乎他的缓存数据存到哪个Redis分片中了,只在乎他的数据是否存到了Redis缓存中。
但对于开发者而言:我需要清楚的知道数据是存到了那个Redis分片中,以后从哪个分片中取数据。
Redis分片主要的目的: 实现缓存区的扩容。
3.0 实操:关闭现在正在运行的redis
在任意目录下,执行:redis-cli shutdown
校验看看,6379这个redis是否被关掉了。
注意:只有关6379的redis时不用写端口号,关别的redis时需要在shutdown前面写上端口号。
3.1 实操:在redis根目录下新建个文件夹shards
3.2准备3份配置文件(老师要求配置3个redis分片)
将redis目录下的redis.conf文件,复制3份,放进shards文件夹中。
分别命名为6379.conf,6380.conf,6381.conf
因为启动redis时一定会用到这个.conf文件,所以想启动几个redis,就要准备几个.conf文件。
3.3 修改各个conf文件中的端口号
注意:根据关键字查询的命令 :/关键字
3.4 启动redis
需要在这几个配置文件所在的包中,执行这几个命令!!!
注意:启动redis的命令是上面那样。
进入redis的命令是 redis-cli -p 6380
3.5 redis分片入门案例
@Test
public void testShards(){
List<JedisShardInfo> list = new ArrayList<>();
list.add(new JedisShardInfo("192.168.126.129",6379));
list.add(new JedisShardInfo("192.168.126.129",6380));
list.add(new JedisShardInfo("192.168.126.129",6381));
//redis分片需要用到的对象ShardedJedis
ShardedJedis shardedJedis = new ShardedJedis(list);
shardedJedis.set("2005","redis分片的学习");
System.out.println(shardedJedis.get("2005")); //经过手动笨方法查找,发现这条数据在6381这台redis中
}
4 一致性hash算法 (看数据到底存在哪台redis中了)
4.1 算法介绍
一致性哈希算法在1997年由麻省理工学院提出,是一种特殊的哈希算法,目的是解决分布式缓存的问题。
在移除或者添加一个服务器时,能够尽可能小地改变已存在的服务请求与处理请求服务器之间的映射关系。
一致性哈希解决了简单哈希算法在分布式哈希表( Distributed Hash Table,DHT) 中存在的动态伸缩等问题 。
4.2 一致性hash的原理
常识:
1.哈希:
原理:把输入的任意长度的内容,通过Hash算法变成固定的长度的内容。
2.一般的hash由8位16进制数组成的。
00000000到FFFFFFFF
每一位上的数字有16种可能(0----F)
一个hash共有8位数
所以一个hash的结果可能有2^32种可能,即4294967296种可能。
3.hash算法对相同的数据进行hash运算时 结果必然相同.
4.3 一致性hash的特性
4.3.1 平衡性
①平衡性是指hash的结果应该平均分配到各个节点,这样从算法上解决了负载均衡问题
说明:引入虚拟节点 实现数据的平衡 但是平衡是相对的.不是绝对的.
4.3.2 单调性
②单调性是指在新增或者删减节点时,不影响系统正常运行
虽然引入了新节点,但node1,node2,node3中原有的redis数据不受影响,用户依然可以正确的查询到。
4.3.3 分散性
③分散性是指数据应该分散地存放在分布式集群中的各个节点(节点自己可以有备份),不要只设置一个节点,那样的话会把所有的数据放到一个节点中,这个节点(redis数据库)的压力会很大。