内存的key-value结构数据库非关系型数据库。NoSql数据库并不是要取代关系型数据库,而是关系型数据库的补充缓存、任务队列、消息队列、分布式锁 tar -zxvf redis-4.0.0.tar.gz -C /usr/localyum install gcc-c++makemake install./redis-server./redis-cli的方式来启用服务 ./redis-server ../redis.confauth password即可登录./redis-cli -h localhost -p 6379 -a password一步到位Redis存储的是key-value结构的数据,其中key是字符串类型,value有5种常用的数据类型
字符串string:普通字符串,常用
哈希hash:适合存储对象
列表list: 按照插入顺序排序,可以有重复元素
集合set:无序集合,没有重复元素
有序集合sorted set:有序集合,没有重复元素,每个元素都会关联一个double类型的分数(socre),redis是通过分数来为集合中的成员进行从小到大的排序,有序集合的成员是唯一的,但分数却可以重复
public void test(){
//1.获取连接
Jedis jedis = new Jedis("localhost",6379);
//2.执行具体的操作
jedis.set("username","xiaoming");
String username = jedis.get("username");//正常键值对的操作
System.out.println(username);
jedis.hset("myhash","addr","bj");//哈希字段的操作
jedis.keys("*");//获取所有
//3.关闭连接
jedis.close();
}
<!-- https://mvnrepository.com/artifact/org.springframework.data/spring-data-redis -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
</dependency>
SpringDataRedis中提供了一个高度封装的类:RedisTemplate,针对jedis客户端中的大量api进行了归类和封装,将同一类型操作封装为operation接口
配置redis
spring:
application:
name:sprintdataredis_demo
redis:
host: localhost
port: 6379
password: 123456
jedis:
pool:
max-active: 8 #最大连接数
max-wait: 1ms #连接池中最大阻塞等待时间
max-idle: 4 # 连接池中的最大空闲连接
min-idle: 0 #连接池中的最小空闲连接
database: 0 # 0-15 可以根据conf里面的数量来进行配置,现在默认是16个,因此下标是0-15
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate<Object,Object> redisTemplate(RedisConnectionFactory connectionFactory){
RedisTemplate<Object,Object> redisTemplate = new RedisTemplate<>();
//默认的key序列化器为:JdkSerializationRedisSerializer
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(connectionFactory);
return redisTemplate;
}
}
redisTemplate.opsForValue().set("city","beijing",10, TimeUnit.SECONDS);
//设置过期的数据,10秒的话该数据过期
//setNx,只有当键值不存在的时候才会去设置这个值
Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent("city123", "nj");
/**
* 操作Set类型的数据
*/
@Test
public void testSet(){
SetOperations setOperations = redisTemplate.opsForSet();
//存值,可以一次存多个
setOperations.add("myset","a","b","c");
//取值
Set myset = setOperations.members("myset");
for (Object o : myset) {
System.out.println((String) o);
}
//删除成员,可以指定删除某些元素
setOperations.remove("myset","a","b");
}
/**
* 操作List类型的数据
*/
public void testList(){
//List放置的话是从右到左的,就是先放的肯定是是在左边
ListOperations listOperations = redisTemplate.opsForList();
//从左边存储值(单个)
listOperations.leftPush("item1","a");
//从右边存储值(单个)
listOperations.rightPush("item2","c");
//从左边存储值,多个
listOperations.leftPushAll("item3","a","b","c");
//从右边存储值,多个
listOperations.leftPushAll("item4","a","b","c");
//取值
listOperations.range("item3",0,-1);//-1表示取出所有的值
//获取列表长度,并模拟从右边删除元素
Long item3Size = listOperations.size("item3");
int item3SizeInt = item3Size.intValue();
for (int i = 0; i < item3SizeInt; i++) {
Object item3 = listOperations.rightPop("item3");//出队,右边的出去一个元素
System.out.println((String) item3);
}
}
/**
* 操作Zset(有序集合数据)
*/
@Test
private void testZset(){
ZSetOperations zSetOperations = redisTemplate.opsForZSet();
//存值,成员按照从小到大排序
zSetOperations.add("myZset","a",10.0);
zSetOperations.add("myZset","b",9.0);
zSetOperations.add("myZset","c",8.0);
//取值
Set<String> myZset = zSetOperations.range("myZset", 0, -1);
for (String s : myZset) {
System.out.println(s);
}
//修改分数,为指定的某个元素,追加得分
zSetOperations.incrementScore("myZset","b",20.0);
//删除成员,可以传入多个参数表示批量删除
zSetOperations.remove("myZset","a","b");
}
/**
* 通用操作
*/
@Test
public void testCommon(){
//获取redis中所有的key
Set<String> keys = redisTemplate.keys("*");
for (String key : keys) {
System.out.println(key);
}
//判断某个key是否存在
Boolean myKey = redisTemplate.hasKey("myKey");
System.out.println(myKey);
//删除指定的key
redisTemplate.delete("item3");
//获取指定key对应的value的数据类型
redisTemplate.type("item1");
}
用户点餐数据全部放在数据库中,其增删查改的功能全部都是基于MySql所实现的v1.0,将代码提交到新的分支上git add .
git commit -m "v1.0:缓存优化"
git push --set-upstream origin v1.0
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
redis:
host:
port:
password:
database:
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
//该对象是自动创建的,当不存在的时候才会创建@OnMissingBean
@Bean
public RedisTemplate<Object,Object> redisTemplate(RedisConnectionFactory connectionFactory){
RedisTemplate<Object,Object> redisTemplate = new RedisTemplate<>();
//默认的key序列化器为:JdkSerializationRedisSerializer
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(connectionFactory);
return redisTemplate;
}
}
之前所实现的业务功能是这样的:随机生成的验证码保存在HttpSession中,现在需要改造为将验证码缓存在Redis中,实现思路如下:
在服务端UserController注入RedisTemplate对象,用于操作redis
在服务端UserController的sendMsg方法,将随机生成的验证码缓存到Redis中,设置有效期为5分钟
在服务端UserController的login方法中,从Redis中获取缓存的验证码,如果登录成功则删除Redis中的验证码
//将验证码保存到redis中,并且设置有效期为5分钟
redisTemplate.opsForValue().set(phone,code, 5,TimeUnit.MINUTES);
String codeInRedis = (String) redisTemplate.opsForValue().get(phone);
redisTemplate.delete(phone);
脏读,我们在学习操作系统的时候,当我们将磁盘中的数据页调入内存,假设有一个进程A对数据进行修改了,这时候就产生了一种情况,内存中的数据和磁盘中的数据不一样了,这时候假如有另外一个进程B去读磁盘中对应这一页的数据,读出来使用,用完之后A先写回去,B再写回去,这时候就产生了数据不一致的情况,产生了错误,而且很明显可以感知到,如果写回的顺序不一样,那么产生的结果也不一样。 List<DishDto> dishDtos = null;
//1.先从redis中缓存数据
//动态构造key
String key = "dish_"+dish.getCategoryId()+"_"+dish.getStatus();
//如果存在,直接返回,无需返回数据库
dishDtos = (List<DishDto>) redisTemplate.opsForValue().get(key);
//如果不存在,需要查询数据库,将查询带的菜品数据缓存到redis
if(dishDtos!=null){
return dishDtos;
}
//首次查询数据库,将查询到的缓存到redis
redisTemplate.opsForValue().set(key,dishDtoes,60, TimeUnit.MINUTES);//缓存60分钟
//清理所有菜品的缓存数据
//Set keys = redisTemplate.keys("dish_*");
//redisTemplate.delete(keys);
//精确清理缓存数据
//只是清理某个分类下面的菜品缓存数据
String key = "dish_"+dishDto.getCategoryId()+"_1";
redisTemplate.delete(key);
Spring Cache是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能够实现缓存功能
Spring Cache提供了一层抽象,底层可以切换不同的cache实现,具体就是通过CacheManager接口来统一不同的缓存技术
CacheManager是Spring提供的各种缓存技术抽象接口
| 注解 | 说明 |
|---|---|
| @EnableCaching | 开启缓存注解功能 |
| @Cacheable | 在方法执行前Spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据,若没有数据,则调用方法并将方法返回值放到缓存中 |
| @CachePut | 将方法的返回值放到缓存中 |
| @CacheEvict | 将一条或多条数据从缓存中删除 |
//CachePut将方法返回值放入缓存
//value:缓存的名称,每个缓存名称下面可以有多个key
//key:缓存的key,用SpEL表达式来动态计算key
@CachePut(value="userCache",key="#result.id")
//注:result是该方法的返回值
//CacheEvict:清除缓存中key所对应的数据
//#root.args[0]获取参数表中的第一个数据
//#result.id:从返回结果中获得id属性值
@CacheEvict(value="userCache",key="")
//Cacheable在方法执行前spring会先查看缓存中是否有数据,如果有数据,则直接返回缓存数据,如果没有数据,则会调用方法将方法返回值放到缓存中
//该注解还可以解决缓存穿透(redis和数据库中),体现在当key所对应的val在数据库中并不存在的时候,这时候会将一个空占位数据缓存到缓存中(缓存空对象,可以预防高并发的时候访问不存在的数据,服务端缓存空数据,客户端访问时直接返回空数据)
//condition可以用来指定什么情况下缓存数据,这里给的条件是当查询得到的result不为空的时候才把数据缓存到服务端(但Cacheable没有这个上下文对象)
//unless是除非的意思,也就是除非#result==null的时候就不缓存
//注意:key应该是有唯一格式,用来规定查询何种缓存
@Cacheable(value="userCache",key="",unless = "#result==null")
<dependency>
<groupId>org.springframework.datagroupId>
<artifactId>spring-data-redisartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-cacheartifactId>
<version>2.7.2version>
dependency>
cache:
redis:
time-to-live: 1800000 #设置缓存30分钟过期
@EnableCaching注解,开启缓存功能Cacheable注解CacheEvict注解 @GetMapping("/list")
@Cacheable(value ="setmealCache",key = "#categoryId+'_'+#status")
public Result<List<SetmealDto>> list(Long categoryId,int status){
List list = setmealService.getByCategoryId(categoryId,status);
return Result.success(list);
}
在做完这一步之后,会发现报错:DefaultSerializer requires a Serializable payload but received an object of type
这是因为要缓存的JAVA对象必须实现Serializable接口,因为Spring会先将对象序列化在存入Redis,将缓存实体类继承Serializable
public class Result <T> implements Serializable
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WhFXHp7j-1660808288088)(./09-image/dxfl-01.png)]
写主库,读从库,分别将压力压在不同的数据库上异步的复制过程,底层是基于Mysql数据库自带的二进制日志功能,就是一台或者多态Mysql数据库(slave,即从库)从另一台MySql数据库(master,即主库)进行日志的复制然后再解析日志并应用到自身,最终实现从库数据和主库的数据保持一致。MySql主从复制是MySql数据库自带功能,而无需借助第三方工具[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-flfFooRz-1660808288089)(./09-image/zcfz.png)]
这里笔者准备了两台虚拟机作为主库和从库,分别安装好linux和mysql并且配置好静态ip,要注意把访问端口(3306)给开放出来
这里我使用的mysql连接工具是DataGrip,注意一点,在连接的时候记得在url上将useSSL这个选项给设置为false,否则会连接失败,如果是使用navicate的话不会有这个问题,这里给两个连接
https://blog.csdn.net/paincupid/article/details/122745473
https://blog.csdn.net/JesusMak/article/details/106911908?spm=1001.2101.3001.6650.3&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-3-106911908-blog-122745473.t0_searchtargeting_v1&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-3-106911908-blog-122745473.t0_searchtargeting_v1
如果使用DataGrip连接有问题的话,请参考以上两篇文章
配置主库:
[第一步] 修改Myusql数据库的配置文件/etc/my.cnf
log_bin=mysql-bin #[必须]启用二进制日志
server-id=128 #[必须]服务器唯一ID,只需要确保其id是唯一的就好
systemctl restart mysqld
grant replication slave on *.* to 'xiaoming'@'%' identified by 'Root@123456';
xiaoming,密码为Root@123456,并且给xiaoming用户授予replication slave权限,常用语建立复制时所需要用到的用户权限,也就是slave必须被master授权具有该权限的用户,才能通过该用户复制,这是因为主库和从库之间需要互相通信,处于安全考虑,只有通过验证的从库才能从主库中读取二进制数据mysql> show master status;
+------------------+----------+--------------+------------------+-------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+------------------+----------+--------------+------------------+-------------------+
| mysql-bin.000001 | 441 | | | |
+------------------+----------+--------------+------------------+-------------------+
/etc/my.cnfchange master to
master_host='192.168.132.128',master_user='xiaoming',master_password='Root@123456',master_log_file='mysql-bin.000001',master_log_pos=441;
start slave;
如果出现了run Stop错误,则将I/O线程给终止,重新启动即可stop slave;
[第四步]登录Mysql数据库,执行SQL,查看从库的状态
show slave status;
并发读操作和较少写操作类型的应用系统来说,将数据库拆分为主库和从库,主库主要负责处理事务性的增删改操作,从库负责处理查询操作,能够有效的避免由数据更新导致的行锁,使得整个系统的查询性能得到极大改善
<dependency>
<groupId>org.apache.shardingspheregroupId>
<artifactId>sharding-jdbc-spring-boot-starterartifactId>
<version>4.0.0-RC1version>
dependency>
[第一步]导入sql文件,执行sql
[第二步]git新建分支v1.1并且签出,开始项目优化
[第三步]导入Sharding-jdbc包
[第四步]修改yml文件
server:
#tomcat访问端口
port: 8080
spring:
main:
allow-bean-definition-overriding: true
shardingsphere:
datasource:
names:
master,slave
# 主数据源
master:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.132.128:3306/reggie?useSSL=false?characterEncoding=utf-8
username: root
password: 123456
# 从数据源
slave:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.132.101:3306/reggie?useSSL=false?characterEncoding=utf-8
username: root
password: 123456
masterslave:
# 读写分离配置
load-balance-algorithm-type: round_robin
# 最终的数据源名称
name: dataSource
# 主库数据源名称
master-data-source-name: master
# 从库数据源名称列表,多个逗号分隔
slave-data-source-names: slave
props:
sql:
show: true #开启SQL显示,默认false
application:
#非必须项,可以手动指定
name: reggie
redis:
host: 192.168.132.128
port: 6379
password: 123456
database: 0
cache:
redis:
time-to-live: 1800000 #设置缓存30分钟过期
mybatis-plus:
configuration:
#在映射实体或者属性时,将数据库中表名和字段名中的下划线去掉,按照驼峰命名法映射
#这是因为表字段名是用下划线做分割的,而实体类使用驼峰类命名法的
#address_book->AddressBook
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
#雪花算法
id-type: ASSIGN_ID
reggie:
path: H:/download/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yEdn2pj0-1660808288090)(./09-image/qhdfl.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-28kQgdRx-1660808288090)(./09-image/lc.png)]
使用Swagger你只需要安装它的规范去定义接口以及接口相关的信息,再通过Swagger衍生出来的一些列项目和工具就可以做到生成各种格式的接口文档,以及在线接口调试页面等
使用方式
导入knife4j的maven坐标
<dependency>
<groupId>com.github.xiaoymingroupId>
<artifactId>knife4j-spring-boot-starterartifactId>
<version>3.0.2version>
dependency>
导入knife4j相关配置类
@EnableSwagger2
@EnableKnife4j
@Slf4j
@Configuration
@EnableSwagger2
@EnableKnife4j
public class WebMvcConfig extends WebMvcConfigurationSupport {
@Bean
public Docket createRestApi() {
// 文档类型
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.example.controller"))
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("瑞吉外卖")
.version("1.0")
.description("瑞吉外卖接口文档")
.build();
}
}
设置静态资源映射,否则无法访问静态页面
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
log.info("静态资源映射到了");
registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");//设置要映射哪些访问路径
registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}
在LoginCheckFilter中设置不需要处理的请求路径
String[] urls = new String[]{
"/employee/login",//1.如果人家请求的路径就是来登录的,直接放行
"/employee/logout",//2.退出则直接放行
"/backend/**", //关于页面的显示可以交给前端工程师来做,我们要做的是当用户未登录时,屏蔽请求数据的接口
"/front/**",
"/common/**",
"/user/sendMsg",//移动端发送短信
"/user/login",//移动端登录
"/doc.html",
"/webjars/**",
"/swagger/resources",
"/v2/api-docs"
};
.addResourceHandler(“/webjars/**”).addResourceLocations(“classpath:/META-INF/resources/webjars/”);
}
```
在LoginCheckFilter中设置不需要处理的请求路径
String[] urls = new String[]{
"/employee/login",//1.如果人家请求的路径就是来登录的,直接放行
"/employee/logout",//2.退出则直接放行
"/backend/**", //关于页面的显示可以交给前端工程师来做,我们要做的是当用户未登录时,屏蔽请求数据的接口
"/front/**",
"/common/**",
"/user/sendMsg",//移动端发送短信
"/user/login",//移动端登录
"/doc.html",
"/webjars/**",
"/swagger/resources",
"/v2/api-docs"
};