文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

黑马点评项目总结

2023-09-08 09:28

关注

黑马点评

一、短信登陆功能

1.基于session实现

在这里插入图片描述

2.基于session实现登陆的问题

单体应用时用户的会话信息保存在session中,session存在于服务器端的内存中,由于前前后后用户只针对一个web服务器,所以没啥问题。但是一到了web服务器集群的环境下(我们一般都是用Nginx做负载均衡,若是使用了轮询等这种请求分配策略),就会导致用户小a在A服务器登录了,session存在于A服务器中,但是第二次请求被分配到了B服务器,由于B服务器中没有用户小a的session会话,导致用户小a还要再登陆一次,以此类推。这样用户体验很不好。当然解决办法也有很多种,比如同一个用户分配到同一个服务处理、使用cookie保持用户会话信息等。
因此,要解决这样的问题必须满足以下条件:

3.基于redis实现短信登陆

在这里插入图片描述

发送验证码:

@PostMapping("code")public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {    return userService.sendCode(phone,session);}@Overridepublic Result sendCode(String phone, HttpSession session) {    //1.校验手机号    if (RegexUtils.isPhoneInvalid(phone)) {        //2.如果不符合,返回错误信息        return Result.fail("手机号格式错误!");    }    //3.符合则生成验证码    final String code = RandomUtil.randomNumbers(6);    //4.保存验证码到redis    stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);    //5.发送验证码    log.debug("发送短信验证码成功,验证码:{}",code);    //6.返回null    return Result.ok();}

验证登陆功能:
login方法会把生成的token返回给前端,浏览器会将其保存到session中。

@PostMapping("/login")public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){    return userService.login(loginForm,session);}@Overridepublic Result login(LoginFormDTO loginForm, HttpSession session) {    //1.校验手机号    final String phone = loginForm.getPhone();    if (RegexUtils.isPhoneInvalid(phone)) {        //2.如果不符合,返回错误信息        return Result.fail("手机号格式错误!");    }    //2.校验验证码,从redis中获取    final String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY+phone);    final String code = loginForm.getCode();    if(cacheCode==null||!cacheCode.equals(code)){        //3.不一直,报错        return Result.fail("验证码错误");    }    //4.一致,根据手机号查询用户    User user = query().eq("phone", phone).one();    //5.判断用户是否存在    if (user == null) {        //6.不存在,创建新用户并保存        user = createUserWithPhone(phone);    }    //7.保存用户信息到redis中    //7.1随机生成token,作为登陆令牌    String token = UUID.randomUUID().toString(true);    //7.2将User对象转为HashMap存储    UserDTO userDTO = BeanUtil.copyProperties(user,UserDTO.class);    final Map<String, Object> map = BeanUtil.beanToMap(userDTO, new HashMap<>(),            CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName,fieldValue)->{                return fieldValue.toString();            })    );    //7.3存储    stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY+token,map);    //7.4设置token有效期    stringRedisTemplate.expire(LOGIN_USER_KEY+token,3000,TimeUnit.MINUTES);    //8.返回token    return Result.ok(token);}private User createUserWithPhone(String phone) {    User user = new User();    user.setPhone(phone);    user.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomString(5));    save(user);    return user;}

这里使用redis的hash结构存储user信息,原因是:

在这里插入图片描述
拦截器:

定义UserHolder工具类:

public class UserHolder {    private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();    public static void saveUser(UserDTO user){        tl.set(user);    }    public static UserDTO getUser(){        return tl.get();    }    public static void removeUser(){        tl.remove();    }}

刷新token拦截器:

@Slf4jpublic class RefreshTokenInterceptor implements HandlerInterceptor {    private StringRedisTemplate stringRedisTemplate;    public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate){        this.stringRedisTemplate = stringRedisTemplate;    }    @Override    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {        //1.获取请求头中的token        final String token = request.getHeader("authorization");        if (token == null) {            return true;        }        //2.获取redis中的用户        final Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(LOGIN_USER_KEY + token);        //3.判断用户是否存在        if (userMap.isEmpty()) {            return true;        }        //5.将查询到的Hash数据转换为UserDto对象        final UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);        //6.存在,保存用户信息到ThreadLocal        UserHolder.saveUser(userDTO);        //7.刷新token有效期        stringRedisTemplate.expire(LOGIN_USER_KEY+token,3000, TimeUnit.MINUTES);        //8.放行        return true;    }    @Override    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {        UserHolder.removeUser();    }}

登陆拦截器:

public class LoginInterceptor implements HandlerInterceptor {    @Override    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {        //1.判断是否需要拦截(ThreadLocal中是否有用户)        if(UserHolder.getUser()==null){            response.setStatus(401);            return false;        }        //8.放行        return true;    }    @Override    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {        UserHolder.removeUser();    }}

在配置类中配置拦截器:

@Configurationpublic class MvcConfig implements WebMvcConfigurer {    @Resource    private StringRedisTemplate stringRedisTemplate;    @Override    public void addInterceptors(InterceptorRegistry registry) {        //登陆拦截器        registry.addInterceptor(new LoginInterceptor()).excludePathPatterns(                "/user/code","/user/login","/blog/hot","/shoppublic <R,ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Long time, TimeUnit unit,Function<ID,R> dbFallback){    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){        //将空值写入reddis        stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL, TimeUnit.MINUTES);        return null;    }    //6.存在,写入redis    this.set(key,r,time,unit);    //7.返回    return r;}

b.缓存雪崩

缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
解决方案:

c.缓存击穿

缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
常见的解决方案有两种:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

4.基于逻辑过期解决缓存击穿问题

在这里插入图片描述

private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);public <R,ID> R queryWithLogicalExpire(String keyPrefix,ID id,Class<R> type, Long time, TimeUnit unit,Function<ID,R> dbFallback){    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);    JSONObject data = (JSONObject) redisData.getData();    R r = JSONUtil.toBean(data, type);    LocalDateTime expireTime = redisData.getExpireTime();    //5.判断是否过期    if(expireTime.isAfter(LocalDateTime.now())){        //5.1未过期,直接返回        return r;    }    //5.2已过期,需要缓存重建    //6.缓存重建    //6.1获取互斥锁    String lockkey = LOCK_SHOP_KEY + id;    boolean lock = tryLock(lockkey);    //6.2判断是否获取锁成功    if(lock){        //6.3成功,开启独立线程,实现缓存重建        CACHE_REBUILD_EXECUTOR.submit(()->{            try {                //查询数据库                R r1 = dbFallback.apply(id);                //写入redis                this.setWithLogicalExpire(key,r1,time,unit);            } catch (Exception e) {                e.printStackTrace();            } finally {                //释放锁                unlock(lockkey);            }        });    }    //6.4返回商铺信息    return r;}

三、优惠券秒杀

1.优惠券秒杀下单

一般流程:
在这里插入图片描述

2.超卖问题

请求a查询库存,发现库存为1,请求b这时也来查询库存,库存也为1,然后请求a让数据库减1,这时候b查询到的仍然是1,也继续让库存减1,就会导致超卖。

超卖问题有以下几个解决方案:

实现乐观锁主要有以下两种方法:

  1. 版本号法

每次更新数据库的时候按照版本查询,并且要更新版本。
在这里插入图片描述

  1. CAS

CAS是英文单词Compare And Swap的缩写,翻译过来就是比较并替换
CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。
在这里插入图片描述

CAS的缺点:

CPU开销较大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。

不能保证代码块的原子性
CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。

3.一人一单功能

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

在这里插入图片描述
这样的方式会产生并发安全问题:
在这里插入图片描述

通过加锁可以解决在单机情况下的一人一单安全问题,但是在集群模式下就不行了(每个jvm都有自己的锁监视器,集群模式下各个服务器的锁不共享)。
因此,我们的解决方案就是实现一个共享的锁监视器,即:
分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁。

4.基于redis的分布式锁

a.setnx命令

setnx = SET if Not eXists

在这里插入图片描述

b.普通setnx分布式锁出现的问题

在某个线程获取锁执行业务时若发生阻塞,且阻塞过程中锁超时,此时另一个线程同样来请求锁,发现可以获取锁,但实际上前一个线程还没执行完。

解决方案:

在这里插入图片描述

四、消息队列优化

(三四章先占坑)

五、达人探店

1.发布探店笔记

简单的crud
在这里插入图片描述

2.实现点赞功能

需求:

实现步骤:

  1. 给Blog类中添加一个isLike字段,标示是否被当前用户点赞
  2. 修改点赞功能,利用Redis的set集合判断是否点赞过,未点赞过则点赞数+1,已点赞过则点赞数-1
  3. 修改根据id查询Blog的业务,判断当前登录用户是否点赞过,赋值给isLike字段
  4. 修改分页查询Blog业务,判断当前登录用户是否点赞过,赋值给isLike字段

3.点赞排行榜

需求:按照点赞时间先后排序,返回Top5的用户
使用SortedSet:

在这里插入图片描述
完整代码:
BlogController

@RestController@RequestMapping("/blog")public class BlogController {    @Resource    private IBlogService blogService;    @PutMapping("/like/{id}")    public Result likeBlog(@PathVariable("id") Long id) {        return blogService.likeBlog(id);    }    @GetMapping("/hot")    public Result queryHotBlog(@RequestParam(value = "current", defaultValue = "1") Integer current) {        return blogService.queryHotBlog(current);    }    @GetMapping("/{id}")    public Result queryBlogById(@PathVariable("id") String id){        return blogService.queryBlogById(id);    }    @GetMapping("/likes/{id}")    public Result queryBlogLikes(@PathVariable("id") String id) {        return blogService.queryBlogLikes(id);    }}

IBlogService

public interface IBlogService extends IService<Blog> {    Result queryBlogById(String id);    Result queryHotBlog(Integer current);    Result likeBlog(Long id);    Result queryBlogLikes(String id);}

BlogServiceImpl

Servicepublic class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {    @Autowired    private IUserService userService;    @Autowired    private StringRedisTemplate stringRedisTemplate;    @Override    public Result queryHotBlog(Integer current) {        // 根据用户查询        Page<Blog> page = query()                .orderByDesc("liked")                .page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));        // 获取当前页数据        List<Blog> records = page.getRecords();        // 查询用户        records.forEach(blog -> {            this.queryBlogUser(blog);            this.isBlogLiked(blog);        });        return Result.ok(records);    }    @Override    public Result likeBlog(Long id) {        // 1、获取登录用户        UserDTO user = UserHolder.getUser();        // 2、判断当前登录用户是否已经点赞        Double score = stringRedisTemplate.opsForZSet().score(RedisConstants.BLOG_LIKED_KEY + id, user.getId().toString());        if(score == null) {            // 3、如果未点赞,可以点赞            // 3.1、数据库点赞数 +1            boolean isSuccess = update().setSql("liked = liked+1").eq("id", id).update();            // 3.2、保存用户到 Redis 的 set 集合            if(isSuccess){                // 时间作为 key 的 score                stringRedisTemplate.opsForZSet().add(RedisConstants.BLOG_LIKED_KEY + id, user.getId().toString(), System.currentTimeMillis());            }        } else {            // 4、如果已点赞,取消点赞            // 4.1、数据库点赞数 -1            boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update();            // 4.2、把用户从 Redis 的 set 集合移除            if(isSuccess){                stringRedisTemplate.opsForZSet().remove(RedisConstants.BLOG_LIKED_KEY + id, user.getId().toString());            }        }        return Result.ok();    }    @Override    public Result queryBlogLikes(String id) {        String key = RedisConstants.BLOG_LIKED_KEY + id;        // 查询 top5 的点赞用户        Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);        if(top5 == null){            return Result.ok(Collections.emptyList());        }        // 解析出其中的用户id        List<Long> ids = top5.stream().map(Long::valueOf).collect(Collectors.toList());        String join = StrUtil.join(",", ids);        // 根据用户id查询用户        List<UserDTO> userDTOS = userService.query().in("id", ids).last("order by filed(id, "+join+")").list()                .stream()                .map(user -> BeanUtil.copyProperties(user, UserDTO.class))                .collect(Collectors.toList());        return Result.ok(userDTOS);    }    private void queryBlogUser(Blog blog) {        Long userId = blog.getUserId();        User user = userService.getById(userId);        blog.setName(user.getNickName());        blog.setIcon(user.getIcon());    }    @Override    public Result queryBlogById(String id) {        Blog blog = getById(id);                if(blog == null){            return Result.fail("笔记不存在!");        }        queryBlogUser(blog);        // 查询 Blog 是否被点赞        isBlogLiked(blog);        return Result.ok(blog);    }    private void isBlogLiked(Blog blog) {        UserDTO user = UserHolder.getUser();        if(user == null){            return;        }        Long userId = user.getId();        String key = RedisConstants.BLOG_LIKED_KEY + blog.getId();        Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());        blog.setIsLike(score != null);    }}

六、好友关注

1.关注和取关

在这里插入图片描述

基于mysql实现:
在这里插入图片描述
基于redis实现:
设置一个给每个用户设置一个key,利用set结构存储关注该用户的人。
代码:

@PutMapping("/{id}/{isFollow}")public Result follow(@PathVariable("id") Long followUserId,@PathVariable("isFollow") Boolean isFollow){    return followService.follow(followUserId,isFollow);}@Overridepublic Result follow(Long followUserId, Boolean isFollow) {    Long userId = UserHolder.getUser().getId();    String key = "follows:" + userId;    //1.判断关注还是取关    if(isFollow){        //2.关注,新增数据        Follow follow = new Follow();        follow.setUserId(userId);        follow.setFollowUserId(followUserId);        boolean success = save(follow);        if(success){            //把关注用户的id,放入redis的set集合            stringRedisTemplate.opsForSet().add(key,followUserId.toString());        }    }else{        //3.取关,删除数据        boolean success = remove(new QueryWrapper<Follow>().eq("user_id", userId).eq("follow_user_id", followUserId));        //把关注的用户id从redis集合中移除        if(success)  stringRedisTemplate.opsForSet().remove(key,followUserId.toString());    }    return Result.ok();}
@GetMapping("/or/not/{id}")public Result isFollow(@PathVariable("id") Long followUserId){    return followService.isFollow(followUserId);}@Overridepublic Result isFollow(Long followUserId) {    Long userId = UserHolder.getUser().getId();    //1.查询是否关注    Integer count = query().eq("user_id", userId).eq("follow_user_id", followUserId).count();    return Result.ok(count>0);}

2.共同关注

在这里插入图片描述
在关注点击关注用户时,用redis的set结构存储自己关注了哪些用户,然后利用集合的交集就能轻松求出共同关注的用户了。

@GetMapping("/common/{id}")public Result followCommons(@PathVariable("id") Long id){    return followService.followCommons(id);}@Overridepublic Result followCommons(Long id) {    //1.获取当前用户    Long userId = UserHolder.getUser().getId();    String key = "follows:" + userId;    //2.求交集    String key2 = "follows:" + id;    Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key, key2);    if(intersect==null||intersect.isEmpty()){        //无交集        return Result.ok(Collections.emptyList());    }    //3.解析id集合    List<Long> ids = intersect.stream().map(Long::valueOf).collect(Collectors.toList());    //4.查询用户    List<UserDTO> users = userService.listByIds(ids)            .stream()            .map(user -> BeanUtil.copyProperties(user, UserDTO.class))            .collect(Collectors.toList());    return Result.ok(users);}

3.关注推送

在这里插入图片描述

Feed流产品有两种常见模式:

本例中的个人页面,是基于关注的好友来做Feed流,因此采用Timeline的模式。该模式的实现方案有三种:

这里基于推模式模式实现关注推送,需求:

4.实现推送功能

推送:

@PostMappingpublic Result saveBlog(@RequestBody Blog blog) {    return blogService.saveBlog(blog);}@Overridepublic Result saveBlog(Blog blog) {    // 1.获取登录用户    UserDTO user = UserHolder.getUser();    blog.setUserId(user.getId());    // 2.保存探店博文    boolean success = this.save(blog);    if(!success) return Result.fail("新增笔记失败!");    //3.查询笔记作者的所有粉丝    List<Follow> follows = followService.query().eq("follow_user_id", user.getId()).list();    //4.推送笔记id给有所粉丝    for (Follow follow : follows) {        //4.1获取粉丝id        Long followId = follow.getUserId();        //4.2推送        String key = FEED_KEY+followId;        stringRedisTemplate.opsForZSet().add(key,blog.getId().toString(),System.currentTimeMillis());    }    // 返回id    return Result.ok(blog.getId());}

读取:

@GetMapping("/of/follow")public Result queryBlogOfFollow(        @RequestParam("lastId") Long max,@RequestParam(value = "offset",defaultValue = "0") Integer offset){    return blogService.queryBlogOfFollow(max,offset);}@Overridepublic Result queryBlogOfFollow(Long max, Integer offset) {    //1.获取当前用户    Long userId = UserHolder.getUser().getId();    //2.查询收件箱    String key = FEED_KEY + userId;    Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet()            .reverseRangeByScoreWithScores(key, 0, max, offset, 2);    if(typedTuples==null||typedTuples.isEmpty()) return Result.ok();    //3.解析数据:blogId,minTime(时间戳),offset    List<Long> ids = new ArrayList<>(typedTuples.size());    long minTime = 0;    int os = 1;    for (ZSetOperations.TypedTuple<String> tuple : typedTuples) {        //3.1查询id        String idStr = tuple.getValue();        ids.add(Long.valueOf(idStr));        //4.2获取分数(时间戳)        if(tuple.getScore().longValue()==minTime){            os++;        }else os  = 1;        minTime = tuple.getScore().longValue();    }    //4.根据id查询blog    String idStr = StrUtil.join(",", ids);    List<Blog> blogs = query()            .in("id",ids).last("ORDER BY FIELD(ID,"+idStr+")").list();    for (Blog blog : blogs) {        queryBlogUser(blog);        isBlogLiked(blog);    }    //5.封装并返回    ScrollResult r = new ScrollResult();    r.setList(blogs);    r.setOffset(os);    r.setMinTime(minTime);    return Result.ok(r);}

七、签到功能

1.数据库实现

在这里插入图片描述

2.redis实现

在这里插入图片描述

3.具体代码

@PostMapping("/sign")public Result sign(){    return userService.sign();} @Override public Result sign() {     //1.获取当前用户     Long userId = UserHolder.getUser().getId();     //2.获取日期     LocalDateTime now = LocalDateTime.now();     //3.拼接key     String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));     String key = USER_SIGN_KEY + userId + keySuffix;     //4.获取今天是本月的第几天     int dayOfMonth = now.getDayOfMonth() - 1;     //5.写入redis     stringRedisTemplate.opsForValue().setBit(key,dayOfMonth,true);     return Result.ok(); }

来源地址:https://blog.csdn.net/qq_45733304/article/details/126443684

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-服务器
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯