面试项目准备:黑马点评项目总结


1. 项目介绍

  黑马点评项目是一个前后端分离项目,类似于大众点评,实现了发布查看商家,达人探店,点赞,关注等功能,业务可以帮助商家引流,增加曝光度,也可以为用户提供查看提供附近消费场所,主要。用来配合学习Redis的知识。
  基于 Redis + Springboot的点评APP ,实现了短信验证码登录、查找店铺、秒杀优惠券、发表点评、关注推送的完 整业务流程。

1.1 项目使用的技术栈

  SpringBoot+Mysql+Lombok+MyBatis-Plus+Hutool+Redis

1.2 项目架构

在这里插入图片描述
后端部署在Tomcat上,前端部署在Nginx。

Nginx作用:

1. 反向代理Tomcat服务器,解决多台服务器,session不共享问题,隐藏真实服务地址。
2. 负载均衡降低服务器压力。三种负载均衡方式:轮询法(默认方法)、weight权重模式(加权轮询)、ip_hash
Nginx的静态处理能力很强,但是动态处理能力不足,因此,在企业中常用动静分离技术。


2. 各个功能模块

2.1 登录模块

短信登录功能(基于session)

在这里插入图片描述

  1. 发送验证码
    校验手机号、判断格式是否正确、正确生成验证码、发送验证码。
  2. 校验手机号和验证码
    校验手机号、校验验证码、查找用户、如果没有创建用户,保存用户到session。

以上完成的两步把用户信息保存到session中了。然而有许多页面都需要用户信息和校验登录状态。

  1. 校验登录状态
     访问不通的后端控制器,要获取数据之前需要校验登录状态,用拦截器实现最好,减少代码冗余。
     拦截器实现,访问之前从session中获取用户,如果用户存在放行,并且把用户保存到ThreadLocal中去,不同的线程互不干扰。访问之后,把ThreadLocal保存的信息删除。
     配置拦截器生效,选择要拦截的请求或是排除不拦截的。
基于redis的短信登录

 session共享问题:多台Tomcat并不共享session存储空间,当请求切换到不同服务器会导致数据丢失问题。(可以用Tomcat间的数据同步解决,但还是会出现数据不一致和占用内存问题)
 session代替方案应该满足:

  • 数据共享
  • 内存存储
  • key,value结构
    使用redis代替session是完全可以的

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
问题:是访问不拦截的页面,token不会刷新。而session是访问哪个页面都会刷新。
优化:再加一个拦截器,拦截所有请求并且有token的话就刷新,第二个则判断用户是否在ThreadLocal中存在。这样就不会出现不刷新的现象。


2.2 用户查询缓存模块

 什么是缓存?
数据交换的缓冲区(cache),是贮存数据的临时地方,一般读写性能高。
 缓存的作用:
1. 降低后端负载。
2. 提高读写效率,降低响应时间。
 缓存的成本:
1. 数据一致性成本
2. 代码维护成本
3. 运维成本

添加Redis缓存
在这里插入图片描述
根据id查询店铺缓存的流程
在这里插入图片描述
在这里插入图片描述
主动更新策略
在这里插入图片描述

在这里插入图片描述

先删除缓存,在操作数据库

  • 在线程1 删除缓存后,线程2查询缓存未命中,然后去查询数据库,最后把查询结果写入缓存。但此时,线程1更新数据库的操作还没有完成,线程2查到的是旧的值,写入了缓存。当线程1更新完数据库之后,就会造成数据库和缓存数据不一致问题。

先操作数据库,再删除缓存

  • 要想并发问题发生,首先要线程1查询缓存,刚好缓存失效,然后去查询数据库。此时线程2要去更新数据库,然后去删除缓存。如果线程2在线程1的写入缓存之前更新完数据库和删除完缓存,南无就会造成数据不一致问题。但毕竟缓存的操作速度快和线程1查询时缓存刚好失效并且线程2要去更新数据库。这些事情发生的概率极小。

所以选择先操作数据库,再删除缓存。(给数据库操作加锁的话,应该可以解决并发问题)

在这里插入图片描述

缓存穿透

  • 是指用户要查询的数据在缓存和数据库中都没有,这样缓存永远不会生效,所有的请求都会到达数据库,造成数据库巨大的压力。

在这里插入图片描述
常见的解决方案有两种

  • 缓存空对象
    • 优点:实现简单,维护方便。
    • 缺点:内存的消耗、短期的数据不一致。
  • 布隆过滤器
    • 优点:内存占用少,没有多余的key.
    • 缺点:实现复杂、存在误差。

查询店铺的缓存穿透解决(使用缓存空对象方式)
在这里插入图片描述
代码如下:

public Shop queryWithPassThrough(Long id) {
        String key = CACHE_SHOP_KEY + id;
        //1 从Redis查询商铺信息
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //2 判断是否存在,isNotBlank(null," ", "")
        if (StrUtil.isNotBlank(shopJson)) {
            //3 存在,直接返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return shop;
        }

        if (shopJson != null) { //因为等于null是没有查到缓存,其他的""、" "是缓存的空对象直接返回
            //返回错误信息
            return null;
        }

        //4 不存在,根据id查询数据库
        Shop shop = getById(id);
        //5 不存在,返回错误
        if (shop == null) {
            //将空值写入Redis
            stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES); //设置ttl
            return null;
        }

        //6 存在,写入Redis
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);

        //7 返回

        return shop;
    }

null是指没有这个对象,空值(空字符串)是有这个对象,但是里面的内容为空

缓存穿透的解决方案还有哪些?

  • 缓存null值
  • 布隆过滤
  • 增强id的复杂度,避免被猜测id规律
  • 做好数据的基础格式校验
  • 加强用户权限校验
  • 做好热点参数限流

缓存雪崩
 缓存雪崩是指在一段时间大量的key过期或者Redis宕机,导致大量的请求打到数据库,给数据库造成巨大的压力。
在这里插入图片描述
解决方案

  • 给不同可以的TTL添加随机值
  • 利用Redis集群提高服务的可用性
  • 给业务添加降级限流策略
  • 给业务添加多级缓存(浏览器缓存、Nginx缓存、Tomcat缓存等)

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

常见的解决方案有两种

  • 互斥锁
  • 逻辑过期
    在这里插入图片描述
    在这里插入图片描述
    在业务中经常会遇到一致性和可用性的选择。
    需求:修改根据id查询商铺的业务,基于逻辑过期方式来解决缓存击穿问题。
    在这里插入图片描述
    代码如下
 private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    //缓存击穿 逻辑过期
    public Shop queryWithLogicalExpire(Long id) {
        String key = CACHE_SHOP_KEY + id;
        //1 从Redis查询商铺信息
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //2 判断是否存在
        if (StrUtil.isBlank(shopJson)) {
            //3 存在,直接返回
            return null;
        }

        // 4.命中,需要先把json反序列化为对象
        RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
        Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        // 5.判断是否过期
        if (expireTime.isAfter(LocalDateTime.now())) {
            // 5.1未过期,直接返回店铺信息
            return null;
        }
        // 5.2已过期,需要缓存重建
        //6.缓存重建
        //6.1获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;
        boolean isLock = tryLock(lockKey);
        //6.2判断获取锁是否成功
        if (isLock){
            if(!expireTime.isAfter(LocalDateTime.now())) {
                //6.3成功开启独立线程
                CACHE_REBUILD_EXECUTOR.submit(() -> {
                    try {
                        this.saveShop2Redis(id, 20L);
                    } finally {
                        unlock(lockKey);
                    }
                });
            }
        }
        //7 返回

        return shop;
    }

缓存工具的封装
代码如下

//缓存穿透 缓存空对象
    public <R, ID> R queryWithPassThrough(
            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 存在,直接返回
            return JSONUtil.toBean(json, type);
        }

        if (json != null) {
            //返回错误信息
            return null;
        }

        //4 不存在,根据id查询数据库
        R r = dbFallback.apply(id);
        //5 不存在,返回错误
        if (r == null) {
            //将空值写入Redis
            stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }

        //6 存在,写入Redis
        this.set(key, r, time, unit);
        //7 返回

        return r;
    }

    //缓存击穿,设置逻辑过期时间
    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.isBlank(json)) {
            //3 存在,直接返回
            return null;
        }

        // 4.命中,需要先把json反序列化为对象
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
        LocalDateTime expireTime = redisData.getExpireTime();
        // 5.判断是否过期
        if (expireTime.isAfter(LocalDateTime.now())) {
            // 5.1未过期,直接返回店铺信息
            return null;
        }
        // 5.2已过期,需要缓存重建
        //6.缓存重建
        //6.1获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;
        boolean isLock = tryLock(lockKey);
        //6.2判断获取锁是否成功
        if (isLock){
            if(!expireTime.isAfter(LocalDateTime.now())) {
                //6.3成功开启独立线程
                CACHE_REBUILD_EXECUTOR.submit(() -> {
                    try {
                        R r1 = dbFallback.apply(id);
                        this.setWithLogicalExpire(key, r1, time, unit);
                    } finally {
                        unlock(lockKey);
                    }
                });
            }
        }
        //7 返回

        return r;
    }

使用方式

 //一行解决缓存穿透,封装了方法
        Shop shop = cacheClient.queryWithPassThrough(CACHE_SHOP_KEY, id, Shop.class, id2 -> getById(id2), CACHE_SHOP_TTL, TimeUnit.MINUTES);

2.3 优惠券秒杀功能

全局唯一ID
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
代码如下

public long nextId(String keyPrefix) {


        //1. 生成时间戳
        LocalDateTime now = LocalDateTime.now();
        long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
        long timeStamp = nowSecond - BEGIN_TIMESTAMP;
        //2. 生成序列号
        String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd")); //方便统计年月日
        long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);//一天下单的量,拼接日期,还有统计效果

        //3. 拼接并返回
        return timeStamp << COUNT_BITS | count;
    }

全局唯一ID生成策略

  • UUID
  • Redis自增
  • snowflake算法
  • 数据库自增

Redis自增ID策略

  • 每天一个key,方便统计订单量
  • ID构造是时间戳+计数器

实现优惠劵秒杀的下单功能
 下单时需要判断两点:

  • 秒杀是否开始或结束,如果尚未开始或已结束则无法下单
  • 库存是否充足,不足则无法下单

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
更新库存和查询版本是数据库自带命令,是原子操作,不会有线程安全问题。
在这里插入图片描述
在这里插入图片描述
如果字段不是库存,需要加版本号,可以通过分段锁提高成功率,例如currentHashMap中的分段锁。

一人一单:同一个优惠劵,一人只能下一单。
在这里插入图片描述
分布式锁
在集群模式下,普通的锁还会出现问题,因为不同jvm有不同的锁监视器。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
代码如下

public class SimpleRedisLock implements ILock{

    private String name;

    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }
    private StringRedisTemplate stringRedisTemplate;

    private static final String KEY_PREFIX = "lock:";
    private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";

    private static final DefaultRedisScript<Long> UNLOCK_SCRIP;
    static {
        UNLOCK_SCRIP = new DefaultRedisScript<>();
        UNLOCK_SCRIP.setLocation(new ClassPathResource("unlock.lua"));
        UNLOCK_SCRIP.setResultType(Long.class);
    }


    @Override
    public boolean tryLock(long timeoutSec) {
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId , timeoutSec, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlock() {
        //调用lua脚本, 判断和释放在一行代码执行,满足原子性。
        stringRedisTemplate.execute(
                UNLOCK_SCRIP,
                Collections.singletonList(KEY_PREFIX + name),
                ID_PREFIX + Thread.currentThread().getId());
    }

分布式锁基于Redis的极端情况,误删情况
在这里插入图片描述
在这里插入图片描述
极端情况下依然会出现线程误删,释放业务阻塞,以判断完毕。
在这里插入图片描述
获取锁标识并判断要和释放锁是原子操作
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
Redisson入门
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
Redis三种消息队列
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.4 好友关注功能

基于Set集合的关注、取关、共同关注、消息推送等功能
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
实现分页查询


3. 总结

使用 Redis 解决了在集群模式下的 Session共享问题,使用拦截器实现用戶的登录校验和权限刷新
基于Cache Aside模式解决数据库与缓存的一致性问题
使用 Redis 对高频访问的信息进行缓存 ,降低了数据库查询的压力 ,解决了缓存穿透、雪崩、击穿问题使用 Redis + Lua脚 本实现对用戶秒杀资格的预检 ,同时用乐观锁解决秒杀产生的超卖问题
使用Redis分布式锁解决了在集群模式下一人一单的线程安全问题
基于stream结构作为消息队列,实现异步秒杀下单
使用Redis的 ZSet 数据结构实现了点赞排行榜功能,使用Set 集合实现关注、共同关注功能

  • 33
    点赞
  • 47
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
黑马点评是一个项目,可以通过登录系统来使用。首先,您需要启动黑马点评项目,并登录系统。登录后,您可以按F12键,然后选择Network选项,再选择Header选项,这样就可以看到authorization字段。 在代码改进中,SimpleRedisLock类中的ID_PREFIX是通过UUID.randomUUID().toString(true)生成的一个唯一标识。tryLock方法用于获取锁,它会将当前线程的ID作为value与name对应的key存入Redis中,设置超时时间为timeoutSec秒。unlock方法用于释放锁,它会获取当前线程的ID,并与Redis中存储的name对应的value进行比较,如果一致,则删除该key。通过这样的方式,可以保证判断锁标识和释放锁的原子性。 在使用postman进行接口测试时,您可以右键HTTP请求,然后选择添加查看结果树及聚合报告。此外,您还可以添加身份验证token,通过右键HTTP请求,选择添加Config Element,然后选择HTTP Header Manager,并设置相应的参数。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [黑马点评-优惠券秒杀](https://blog.csdn.net/weixin_57393590/article/details/127309715)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *3* [黑马点评项目-优惠券秒杀](https://blog.csdn.net/dingd1234/article/details/124438307)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值