外卖点餐系统07

外卖点餐系统07

缓存菜品

问题说明

用户通过微信小程序查询菜品,小程序会将请求发送给后端服务,后端就会查询MySQL数据库。
用户端小程序展示的菜品数据都是通过查询数据库获得,如果用户端访问量比较大,数据库访问压力随之增大,就会造成系统响应慢,用户体验差

解决思路

  • 通过Redis来缓存菜品数据,减少数据库查询操作。
    Redis缓存数据,减少数据库访问.png
    微信小程序查询数据后,会向后端服务发送请求,判断请求的数据在缓存中是否存在,如果存在直接读取缓存,不存在再查询MySQL,并将该数据载入缓存。
  • 缓存逻辑分析
    缓存逻辑分析,页面显示按照菜谱分类.png
    微信小程序展示菜品粒度是根据分类展示,所以缓存数据应该根据分类缓存菜品。
    1. 每个分类下的菜品保存一份缓存数据
    • 使用分类id作为key,分类下的菜品数据使用String字符串保存,分类下的菜品应该是List集合,在Java中可以通过序列化将这个集合转换成Redis的字符串类型
    1. 数据库中菜品数据有变更时及时清理缓存数据

代码开发

  1. 修改用户端接口 DishController 的 list 方法,加入缓存处理逻辑
 public Result<List<DishVO>> list(Long categoryId) {
        // 微信小程序通过这个controller显示商品浏览界面,
        // 这个请求操作经常被执行,每次请求都需要查询数据库,如果用户过多会对MySQL造成太大压力,所以我们将每个分类的菜品缓存起来

        // 查询redis中是否存在菜品数据,查询数据需要获取key,所以我们首先要构造redis中的key
        // 我们统一key规则,dish_分类id
        String key = "dish_" + categoryId;
        List<DishVO> list = (List<DishVO>) redisTemplate.opsForValue().get(key);
        // 如果存在,直接返回菜品数据,不需要查询MySQL
        if(list == null && list.size() > 0){
            return Result.success(list);
        }
        // 如果不存在,查询MySQL,并将查询到数据,放入redis
        Dish dish = new Dish();
        dish.setCategoryId(categoryId);
        dish.setStatus(StatusConstant.ENABLE);//查询起售中的菜品
        list = dishService.listWithFlavor(dish);

        redisTemplate.opsForValue().set(key, list);
        return Result.success(list);
    }
  1. 修改管理端接口 DishController 的相关方法,加入清理缓存的逻辑,需要改造的方法:
  • 新增菜品 save
  • 修改菜品 update
  • 批量删除菜品 deleteBatch
  • 起售、停售菜品 startOrStop
DishController.java
    /**
     * 新增菜品
     * @param dishDTO
     * @return
     */
    @PostMapping
    @ApiOperation("新增菜品")
    public Result save(@RequestBody DishDTO dishDTO) {
      log.info("新增菜品:{}", dishDTO);
      dishService.saveWithFlavor(dishDTO);        // 新增菜品时也需要操作flavor表
    
      // 清理缓存数据,这里可以精确清理改变的那个分类
      String key = "dish_" + dishDTO.getCategoryId();
      cleanCache(key);
    
      return Result.success();
    }
    
    /**
     * 统一清理菜品缓存数据
     * @param pattern
     */
    private void cleanCache(String pattern) {
        // Set keys = redistemplate.keys("dish_*");         // 获取redis中 以dish_开头的所有key
        Set keys = redisTemplate.keys(pattern );
        redisTemplate.delete(keys);
    }

使用Spring Cache缓存框架 缓存套餐

Spring Cache

Spring Cache是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能。

Spring Cache 提供了一层抽象,底层可以切换不同的缓存实现,例如:

  • EHCache
  • Caffeine
  • Redis
    这让我们能够轻松的切换缓存实现。
    为了在项目中使用Spring Cache,只需要在项目pom.xml文件中导入Maven坐标,项目导入redis的Maven坐标后,Spring Cache自动使用redis缓存实现。

Spring Cache常用注解

注解 说明
@EnableCaching 开启缓存注解功能,通常加在启动类上
@Cacheable 在方法执行前先查询缓存中是否有数据,如果有数据,则直接返回缓存数据;如果没有缓存数据,调用方法并将方法返回值放到缓存中
@CachePut 将方法的返回值放到缓存中
@CacheEvict 将一条或多条数据从缓存中删除

举例:

@PostMapping
// 使用Spring Expression Language (SpEL)expression for computing the key dynamically
// 这里的key是通过Spring EL动态获得的
@CachePut(cacheNames = "userCache", key = "#user.id")   // 如果使用Spring Cache缓存数据,key的生成:userCache::2
public User save(@RequestBody User user) {
    userMapper.insert(user);
    return user;
}

这里涉及到了Redis的目录结构,redis使用冒号" : "来逻辑上形成树形目录

缓存套餐代码开发

具体思路如下:

  • 导入Spring Cache和Redis相关maven坐标
  • 在启动类上加入@EnableCaching注解,开启缓存注解功能
  • 在用户端接口SetmealController的list方法(因为用户端是通过该方法查询套餐较多)上加入@Cacheable注解
  • 在管理端接口SetmealController的save、delete、update、startOrStop等方法上加入CacheEvict注解

购物车代码开发

需求分析和设计

  1. 需求分析: 用户暂存所选菜品和套餐功能
  • 对于套餐直接加入购物车
  • 菜品如果没有设置口味数据,直接添加到购物车
  • 菜品如果设置口味数据,需要先选择口味数据,再加入购物车
  • 如果商品已经添加购物车,想要添加同样的商品,直接增加份数
  1. 接口设计:
  • 请求方式:POST
  • 请求路径:/user/shoppingCart/add
  • 请求参数:套餐id、菜品id、口味
  • 返回结构:code、data、msg
    购物车接口设计.png
  1. 数据库设计(shopping_cart):
  • 作用:暂时存放所选商品的地方
  • 选的什么商品:套餐setmeal_id, 菜品dish_id,口味字段dish_flavor
  • 每个商品买了几个:商品数量 number
  • 不同用户的购物车需要区分开: user_id
字段名 数据类型 说明 备注
id bigint 主键 自增
name varchar(32) 商品名称 冗余字段
image varchar(255) 商品图片路径 冗余字段
user_id bigint 用户 id 逻辑外键
dish_id bigint 菜品 id 逻辑外键
setmeal_id bigint 套餐 id 逻辑外键
dish_flavor varchar(50) 菜品口味
number int 商品数量
amount decimal(10, 2) 商品单价 冗余字段
create_time datetime 创建时间
冗余字段,反复出现重复的字段,在菜品表dish中含有name,image,amount这些字段,但是这个表再加上这些冗余字段,可以提高查询数据效率,因为增加了冗余字段就只需要单表查询,如果不设置这些冗余字段可能就需要查找套餐表或者菜品表。
注意:冗余字段不能大量使用,设置的字段不能经常变化

购物车代码开发

购物车增加商品

我们要将某个菜品添加到购物车之前要先判断该菜品是否已经存在,如果存在,只需要执行修改操作数量字段加一,如果不存在,再执行插入操作。

/**
     * 添加购物车
     * @param shoppingCartDTO
     */
    public void addShoppingCart(ShoppingCartDTO shoppingCartDTO) {
        // 传入的shoppingCartDTO中,可能包含 setmealId或者dishId,dishFlavor
        // 不同用户需要不同的购物车,所以我们需要首先获取当前用户id
        ShoppingCart shoppingCart = new ShoppingCart();
        BeanUtils.copyProperties(shoppingCartDTO, shoppingCart);
        // 通过threadLocal获取当前用户id,在拦截器中解析token获取到了用户id,并在拦截器中存储到threadlocal中
        Long userId = BaseContext.getCurrentId();
        shoppingCart.setUserId(userId);

        // 我们要将某个菜品添加到购物车之前要先判断该菜品是否已经存在,使用套餐或者菜品id加上用户id查询菜品是否依旧存在购物车
        // select * from shopping_cart where user_id = ? and setmeal_id = ?
        // select * from shopping-cart where user_id = ? and dish_id = ? and dish_flavor = ?
        // 使用MyBatis动态SQL实现
        List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart);

        // 如果存在,只需要执行修改操作数量字段加一
        if(list != null && list.size() > 0) {
            ShoppingCart cart = list.get(0);
            cart.setNumber(cart.getNumber() + 1);
            shoppingCartMapper.update(cart);
        } else {
            //如果不存在,再执行插入操作
            // 判断添加到购物车的商品是菜品还是套餐,只有知道了,才能去具体查某个表
            Long dishId = shoppingCartDTO.getDishId();
            if(dishId != null) {
                // 本次添加到购物车的商品是菜品,根据dishID查出菜品信息,拷贝到ShoppingCart然后插入
                Dish dish = dishMapper.getById(dishId);
                shoppingCart.setName(dish.getName());
                shoppingCart.setAmount(dish.getPrice());
                shoppingCart.setImage(dish.getImage());
            } else {
                // 本次添加到购物车的商品是套餐
                Setmeal setmealId = setmealMapper.getById(shoppingCartDTO.getSetmealId());
                shoppingCart.setName(setmealId.getName());
                shoppingCart.setAmount(setmealId.getPrice());
                shoppingCart.setImage(setmealId.getImage());
            }

            shoppingCart.setNumber(1);
            shoppingCart.setCreateTime(LocalDateTime.now());
            shoppingCartMapper.insert(shoppingCart);
        }

    }

删除购物车中的商品

/**
     * 删除购物车商品
     * @param shoppingCartDTO
     */
    public void subShoppingCart(ShoppingCartDTO shoppingCartDTO) {
        // 如果要删除的菜品或者套餐,份数大于1,直接修改number字段
        // 如果要删除的商品只有一份,定执行删除操作,直接删除
        ShoppingCart shoppingCart = new ShoppingCart();
        BeanUtils.copyProperties(shoppingCartDTO, shoppingCart);
        Long userId = BaseContext.getCurrentId();
        shoppingCart.setUserId(userId);
        // 获取要删除的商品份数
        List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart);
        if(list != null && list.size() > 0) {
            ShoppingCart cart = list.get(0);
            int number = list.get(0).getNumber();
            if (number > 1) {
                cart.setNumber(number - 1);
                shoppingCartMapper.update(cart);
            } else {
                // 如果要删除的商品只有一份,执行删除操作,确定执行删除操作,要判断时删除菜品,还是套餐
                shoppingCartMapper.subDelete(shoppingCart);
            }
        }
        // delete from shopping_cart where dish_id = #{shoppingCartDTO.dishId}
        // delete from shopping_cart where setmeal_id = #{shoppingCartDTO.setmealId}
    }

Read more

如何设计秒杀系统

什么是秒杀设计 秒杀设计 是一种针对高并发、大流量场景的系统设计,通常应用于电商活动中(如限时抢购、促销等),用户在非常短的时间内大量涌入系统,抢购有限的商品或优惠。这种场景下,系统需要能够承受巨大的瞬时并发请求,同时保证数据的一致性和业务的正确性。 秒杀技术分析 秒杀系统贯穿活动、商品和下单三个领域,涵盖了页面静态化、接口限流、Redis预减库存、异步下单和接口动态化等技术。 需要迎接的挑战有: 1. 高并发和压力测试:秒杀活动会带来巨大的流量,服务器和数据库的并发处理能力是关键。 2. 保证数据一致性:抢购涉及到商品库存的实时减少,需要保证在高并发场景下库存数据的准确性和一致性 3. 防止超卖和重复购买:确保同一商品不会被重复购买,同时避免超卖,即使是在极端的高并发情况下 4. 分布式锁和限流:使用分布式锁来保护关键资源,限制用户访问频率以免系统崩溃 5. 性能优化:包括代码层面的优化、数据库的优化、缓存的使用等,以提高系统性能和响应速度。 页面静态化 秒杀页面静态化是将动态生成的秒杀页面转换为静态HTML页面,从而提高页面响应速度和系统性能

By Yucan Huang

分布式系统下雪花算法生成全局唯一ID

雪花算法 在分布式系统中,为了避免多个节点生成相同的唯一标识符id,我们通常使用一种全局唯一ID生成策略,而Snowflake Algorithm就是广泛使用的解决方案之一。 雪花算法简介 雪花算法生成的 ID 是一个 64 位的二进制整数,具有以下组成部分: 部分 字节长度 描述 符号位 1bit 固定为0,因为生成的ID是整数 时间戳 41bits 表示时间戳,单位是毫秒,可以存储大约68年的时间 机器ID 10bits 用于标识不同的机器(节点)。10位可以将其分数据中心ID(5位)和机器ID(5位),共支持1024台机器同时生成ID 序列号 12bits 同一毫秒内的序列号,用来区分在同一台机器、同一时间戳下生成的多个ID,最大支持同一毫秒生成4096个唯一ID 代码实现 package com.can.springbootmessage; public class SnowflakeIdGenerator { // 起始时间戳,Long是64位,

By Yucan Huang

责任链设计模式

什么是责任链模式 项目有个请求,需要有对应的服务来处理,然后这个请求可能需要被很多个层级权限的服务来处理。我们将这些处理该请求的服务放在一条链上,链从前往后,是层级更高的服务,第一个服务处理不了,传递到链上的下一个服务,直到这个请求被处理成功。 责任链模式(Chain of Responsibility)是一种处理请求的模式,它让多个处理器都有机会处理该请求,直到其中某个处理器成功处理该请求,责任链模式把多个处理器串成链,然后让请求在链上传递。 1. 如何把多个处理器串成链,然后让请求在链上传递 客户端发送请求,处理类去处理它的请求,所以有个处理方法(handleRequest),处理类要连接在一起,所以**处理类要有一个方法(成员变量nextHandler)**指向下一个处理类。 我们抽象出一个公共的父类,然后去定义不同的处理类,这些处理类通过nextHandler连接起来。 2. 代码演示 package interview.pattern; public class ChainRespPattern { Handl

By Yucan Huang