仿大众点评——秒杀系统部分01

news2025/3/2 1:12:15

秒杀系统

全局ID生成器

全局唯一ID生成策略:

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

这里使用Redis自增的数值,并拼接一些其它信息
Redis自增ID策略:

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

在这里插入图片描述

ID的组成部分:

  • 符号位:1bit,永远为0,表示为正数
  • 时间戳:31bit,以秒为单位,可以使用69年
  • 序列号:32bit,秒内的计数器,每秒支持2*32次方个不同的ID
@Component
public class RedisIdWorker {

    private  static final long BEGIN_TIMESTAMP=1640995200L;

    private static final int COUNT_BITS=32;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    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("yyyyMMdd"));
        long count =stringRedisTemplate.opsForValue().increment("icr:"+keyPrefix+":"+date);
        //        3.拼接并返回
        return timestamp <<COUNT_BITS | count;
    }
}

实现秒杀下单

下单时需要判断两点:

  • 秒杀是否开始或结束,如果尚未开始或已经结束则无法下单
  • 库存是否充足,不足则无法下单
    @Resource
    private ISeckillVoucherService seckillVoucherService;

    @Resource
    private SeckillVoucherMapper seckillVoucherMapper;

    @Resource
    private RedisIdWorker redisIdWorker;

    //    秒杀优惠券订单
    @Transactional
    public Result seckillVoucherOrder(Long voucherId) {
//        1.根据id查询优惠券
        SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
//        2.判断秒杀是否开始
        if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {
//           秒杀未开始
            return Result.fail("秒杀尚未开始");
        }
//        3.判断秒杀是否结束
        if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {
//           秒杀已经结束
            return Result.fail("秒杀已经结束");
        }
//        4.判断库存是否充足
        if (seckillVoucher.getStock() < 1){
//           库存不足
            return Result.fail("库存不足");
        }
//        5.扣减库存
        UpdateWrapper<SeckillVoucher> updateWrapper = new UpdateWrapper<>();
        updateWrapper.set("stock",seckillVoucher.getStock() - 1);
        int update = seckillVoucherMapper.update(null, updateWrapper);
//        6.创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
//        订单id
        long orderId = redisIdWorker.uniqueId("order");
        voucherOrder.setId(orderId);
//        用户id
        voucherOrder.setVoucherId(UserHolder.getUser().getId());
//        代金券id
        voucherOrder.setUserId(voucherId);
        save(voucherOrder);
//        7.返回订单id
        return Result.ok(orderId);
    }

结果如下:
在这里插入图片描述
在这里插入图片描述

库存超卖问题(多线程并发问题)分析

就是在高并发的场景下,可能会有多个线程同时进行查询,当商品数量仅剩1个时,多个线程同时查询,都判断为1,都会进行下单。

在这里插入图片描述
使用jmeter测试:

  • jmeter配置:
    在这里插入图片描述
    在这里插入图片描述
  • 测试:出现超卖问题,最多只能卖100件,在高并发的场景下却卖出了200件
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

悲观锁和乐观锁保证库存

使用乐观锁解决库存超卖(多线程并发安全)

采用CAS法解决多线程并发安全问题:

    @Transactional
    public Result seckillVoucher(Long voucherId) {
//        1.根据id查询优惠券
        SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
//        2.判断秒杀是否开始
        if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {
//           秒杀未开始
            return Result.fail("秒杀尚未开始");
        }
//        3.判断秒杀是否结束
        if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {
//           秒杀已经结束
            return Result.fail("秒杀已经结束");
        }
//        4.判断库存是否充足
        if (seckillVoucher.getStock() < 1){
//           库存不足
            return Result.fail("库存不足");
        }
//        5.扣减库存
        boolean update = seckillVoucherService
                .update()
                .setSql("stock = stock -1")
                .eq("voucher_id", voucherId)
                .gt("stock", 0).update();  //设置库存大于0
        if (!update){
            return Result.fail("库存不足!");
        }

//        6.创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
//        订单id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
//        用户id
        voucherOrder.setUserId(UserHolder.getUser().getId());
//        代金券id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);
//        7.返回订单id
        return Result.ok(orderId);
    }
  • jmeter测试:

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

使用悲观锁实现一人一单功能

需求:修改秒杀业务,要求同一个优惠券,一个用户只能下一单
在这里插入图片描述

  • 注意:在下面代码对createVoucherOrder要进行AOP代理,不能直接用this进行调用,否则会产生spring的事务失效现象
<!--        基于aop代理工厂面向切面编程所需依赖-->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
        </dependency>

暴露代理对象
在这里插入图片描述

    //    秒杀优惠券订单
    public Result seckillVoucher(Long voucherId) {
//        1.根据id查询优惠券
        SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
//        2.判断秒杀是否开始
        if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {
//           秒杀未开始
            return Result.fail("秒杀尚未开始");
        }
//        3.判断秒杀是否结束
        if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {
//           秒杀已经结束
            return Result.fail("秒杀已经结束");
        }
//        4.判断库存是否充足
        if (seckillVoucher.getStock() < 1) {
//           库存不足
            return Result.fail("库存不足");
        }
        Long userId = UserHolder.getUser().getId();
//        确保当用户id一样时,锁就会一样
        synchronized (userId.toString().intern()) {
//          createVoucherOrder不具有事务功能,需要获得当前对象的代理对象
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        }
    }


    @Transactional
    public Result createVoucherOrder(Long voucherId) {
        Long userId = UserHolder.getUser().getId();
        //查询用户是否已经购买过了
        int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        if (count > 0) {
            return Result.fail("您已经购买过了!");
        }

//        6.扣减库存
        boolean update = seckillVoucherService
                .update()
                .setSql("stock = stock -1")
                .eq("voucher_id", voucherId)
                .gt("stock", 0).update();
        if (!update) {
            return Result.fail("库存不足!");
        }

//        7.创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
//        订单id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
//        用户id
        voucherOrder.setUserId(userId);
//        代金券id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);
//        8.返回订单id
        return Result.ok(orderId);
    }
  • jmeter测试:
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

集群下线程并发安全问题

  • 开启两个tomcat服务器
    在这里插入图片描述
  • 修改nginx配置文件
    在这里插入图片描述
  • postman测试(debug调试):同时发送两个请求
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    放行之后数据库中有两条数据:
    在这里插入图片描述
    在这里插入图片描述
  • 出现以上问题的原因是因为多个JVM都是属于自己的锁监视器,每个JVM中的线程运行时,都会根据自己的锁监视器进行多线程之间的调用。而不会和其他JVM中的锁监视器有关系。所以集群部署的方式下,使用synchronized锁并不能解决多线程并发安全问题。
    在这里插入图片描述

使用分布式锁优化一人一单问题

使用悲观锁解决一人一单问题时时采用synchronize(同步锁)的方式来实现,但是在集群部署的模式下并不能解决多线程并发的安全性问题。所以可以采用Redis中的setnx在集群当中充当锁监视器,实现在多个服务器当中只有一个锁。
在这里插入图片描述

  • 创建锁监视器
public class SimpleRedisLock implements ILock{

    private StringRedisTemplate stringRedisTemplate;

    private String name;

    public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.name = name;
    }
    private static final String ID_PREFIX= UUID.randomUUID().toString(true)+"-";

    private static final String KEY_PREFIX="lock:";

    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
    static {
        UNLOCK_SCRIPT=new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        UNLOCK_SCRIPT.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() {
        stringRedisTemplate.execute(
                UNLOCK_SCRIPT,
                Collections.singletonList(KEY_PREFIX+name),
                ID_PREFIX+Thread.currentThread().getId());
    }
    */
    public void unlock() {
        String threadId=ID_PREFIX+Thread.currentThread().getId();

        String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
        if(threadId.equals(id)) {
            stringRedisTemplate.delete(KEY_PREFIX + name);
        }
    }
}
  • 调用分布式锁,实现一人一单功能优化,在集群部署下不会出现多线程并发的安全性问题。
    public Result seckillVoucher(Long voucherId) {
//        1.根据id查询优惠券
        SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
//        2.判断秒杀是否开始
        if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {
//           秒杀未开始
            return Result.fail("秒杀尚未开始");
        }
//        3.判断秒杀是否结束
        if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {
//           秒杀已经结束
            return Result.fail("秒杀已经结束");
        }
//        4.判断库存是否充足
        if (seckillVoucher.getStock() < 1) {
//           库存不足
            return Result.fail("库存不足");
        }
        Long userId = UserHolder.getUser().getId();
//        创建分布式锁对象
        SimpleRedisLock distriLock = new SimpleRedisLock( stringRedisTemplate,"order:" + userId);
        boolean isLock = distriLock.tryLock(1200L);
//        判断是否获取锁成功
        if (!isLock) {
//            获取锁失败
            return Result.fail("不允许重复下单");
        }
//        获取锁成功
//        createVoucherOrder不具有事务功能,需要获得当前对象的代理对象
        try {
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } finally {
            distriLock.unlock();
        }
    }

    //    扣减库存、创建订单
    @Transactional
    public Result createVoucherOrder(Long voucherId) {
        Long userId = UserHolder.getUser().getId();
        //查询用户是否已经购买过了
        int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        if (count > 0) {
            return Result.fail("您已经购买过了!");
        }

//        6.扣减库存
        boolean update = seckillVoucherService
                .update()
                .setSql("stock = stock -1")
                .eq("voucher_id", voucherId)
                .gt("stock", 0).update();
        if (!update) {
            return Result.fail("库存不足!");
        }

//        7.创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
//        订单id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
//        用户id
        voucherOrder.setUserId(userId);
//        代金券id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);
//        8.返回订单id
        return Result.ok(orderId);
    }
  • postman(debug)测试:
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

分布式锁误删优化

为了防止因为线程阻塞而导致的分布式锁误删问题,在线程获取分布式锁的时候,向缓存中添加分布式锁的标识。当线程要释放锁的时候,查询缓存中的分布式锁的标识是否和自己的相同,相同的话就释放锁,不同的话就不做操作。

  • 使用Lua脚本实现分布式锁的原子性
if(redis.call('get',KEYS[1])==ARGV[1]) then
    return redis.call('del',KEYS[1])
end
return 0
public class SimpleRedisLock implements ILock{

    private StringRedisTemplate stringRedisTemplate;

    private String name;

    public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.name = name;
    }
    private static final String ID_PREFIX= UUID.randomUUID().toString(true)+"-";

    private static final String KEY_PREFIX="lock:";

    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
    static {
        UNLOCK_SCRIPT=new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        UNLOCK_SCRIPT.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() {
        stringRedisTemplate.execute(
                UNLOCK_SCRIPT,
                Collections.singletonList(KEY_PREFIX+name),
                ID_PREFIX+Thread.currentThread().getId());
    }
    /*public void unlock() {
        String threadId=ID_PREFIX+Thread.currentThread().getId();

        String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
        if(threadId.equals(id)) {
            stringRedisTemplate.delete(KEY_PREFIX + name);
        }
    }*/
}
  • jmeter压测
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

使用Redisson实现分布式锁

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

    public Result seckillVoucher(Long voucherId) {
//        1.根据id查询优惠券
        SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
//        2.判断秒杀是否开始
        if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {
//           秒杀未开始
            return Result.fail("秒杀尚未开始");
        }
//        3.判断秒杀是否结束
        if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {
//           秒杀已经结束
            return Result.fail("秒杀已经结束");
        }
//        4.判断库存是否充足
        if (seckillVoucher.getStock() < 1) {
//           库存不足
            return Result.fail("库存不足");
        }
        Long userId = UserHolder.getUser().getId();

        RLock lock = redissonClient.getLock("lock:order:" + userId);

        boolean isLock = lock.tryLock();
//        判断是否获取锁成功
        if (!isLock) {
//            获取锁失败
            return Result.fail("不允许重复下单");
        }
//        获取锁成功,创建订单
        try {
            return createVoucherOrder(voucherId);
        } finally {
            lock.unlock();
        }
    }

    //    扣减库存、创建订单
    @Transactional
    public Result createVoucherOrder(Long voucherId) {
        Long userId = UserHolder.getUser().getId();
        //查询用户是否已经购买过了
        int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        if (count > 0) {
            return Result.fail("您已经购买过了!");
        }

//        6.扣减库存
        boolean update = seckillVoucherService
                .update()
                .setSql("stock = stock -1")
                .eq("voucher_id", voucherId)
                .gt("stock", 0).update();
        if (!update) {
            return Result.fail("库存不足!");
        }

//        7.创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
//        订单id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
//        用户id
        voucherOrder.setUserId(userId);
//        代金券id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);
//        8.返回订单id
        return Result.ok(orderId);
    }
  • jmeter压测
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

秒杀优化(异步秒杀)

问题描述:在之前的秒杀业务中,客户端向Nginx代理服务器发送请求,Nginx做负载代理到Tomcat服务器,整个业务流程中,查询优惠券、查询订单、减库存、创建订单都是操作数据库来完成的。对数据库做太多的读写操作的话整个业务耗时就会很长,并发能力就会很差。
在这里插入图片描述

采用异步操作进行优化:

  • 将校验用户购买资格的业务流程放到Redis缓存当中,当客户端发送请求时就会在缓存当中判断用户的购买资格,如果没有购买资格就直接返回错误。
  • 如果有购买资格就保存优惠券、用户、订单id到阻塞队列,然后后台数据库异步读取队列中的信息,完成下单。
    在这里插入图片描述

为了保证判断用户是否有购买资格的业务的原子性,需要使用Lua脚本执行业务。如果用户没有购买资格,就直接返回异常。如果有购买资格,完成将优惠券、用户、订单id写入阻塞队列,等待数据库完成异步下单操作。
在这里插入图片描述
需求:

  • 新增秒杀优惠券的同时,将优惠券信息保存到Redis中
  • 基于Lua脚本,判断秒杀库存、一人一单,决定用户是否抢购成功
  • 如果抢购成功,将优惠券id和用户id封装后存入消息队列
  • 开启线程任务,不断从消息队列中获取信息,实现异步下单功能

在创建秒杀券的同时将秒杀券的库存存入缓存当中

@SpringBootTest
class HmDianPingApplicationTests {

    @Resource
    private ShopServiceImpl shopService;
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Test
    void testSaveShop(){
        shopService.saveShop2Redis(1L,10L);
    }

    @Resource
    private VoucherServiceImpl voucherService;
    @Test
    void add(){
        Voucher voucher = new Voucher();
        voucher.setShopId(1L);
        voucher.setTitle("200元代金券");
        voucher.setSubTitle("周一至周五均可使用")
                .setRules("全场通用\\n无需预约\\n可无限叠加\\n不兑现、不找零\\n仅限堂食")
                .setPayValue(8000L)
                .setActualValue(10000L)
                .setType(1)
                .setStock(100)
                .setBeginTime(LocalDateTime.of(2022,10,10,0,0,0))
                .setEndTime(LocalDateTime.of(2022,11,29,0,0,0));
        voucherService.addSeckillVoucher(voucher);
    }
    @Test
    void loadShopDats(){
        //1.查询店铺信息
        List<Shop> list = shopService.list();
        //2.把店铺分组,按照typeId分组,typeId一致放到一个集合
        Map<Long, List<Shop>> map = list.stream().collect(Collectors.groupingBy(Shop::getTypeId));
        //3.分批完成写入Redis
        for (Map.Entry<Long, List<Shop>> entry : map.entrySet()) {
            Long typeId= entry.getKey();
            String key=SHOP_GEO_KEY+typeId;
            //3.2.获取同类型的店铺的集合
            List<Shop> value = entry.getValue();
            List<RedisGeoCommands.GeoLocation<String>> locations = new ArrayList<>(value.size());
            //3.3.写入redis
            for (Shop shop : value) {
                locations.add(new RedisGeoCommands.GeoLocation<>(
                        shop.getId().toString(),
                        new Point(shop.getX(),shop.getY())
                ));
            }
            stringRedisTemplate.opsForGeo().add(key,locations);
        }
    }
}

@Override
    @Transactional
    public void addSeckillVoucher(Voucher voucher) {
        // 保存优惠券
        save(voucher);
        // 保存秒杀信息
        SeckillVoucher seckillVoucher = new SeckillVoucher();
        seckillVoucher.setVoucherId(voucher.getId());
        seckillVoucher.setStock(voucher.getStock());
        seckillVoucher.setBeginTime(voucher.getBeginTime());
        seckillVoucher.setEndTime(voucher.getEndTime());
        seckillVoucherService.save(seckillVoucher);
        //秒杀到库存

        stringRedisTemplate.opsForValue().set(SECKILL_STOCK_KEY+voucher.getId(),voucher.getStock().toString());
    }

在这里插入图片描述

基于Lua脚本完成用户下单资格验证

--1.参数列表
--1.1.优惠券id
local voucherId =ARGV[1]
--1.2。用户id
local userId = ARGV[2]
--1.3.订单id
local orderId=ARGV[3]

--2.数据key
--2.1.库存key
local stockKey = 'seckill:stock:' .. voucherId
--2.2.订单key
local orderKey = 'seckill:order:' .. voucherId
--3.脚本业务
--3.1.判断库存是否充足get stockKey
if( tonumber (redis.call('get' , stockKey)) <= 0) then
    --3.2库存不足,返回1
    return 1
end
--3.2.判断用户是否下单 sismember orderKey userId
if(redis.call('sismember', orderKey,userId) == 1) then
    --3.3.存在。说明是重复下单。返回2
    return 2
end
--3.4.扣库存 incrby stockKey -1
redis.call('incrby' , stockKey , -1)
--3.5.下单保存用户sadd orderKey userId
redis.call('sadd', orderKey,userId )
--3.6 发送消息到队列当中XADD stream.orders * k1 v1 k2 v2..
redis.call('xadd', 'stream.orders' , '*' , 'userId', userId ,'voucherId' ,voucherId,'id', orderId)
return 0

如果抢购成功,将优惠券id和用户id封装后存入消息队列

 @Override
    public Result seckillVoucher(Long voucherId) {

        Long userId = UserHolder.getUser().getId();
        //执行lua脚本
        Long result = stringRedisTemplate.execute(
                SECKILL_SCRIPT,
                Collections.emptyList(),
                voucherId.toString(),
                userId.toString());
        //判断是否为0
        int r = result.intValue();
        if(r != 0){
            //不为0,代表没有购买资格
            return Result.fail(r == 1? "库存不足" : "不能重复下单");
        }
        //2.2 为0,有购买资格,把下单信息保存到阻塞队列
        long orderId = redisIdWorker.nextId("order");
        //订单信息
        VoucherOrder voucherOrder = new VoucherOrder();
        voucherOrder.setId(orderId);
        voucherOrder.setUserId(userId);
        voucherOrder.setVoucherId(voucherId);

        //orderTasks.add(voucherOrder);
        return Result.ok(orderId);
    }

开启线程任务,不断从消息队列中获取信息,实现异步下单功能

jmeter压测

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Unity接入日志插件Log4Net

前言 log4net是一个日志插件&#xff0c;可以帮助我们把控制台输出的日志写入到本地。这个功能说简单就简单&#xff0c;说复杂其实还挺复杂。 为什么这么说呢&#xff0c;首先文件写入本地确实简单&#xff0c;但是如果你要实现一下功能就没那么简单了。 1.把每行日志按照指…

2022年“新一代”设备管理系统——支持低代码平台

在现代化企业的信息化管理体系建设中&#xff0c;设备管理系统被看作是重中之重。因为设备是工厂的主要生产要素&#xff0c;而且随着生产设备的日益增多&#xff0c;设备的重要性日益凸显。如何妥善管理这些设备也成了企业管理者经常考虑的问题。单纯依靠人工管理逐渐不能满足…

uniapp实现下拉刷新及上拉(分页)加载更多(app,H5,小程序均可使用)

开门见山地说&#xff0c;在移动端开发中&#xff0c;80%的项目都会涉及到列表展示&#xff0c;而有了列表不可避免的需求就是列表的下拉刷新和上拉加载更多。本篇文章主要介绍在使用uniapp开发移动端的过程中&#xff0c;比较好用的一个下拉及上拉组件&#xff0c;节约大家选择…

5.2启动内存分页机制,畅游虚拟空间

5.2启动内存分页机制&#xff0c;畅游虚拟空间 即使机器上只有512MB的内存&#xff0c;每个进程自己的内存空间也是4GB&#xff0c;这4GB便是指的虚拟内存空间。下面就是讲解虚拟内存空间是怎么来的。 5.2.1内存为什么要分页 问题场景&#xff1a;由于多进程的发展&#xff…

软考 - 计算机组成与结构

数据计算 数据的进制转化 十六进制符号 0X 或 H&#xff0c;可表示为0x18F 或 18FH m进制转n进制&#xff1a;先将m进制转化为十进制数&#xff0c;再将十进制数转化为n进制数&#xff08;2进制可直接转8进制&#xff08;3位&#xff09;和16进制&#xff08;4位&#xff09…

Python如何自动操作电脑桌面应用程序

前言 本文是该专栏的第2篇,后面会持续分享python的各种黑科技知识,值得关注。 熟悉python的朋友,都知道python可以做自动化,比如说selenium,pyppeteer,airtest等等。 但你是否听说过python可以来自动操作电脑桌面的应用程序呢,趟若临时接到某个需求,让你用python脚本…

基于zynq7100的OV5640摄像头照相机实验,提供工程源码和技术支持

目录1.设计架构2.工程简介3.zynq配置4.sd卡文件系统FATFS配置5.sd卡文件系统FATFS读写测试6.OV5640摄像头显示测试7.OV5640摄像头循环拍照测试8.OV5640摄像头按键拍照测试9.上板调试10.福利领取1.设计架构 设计框图如下&#xff1a; 采用Xilinx官方推荐的VDMA架构实现图像缓存…

TiDB丨一次TiDB GC阻塞引发的“惨案”......

前不久&#xff0c;从项目一线同学得到某集群的告警信息&#xff0c;某个时间段 TiDB duration 突然异常升高&#xff0c;持续时间6小时左右&#xff0c;需要定位到具体原因。 于是乎&#xff0c;我们就来一场关于TiDB GC阻塞的排查...... 分析过程 第一招 初步判断 既然…

Docker+nginx在CVM的机器远程发布hellogin

有两种方式&#xff0c;一种通过docker容器安装&#xff0c;一种是直接安装, 这里我们通过docker服务安装 常用操作 images是查询当前机器上所有的镜像有哪些 docker images删除镜像 docker rmi [MAGE ID ]可以查当前运行中的容器 docker ps -a开始/停止/删除容器 docker…

动态树的最值

一 问题描述 一棵树有 N 个节点&#xff0c;每个节点都有一个权值 Wi &#xff0c;有 4 种操作。 ① 1 x y &#xff0c;在两个节点 x、y 之间添加一条新边。因此&#xff0c;在这种操作之后&#xff0c;两棵树将连接成一棵新树。 ② 2 x y &#xff0c;在树集合中找到包含节…

LeetCOde-剑指46-把数字翻译成字符串

1、动态规划法 我们通过观察可以发现&#xff0c;假如我们使用数组dp[i]dp[i]dp[i]来记录前iii位可能构成的字符串个数&#xff1a;1、当新加入的第i1i1i1位和第iii位能够构成一个大于9小于26的数字时&#xff0c;dp[i1]dp[i]dp[i−1]dp[i1]dp[i]dp[i-1]dp[i1]dp[i]dp[i−1]&a…

容器化部署(k8s)任务调度平台xxl-job(部署过程及踩坑问题记录)

文章预览&#xff1a;1 部署过程&#xff08;下方ip代表服务器的ip哈&#xff09;1.1 制作服务打包镜像DockerFile1.2 制作执行脚本run.sh1.3 jar包上上传1.4 kuboard创建----配置信息2 踩坑问题记录2.1 日志抛出异常2.2 原因分析2.3 过程分析及解决2.4 执行调度测试导入sql等过…

Baklib|SaaS产品,实现企业流程数字化

正如许多科技潮流一样&#xff0c;“SaaS”这个词也逐渐成为企业经理们谈论的话题。然而&#xff0c;如果您对“SaaS”一无所知&#xff0c;您可能会感到困惑并容易忽略它。那么&#xff0c;什么是“SaaS”&#xff1f;它的优点是什么&#xff1f;它如何帮助企业实现数字化转型…

SSM+VUE+ElementUI实现宠物领养系统,期末大作业

SSMVUEElementUI实现宠物领养系统 系统角色 领养人&#xff0c;管理员 系统功能 本系统的功能主要分为四大模块&#xff1a; 领养人用户模块&#xff1a;注册、领养人登录、申请领养、查看小动物信息、发布留言领养机构员工用户模块&#xff1a;领养机构员工登录、增加小动…

地平线开发者社区真心话大冒险,邀你闯关!

Hello&#xff01; 各位初次见面的萌新和久经沙场的社牛 目前开发者社区已成立两年有余 感谢大家一路上的支持和理解 今天&#xff0c;我们也准备了一些小礼品 希望倾听大家作为用户和开发者的真心话 同时&#xff0c;也欢迎初次见面的萌新们一同冒险 期待陪伴大家走过更…

Protect Privacy from Gradient Leakage Attack in Federated Learning

wangjunxiao/GradDefense: Defense against Gradient Leakage Attack (github.com) Summary 针对DGA和DIA攻击&#xff0c;提出了一个轻量、保证训练准确性、够用的的防御机制。防御机制主要包括随机layer添加扰动&#xff0c;然后进行梯度补偿来减少噪声对模型准确性的影响。…

CORS处理跨域问题

“前后端分离的项目必然会遇到一个典型的问题——跨域问题。” 跨域 要解决跨域问题&#xff0c;首先得知道什么是跨域&#xff1f; 首先&#xff0c;跨域是访问的域名或IP、端口三者有一不同都属于跨域。&#xff08;注意请求路径不是&#xff09;&#xff0c;即使在本地测试&…

【计算机网络】学习笔记--第一章

【计算机网络】学习笔记--第一章基本概念端系统之间的通信客户-服务器方式&#xff08;C/S方式&#xff09;对等连接方式&#xff1a;三种交换方式电路交换&#xff08;Circuit Switching&#xff09;分组交换&#xff08;Packet Switching&#xff09;报文交换&#xff08;Mes…

centos7 安装与卸载 Mysql 5.7.27(详细完整教程)

目录 卸载 安装 卸载 1、关闭MySQL服务 systemctl stop mysqld2、使用 rpm 命令查看已安装的安装包 [nameVM-20-12-centos mysql1]$ rpm -qa|grep mysql 3、使用yum卸载安装的mysql [nameVM-20-12-centos mysql1]$ sudo yum remove mysql mysql-server mysql-libs mysql…

【LeetCode每日一题:775.全局倒置与局部倒置~~~维护前缀最大值】

题目描述 给你一个长度为 n 的整数数组 nums &#xff0c;表示由范围 [0, n - 1] 内所有整数组成的一个排列。 全局倒置 的数目等于满足下述条件不同下标对 (i, j) 的数目&#xff1a; 0 < i < j < n nums[i] > nums[j] 局部倒置 的数目等于满足下述条件的下标 …