一:Redis的用途:
我觉得从这个项目的背景来分析吧
第一个项目场景:
我项目是伙伴匹配系统,伙伴匹配系统的主页面需要去查询用户
这里就会有个问题了
当我们的用户多了,我们应该如何去操作呢,还是全部查出来嘛,这肯定是不行的。
数据查询慢怎么办?
用缓存:提前把数据取出来保存好,我们都知道MySql的数据是存储在磁盘中的,所以,你每次去查都是直接在磁盘中查,就很慢
我们提前把数据查出来放到读写更快的介质中,这样就起到了提高效率的功能。
第二个项目场景:
第二个项目场景还是这个伙伴匹配系统
这一次的场景是什么呢?
是关于一个分布式登录的cookie共享
大家设想一个场景,一个项目如果做大了,用户比较多,那肯定不可能只部署在同一台服务器上,肯定要多服务器部署,那这个时候问题就来了,我们如果一开始访问的是服务器A,我们在服务器A上登录了,浏览器生成了一个cookie,我们可以在服务器A上登录了,然后假设有一天服务器A很多人突然一起访问,非常的拥挤,根据负载均衡,我们可能会访问服务器B,那这个时候问题就出现了,我们已经在服务器A上登录了,cookie已经种在了服务器A上,那我们从服务器B上登录,那还得再登录一边嘛,那如果一个项目同时部署在50台服务器上,那我们还得登录50遍嘛,这听起来就很离谱。
所以我们的解决办法就是:将用户信息存储在一个公共存储的地方 就是下面要说的Redis
第三个项目场景:
第三个项目场景是苍穹外卖
在苍穹外卖里面用Redis,其实就是获取了一个店铺的状态,这个和第一个项目场景的思路有点像,这里就不赘述了。
二:Redis入门:
1:Redis的概念:
我觉得可以先看这一篇,讲得很有意思,也可以对这个redis有一个初步得了解
我觉得这个redis得功能有点像cache,用来提高访问效率得东西。
2:Redis的安装:
indows版下载地址:https://github.com/microsoftarchive/redis/releases
直接解压
启动redis:
在当前文件夹下打开cmd,
然后输入redis-server.exe redis.windows.conf即可
关闭redis:ctrl+c
连接客户端服务:
记住连接客户端服务之前别关那个启动redis的那个命令行窗口
要不然就会报:
Could not connect to Redis at 127.0.0.1:6379: 由于目标计算机积极拒绝,无法连接。
这个错误。
连接别的redis:
先用exit命令退出客户端服务。
然后用redis-cli.exe -h (你要连接的地址) -p (端口号)
修改redis的密码:
在redis.windows.conf中配置密码:
小技巧可以在大段的文本文件中用ctrl+f快捷键来查找。
设置完密码之后,别人想连接我们的redis服务就需要密码了
redis的可视化工具:
三:Redis常用的数据类型:
Redis存储的是key-value结构的数据,其中key是字符串类型,value有5种常用的数据类型:
- 字符串 string
- 哈希 hash
- 列表 list
- 集合 set
- 有序集合 sorted set / zset
redis中的数据类型,特指的是value的数据类型。
1:字符串操作命令:
Redis 字符串类型常用命令:
- SET key value 设置指定key的值(像mysql中的insert)
- GET key 获取指定key的值(像mysql中的select)
- SETEX key seconds value 设置指定key的值,并将 key 的过期时间设为 seconds 秒(短信验证码)
- SETNX key value 只有在 key 不存在时设置 key 的值(分布式锁)
稍微说一下最后一个命令:setnx ,这个命令如果成功(说明redis里面没有你输入的键值对)
结果会返回1,反之结果返回0
2:哈希表操作命令:
Redis hash 是一个string类型的 field 和 value 的映射表,hash特别适合用于存储对象,常用命令:
- HSET key field value 将哈希表 key 中的字段 field 的值设为 value
- HGET key field 获取存储在哈希表中指定字段的值
- HDEL key field 删除存储在哈希表中的指定字段
- HKEYS key 获取哈希表中所有字段
- HVALS key 获取哈希表中所有值
3:列表操作命令 :
Redis 列表是简单的字符串列表,按照插入顺序排序,
- 常用命令: LPUSH key value1 [value2] 将一个或多个值插入到列表头部(左边)
- LRANGE key start stop 获取列表指定范围内的元素
- RPOP key 移除并获取列表最后一个元素(右边)
- LLEN key 获取列表长度
这里插入的时候有点类似于链表的头插法
用-1 表示最后一个元素的末尾。
rpop 弹出的是最先插入的元素
4:集合操作命令:
Redis set 是string类型的无序集合。集合成员是唯一的,集合中不能出现重复的数据,
常用命令:
- SADD key member1 [member2] 向集合添加一个或多个成员
- SMEMBERS key 返回集合中的所有成员
- SCARD key 获取集合的成员数
- SINTER key1 [key2] 返回给定所有集合的交集
- SUNION key1 [key2] 返回所有给定集合的并集
- SREM key member1 [member2] 删除集合中一个或多个成员
5:有序集合操作命令:
Redis有序集合是string类型元素的集合,且不允许有重复成员。每个元素都会关联一个double类型的分数。
常用命令:
- ZADD key score1 member1 [score2 member2] 向有序集合添加一个或多个成员
- ZRANGE key start stop [WITHSCORES] 通过索引区间返回有序集合中指定区间内的成员 ZINCRBY key increment member 有序集合中对指定成员的分数加上增量 increment ZREM key member [member ...] 移除有序集合中的一个或多个成员
这整个排序可以看成一个大根堆。
输出从大到小
6:通用命令:
Redis的通用命令是不分数据类型的,都可以使用的命令:
- KEYS pattern 查找所有符合给定模式( pattern)的 key
- EXISTS key 检查给定 key 是否存在(存在返回1,不存在返回0)
- TYPE key 返回 key 所储存的值的类型
- DEL key 该命令用于在 key 存在是删除 key
四:在Java中操作Redis:
Spring Data Redis:
Spring Data Redis 是 Spring 的一部分,对 Redis 底层开发包进行了高度封装。 在 Spring 项目中,可以使用Spring Data Redis来简化操作。
使用方式:
-
操作步骤: 导入Spring Data Redis 的maven坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
-
配置Redis数据源 编写配置类,
redis:
host: ${sky.redis.host}
port: ${sky.redis.port}
password: ${sky.redis.password}
database: ${sky.redis.password}
具体的信息配置在另一个的配置环境中。
-
创建RedisTemplate对象
package com.sky.config;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
@Slf4j
public class RedisConfiguration {
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){
log.info("开始创建redis对象");
RedisTemplate redisTemplate = new RedisTemplate();
//设置redis的连接工厂对象
redisTemplate.setConnectionFactory(redisConnectionFactory);
//设置redis key的序列化器
redisTemplate.setKeySerializer(new StringRedisSerializer());
return redisTemplate;
}
}
Redis是默认有一个序列化器的,不过那个会导致写入Redis中的key为乱码
所以我们需要单独配置一个序列化器。
-
通过RedisTemplate对象操作Redis
package com.sky;
import io.lettuce.core.ScriptOutputType;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.*;
@SpringBootTest
@Slf4j
public class SpringDateRedis {
@Autowired
private RedisTemplate redisTemplate;
@Test
public void testRedisTemplate(){
System.out.println(redisTemplate);
final ValueOperations valueOperations = redisTemplate.opsForValue();//字符串对象
final HashOperations hashOperations = redisTemplate.opsForHash();//哈希对象
final ListOperations listOperations = redisTemplate.opsForList();//列表对象
final SetOperations setOperations = redisTemplate.opsForSet();//set集合对象
final ZSetOperations zSetOperations = redisTemplate.opsForZSet();//zset有序集合对象
}
}
创建了一个测试类,通过注入的方式获取了在IOC容器中的redistemplate对象
并且通过了这个redistemplate对象的五个方法不同获取redis中的五种不同的数据类型。
在Java中操作Redis的字符串命令:
/*
测试字符串操作命令
*/
@Test
public void testString(){
ValueOperations valueOperations = redisTemplate.opsForValue();
//set
valueOperations.set("city","北京");
//get
final String city = (String) valueOperations.get("city");
System.out.println(city);
//setex
valueOperations.set("code","123489",3, TimeUnit.MINUTES);
//setnx
valueOperations.setIfAbsent("lock","1");
}
在java中有些方法和redis中的这个操作命令名称不同,注意一下就好,效果没区别。
在Java中操作Redis的哈希表命令:
/*
测试哈希操作命令
*/
@Test
public void testHash(){
final HashOperations hashOperations = redisTemplate.opsForHash();//哈希对象
//hset
hashOperations.put("id","name","小明");
hashOperations.put("id","idcard","123");
hashOperations.put("id","gender","男");
//hget
Object name = hashOperations.get("id", "name");
System.out.println(name);
//hdel
hashOperations.delete("id","name");
//hkeys
final Set ids = hashOperations.keys("id");
Iterator<Integer> iterator = ids.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
//hvals
for (Object id : hashOperations.values("id")) {
System.out.println(id);
}
}
在Java中操作Redis的列表命令:
/*
测试列表操作命令
*/
@Test
public void testList(){
final ListOperations listOperations = redisTemplate.opsForList();//列表对象
//lpush
listOperations.leftPushAll("mylist","a","b","c");
//lrange
final List mylist = listOperations.range("mylist", 0, -1);
System.out.println(mylist);
//rpop
final Object o = listOperations.rightPop("mylist");
System.out.println(o);
//llen
final Long len = listOperations.size("mylist");
System.out.println(len);
}
在Java中操作Redis的集合命令:
/*
测试结合操作命令
*/
@Test
public void testSet(){
final SetOperations setOperations = redisTemplate.opsForSet();//set集合对象
//sadd
setOperations.add("set1","a","b","c");
setOperations.add("set2","a","b","x","y");
//smembers
final Set set1 = setOperations.members("set1");
System.out.println(set1);
//scard
final Long len = setOperations.size("set1");
System.out.println(len);
//sinter
final Set intersect = setOperations.intersect("set1", "set2");
System.out.println(intersect);
//sunion
final Set union = setOperations.union("set1", "set2");
System.out.println(union);
//srem
setOperations.remove("set1","a");
final Set newset1 = setOperations.members("set1");
System.out.println(newset1);
}
在Java中操作Redis的有序集合命令:
/*
有序集合操作命令
*/
@Test
public void TestZSet(){
final ZSetOperations zSetOperations = redisTemplate.opsForZSet();//zset有序集合对象
//zadd
zSetOperations.add("zset1","a",10.0);
zSetOperations.add("zset1","b",10.2);
zSetOperations.add("zset1","c",10.5);
//zrange
final Set zset1 = zSetOperations.range("zset1", 0, -1);
System.out.println(zset1);
//zincrby
zSetOperations.incrementScore("zset1","c",10);
//zrem
zSetOperations.remove("zset1","a","b");
final Set zset2 = zSetOperations.range("zset1", 0, -1);
System.out.println(zset2);
}
在Java中操作Redis的通用命令:
/*
通用命令
*/
@Test
public void TestCommon(){
//keys pattern
final Set keys = redisTemplate.keys("*");
System.out.println(keys);
//exists key
final Boolean name = redisTemplate.hasKey("name");
System.out.println(name);
//typekey
for(Object key:keys){
final DataType type = redisTemplate.type(key);
System.out.println(type);
}
//del key
redisTemplate.delete("mylist");
}
五:数据查询(第一个项目场景):
当分析场景之后,写代码就会简单很多了。
分为下面的四步:
1:先获取当前用户id
2:根据id进行查找缓存
3:有缓存直接返回,没缓存再查找数据库
4:写缓存(try-catch 一下)
注意这里需要设置一下缓存的过期时间
为什么要设置缓存的过期时间?
不设置过期时间的话,就会导致缓存只进不出,当Redis满了以后,就会触发Redis的淘汰机制
根据Redis的算法自动淘汰掉一些数据,这些数据如果很有用,那结果就很麻烦了
代码:
@GetMapping("/recommend")
public BaseResponse<Page<User>> usersRecommend(long pageSize,long pageNum,HttpServletRequest request){
log.info("主页推荐成员");
/*
1:先获取当前用户id
2:根据id进行查找缓存
3:有缓存直接返回,没缓存再查找数据库
4:写缓存(try-catch 一下)
*/
User user = userService.getLoginUser(request);
final Long userId = user.getId();
ValueOperations valueOperations = redisTemplate.opsForValue();
String key = String.format("shayu:user:recommend:%s",userId);
Page<User> userList = (Page<User>)valueOperations.get(key);
if(userList != null){
return ResultUtils.success(userList);
}
QueryWrapper<User> queryWrapper = new QueryWrapper();
Page userPage = new Page<User>(pageNum,pageSize);
userList = userService.page(userPage,queryWrapper);
try {
valueOperations.set(key,userList,30000, TimeUnit.MILLISECONDS);
} catch (Exception e) {
log.info(String.valueOf(e));
}
return ResultUtils.success(userList);
}
这个时候根据逻辑我们也可以分析出一个问题:
就是当我们查找不到这个用户id的时候,我们是不是还是得数据库中读取
那如果你是第一个用户,那你是不是一定访问不到数据,所以这里还能进行优化
缓存预热:
问题:第一个用户访问还是很慢(加入第一个老板),也能一定程度上保护数据库
缓存预热的优点:
- 解决上面的问题,可以让用户始终访问很快
缺点:
- 增加开发成本(你要额外的开发、设计)
- 预热的时机和时间如果错了,有可能你缓存的数据不对或者太老
- 需要占用额外空间
怎么缓存预热?
- 定时
- 模拟触发(手动触发)
实现
用定时任务,每天刷新所有用户的推荐列表
注意点:
- 缓存预热的意义(新增少、总用户多)比如你这个系统里面有10000个人,每次都新增20个,那这样就可以理解为新增少,总用户多。
- 缓存的空间不能太大,要预留给其他缓存空间
- 缓存数据的周期(此处每天一次)
定时的方法实现缓存预热:
用这个定时的方法实现需要用到spring中的一个工具(我不知道工具这个形容是否准确,spring整合了一个定时框架)
Spring Scheduler(spring boot 默认整合了)
具体使用:
Spring Task及订单状态定时处理_java实现订单状态自动修改-CSDN博客
我这边直接贴代码:
package com.usercenter.usercenterproject.task;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.usercenter.usercenterproject.Pojo.ResultUtils;
import com.usercenter.usercenterproject.Pojo.User;
import com.usercenter.usercenterproject.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* 预热缓存
*/
@Component
@Slf4j
public class PrecacheTask {
@Resource
private RedisTemplate redisTemplate;
@Autowired
private UserService userService;
List<Integer> mainUserList = new ArrayList<>();
@Scheduled(cron = "0 12 1 * * *")
public void precachetask(){
mainUserList.add(5,6);
for (Integer userId : mainUserList) {
ValueOperations valueOperations = redisTemplate.opsForValue();
String key = String.format("shayu:user:recommend:%s",userId);
QueryWrapper<User> queryWrapper = new QueryWrapper();
Page userPage = new Page<User>(1,20);
Page<User> userList = userService.page(userPage,queryWrapper);
try {
valueOperations.set(key,userPage,30000, TimeUnit.MILLISECONDS);
} catch (Exception e) {
log.info(String.valueOf(e));
}
}
}
}
提醒一下:启动类添加注解 @EnableScheduling
六:分布式共享Cookie(第二个项目场景):
这个项目场景说的天花乱坠,实际代码实现起来只需要配置一下就行。
先导入maven坐标:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
修改spring-session 存储配置
spring:
session:
timeout: 86400
store-type: redis
默认是 none ,表示存储在单挑服务器
store-type: redis :表示从redis中读写 session
等配置之后,剩下的都是前端的事情了:
前端要做的事情也很简单:就是在往后端发送请求的时候携带上cookie就行
下面这个是axious的拦截器举例:
const myAxios = axios.create({
baseURL: 'http://localhost:8080/api',
});
myAxios.defaults.withCredentials = true;
七:店铺状态设置(第三个项目场景):
接口设计:
- 设置营业状态
- 管理端查询营业状态
- 用户端查询营业状态
具体的代码实现:
因为我们用了reids缓存技术,所以我们这里就没有设计到数据库的操作,所有的操作都是在Controll层进行(也是因为这个接口功能比较简单)
服务端Controll层:
package com.sky.controller.admin;
import com.sky.result.PageResult;
import com.sky.result.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.*;
@RestController("adminShopController")
@Slf4j
@RequestMapping("/admin/shop")
@Api(tags = "店铺相关接口")
public class ShopController {
public static final String KEY = "Shop_Status";
@Autowired
private RedisTemplate redisTemplate;
/**
* 设置营业状态
* @param status
* @return
*/
@PutMapping("/{status}")
@ApiOperation("设置营业状态")
public Result SetShopStatus(@PathVariable Integer status){
log.info("设置店铺的状态:{}",status==1?"营业中":"打烊中");
redisTemplate.opsForValue().set(KEY,status);
return Result.success();
}
/**
* 管理端查询营业状态
* @return
*/
@GetMapping("/status")
@ApiOperation("管理端查询营业状态")
public Result SelectShopStatus(){
final Integer status = (Integer) redisTemplate.opsForValue().get(KEY);
log.info("获取店铺的状态:{}",status);
return Result.success(status);
}
}
- 设置营业状态
设置营业状态用的是redis中最简单字符串,然后设置了一个key叫做Shop_Status,然后value就是status。
- 管理端查询营业状态
查询状态因为直接放在了redis中,所有,直接用get方法获取即可
客户端Controll层:
package com.sky.controller.user;
import com.sky.result.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.*;
@RestController("userShopController")
@Slf4j
@RequestMapping("/user/shop")
@Api(tags = "店铺相关接口")
public class ShopController {
public static final String KEY = "Shop_Status";
@Autowired
private RedisTemplate redisTemplate;
/**
* 用户端查询营业状态
* @return
*/
@GetMapping("/status")
@ApiOperation("用户端查询营业状态")
public Result SelectShopStatus(){
final Integer status = (Integer) redisTemplate.opsForValue().get(KEY);
log.info("获取店铺的状态:{}",status);
return Result.success(status);
}
}
客户端和服务端的唯一区别就是路径不同。