SpringBoot+SSM项目实战 苍穹外卖(7)(Spring Cache)

继续上一节的内容,本节实现缓存菜品、缓存套餐、添加购物车、查看购物车和清空购物车功能。

目录

  • 缓存菜品
  • 缓存套餐(基于Spring Cache)
    • @EnableCaching、@Cacheable、@CachePut和@CacheEvict
    • Spring Cache实现缓存套餐
  • 添加购物车
  • 查看购物车
  • 清空购物车





缓存菜品

用户端小程序展示的菜品数据都是通过查询数据库获得,如果用户端访问量比较大,数据库访问压力随之增大。结果:系统响应慢、用户体验差

在这里插入图片描述

可以通过Redis来缓存菜品数据,减少数据库查询操作。缓存逻辑分析:每个分类下的菜品保存一份缓存数据;数据库中菜品数据有变更时清理缓存数据。

在这里插入图片描述

修改用户端接口 DishController 的 list 方法,加入缓存处理逻辑:

@Autowired
private RedisTemplate redisTemplate;
/**
 * 根据分类id查询菜品
 *
 * @param categoryId
 * @return
 */
@GetMapping("/list")
@ApiOperation("根据分类id查询菜品")
public Result<List<DishVO>> list(Long categoryId) {
    //构造redis中的key,规则:dish_分类id
    String key = "dish_" + categoryId;
    //查询redis中是否存在菜品数据
    List<DishVO> list = (List<DishVO>) redisTemplate.opsForValue().get(key);
    if(list != null && list.size() > 0){
        //如果存在,直接返回,无须查询数据库
        return Result.success(list);
    }

    
    Dish dish = new Dish();
    dish.setCategoryId(categoryId);
    dish.setStatus(StatusConstant.ENABLE);//查询起售中的菜品

    //如果不存在,查询数据库,将查询到的数据放入redis中
    list = dishService.listWithFlavor(dish);
    
    redisTemplate.opsForValue().set(key, list);

    return Result.success(list);
}

为了保证数据库和Redis中的数据保持一致,修改管理端接口 DishController 的相关方法,加入清理缓存逻辑。

需要改造的方法:新增菜品、修改菜品、批量删除菜品和起售、停售菜品。

在管理端DishController中添加清理缓存的方法,保证数据一致性:

/**
 * 清理缓存数据
 * @param pattern
 */
private void cleanCache(String pattern){
    Set keys = redisTemplate.keys(pattern);
    redisTemplate.delete(keys);
}

然后优化以前的方法:
1). 新增菜品优化

@PostMapping
@ApiOperation("新增菜品")
public Result save(@RequestBody DishDTO dishDTO) {
    log.info("新增菜品:{}", dishDTO);
    dishService.saveWithFlavor(dishDTO);

    //清理缓存数据
    String key = "dish_" + dishDTO.getCategoryId();
    cleanCache(key);
    return Result.success();
}

2). 菜品批量删除优化

@DeleteMapping
@ApiOperation("菜品批量删除")
public Result delete(@RequestParam List<Long> ids) {
    log.info("菜品批量删除:{}", ids);
    dishService.deleteBatch(ids);

    // 可能影响多个菜品分类,要知道影响哪些分类还需要等查询之后查数据库才知道比较麻烦
    // 所以直接将redis里所有的菜品缓存数据清理掉,删掉所有以dish_开头的key
    cleanCache("dish_*");
    return Result.success();
}

3). 修改菜品优化

@PutMapping
@ApiOperation("修改菜品")
public Result update(@RequestBody DishDTO dishDTO) {
    log.info("修改菜品:{}", dishDTO);
    dishService.updateWithFlavor(dishDTO);

    // 因为如果只是修改基础属性还好 但如果是修改菜品的分类那要影响俩个key
    // 也干脆将所有的菜品缓存数据清理掉,所有以dish_开头的key
    cleanCache("dish_*");
    return Result.success();
}

4). 菜品起售停售优化

@PostMapping("/status/{status}")
@ApiOperation("修改菜品销售状态")
public Result updateStatus(@PathVariable Integer status,Long id){
    log.info("根据分类id修改菜品销售状态:{}", status);
    dishService.updateStatusById(status,id);

    // 还需要去根据id查询分类id 比较麻烦
    // 也是将所有的菜品缓存数据清理掉,所有以dish_开头的key
    cleanCache("dish_*");
    return Result.success();
}

个人觉得老师这部分的代码写得有些粗暴,大部分都选择了直接删除,但是可能这样在某种程度上也好,不用增加代码逻辑显得过于臃肿,因为本身修改菜品的情况就比较少。


但是新增菜品和删除菜品时感觉还是无需删除,因为新增菜品默认是停售状态,删除菜品时这些菜品必须是停售状态才能删除,只有修改这个菜品的销售状态为起售之后才会在用户端显示该菜品,所以这些地方根本不需要删除redis缓存,在起售停售里删除缓存就行。


然后感觉需要使用缓存的地方还是挺多的,如果大部分都是直接清理掉所有的缓存数据,那这部分的代码是不是用AOP来实现,在需要使用缓存清理的地方加上响应注解更加整洁呢?

下面进行功能测试,可以通过如下方式进行测试:查看控制台sql、前后端联调、查看Redis中的缓存数据

1). 加入缓存

当第一次查询某个分类的菜品时,会从数据为中进行查询,同时将查询的结果存储到Redis中,在后续的访问,若查询相同分类的菜品时,直接从Redis缓存中查询,不再查询数据库。登录小程序:选择蜀味牛蛙(id=17)

在这里插入图片描述

查看控制台sql:有查询语句,说明是从数据库中进行查询

在这里插入图片描述

查看Redis中的缓存数据:说明缓存成功

在这里插入图片描述

再次访问:选择蜀味牛蛙(id=17)

在这里插入图片描述

说明是从Redis中查询的数据。

2). 菜品修改

比如当在后台修改菜品数据时,为了保证Redis缓存中的数据和数据库中的数据时刻保持一致,当修改后,需要清空对应的缓存数据。用户再次访问时,还是先从数据库中查询,同时再把查询的结果存储到Redis中,这样,就能保证缓存和数据库的数据保持一致。
进入后台:修改蜀味牛蛙分类下的任意一个菜品,当前分类的菜品数据已在Redis中缓存。然后修改菜品,查看Redis中的缓存数据发现修改时,已清空缓存。

在这里插入图片描述

测试完毕提交代码到github。





缓存套餐(基于Spring Cache)

Spring Cache 是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能。其提供了一层抽象,底层可以切换不同的缓存实现,例如:EHCache、Caffeine、Redis(常用)

在SpringCache中提供了很多缓存操作的注解,常见的是以下的几个:

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

在spring boot项目中,使用缓存技术只需在项目中导入相关缓存技术的依赖包,并在启动类上使用@EnableCaching开启缓存支持即可。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
    <version>2.7.3</version> 
</dependency>


@EnableCaching、@Cacheable、@CachePut和@CacheEvict

下面用一个入门案例来学习一下SpringCache注解的使用,导入老师提供的基础工程:底层已使用Redis缓存实现:

在这里插入图片描述

创建名为spring_cache_demo数据库,将springcachedemo.sql脚本直接导入数据库中。然后在引导类上加@EnableCaching:

@Slf4j
@SpringBootApplication
@EnableCaching//开启缓存注解功能
public class CacheDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(CacheDemoApplication.class,args);
        log.info("项目启动成功...");
    }
}


@CachePut注解

@CachePut 说明:作用: 将方法返回值,放入缓存。
​value: 缓存的名称, 每个缓存名称下面可以有很多key
key: 缓存的key ----------> 支持Spring的表达式语言SPEL语法

UserController的save方法是用来保存用户信息的,我们希望在该用户信息保存到数据库的同时,也往缓存中缓存一份数据,我们可以在save方法上加上注解 @CachePut,用法如下:

/**
 * CachePut:将方法返回值放入缓存
 * value:缓存的名称,每个缓存名称下面可以有多个key
 * key:缓存的key
 */
 @PostMapping
 @CachePut(value = "userCache", key = "#user.id")
 public User save(@RequestBody User user){
     userMapper.insert(user);
     return user;
 }

上面这样写,假如方法运行完,生成的主键为1,那么在redis中存储的key是userCache::1,存储的value是方法返回值,存储的key是这个格式是因为指定了注解的两个属性
userCache为注解的value属性,然后自动加上::,后面跟着的key属性是可变参数,这个也是自己指定。key的写法如下:

#user.id : #user指的是方法形参的名称, id指的是user的id属性 , 也就是使用user的id属性作为key ;

#result.id : #result代表方法返回值,该表达式 代表以返回对象的id属性作为key ;

#p0.id:#p0指的是方法中的第一个参数,id指的是第一个参数的id属性,也就是使用第一个参数的id属性作为key ;
#a0.id:#a0指的是方法中的第一个参数,id指的是第一个参数的id属性,也就是使用第一个参数的id属性作为key ;
#root.args[0].id:#root.args[0]指的是方法中的第一个参数,id指的是第一个参数的id属性,也就是使用第一个参数的id属性作为key ;

后面三种其实是同一个意思,推荐使用第一种,因为比较直观。
启动服务,通过swagger接口文档测试,访问UserController的save()方法。因为id是自增,所以不需要设置id属性。访问完之后:

在这里插入图片描述

在这里插入图片描述

在这个工程里使用的是1号数据库所以在db1。然后我们发现redis种的key也是支持这种树形结构的,在userCache文件夹下有一个Empty文件夹,然后是userCache::1。这个层次是怎么划分出来的呢?为什么中间有个Empty呢?因为userCache::1中间有两个冒号,一个冒号代表一级。可以自己去redis命令行里试一下命令:set a:b:c:d test 就能看到四级的key



@Cacheable注解

​ 作用: 在方法执行前,spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据,调用方法并将方法返回值放到缓存中
value: 缓存的名称,每个缓存名称下面可以有多个key
​key: 缓存的key ----------> 支持Spring的表达式语言SPEL语法

在getById上加注解@Cacheable

/**
* Cacheable:在方法执行前spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据,调用方法并将方法返回值放到缓存中
* value:缓存的名称,每个缓存名称下面可以有多个key
* key:缓存的key
*/
@GetMapping
@Cacheable(cacheNames = "userCache",key="#id")
public User getById(Long id){
    User user = userMapper.getById(id);
    return user;
}

重启服务,通过swagger接口文档测试,访问UserController的getById()方法。第一次访问,会请求我们controller的方法,查询数据库。后面再查询相同的id,就直接从Redis中查询数据,不用再查询数据库了,就说明缓存生效了。

在这里插入图片描述



@CacheEvict注解注解

​ 作用: 清理指定缓存
value: 缓存的名称,每个缓存名称下面可以有多个key
​key: 缓存的key ----------> 支持Spring的表达式语言SPEL语法

在 delete 方法上加注解@CacheEvict

@DeleteMapping
@CacheEvict(cacheNames = "userCache",key = "#id")//删除某个key对应的缓存数据
public void deleteById(Long id){
    userMapper.deleteById(id);
}

@DeleteMapping("/delAll")
@CacheEvict(cacheNames = "userCache",allEntries = true)//删除userCache下所有的缓存数据
public void deleteAll(){
    userMapper.deleteAll();
}

重启服务,通过swagger接口文档测试,访问UserController的deleteAll()方法。查看user表:数据清空。查询Redis缓存数据,userCache下所有的缓存数据被清空。

在这里插入图片描述

在这里插入图片描述





Spring Cache实现缓存套餐

1). 导入Spring Cache和Redis相关maven坐标(已实现)

<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

2). 在启动类上加入@EnableCaching注解,开启缓存注解功能

@SpringBootApplication
@EnableTransactionManagement //开启注解方式的事务管理
@Slf4j
@EnableCaching // 开启缓存注解功能
public class SkyApplication {
	...
}

3). 在用户端接口SetmealController的 list 方法上加入@Cacheable注解

/**
 * 条件查询
 *
 * @param categoryId
 * @return
 */
@GetMapping("/list")
@ApiOperation("根据分类id查询套餐")
@Cacheable(cacheNames = "setmealCache",key = "#categoryId") //key可能为setmealCache::100
public Result<List<Setmeal>> list(Long categoryId) {
    Setmeal setmeal = new Setmeal();
    setmeal.setCategoryId(categoryId);
    setmeal.setStatus(StatusConstant.ENABLE);

    List<Setmeal> list = setmealService.list(setmeal);
    return Result.success(list);
}

4). 在管理端接口SetmealController的 save、delete、update、updateStatus等方法上加入CacheEvict注解

@PostMapping
@ApiOperation(value = "新增套餐")
@CacheEvict(cacheNames = "setmealCache",key = "#setmealDTO.categoryId")//key: setmealCache::100
public Result save(@RequestBody SetmealDTO setmealDTO){
    log.info("新增套餐:{}",setmealDTO);
    setmealServices.save(setmealDTO);
    return Result.success();
}

@DeleteMapping
@ApiOperation("套餐批量删除")
@CacheEvict(cacheNames = "setmealCache",allEntries = true)
public Result delete(@RequestParam List<Long> ids) {
    log.info("套餐批量删除:{}", ids);
    setmealServices.deleteBatch(ids);
    return Result.success();
}

@PutMapping
@ApiOperation("修改套餐")
@CacheEvict(cacheNames = "setmealCache",allEntries = true)
public Result update(@RequestBody SetmealVO setmealVO) {
    log.info("修改套餐:{}", setmealVO);
    setmealServices.updateWithDishes(setmealVO);
    return Result.success();
}

@PostMapping("/status/{status}")
@ApiOperation("修改套餐销售状态")
@CacheEvict(cacheNames = "setmealCache",allEntries = true)
public Result updateStatus(@PathVariable Integer status,Long id){
    log.info("根据套餐id修改套餐销售状态:{}", status);
    setmealServices.updateStatusById(status,id);
    return Result.success();
}

这里解释一下为什么在修改套餐的时候不是去像新增套餐那样精确的根据分类id来删,而是全部删除。因为修改套餐的时候很有可能改变了套餐的分类。像根据套餐id修改销售状态那就属于是没办法精确删除,参数里就没有分类id。
但是这里因为新增套餐默认是停售,删除套餐必须套餐是停售才能删除,所以倒是感觉也没必要在这里清缓存了,前端根本看不到这些数据,你新增的时候清一下,然后用户前端查套餐的时候又得去数据库读了,感觉在修改套餐销售状态里清就行。感觉这个技术使用起来还是比较抽象的。

我看视频弹幕也有人问为什么不在管理端的新增套餐里使用@CachePut注解。
要记住我们在用户端的根据分类id查询套餐函数上加上了@Cacheable注解,所以用户在查的时候如果缓存里没有数据是自动会将查询到的数据存储到缓存的。用户端查询数据,所以设置缓存,而管理端的增删改会导致数据库和缓存数据不一致,那当然是要在管理端进行缓存删除了,要理清这个思路。

功能测试通过前后端联调方式来进行测试,同时观察redis中缓存的套餐数据。和缓存菜品功能测试基本一致,不再赘述。

测试完毕提交代码到github。





添加购物车

用户可以将菜品或者套餐添加到购物车。对于菜品来说,如果设置了口味信息,则需要选择规格后才能加入购物车;对于套餐来说,可以直接点击加号将当前套餐加入购物车。在购物车中可以修改菜品和套餐的数量,也可以清空购物车。

请添加图片描述

通过上述原型图,设计出对应的添加购物车接口。添加购物车时,有可能添加菜品,也有可能添加套餐。故传入参数要么是菜品id,要么是套餐id。

请添加图片描述

用户的购物车数据,也是需要保存在数据库中的,购物车对应的数据表为shopping_cart表,具体表结构如下:

字段名数据类型说明备注
idbigint主键自增
namevarchar(32)商品名称冗余字段
imagevarchar(255)商品图片路径冗余字段
user_idbigint用户id逻辑外键
dish_idbigint菜品id逻辑外键
setmeal_idbigint套餐id逻辑外键
dish_flavorvarchar(50)菜品口味
numberint商品数量
amountdecimal(10,2)商品单价冗余字段
create_timedatetime创建时间

购物车数据是关联用户的,在表结构中,我们需要记录,每一个用户的购物车数据是哪些
菜品列表展示出来的既有套餐,又有菜品,如果用户选择的是套餐,就保存套餐ID(setmeal_id),如果用户选择的是菜品,就保存菜品ID(dish_id)
对同一个菜品/套餐,如果选择多份不需要添加多条记录,增加数量number即可
在sky-pojo模块,ShoppingCartDTO.java已定义



Controller层

创建controller.user.ShoppingCartController:

@RestController
@RequestMapping("/user/shoppingCart")
@Slf4j
@Api(tags = "C端-购物车接口")
public class ShoppingCartController {

    @Autowired
    private ShoppingCartService shoppingCartService;

    /**
     * 添加购物车
     * @param shoppingCartDTO
     * @return
     */
    @PostMapping("/add")
    @ApiOperation("添加购物车")
    public Result<String> add(@RequestBody ShoppingCartDTO shoppingCartDTO){
        log.info("添加购物车:{}", shoppingCartDTO);
        shoppingCartService.addShoppingCart(shoppingCartDTO);//后绪步骤实现
        return Result.success();
    }
}

Service层实现类

ShoppingCartServiceImpl

@Service
public class ShoppingCartServiceImpl implements ShoppingCartService {

    @Autowired
    private ShoppingCartMapper shoppingCartMapper;
    @Autowired
    private DishMapper dishMapper;
    @Autowired
    private SetmealMapper setmealMapper;

    /**
     * 添加购物车
     *
     * @param shoppingCartDTO
     */
    public void addShoppingCart(ShoppingCartDTO shoppingCartDTO) {
        ShoppingCart shoppingCart = new ShoppingCart();
        BeanUtils.copyProperties(shoppingCartDTO, shoppingCart);
        //只能查询自己的购物车数据
        shoppingCart.setUserId(BaseContext.getCurrentId());

        //判断当前商品是否在购物车中 注意实现这个接口时如果是不同口味的同一个菜品 也需要单独插入 而不能在它基础上数量加1
        List<ShoppingCart> shoppingCartList = shoppingCartMapper.list(shoppingCart);

        if (shoppingCartList != null && shoppingCartList.size() == 1) {
            //如果已经存在,就更新数量,数量加1
            shoppingCart = shoppingCartList.get(0);
            shoppingCart.setNumber(shoppingCart.getNumber() + 1);
            shoppingCartMapper.updateNumberById(shoppingCart);
        } else {
            //如果不存在,插入数据,数量就是1

            //判断当前添加到购物车的是菜品还是套餐
            Long dishId = shoppingCartDTO.getDishId();
            if (dishId != null) {
                //添加到购物车的是菜品
                Dish dish = dishMapper.getById(dishId);
                shoppingCart.setName(dish.getName());
                shoppingCart.setImage(dish.getImage());
                shoppingCart.setAmount(dish.getPrice());
            } else {
                //添加到购物车的是套餐
                Setmeal setmeal = setmealMapper.getById(shoppingCartDTO.getSetmealId());
                shoppingCart.setName(setmeal.getName());
                shoppingCart.setImage(setmeal.getImage());
                shoppingCart.setAmount(setmeal.getPrice());
            }
            shoppingCart.setNumber(1);
            shoppingCart.setCreateTime(LocalDateTime.now());
            shoppingCartMapper.insert(shoppingCart);
        }
    }
}

Mapper层

ShoppingCartMapper

@Mapper
public interface ShoppingCartMapper {
    /**
     * 条件查询
     *
     * @param shoppingCart
     * @return
     */
    List<ShoppingCart> list(ShoppingCart shoppingCart);

    /**
     * 更新商品数量
     *
     * @param shoppingCart
     */
    @Update("update shopping_cart set number = #{number} where id = #{id}")
    void updateNumberById(ShoppingCart shoppingCart);

    /**
     * 插入购物车数据
     *
     * @param shoppingCart
     */
    @Insert("insert into shopping_cart (name, user_id, dish_id, setmeal_id, dish_flavor, number, amount, image, create_time) " +
            " values (#{name},#{userId},#{dishId},#{setmealId},#{dishFlavor},#{number},#{amount},#{image},#{createTime})")
    void insert(ShoppingCart shoppingCart);

}

ShoppingCartMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.ShoppingCartMapper">
    <select id="list" parameterType="ShoppingCart" resultType="ShoppingCart">
        select * from shopping_cart
        <where>
            <if test="userId != null">
                and user_id = #{userId}
            </if>
            <if test="dishId != null">
                and dish_id = #{dishId}
            </if>
            <if test="setmealId != null">
                and setmeal_id = #{setmealId}
            </if>
            <if test="dishFlavor != null">
                and dish_flavor = #{dishFlavor}
            </if>
        </where>
        order by create_time desc
    </select>
</mapper>

注意这里动态sql将口味也视为同一条购物车数据的判断之中,即若是不同的口味需要单独在购物车数据库中插入一条数据。

功能测试进入小程序,添加菜品加入购物车,查询数据库。因为现在没有实现查看购物车功能,所以只能在表中进行查看。在前后联调时,后台可通断点方式启动,查看运行的每一步。

在这里插入图片描述

测试成功,提交代码。





查看购物车

当用户添加完菜品和套餐后,可进入到购物车中,查看购物中的菜品和套餐。

在这里插入图片描述

请添加图片描述

Controller层

在ShoppingCartController中创建查看购物车的方法:

/**
 * 查看购物车
 * @return
 */
@GetMapping("/list")
@ApiOperation("查看购物车")
public Result<List<ShoppingCart>> list(){
    return Result.success(shoppingCartService.showShoppingCart());
}

Service层实现类

在ShoppingCartServiceImpl中实现查看购物车的方法:

/**
 * 查看购物车
 * @return
 */
public List<ShoppingCart> showShoppingCart() {
    return shoppingCartMapper.list(ShoppingCart.
                                   builder().
                                   userId(BaseContext.getCurrentId()).
                                   build());
}

功能测试,当进入小程序时,就会发起查看购物车的请求

在这里插入图片描述

在这里插入图片描述

测试成功,提交代码。





清空购物车

当点击清空按钮时,会把购物车中的数据全部清空。

在这里插入图片描述

在这里插入图片描述

Controller层

在ShoppingCartController中创建清空购物车的方法:

/**
 * 清空购物车商品
 * @return
 */
@DeleteMapping("/clean")
@ApiOperation("清空购物车商品")
public Result<String> clean(){
    shoppingCartService.cleanShoppingCart();
    return Result.success();
}

Service层实现类

在ShoppingCartServiceImpl中实现清空购物车的方法:

/**
 * 清空购物车商品
 */
public void cleanShoppingCart() {
    shoppingCartMapper.deleteByUserId(BaseContext.getCurrentId());
}

Mapper层

在ShoppingCartMapper接口中创建删除购物车数据的方法:

/**
 * 根据用户id删除购物车数据
 *
 * @param userId
 */
@Delete("delete from shopping_cart where user_id = #{userId}")
void deleteByUserId(Long userId);

功能测试,进入到购物车页面,点击清空。

在这里插入图片描述

在这里插入图片描述

数据库该用户的购物数据也被清空:

在这里插入图片描述

测试成功提交代码。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/266404.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

STANFORD斯坦福FS725铷钟

FS725在一个紧凑的半宽2U机箱中集成了一个铷原子振荡器&#xff08;SRS型号PRS10&#xff09;、一个低噪声通用交流电源和分配放大器。它提供稳定和可靠的性能&#xff0c;估计20年的老化率低于510-9&#xff0c;并证明铷原子振荡器的MTBF超过20万小时。FS725是校准和研发实验室…

@z-utils组 重构和自动化实现

highlight: monokai theme: github 包简介 z-utils组 是一个可以在vue/react/pure js 中使用的工具包&#xff0c;它包含三个子类&#xff0c;分别为 z-utils/base, z-utils/react, z-utils/vue 三个分别在不同区域使用。 他是原 zzy-javascript-devtools 的重构版本&#xf…

Java多线程技术四——定时器

1 定时器的使用 在JDK库中Timer类主要负责计划任务的功能&#xff0c;也就是在指定的时间开始执行某一个任务&#xff0c;Timer类的方法列表如下&#xff1a; Timer类的主要作用就是设置计划任务&#xff0c;封装任务的类却是TimerTask&#xff0c;该类的结构如下图 因为TimerT…

计算机网络复习-OSI TCP/IP 物理层

我膨胀了&#xff0c;挂我啊~ 作者简介&#xff1a; 每年都吐槽吉师网安奇怪的课程安排、全校正经学网络安全不超20人情景以及割韭菜企业合作的FW&#xff0c;今年是第一年。。 TCP/IP模型 先做两道题&#xff1a; TCP/IP协议模型由高层到低层分为哪几层&#xff1a; 这题…

Angular 11到升级到 Angular 16

日新月异&#xff0c;与时俱进… 随着Angular版本不断更新&#xff0c;再看所开发的项目版本仍然是Angular 11&#xff0c;于是准备升级 截止发博日最版本是 v17.1.0&#xff0c;考虑到稳定性因素决定升级到v16版本 一&#xff1a;查看 升级指南 二&#xff1a;按照指南&…

【ArduinoOTA无线(OTA)更新的EASY指南】

【ArduinoOTA无线&#xff08;OTA&#xff09;更新的EASY指南】 1. 前言2. 了解 ESP32 的 ArduinoOTA3. 无线更新案例4. ArduinoOTA入门5. 安装必备组件6. 设置硬件7. ESP32 OTA 的最低代码8. 按照我们的流程学习Arduino编程➜9. 这对OTA来说非常重要10. 通过无线方式将草图上传…

IIS服务器的配置与管理

1) 安装IIS服务器&#xff0c;并添加站点&#xff0c;该服务器的IP地址为192.168.1.xx 。 2) 配置网站&#xff0c;并设置该站点不允许匿名访问&#xff0c;仅允许使用自己的本地用户登录连接。 3) 配置网站&#xff0c;限制拒绝192.168.1.100IP地址访问 。 4) 客户端使用19…

【C++11特性篇】玩转C++11中的包装器(function&bind)

前言 大家好吖&#xff0c;欢迎来到 YY 滴C系列 &#xff0c;热烈欢迎&#xff01; 本章主要内容面向接触过C的老铁 主要内容含&#xff1a; 欢迎订阅 YY滴C专栏&#xff01;更多干货持续更新&#xff01;以下是传送门&#xff01; 目录 一.为什么需要包装器function&#xff…

一篇文章带你搞定CTFMice基本操作

CTF比赛是在最短时间内拿到最多的flag&#xff0c;mice必须要有人做&#xff0c;或者一支战队必须留出一块时间专门写一些mice&#xff0c;web&#xff0c;pwn最后的一两道基本都会有难度&#xff0c;这时候就看mice的解题速度了&#xff01; 说实话&#xff0c;这是很大一块&…

【ubuntu 22.04】安装vscode并配置正常访问应用商店

注意&#xff1a;要去vscode官网下载deb安装包&#xff0c;在软件商店下载的版本不支持输入中文 在ubuntu下用火狐浏览器无法访问vscode官网&#xff0c;此时可以手动进行DNS解析&#xff0c;打开DNS在线查询工具&#xff0c;解析以下主机地址&#xff08;复制最后一个IP地址&a…

c++动态内存与智能指针

前言 静态内存&#xff1a;用于保存局部静态变量、类内的静态数据成员以及全局变量栈&#xff1a;用于保存函数内部的非static变量堆&#xff1a;存储动态分配的对象&#xff08;程序运行时分配的对象&#xff09; 静态内存和栈内存的对象由编译器自动创建和销毁 而堆区的动态…

​ 全球云科技基础设施:亚马逊云科技的海外服务器网络如何演进

在当今数字化浪潮愈发汹涌的时代&#xff0c;科技公司的发展不仅需要更强大的计算能力和创新性技术&#xff0c;还需要对环境的高度责任感。在这一背景下&#xff0c;亚马逊云科技的海外服务器产品成为了推动清洁、高效数字未来的领导者之一。亚马逊云科技的高级主管阿比谢克夏…

2024年开通抖店,相关注意事项,新手一定要注意这几个细节

我是王路飞。 马上2024年了&#xff0c;如果你在23年没开通抖店的话&#xff0c;那么2024年就一定不能再错过了。 今天给你们说下&#xff0c;在2024年开通抖店&#xff0c;新手需要注意的相关事项和细节。 内容来源于【醒醒团队-电商王路飞】 首先&#xff0c;开店的时候&a…

Unity中Shader观察空间推导(在Shader中实现)

文章目录 前言一、观察空间矩阵推导1、求观察空间基向量2、求观察空间的基向量在世界空间中的矩阵 的 逆矩阵2、求平移变换矩阵3、相乘得出 观察空间转化矩阵4、得到顶点的世界空间坐标&#xff0c;然后转化到观察空间5、把观察空间坐标转化为齐次裁剪坐标输出到屏幕 二、最终效…

测试开发体系介绍——测试体系介绍-L3

目录&#xff1a; 测试框架体系TDDDDTBDDATDD介绍 测试框架是什么&#xff1f;测试框架的价值&#xff1a;测试框架的收益&#xff1a;常见测试框架类型&#xff1a;TDDBDDBehaviorDrivenDevelopmentATDDAcceptanceTestDrivenDevelopmentMBTModelBasedTestingDDTDataDrivenTes…

HarmonyOS 签名打包Hap(s)、App(s)

1. 基本概念 HarmonyOS应用通过数字证书&#xff08;.cer文件&#xff09;和Profile文件&#xff08;.p7b文件&#xff09;来保证应用的完整性&#xff0c;数字证书和Profile文件可通过申请发布证书和Profile文件获取。   申请数字证书和Profile文件前&#xff0c;首先需要通…

mySQL数据库用户管理

目录 1.创建外键约束 外键的定义 主键表和外键表的理解 具体操作 2.数据库用户管理 新建用户 查看用户信息 重命名用户 删除用户 修改当前和其他用户登录密码 忘记 root密码的解决办法 3.数据库用户授权 授予权限 查看权限 撤销权限 1.创建外键约束 外键的定义…

又是阿里,通义灵码免费平替GitHub Copilot

毫无疑问&#xff0c;人工智能已经在影响着我们日常生活的方方面面&#xff0c;同样的在软件开发领域&#xff0c;AI正在改变我们的开发方式。在软件开发领域&#xff0c;尽管有许多强大的AI编码工具&#xff0c;但国产&#xff0c;免费&#xff0c;使用门槛低&#xff0c;用起…

Fastjson 常用语法

一.Json数据格式回顾 1.1 什么是json JSON:(JavaScript Object Notation, JS 对象简谱) 是一种轻量级的数据交换格式。它基于 ECMAScript(欧洲计算机协会制定的js规范)的一个子集&#xff0c;采用完全独立于编程语言的文本格式来存储和表示数据。简洁和清晰的层次结构使得 JSO…

省时攻略:快速获得Creo安装包,释放创意天才!

不要再在网上浪费时间寻找Creo的安装包了&#xff0c;一键下载安装&#xff0c; 你要的一切都可以在这里找到&#xff01;我们深知在海量的信息中寻找合适的软件包并非易事&#xff0c;而且往往还伴随着繁琐的安装过程。然而&#xff0c;现在有了我们&#xff0c;一切变得轻松简…