文章目录
之前的课程一直是在搭项目,也是为接下来的技术讲解做准备。今天就是开始第一个缓存技术-Redis的教程
1.Redis
在电商网站中,用户的查询的需求量远远高于更新的数据量,所以为了提高服务器响应的能力.需要添加缓存服务器.
缓存服务器特点
- 缓存数据结构采用k-v格式 key是唯一标识. value一般存储对象JSON数据.
- 缓存数据应该存储到内存中,速度是最快的.
- 定期将缓存数据持久化到硬盘中.
- 为了维护内存大小,定期将缓存数据删除. lru算法 lfu算法
- 编程语言 首选C语言.
1.1 简单介绍
Redis是一个开源(BSD许可),内存存储的数据结构服务器,可用作数据库,高速缓存和消息队列代理。它支持字符串、哈希表、列表、集合、有序集合,位图,hyperloglogs等数据类型。内置复制、Lua脚本、LRU收回、事务以及不同级别磁盘持久化功能,同时通过Redis Sentinel提供高可用,通过Redis Cluster提供自动分区。
速度:
- 读:11.2万/秒
- 写:8.6万/秒
平均10万/秒
1.2 下载和安装
官网下载:官网地址
打开虚拟机,xshell连接上.cd到一个目录(随意)
cd /usr/local/src
将下载的redis压缩包上传。推荐一款ftp软件filezilla。可以百度直接下载即可。
上传后查看
解压文件
tar -xvf redis-5.0.4.tar.gz
删除压缩包文件/修改文件名称
删除压缩包命令 rm
rm redis-5.0.4.tar.gz
输入命令后,要在后面再次输入y,表示确定删除
使用mv命令,将解压后的文件移动到新的文件中,这操作就相当于重命名。
mv redis-5.0.4 redis
1.3 编译和安装
进入到redis目录下 cd redis
进行编译 make
稍等片刻,编译成功
安装redis 输入命令 make install
1.4 修改Redis配置文件
执行编辑指令 vim redis.conf
切记进入编辑时需要注意,在刚进入时还未处于编辑状态,先点击a
,当左下角出现insert时才是真正的编辑状态。重要的是千万千万千万不要用数字键盘输入数字。请自学Linux文件编辑命令
- 去除IP绑定
2.关闭保护模式 将yes改为no
- 开启后台启动
编辑好后,先点击ESC。退出编辑状态。左下角的insert标识消失,才能进行下一步
输入命令:wq
。回车即可
切记每一步操作很重要
1.5 Redis服务器命令
配置好后,我们启动一下试试
redis-server redis.conf
输入命令
ps -ef |grep redis
可以看到启动成功
1.5.1 redis客户端
redis-cli -p 6379
退出就输入exit
1.5.2 关闭redis
方式1:
ps -ef |grep redis 查找redis的PID
kill -9 PID号
方式2:使用命令关闭
redis-cli -p 6379 shutdown
1.5.3 redis 命令
Redis支持五种数据类型:string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合)。
相关基本命令请看 有道云笔记
我们先写个测试试试,看看具体的效果:
1.引入jar包
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
</dependency>
创建TestRedis.java
1.5.3.1 string类型测试
string 是 redis 最基本的类型,一个 key 对应一个 value。
string 类型是二进制安全的。 redis 的 string 可以包含任何数据。比如jpg图片或者序列化的对象。
string 类型是 Redis 最基本的数据类型,string 类型的值最大能存储 512MB。
public class TestRedis {
/**
* 测试String类型操作
* 服务器前提:1.防火墙关闭 2.IP绑定注释 3.保护模式关闭
*/
@Test
public void test01() {
//new Jedis() 第一个参数是服务器ip,第二个是端口号
Jedis jedis = new Jedis("192.168.180.160", 6379);
jedis.set("name","olderSheep");
System.out.println(jedis.get("name"));
}
}
查看运行结果:
为了验证服务器中是否真正的存在,我们去服务器验证一下:
连接服务器:直接进入redis客户端
我们使用get命令去找key为name的值,可以发现已存在。验证成功。
(可跳过)推荐一款连接redis的客户端软件RedisDesktopManager。百度可直接找到,直接下载即可
打开软件,连接
连接名称随便写,Host是服务器ip, port为端口号。直接确认即可
双击,并依次打开,可以看到刚才存储的key:name。右边显示了它的value。这里也可以进行删除等操作。这个客户端特点就是更直观。
1.5.3.2 hash 类型测试
Redis hash 是一个键值(key=>value)对集合。
Redis hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象。
@Test
public void testHash() {
Jedis jedis = new Jedis("192.168.180.160", 6379);
jedis.hset("person", "id", "100");
jedis.hset("person", "name", "人");
jedis.hset("person", "age", "18");
System.out.println(jedis.hgetAll("person"));
}
运行结果:
使用客户端时,先进行右击鼠标先刷新就能看到新添加的数据。
1.5.3.3 List类型测试
list是一个字符串链表,既然是链表,那就可以左,右都可以进行插入数据。(链表也是数据结构的相关知识,不是很明白的朋友,也可以与我一起讨论数据结构与算法。)
若key不存在,创建链表
若key存在,链表添加内容
若链表值全部移除,key也自动消失
效率比较
- 链表的头尾元素操作,效率都非常高
- 链表中间元素操作,效率比较低
操作命令
Lpush:先进后出,在列表头部插入元素
Rpush:先进先出,在列表的尾部插入元素
Lrange:出栈,根据索引,获取列表元素
Lpop:左边出栈,获取列表的第一个元素
Rpop:右边出栈,获取列表的最后一个元素
Lindex:根据索引,取出元素
Llen:链表长度,元素个数
Lrem:根据key,删除n个value
Ltrim:根据索引,删除指定元素
Rpoplpush:出栈,入栈
Lset:根据index,设置value
Linsert before:根据value,在之前插入值
Linsert after:根据value,在之后插入值
注意
出栈,该元素在链表中,就不存在了
左边,默认为列表的头部,索引小的一方
右边,默认为列表的尾部,索引大的一方
我们测试一下
@Test
public void testList() {
Jedis jedis = new Jedis("192.168.15.129", 6379);
jedis.lpush("list", "1","2","3","4");
System.out.println(jedis.rpop("list"));
}
使用lpush插入数据,即为从左边依次插入数据。那顺序即为4,3,2,1。
使用rpop就是获取最右边的数据并删除。也可称右边出栈。
因此结果获取的是1.剩下的数据是4 3 2.
其他命令,朋友可自行测试。
1.6 redis 业务实现
数据添加缓存的条件
- 变化范围小的数据
a.层级菜单. b.部门的组织关系 c.省市县乡 d.邮编等 - 用户频繁访问的数据.
为了将其数据统一成json格式,我们先写一个jsonapi工具。也可以使用阿里提供的api。
我们将其放在jt-common中,创建utils包,将此api放在其中即可。朋友也可随意。
/**
* json转化工具
*/
@Configuration
public class JsonUtil {
private static final ObjectMapper MAPPER = new ObjectMapper();
/**
* 根据API将对象转化为JSON.同时将JSON转化为对象.
*
* @param obj 转化的对象
* @return 转化后的json串
*/
public static String toJSON(Object obj) {
String result = null;
try {
result = MAPPER.writeValueAsString(obj);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException();
}
return result;
}
/**
* json转化为对象
*
* @param json json串
* @param targetClass 要转化的实体class
* @param <T> 转化后的实体对象
* @return
*/
public static <T> T toObject(String json, Class<T> targetClass) {
T obj = null;
try {
obj = MAPPER.readValue(json, targetClass);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException();
}
return obj;
}
}
编辑pro配置文件。ip地址需要添加自己服务器的ip。
#配制单台redis
redis.host=192.168.180.160
redis.port=6379
创建配置类。在jt-common中创建config包,在里创建工具类
@Configuration
@PropertySource("classpath:/properties/redis.properties")
public class RedisConfig {
@Value("${redis.host}")
private String host;
@Value("${redis.port}")
private Integer port;
@Bean
public Jedis jedis() {
return new Jedis(host, port);
}
}
修改ItemCatController
@RequestMapping("/list")
public List<EasyUITree> findItemCatList(@RequestParam(defaultValue="0",name ="id") Long parentId){
//查询一级商品分类信息
// return itemCatService.findEasyUITreeList(parentId);
return itemCatService.findEasyUITreeCache(parentId);
}
ItemCatServiceImpl
引入jedis。我们在配置中使用@Bean注解。注入了一个实例对象。所以在这直接引入,就可以使用
@Autowired
private Jedis jedis;
@Override
public List<EasyUITree> findEasyUITreeCache(Long parentId) {
List<EasyUITree> treeList = new ArrayList<EasyUITree>();
String key = "ITEM_CAT_"+parentId;
//1.根据key查询redis服务器
String result = jedis.get(key);
if(StringUtils.isEmpty(result)) {
//表示缓存没有数据,需要查询数据库
treeList = findEasyUITreeList(parentId);
//将数据保存到缓存中
String value = JsonUtil.toJSON(treeList);
jedis.set(key, value);
System.out.println("查询后台数据库!!!!!");
}else {
//缓存中有数据
treeList = JsonUtil.toObject(result, treeList.getClass());
System.out.println("查询Redis缓存");
}
return treeList;
}
开启项目,我们查看一下
点击新增商品,选择类目。第一次首先会先查数据库。
看后台日志打印
当我们再次访问时:
再看时间对比:
第一次:
第二次:
可以看出时间缩短了。
1.7 AOP缓存实现
1.7.1 AOP(面向切面编程)
利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
主要实现的功能
日志记录,性能统计,安全控制,事务处理,异常处理等等。
主要意图
将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。
底层原理
使用的动态代理模式实现
关于面向切面编程,朋友可自行进行查阅资料,目前本人还没书写相关博客内容,后期会专门加上
1.7.2 业务实现
- 需要自定义注解, Cache_Find
- redis中存储需要 key:value
Key:如果用户自己指定了key,使用用户的.
Key:如果用户自己没有指定,需要自动生成. 类名+方法名+第一个参数
Value:是用户查询的java对象结果的json串
自定义注解
//在运行期生效
@Retention(RetentionPolicy.RUNTIME)
//修饰方法 该注解对谁有效
@Target(ElementType.METHOD)
public @interface CacheFind {
/**
* //用户可以不写,如果为空串表示自动生成key
* @return
*/
String key() default "";
/**
* 0表示用户设置该数据不需要超时时间,如果不等于0则说明用户自己定义了超时时间
* @return
*/
int seconds() default 0;
}
编辑Controller添加注解。那我们就不需要在serviceImpl中自己去写相关查询缓存的方法。
@RequestMapping("/list")
@CacheFind
public List<EasyUITree> findItemCatList(@RequestParam(defaultValue="0",name ="id") Long parentId){
//恢复到一开始调用的方法
return itemCatService.findEasyUITreeList(parentId);
//使用aop,则把这儿注释掉
// return itemCatService.findEasyUITreeCache(parentId);
}
既然不需要在serviceImp中写,那就需要专门处理缓存的api。
创建CacheAspect。并交给spring管理
@Component //将对象交给spring容器管理
@Aspect //表示标识切面 切面=切入点+通知
public class CacheAspect {
@Autowired
private Jedis jedis;
/**
* 环绕通知:
* 1.返回值 必须为object类型
* 表示执行完成业务之后返回用户数据对象
* 2.参数 1.必须位于第1位
* 2.参数类型必须为 ProceedingJoinPoint 因为要控制目标方法执行
*
* 3.关于注解取值规则:
* springAOP中提供了可以直接获取注解的方法,但是要求参数的名称
* 必须一致.否则映射错误
*
* 缓存操作
* 1.根据key 查询缓存服务器redis
*/
@Around("@annotation(cacheFind)")
public Object around(ProceedingJoinPoint joinPoint, CacheFind cacheFind) {
String key = getKey(joinPoint,cacheFind);
String resultJSON = jedis.get(key);
Object resultData = null;
if(StringUtils.isEmpty(resultJSON)) {
//需要执行真实的目标方法
try {
resultData = joinPoint.proceed();
String value = JsonUtil.toJSON(resultData);
//判断数据是否永久保存
if(cacheFind.seconds()>0)
jedis.setex(key, cacheFind.seconds(), value);
else
jedis.set(key, value);
System.out.println("AOP查询数据库成功!!!");
} catch (Throwable e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}else {
//由于业务需要,要获取目标方法的返回值类型
Class returnType = getType(joinPoint);
//表示redis中有数据 将json转化为对象
resultData = JsonUtil.toObject(resultJSON,returnType);
System.out.println("AOP查询缓存成功!!!!");
}
return resultData;
}
private Class getType(ProceedingJoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
return signature.getReturnType();
}
/**
* 策略:
* 1.如果用户有key,使用用户自己的key
* 2.如果用户自己没有定义,则自动生成
* 类名+方法名+第一个参数
* @param joinPoint
* @param cacheFind
* @return
*/
private String getKey(ProceedingJoinPoint joinPoint, CacheFind cacheFind) {
String key = cacheFind.key(); //默认值""
if(StringUtils.isEmpty(key)) {
//用户自动生成
String methodName =
joinPoint.getSignature().getName();
String className =
joinPoint.getSignature().getDeclaringTypeName();
String arg1 = String.valueOf(joinPoint.getArgs()[0]);
//com.ly.controller.list::0
return className+"."+methodName+"::"+arg1;
}else {
return key;
}
}
}
AOP中有五大通知
- 前置通知:在目标方法执行之前执行的通知
- 环绕通知:在目标方法执行之前和之后都可以执行额外代码的通知。
在环绕通知中必须显式的调用目标方法,否则目标方法不会执行。
这个显式调用时通过ProceedingJoinPoint来实现,可以在环绕通知中接收一个此类型的形 参,spring容器会自动将该对象传入,这个参数必须处在环绕通知的第一个形参位置
意思就是说使用ProceedingJoinPoint作为第一个参数,并实现目标方法。其他的参数需要放在第二个或以后
要注意,只有环绕通知可以接收ProceedingJoinPoint,而其他通知只能接收JoinPoint。 - 后置通知:在目标方法执行之后的通知。
4.异常通知:在目标方法抛出异常时执行的通知
5.最终通知:是在目标方法执行之后执行的通知。和后置通知不同的是,后置通知是在方法正常返回后执行的通知,如果方法没有正常返回,比如说抛出异常,则后置通知不会执行。而最终通知无论如何都会在目标方法调用过后执行,即使目标方法没有正常的执行完成。另外,后置通知可以通过配置得到返回值,而最终通知无法得到。
//参数可以有,也可以没有。但其他是固定格式
@Before("...")
public void before(JoinPoint jt){
}
//环绕通知,调用目标方法,是必须使用ProceedingJoinPoint 显式调用,且在第一个参数位置
@Around("...")
public Object around(ProceedingJoinPoint joinPoint){
}
//后置通知
@AfterReturning("...")
public void afterReturn(JoinPoint jt){
}
/*异常通知 可以配置传入JoinPoint获取目标对象和目标方法相关信息,
*但必须处在参数列表第一位,另外,还可以配置参数,让异常通知可以接收到目标方法抛出来的异常对象
*/
@AfterThrowing("...")
public void afterThrowing(JoinPoint joinPoint) {
}
//最终通知
@After("...")
public void after(JoinPoint joinPoint) {
}
说明一下。上述的注解对应不同的通知类型,注解中还有参数。因为内容太多,这里不过多介绍。
切记注解,返回值类型是固定的格式。参数按照要求。其他的随意。
github地址
https://github.com/lmy1965673628/jingtao.git.
总结
这一节主要介绍了redis的安装以及项目的具体应用。aop的原理介绍,aop是spring的核心组成部分,所以关于aop的知识体系也是很多。我会专门去写一篇博客,朋友也可自查资料去学习相关知识。aop是很重要的,很重要的,很重要的。
下一节我们继续介绍redis缓存的持久化,内存优化,集群搭建等知识