
基础篇-redis
-01.Redis入门课程介绍

redis里面存储的都是键值对
redis是一种非关系型数据库,没有表


初始Redis-认识NoSQL
1.结构化与非结构化

sql是结构化的,
每个字段都有对应的约束,很难修改,
表结构也是固定的,这个表规定有几个字段是固定的,不推荐修改,要添加或者减少字段对于整个表都有影响,在表中数据量大的时候表现尤其明显
Nosql是非结构化的
结构比较松散,添加或者减少字段不会有太大的影响
没有特殊的约束
2.关系型与非关系型

sql是关系型数据库,表中的字段可以关联别的表中的字段,要删除该字段sql是不允许的,sql会维护这些关系

Nosql是非关系型数据库,当要维护用户的购物车中有什么商品的时候,sql选择了将表和表进行关联,Nosql选择了把购买的商品存储在json格式中
这种存储方式的缺点:由于没有表的关联,所以Nosql要存储数据就得把数据结合起来存储,在sql中只需要关联商品id的情况下,Nosql需要存储商品的很多信息,就造成了商品信息的重复存储,
Nosql也可以实现数据的关联,但是需要程序员自己手动维护,所以Nosql是非关系型数据库
3.查询方式不同

4.事务方面
事务所需要的:原子性 一致性 隔离性 持久性

sql可以满足ACID
但是Nosql只能基本满足事务

初识Redis-认识Redis

Redis命令-数据结构介绍

redis是一种key-value的数据库,key一般是Stirng类型,value的类型多种多样
基础类型包括String,Hash,LIst,Set
Redis命令-通用命令

Redis命令-String类型

string字符串有多种不同的格式:
10 是 数值类型的字符串,不同类型的字符串存储到底层都是字节数组
数值类型的字符串实现存储为二进制编码再存储为字节数组
字符类型的字符串是先存储为字节码文件再存储为字节数组

由于字符串类型有多种形式:包括数值,字符,浮点型
对于整型 使用自增 incr 和incrby 这两种方法
incrby可以使整型自减

对于浮点型 incrbyfloat
setnx:只有在该key不存在的时候才能新增(真正的新增功能)

setex:创建一个key并设置有效期(效果和exper添加有效期的作用一样)

语法:setex key 时间(秒数) value
Redis命令-Key的层级格式

redis当中没有表的概念,我们该如何区分不同类型的key
在不同类型的key前面拼接一段


按照层级创建key的时候,redis会自动把数据分离


Redis命令-Hash类型

redis是key:value结构,
key通常是字符串
value可以是string字符串类型(包括字符串/字符/json字符串,整型,浮点型),
也可以是hash散列结构
string里面存储的是json字符串,修改起来很不方便,所以用hash散列来存储

在string命令上面加上h就变成了hash命令


hmset可以设置一个key的多个field的value

hmget可以获取一个key的多个field
HMGET key field1 field2 field3 ...

HMGET key field1 field2 field3 ...
第一个参数是哈希键名,后面所有的参数都是要查询的字段名。
所以命令会被 Redis 这样解析:
- 哈希键:
animal - 要查询的字段列表:
age、person、age、person、name、animal、name
一共 7 个字段,所以返回了 7 个结果,每个结果对应一个字段:
| 序号 | 查询的字段 | 返回值 | 说明 |
|---|---|---|---|
| 1 | age |
"3" |
animal 哈希里有这个字段,返回值 |
| 2 | person |
null |
animal 哈希里没有这个字段 |
| 3 | age |
"3" |
重复查询,再次返回 age 的值 |
| 4 | person |
null |
重复查询,再次返回 null |
| 5 | name |
"cat" |
animal 哈希里有这个字段,返回值 |
| 6 | animal |
null |
animal 哈希里没有这个字段 |
| 7 | name |
"cat" |
重复查询,再次返回 name 的值 |
hgetall获取一个key的所有field和value

hkeys 获取一个key里的所有field
hvals 获取一个key里的所有value

hsetnx 判断字段是否存在,若不存在则创建

Redis命令-List类型


lpush 向列表左侧插入元素,就是把元素放在列表左边

list内元素的顺序是先进后出

rpop lpop返回被删除的值 ,后面跟的数字是删除几个元素




Redis命令-Set类型

单集合操作

增sadd
删srem
查 smembers查看所有元素
sismember查看当前元素是否存在
scard查看当前set的元素个数

多集合操作

案例练习

Redis命令-SortedSet类型


相当于set里面需要多传入一个字段score
基础篇-redis的java客户端
Redis的Java客户端-客户端对比

Redis的Java客户端-Jedis快速入门


java
public class jedis {
private Jedis jedis;
//前置操作
@BeforeEach
public void setUp(){
//与redis建立连接
jedis=new Jedis("192.168.150.101",6379);
//设置密码
jedis.auth("123321");
//选择库
jedis.select(0);
}
@Test
public void testJedis(){
jedis.set("key","value");
System.out.println(jedis.get("key"));
}
//关闭连接
@AfterEach
public void tearDown(){
jedis.close();
}
}
Redis的Java客户端-Jedis的连接池

-认识SpringDataRedis
序列化和反序列化:
序列化,把value中存储的对象转换为字符串或者是字节,便于在redis中存储
反序列化:把redis中的字符串或者这字节数组转换为对象,json格式的数据等,便于java程序读取

redisTemplate对于redis的方法做了分类,封装到了不同的类型当中
Redis的Java客户端-RedisTemplate快速入门


-StringRedisTemplate

在自动的序列化和反序列化的过程中必须要知道这个字符串对应哪一个对象,尤其是反序列化的时候,必须要知道是那个对象才能正确的反序列化
必须存储字段:类的class类型,带来了很多的额外开销
所以使用StringRedisTemlate只进行字符串的序列化和反序列化

在需要存储java对象的时候,手动进行对象的序列化和反序列化
读取数据的时候去手动反序列化转成对应的类型

在set之前需要手动进行序列化
实战篇-Redis企业实战课程介绍


短信登录-导入黑马点评项目

-基于session实现短信登录的流程

短信登录-实现发送短信验证码功能



短信登录-实现短信验证码登录和注册功能
java
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
//校验手机号格式是否正确
String phone = loginForm.getPhone();
if(RegexUtils.isPhoneInvalid(phone)){
return Result.fail("手机号格式错误");
}
//校验短信验证码是否正确
if(!loginForm.getCode().equals(session.getAttribute("code"))){
return Result.fail("验证码错误");
}
//查询数据库内有无该手机号
User one = query().eq("phone", phone).one();
//没有该手机号则新建一个手机号
if(one==null){
createWithPhone(phone);
}
//保存用户的信息到session
session.setAttribute("phone",phone);
return Result.ok();
}
private void createWithPhone(String phone) {
//创建用户:包括手机号,用户名
User user = new User();
user.setPhone(phone);
user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomNumbers(8));
//保存该用户到数据库
save(user);
}
短信登录-实现登录校验拦截器
编写登录校验拦截器
清除缓存
java
public class LoginInterceptor implements HandlerInterceptor {
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//获取线程
HttpSession session = request.getSession();
//获取线程内的user对象
Object user = session.getAttribute("user");
//不存在user对象则拦截(不存在user对象说明未登录)
if(user == null){
return false;
}
//将user保存到当前线程
UserHolder.saveUser((User) user);
return true;
}
public void afterHandle(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//清除缓存
UserHolder.removeUser();
}

让拦截器生效
java
@Configuration
public class MVCConfig implements WebMvcConfigurer {
//设置需要拦截的路径
@Override
public void addInterceptors(InterceptorRegistry registry) {
//加入拦截器
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns(//去除不需要拦截的路径
"/user/code",
"/user/login",
"/blog/hot",
"/shop/**",
"/shop-type/**",
"/upload/**",
"/voucher/**"
);
}
}
短信登录-隐藏用户敏感信息

session占用的是tomcat里面的内存信息,存储太多无关信息会占用tomcat

只需要保存userDTO对象中的信息就行
但是这部分改的话太麻烦了,先放着不管
短信登录-session共享的问题分析

最开始是提供了session拷贝的方法,只要做好了配置就能在多台tomcat上进行拷贝复制,
1.但是这样多台tomcat上保存了相同的信息,非常耗费tomcat的内存
2.拷贝session是有延迟的,在延迟内依然会出现数据不一致的情况


短信登录-Redis代替session的业务流程

基于redis实现共享session登录:
需要注意的包括redis的key:session是每一个会话自动创建的session,key都为"code"也不会影响(因为session是不同的,相当于在不同的仓库中),但是redis是所有的验证码都存到了一个地方(相当于在同一个仓库中),所以key就要不同
session是自动维护,自动存取,存的时候,服务器会把对应的session id存到cokkie中,取的时候拿着cookie中的session id就能取出对应的session
redis是手动维护


原本基于session和cookie是cookie中存储着session id,通过session id查询到session,再根据session做登录校验,session就是登录凭证
现在改为token为登录凭证
后端把token返回给前端作为请求头,以后每一次发送请求都会带上这个请求头
短信登录-基于Redis实现短信登录
保存验证码

java
@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Result sendCode(String phone, HttpSession session) {
//校验手机号
if(RegexUtils.isPhoneInvalid(phone)){
return Result.fail("手机号有误");
}
//生成验证码(random工具包下的randomNumbers可以生成指定长度的一串数字)
String c = RandomUtil.randomNumbers(6);
// //将验证码保存到session,以便下次验证码校验
// session.setAttribute("code",c);
//将验证码加入redis存储起来
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone, c);
//发送验证码
log.info("code:{}",c);
return Result.ok();
}
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
//校验手机号格式是否正确
String phone = loginForm.getPhone();
if(RegexUtils.isPhoneInvalid(phone)){
return Result.fail("手机号格式错误");
}
//校验短信验证码是否正确
//获取redis中的验证码
String redisCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
if(!loginForm.getCode().equals(redisCode)){
return Result.fail("验证码错误");
}
//查询数据库内有无该手机号
User one = query().eq("phone", phone).one();
//没有该手机号则新建一个手机号
User user = new User();
if(one==null){
user = createWithPhone(phone);
}
// 保存用户的信息到session
// session.setAttribute("user",user);
//保存用户的信息到redis
//1.随机生成token
String token = UUID.randomUUID().toString();
//2.将user对象转换为map集合
//2.1 因为用的是stringRedisTemplate,所以key和value都得是字符串类型,否则会报错
Map<String, Object> stringObjectMap = BeanUtil.beanToMap(user, new HashMap<>(),
CopyOptions.create()
.setIgnoreNullValue(true)
// 修复:先判断不为null再toString,为空就给空字符串
.setFieldValueEditor((fieldName, fieldValue) -> {
if (fieldValue == null) {
return ""; // 空值给空串,避免空指针
}
return fieldValue.toString();
})
);
//3.存储到redis中
String tokenKey = LOGIN_USER_KEY + token;
stringRedisTemplate.opsForHash().putAll(tokenKey,stringObjectMap);
//3.1设置有效期//但是这个不够完善,这行代码的意思是不管用户是否在使用该网站,只要有效期结束就无效
stringRedisTemplate.expire(tokenKey,LOGIN_USER_TTL, TimeUnit.MINUTES);
//事实上应该在用户每发送一次请求就刷新一次有效时间--在拦截器里完成这个操作
//4.将token返回给前端
return Result.ok(tokenKey);
}
java
public class LoginInterceptor implements HandlerInterceptor {
//因为LoginInterceptor这个类是自己创建出来的,而不是由spring创建的,不能用spring的自动注入
private StringRedisTemplate stringRedisTemplate;
//利用构造函数来注入
public LoginInterceptor(RedisTemplate redisTemplate) {
this.stringRedisTemplate =stringRedisTemplate;
}
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// //获取线程
// HttpSession session = request.getSession();
// //获取线程内的user对象
// Object user = session.getAttribute("user");
//1.获取请求头中的token
String token = request.getHeader("token");
//1.1没有token则报错
if (token == null) {
response.sendRedirect("401");
return false;
}
//2.通过token向redis查询用户
Map<Object,Object> userMap = stringRedisTemplate.opsForHash().entries(token);
//2.1不存在user对象则拦截(不存在user对象说明未登录)
if(userMap == null){
return false;
}
//3.将hash数据转换成user对象
User user = BeanUtil.fillBeanWithMap(userMap, new User(),false);
//4.将user保存到当前线程
UserHolder.saveUser(user);
//5.刷新token有效期
stringRedisTemplate.expire(token,60, TimeUnit.MINUTES);
return true;
}
public void afterHandle(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//清除缓存
UserHolder.removeUser();
}
java
@Configuration
public class MVCConfig implements WebMvcConfigurer {
//这个类的注解底层封装了Component,是由Spring构建的类,可以注入
@Resource
private StringRedisTemplate stringRedisTemplate;
//设置需要拦截的路径
@Override
public void addInterceptors(InterceptorRegistry registry) {
//加入拦截器
registry.addInterceptor(new LoginInterceptor(stringRedisTemplate))
.excludePathPatterns(//去除不需要拦截的路径
"/user/code",
"/user/login",
"/blog/hot",
"/shop/**",
"/shop-type/**",
"/upload/**",
"/voucher/**"
);
}
}

短信登录-解决状态登录刷新的问题

该拦截器有缺陷:只有拦截了的请求才能做刷新token有效期
如果是未被拦截的请求则不会刷新有效期:例如首页和商户的详情页
所以一个用户虽然登陆了,但是如果一直在访问不被拦截器拦截的网页,过了三十分钟还是会刷新

第一个拦截器负责刷新token,对所有路径都放行
第二个拦截器负责拦截除了不需要登陆的路径
java
@Configuration
public class MVCConfig implements WebMvcConfigurer {
//这个类的注解底层封装了Component,是由Spring构建的类,可以注入
@Resource
private StringRedisTemplate stringRedisTemplate;
//设置需要拦截的路径
@Override
public void addInterceptors(InterceptorRegistry registry) {
//先加入刷新token时间的拦截器(addInterceptor方法内有参数order,默认=0,此时按照加入顺序确定先后,或者可以指定order大小)
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate));
//后加入判断是否登陆的拦截器
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns(//去除不需要拦截的路径
"/user/code",
"/user/login",
"/blog/hot",
"/shop/**",
"/shop-type/**",
"/upload/**",
"/voucher/**"
);
}
java
public class RefreshTokenInterceptor implements HandlerInterceptor {
//因为LoginInterceptor这个类是自己创建出来的,而不是由spring创建的,不能用spring的自动注入
private StringRedisTemplate stringRedisTemplate;
//利用构造函数来注入
public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate =stringRedisTemplate;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler){
// //获取线程
// HttpSession session = request.getSession();
// //获取线程内的user对象
// Object user = session.getAttribute("user");
//1.获取请求头中的token
String token = request.getHeader("token");
//1.1没有token则报错
if (token == null) {
return true;
}
//2.通过token向redis查询用户
Map<Object,Object> userMap = stringRedisTemplate.opsForHash().entries(token);
//2.1不存在user对象则拦截(不存在user对象说明未登录)
if(userMap.isEmpty()){
return true;
}
//3.将hash数据转换成user对象
User user = BeanUtil.fillBeanWithMap(userMap, new User(),false);
//4.将user保存到当前线程
UserHolder.saveUser(user);
//5.刷新token有效期
stringRedisTemplate.expire(token,60, TimeUnit.MINUTES);
return true;
}
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//清除缓存
UserHolder.removeUser();
}
}
java
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler){
//判断ThreadLocal里面是否有user对象
//没有则拦截
if(UserHolder.getUser() == null){
response.setStatus(401);
return false;
}
//有则说明已经登录
return true;
}
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//清除缓存
UserHolder.removeUser();
}
}
实战篇-商户查询缓存
-01.什么是缓存
缓存就是数据交换的缓冲区域:当cpu算力发展起来之后,内存读写的速度跟不上spu的算力就发展出来了缓存

缓存包括:
浏览器缓存:浏览器的css js 等静态资源
剩下的未命中缓存交给tomcat服务器,
tomcat服务器会有一个应用层缓存:应用层缓存指的是浏览器给tomcat发起请求时需要加载的
剩下的未命中缓存交给数据库,
数据库也可以添加缓存:数据库给id创建索引,缓存索引数据,

数据一致性成本:在数据更新之后,如果没有及时更新redis里的数据,优先查看redis里的数据就会导致数据不一致
-02.添加商户缓存

java
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public Result queryShopById(Long id) {
//1.根据id查询redis缓存
String key = "cache:shop:id:" + id;
String shopJSON = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isNotEmpty(shopJSON)) {
//如果存在该店铺则返回
//返回的是json字符串,所以用json工具包转成对象
return Result.ok(JSONUtil.toBean(shopJSON, Shop.class));
}
//2.如果redis缓存中不存在该商店,则查询数据库
Shop shop = query().eq("shop_id", id).one();
//2.1不存在则返回错误
if (shop == null) {
return Result.fail("该商店不存在");
}
//2.2如果存在则加入缓存
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop));
return Result.ok(shop);
}
}
-03.缓存练习题分析

