SpringBoot+Redis+Mybatis-plus黑马点评

短信登录

基于Session实现登录

流程:

发送短信验证码-->短信验证码注册登录-->校验登录状态(保存用户到ThreadLocal,方便后续使用)

不能每次请求服务都要进行登录状态校验,解决办法:拦截器

在Spring框架中,拦截器(Interceptor)可以通过实现HandlerInterceptor接口或继承HandlerInterceptorAdapter类来实现。拦截器通常用于在请求到达控制器之前进行预处理,例如身份验证、权限检查等。

1.创建拦截器(拦截器需要实现HandlerInterceptor接口,并重写方法)

2.注册拦截器(通过WebMvcConfigurer接口来注册拦截器。)

3.配置拦截器的排除路径(在实际应用中,某些接口(如登录、注册接口)不需要进行身份验证)

区分一下session,cookie和token

CookieCookie实际上是一小段的文本信息。客户端请求服务器,如果服务器需要记录该用户状态,就使用response向客户端浏览器颁发一个Cookie。客户端浏览器会把Cookie保存起来,服务不保存。每次请求时客户端带上cookie。服务器检查该Cookie,以此来辨认用户状态。

Session: 服务器在处理客户端请求过程中会创建session,并且为该session生存唯一的session ID。

服务器将session ID发送到客户端.当客户端再次请求时,就会带上这个session ID.服务器接收到请求之后就会一句Session ID 找到相应的Session ,完成请求.session是服务本地保存,发给客户端,客户端每次访问都带着,直接和服务的session比对

Token:Token是服务端生成的一串字符串,当作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个Token并将此Token返回给客户端,以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码.token是 服务经过计算发给客户端的,服务不保存,每次客户端来请求,经过解密等计算来验证是否是自己下发的.

JWT:JWT不仅仅是一个生成Token的过程,它还提供了一种结构化、自包含、无状态的Token设计。验证Token时,服务器不需要查询数据库,直接解析和验证签名即可。

机制

存储位置

通信方式

生命周期

Cookie

浏览器

自动Header携带

可设置过期时间

️ Session

服务端

SessionID传递

服务端控制

Token

客户端

手动Header添加

令牌有效期决定

️ JWT

客户端

Bearer Token

包含过期时间声明

集群Session共享问题

session的痛点(负载均衡导致的)

多态Tomcat并不共享session存储空间,当请求切换到不同tomcat服务时导致数据丢失的问题

基于Redis实现共享session登录

发送验证码逻辑实现:

1.校验手机号

2.不符合返回错误信息

3.符合生成验证码

4.保存验证码到redis

5.发送验证码

登录验证逻辑:

1.校验手机号

2.不符合返回错误信息

3.从redis中获取验证码并校验

4.不一致报错,一致,根据手机号查询用户

5.判断用户是否存在

6.保存用户信息到redis

6.1随机生成token,作为登录令牌

6.2将User对象转为Hash存储

6.3存储

7.返回token

登录拦截器的优化

拦截器:

1.获取token

2.查询Redis的用户

3.保存到ThreadLocal

4.刷新token有效器

5.放行

问题:

如果用户登录以后一直请求的是不需要拦截的请求,那么token有效期没有刷新,即使用户一直在使用,也没有做到token更新

解决办法:双拦截器

第一个拦截器拦截所有请求-->第二个拦截器拦截需要登录的请求

商户查询缓存

什么是缓存

缓存:数据交换的缓冲区,是存储数据的临时地方,一般读写性能毕竟高

浏览器(浏览器缓存)--->Redis(应用层缓存)--->数据库(数据库缓存)

缓存的作用:

1.降低后端负载 2.提高读写效率,降低相应时间

缓存的成本

1.数据一致性成本 2.代码维护成本 3.运维成本

添加Redis缓存

客户端请求优先到达缓存,如果缓存命中直接返回数据,如果未命中,请求数据库,并且写入缓存

缓存更新:

内存淘汰:不用自己维护,内存不足时自动淘汰部分数据

超时剔除:给缓存数据添加TTL时间,到期后自动删除缓存

主动更新:当涉及到数据库的增删改时,主动更新Redis缓存

主动更新策略:

1.在更新数据库的同时更新缓存------常用

2.数据库和缓存整合为一个服务,由服务来维护一致性

3.调用者只操作缓存,由其它线程异步的缓存数据持久到数据库,保证最终一致

操作缓存和数据库时有三个问题需要考虑:

1.删除缓存还是更新缓存?
2.如何保证缓存与数据库的操作同时成功或失败?

3.先操作缓存还是先操作数据库?

都有可能出现线程不安全的问题。 方案二的不安全几率更低---常用

缓存穿透

缓存穿透是指客户端请求的数据在缓存和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。

常见的解决方案:

缓存空对象:缓存null

优点:实现简单,维护方便

缺点:额外的内存消耗,可能造成短期的不一致

布隆过滤:

客户端-->布隆过滤器-->Redis

布隆过滤器:利用hash存储数据库里的字段

优点:内存占用少,没有多余key

缺点:实现复杂,可能存在误判可能

缓存雪崩

在同一时段内大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大的压力

解决方案:

给不同的key的TTL添加随机值

利用Redis集群提高服务的可用性

给缓存业务添加降级限流策略

给业务添加多级缓存

缓存击穿

也叫热点Key问题,就是一个高并发访问并且缓存重建业务较复杂的key突然失效了,无效的请求访问会在瞬间给数据库带来巨大的冲击

常见的解决方案:

互斥锁

逻辑过期

缓存工具封装

方法1:将任意java对象序列化为json并存储在String类型的key中,并且可以设置TTL过期时间

方法2:将任意java对象序列化为json并存储在String类型的key中,并且可以设置逻辑过期时间,用于处理缓存击穿问题

方法3:根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题

方法4:根据指定的key查询缓存,并反序列化为指定类型,利用逻辑过期的方式解决缓存击穿问题

Public class CacheClient{
    private StringRedisTemplate stringRedisTemplate;

    public void set(String key, Object object,Long time,TimeUnit unit){
        StringRedisTemplate.opsForValue().set(key,JSONUTIL.toJsonStr(value),time,unit)
    }
    
    public void setWithLogicalExpire(String key, Object object,Long time,TimeUnit unit){
        //设置逻辑过期
        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
        StringRedisTemplate.opsForValue().set(key,JSONUTIL.toJsonStr(redisData))
    }

    public <R, ID> R queryWithPassThrough(Long time,TimeUnit unit,
        String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback){
        String key = keyPrefix + id;
        //1.从Redis中查询商铺缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        //2.判断是否存在
        if(StrUtil.isNotBlank(json)){
            return JSONUtil.toBean(json,type);
        }
        //3.判断命中的是否是空值
        is(json != null){
            return null;
        }
        //4.从数据库进行查询
        R r = dbFallback.apply(id);
        //5.不存在,返回错误
        if(r == null){
            //将空值写入redis
            stringRedisRemplate.opsForValue().set(key,"",null,TimeUnit.MINUTES);
            return null;
        }
        //6.存在,写入redis
        this.set(key,r,time,unit);
    }

    public <R, ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
    String key = keyPrefix + id;
    // 1.从Redis中查询缓存
    String json = stringRedisTemplate.opsForValue().get(key);
    // 2.判断是否存在
    if (StrUtil.isNotBlank(json)) {
        // 3.反序列化为RedisData对象
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        R r = redisData.getData();
        LocalDateTime expireTime = redisData.getExpireTime();
        // 4.判断是否逻辑过期
        if (expireTime.isAfter(LocalDateTime.now())) {
            // 5.未过期,直接返回缓存数据
            return r;
        } else {
            // 6.已过期,更新缓存
            // 6.1 从数据库查询最新数据
            R latestData = dbFallback.apply(id);
            // 6.2 如果数据库中没有数据,返回null
            if (latestData == null) {
                return null;
            }
            // 6.3 更新缓存
            RedisData newRedisData = new RedisData<>();
            newRedisData.setData(latestData);
            newRedisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(newRedisData));
            // 6.4 返回最新数据
            return latestData;
        }
       } else {
        // 7.缓存中没有数据,从数据库查询
        R r = dbFallback.apply(id);
        // 8.如果数据库中没有数据,返回null
        if (r == null) {
            return null;
        }
        // 9.将数据写入缓存
        RedisData redisData = new RedisData<>();
        redisData.setData(r);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
        // 10.返回查询结果
        return r;
         } 
       }  
}

优惠劵秒杀

全局唯一ID

全局ID生成器

是一种在分布式系统统一下来生成全局唯一ID的工具,一般要满足以下特性:

唯一性 高可用 高性能 递增性 安全性

为了增加ID的安全性,我们可以不适用Redis自增的数值,而是拼接一些其他信息

符号位+时间戳+序列号

实现优惠卷秒杀下单

下单时需要判断两点:

1.秒杀是否开始或结束

2.库存是否充足

超卖问题

在高并发的场景下,多个线程同时发送请求,此时库存还够,每个线程都认为库存还够,每个线程都执行了优惠卷下单,此时库存出现超卖问题。-->并发安全问题

解决方案:

悲观锁vs乐观锁

乐观锁的关键时判断之前查询得到的数据是否有被修改过,常见的方式有两种

版本号法(对数据修改以后版本号加1,修改数据前先判断是否和查询到的版本号对应)

CAS法(在修改前判断查询到的数据是否和之前查询到的数据保持一致-->改进:只要库存大于0就行)

乐观锁的弊端:成功率太低

一人一单

要求同一个优惠卷,一个用户只能下一单

根据优惠卷id和订单id进行查询-->判断订单是否存在-->如果订单存在,不允许下单

同样存在多线程并发的问题,解决方案:悲观锁

通过加锁可以解决在单机情况下的一人一单安全问题,但在集群模型下就不行了

在JVM内部只有一个锁监视器

分布式锁

分布式锁概述

分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁

满足的基本特性:

多进程可见 互斥 高可用 安全性 高性能

基于Redis的分布式锁
实现分布式锁需要实现的两个基本方法:
获取锁:
互斥:确保只有一个线程获取锁
1.添加锁:SETNX lock thread1
2.添加锁过期时间,避免服务宕机引起的死锁:EXPIRE lock 10
3.合并:SET lock thread1 NX EX 10
释放锁:
1.手动释放
2.超时释放:DEK key
基于Redis实现分布式锁1

public interface ILock {
    boolean tryLock(long timeoutSec);
    void unlock();
}

特殊情况依然存在安全问题

基于Redis实现分布式锁2

改进Redis的分布式锁:
1.在获取锁时存入线程标示(可以用UUID表示)
2.在释放锁的时先获取锁中的线程标示,判断与当前线程标示是否一致一致则释放锁,不一致则不释放锁

特殊情况依然存在安全问题

基于Redis实现分布式锁3

Redis的Lua脚本,Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行的原子性

Lua脚本:
if(redis.call('get',KEYS[1] == ARGV[1])) then
    return redis.call('del',KEY[1])
end
return 0

总结

分布式锁Redisson

基于setnx实现的分布式锁存在下面的问题:

1.不可重入:同一个线程无法多次获取同一把锁

2.不可重试:获取锁只尝试一次就返回false,没有重试机制

3.超时释放:业务执行耗时较长,也会导致锁释放,存在安全隐患

4.主从一致性:如果Redis提供了主从集群,主从同步存在延迟,主宕机时,如果从并同步主中的锁数据,则会出现锁实现

Redisson是一个在Redis的基础上实现的Java驻内存数据网格。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。

官网地址: https://redisson.org

Redisson入门:

1.引入依赖

2.配置Redisson客户端

Redisson可重入锁原理

在获取锁的时候,判断一下当前拥有锁的是否是当前线程,如果是在同一线程,只需要把重入的次数+1

释放锁的时候,重入次数-1,重入次数减为0时,释放锁。

Redisson分布式锁主从一致性问题

解决:简单粗暴

总结:

Redis秒杀优化

逻辑实现

逻辑业务串行执行,所用时间为所需业务时间之和

优化思路:

主线程负责进行资格判断,开启新的线程处理耗时比较长的对订单的后续操作

主线程进行资格判断

把下单信息封装成对象-->将下单信息放入阻塞队列-->开启独立线程实现异步下单

Redis消息队列实现异步秒杀

消息队列:字面意思就是存放消息的队列。

生产者:判断秒杀时间和库存 校验一人一单 发送优惠卷id和用户id到消息队列

消费者:接收消息,完成下单

Redis提供了三种不同的方式来实现消息队列

List结构:基于List结构模拟消息队列

PubSub:基于点对点消息模型

Stream:比较完善的消息队列模型

基于List结构模拟消息队列

基于PubSub的消息队列

基于Stream的消息队列

基于Stream的消息队列——消费者组

总结

达人探店

点赞

需求:

一个人只能对同一篇笔记进行一次点赞,如果点赞完以后则取消点赞

如果当前用户已经点赞,则点赞按钮高亮显示

点赞排行榜

将时间戳作为score传入,实现按照点赞时间排序

好友关注

需求:

1.关注和取关接口

2.判断是否关注的接口

3.共同关注:利用Redis的set的求交集功能

Feed流实现方案分析

Feed流的实现方案1:

拉模式:也叫做读扩散

推模式

推拉结合模式

<think>好的,我现在需要处理用户关于“黑马点评介绍”的查询。用户想了解黑马点评的相关信息,可能是一个项目或者课程案例。首先,我需要明确用户的具体需求是什么,是技术架构、功能模块,还是学习资源? 接下来,调用网络搜索工具,输入关键词“黑马点评介绍”、“黑马点评项目”、“黑马点评功能”。查看搜索结果,发现大部分资料来自培训机构,比如传智播客的黑马程序员课程。黑马点评可能是一个教学项目,用于教授分布式系统和Redis应用。 根据搜索结果,黑马点评是一个模拟大众点评的实战项目,重点在于Redis的应用,比如缓存、分布式锁、秒杀等。技术栈可能包括SpringBootRedis、MySQL、MyBatis-Plus。需要整合这些信息,分模块介绍,比如项目定位、技术架构、核心功能、适用场景。 用户可能需要详细的技术细节,比如如何实现缓存击穿解决方案,或者如何用Redis实现分布式锁。同时,要避免使用第一人称,按照要求分点说明,每个部分换行。需要检查是否有代码或公式,如果有,要正确格式化。 最后,生成相关问题时要基于项目本身,考虑用户可能进一步想知道的技术点、学习资源或部署方式。比如项目是否开源,涉及哪些技术难点,适合的学习阶段等。确保问题相关且具体,帮助用户深入探索。</think>黑马点评是传智播客旗下黑马程序员课程中的实战项目案例,定位为本地生活服务平台,模拟大众点评的业务场景。该项目主要用于教学分布式系统开发及Redis实战应用,帮助学习者掌握企业级项目开发流程和高并发场景解决方案。 **技术架构** 技术栈包含SpringBoot+MySQL+MyBatis-Plus+Redis+Nginx,采用前后端分离架构。前端使用Vue.js实现页面交互,后端通过Restful API提供数据接口。项目部署采用Docker容器化方案,支持多环境配置。 **核心功能模块** - 商户查询模块:实现LBS地理定位查询,结合Redis GEO存储商户坐标数据 - 优惠券秒杀模块:采用Redis+Lua脚本实现库存原子性操作,通过Redisson分布式锁解决超卖问题 - 签到统计模块:使用Redis BitMap实现每日签到功能,支持连续签到统计 - 缓存优化模块:实现多级缓存架构,采用缓存穿透解决方案(布隆过滤器+空值缓存) - 好友关注模块:基于Redis ZSet实现Feed流推送,支持按时间排序的动态更新 **Redis实战应用** 1. 分布式锁实现:通过`SETNX key value`命令配合Lua脚本保证原子性 ```lua if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then return redis.call('expire', KEYS[1], ARGV[2]) else return 0 end ``` 2. 秒杀库存扣减:使用Hash结构存储库存信息,扣减公式为 $$stock = redis.call('HINCRBY', KEYS[1], 'stock', -1)$$ **适用场景** 项目适合具备Java Web基础的学习者,作为分布式系统开发入门案例。通过该案例可掌握: - 基于Redis的缓存设计模式 - 高并发场景解决方案 - 分布式系统常见问题处理 - 微服务架构下的性能优化
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值