• 分布式锁实现方案


    分布式锁

    1 什么是分布式锁

    ​ 就是在分布式环境下,保证某个公共资源只能在同一时间被多进程应用的某个进程的某一个线程访问时使用锁。

    2 几个使用场景分析

    一段代码同一时间只能被同一个不同进程的一个线程执行

    • 库存超卖 (库存被减到 负数),上面案例就是库存超卖

    • 定时任务

    • 分布式缓存中缓存同步

    • 转账(多个进程修改同一个账户)

    3 需要什么样的分布式锁-特征

    • 可以保证在分布式部署的应用集群中同一个方法在同一时间只能被一台机器上的一个线程执行。(互斥性)

    • 这把锁要是一把可重入锁(避免死锁)(重入性)

    • 这把锁最好是一把阻塞锁(自旋)(根据业务需求考虑要不要这条)

    • 这把锁最好是一把公平锁(根据业务需求考虑要不要这条)

    • 获取锁和释放锁的性能要好

    4 常见的分布式锁解决方案

    1.4.1. 思路

    • 当在分布式模型下,数据只有一份(或有限制),此时需要利用锁的技术控制某一时刻修改数据的进程数。

    • 与单机模式下的锁不仅需要保证进程可见,还需要考虑进程与锁之间的网络问题。

    • 分布式锁还是可以将标记存在公共内存(redis),只是该内存不是某个进程分配的内存而是公共内存如 Redis、Memcache。至于利用数据库、文件(oss),zk等做锁与单机的实现是一样的,只要保证标记能互斥就行。

      在多个进程公共能够访问地方放个标识!一个进程的某个线程进去时,标识已经进去(获取到锁),其他线程就要等待!直到原来的线程释放,新的线程才能可以获取锁.

    1.4.2. 分布式锁三种方式

    • 基于数据库操作

    • 基于redis缓存和过期时间

    • 基于zookeeper 临时顺序节点+watch

    ​ 从理解的难易程度角度(从低到高)数据库 > redis > Zookeeper

    ​ 从实现的复杂性角度(从低到高)数据库> redis >Zookeeper

    ​ 从性能角度(从高到低)redis > Zookeeper > 数据库

    ​ 从可靠性角度(从高到低)Zookeeper > redis > 数据库

    Zookeeper >redis>数据库(基本不用)

    基于数据库基本不用,zk或redis要根据项目情况来决定,如果你项目本来就用到zk,就使用zk,否则redis

    分布式环境互斥实现

    1 数据库锁

    1.1 悲观锁 innodb行锁

    • 共享锁(S Lock):允许事务读一行数据,具有锁兼容性质,允许多个事务同时获得该锁。
    • 排它锁(X Lock):允许事务删除或更新一行数据,具有排它性,某个事务要想获得锁,必须要等待其他事务释放该对象的锁。

    X锁和其他锁都不兼容,S锁值和S锁兼容,S锁和X锁都是行级别锁,兼容是指对同一条记录(row)锁的兼容性情况。

    Mysql innodb锁的默认操作:

    • 我们对某一行数据进行查询是会默认使用S锁加锁,如果硬是要把查询也加X锁使用
     	@Select("select * from t_goods where id = #{id}")
        Goods laodByIdForUpdate(Long id);
    
    • 读的时候硬是要加x锁
      @Select("select * from t_goods where id = #{id} **for update**")
        Goods laodByIdForUpdate(Long id); 
    
    • 当我们对某一行数据进行增删改是会加X锁

    1.2 乐观锁

    ​ 乐观锁是相对悲观锁而言的,乐观锁假设数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回给用户错误的信息,让用户决定如何去做。

    ​ 直接用:表中添加一个时间戳或者版本号的字段来实现,update account set version = version + 1 where id = #{id} and version = #{oldVersion} 当更新不成功,客户端重试,重新读取最新的版本号或时间戳,再次尝试更新,类似 CAS 机制,推荐使用。

    2 分布式锁

    疑问?既然可以使用数据库悲观锁和乐观锁保证分布式环境的互斥!那为什么还要分布式锁!

    有的操作是没有数据库参与的,又想分布式环境互斥! 就必须使用分布式锁!

    2.1 基于数据库的

    • 方案1 主键

      主键不能重复

        //基于数据库的分布式锁实现
      public class DbGoodsLock {
      
          private Long goodsId = null;
          public DbGoodsLock(Long goodsId) {
              this.goodsId = goodsId;
          }
      
          /**
           * 能插入就能获取获取锁
           * @return
           */
          public  boolean  trylock(){
              Connection connection = null;
              try{
                 connection  = JDBCUtils.getConnection();
                  Statement statement = connection.createStatement();
      
                  statement.execute("insert into t_goods_lock(id) values("+this.goodsId+")");
                  System.out.println(Thread.currentThread().getName()+"加锁,插入数据 goodsId="+goodsId);
                  return true;
              }catch (Exception e) {
                  //e.printStackTrace();
                  System.out.println(Thread.currentThread().getName()+"加锁异常====================:"+e.getMessage());
                  return false;
              }
              finally {
                  if (connection != null) {
                      try {
                          connection.close();
                      } catch (SQLException e) {
                          e.printStackTrace();
                      }
                  }
              }
          }
      
          //阻塞获取锁
          public  void lock(){
              if (trylock())
                  return;
      
              try {
                  Thread.sleep(10);
                  System.out.println("尝试获取锁...");
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              lock();
          }
      
      
          //释放锁
          public  boolean  unlock(){
              Connection connection = null;
              try{
                  connection  = JDBCUtils.getConnection();
                  Statement statement = connection.createStatement();
      
                  statement.execute("delete from t_goods_lock where id = "+goodsId);
                  System.out.println(Thread.currentThread().getName()+"解锁,删除数据 goodsId="+goodsId);
                  return true;
              }catch (Exception e) {
                  System.out.println(Thread.currentThread().getName()+"解锁异常====================:"+e.getMessage());
                  //e.printStackTrace();
                  return false;
              }
              finally {
                  if (connection != null) {
                      try {
                          connection.close();
                      } catch (SQLException e) {
                          e.printStackTrace();
                      }
                  }
              }
          }
      }
      
      
    • 方案2

      唯一字段不能重复,和上面原来一样

    • l 数据库是单点?搞两个数据库,数据之键双向同步,一旦挂掉快速切换到备库上。 主备切换

    • l 没有失效时间?只要做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍。

    • l 非阻塞的?搞一个 while 循环,直到 insert 成功再返回成功。

    • l 非重入的?在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。

      n 获取:再次获取锁的同时更新count(+1).

      n 释放:更新count-1,当count==0删除记录。

      l 非公平的?-mq

    ​ 数据库实现分布式锁,一般都很少用

    2.2 redis

    方案1:原生

    1 setnx(如果不存在才设置成功)+del没有可以就添加  setnx goods_id = 1   del goods_id
    2 expire+watchdog续约时间(不好做,我不做)
    3 value是uuid,获取判断,删除
    4 lua脚本
    
    public interface IDistributedLock {
        /**
         * 自旋上锁
         */
        void lock();
    
        /**
         * 释放锁
         */
        void unlock();
    
        /**
         * 尝试获取锁
         */
        boolean tryLock();
    }
    
    
    public class RedisLock implements IDistributedLock {
    
        private String resourceName;
        private String lockVal;  //try del都有用到uuid,所以构造的时候产生一个成员变量
        private RedisTemplate redisTemplate;
    
        //不交给spring管理就一般用不了RedisTemplate
        public RedisLock(String resourceName) {
            this.resourceName = resourceName;
            this.lockVal = UUID.randomUUID().toString();
            ApplicationContext context = ApplicationContextHolder.getApplicationContext();
            redisTemplate = (RedisTemplate) context.getBean("redisTemplate");
        }
    
        @Override
        public void lock() {
            if (tryLock())
                return;
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            lock();
        }
    
    
        @Override
        public void unlock() {
           //get check del--->lua   key value
            /*if redis.call('get', KEYS[1]) == KEYS[2] then
               return redis.call('del', KEYS[1])
            else
                return 0
                end*/
    
            List<String> params = Arrays.asList(resourceName, lockVal); //goods_1 jfjjfjflfjof
            redisTemplate.execute(redisScript(),params);
    
        }
    
        public RedisScript<Long> redisScript(){
            DefaultRedisScript<Long> script = new DefaultRedisScript<>();
            script.setResultType(Long.class);
            //script.setScriptSource(new ResourceScriptSource(new ClassPathResource("redis.lua")));
            script.setScriptText("if redis.call('get', KEYS[1]) == KEYS[2] then return redis.call('del', KEYS[1]) else return 0 end");
            return script;
        }
    
        @Override
        public boolean tryLock() {
            //uuid setnx expire
            //hash K(resourceName) k(uuid) v(count)+
    //        redisTemplate.opsForHash().putIfAbsent(resourceName,lockVal,1);
    //        redisTemplate.expire(resourceName,3,TimeUnit.SECONDS);
            Boolean result = redisTemplate.opsForValue().setIfAbsent(resourceName, lockVal, 10, TimeUnit.SECONDS);
            System.out.println(resourceName);
            System.out.println(lockVal);
            return  result;
        }
    }
    
    
    
    @Service
    public class GoodsServiceImpl_redis_Lock implements IGoodsService {
    
    //    @Autowired
    //    private IDistributedLock lock;
        @Autowired
        private GoodsMapper goodsMapper;
        @Override
        public Goods getById(Long id) {
            return goodsMapper.laodById(1L);
        }
    
        @Override
        @Transactional
        public void updateNum(Map<String,Object> params) {
            Long goodsId = (Long) params.get("id");
            Integer num = (Integer) params.get("num");
            String resourceName = "goods_"+goodsId;
            IDistributedLock lock = new RedisLock(resourceName);
            try{
                lock.lock();
                System.out.println(Thread.currentThread().getName()+" get lock!");
                Goods goods = goodsMapper.laodById(goodsId);
                Thread.sleep(4000);
                System.out.println(goods);
                System.out.println(Thread.currentThread().getName()+goods.getCount()+":"+num);
                if (goods.getCount()>=num){
                    goodsMapper.updateNum(params);
                    System.out.println(Thread.currentThread().getName()+"buy "+num+"!");
    
                }
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                if (lock != null) {
                    lock.unlock();
                }
            }
    
    
        }
    }
    
    

    方案2:框架实现

    ​ 业界也提供了多个现成好用的框架予以支持分布式锁,比如Redisson、spring-integration-redis,redlock redisson,redlock 底层原理

    <dependency>
        <groupId>org.redissongroupId>
        <artifactId>redissonartifactId>
        <version>3.2.3version>
    dependency>
    
    @Value("${spring.redis.host}")
        private String redisHost;
    
        @Value("${spring.redis.port}")
        private int redisPort;
    
        @Value("${spring.redis.database}")
        private int redisdatabase;
    
        @Value("${spring.redis.password}")
        private String redisPassword;
    
        @Bean
        public RedissonClient redissonClient() {
            Config config = new Config();
            config.useSingleServer().setAddress(redisHost + ":" + redisPort);
            config.useSingleServer().setDatabase(redisdatabase);
            config.useSingleServer().setPassword(redisPassword);
            RedissonClient redisson = Redisson.create(config);
            return redisson;
        }
    
    @Service
    public class GoodsServiceImpl_redission implements IGoodsService {
    
        @Autowired
        private GoodsMapper goodsMapper;
    
        @Autowired
        private RedissonClient redissonClient;
    
        @Override
        public void updateNum(Map<String,Object> params) {
            Long goodsId = (Long) params.get("id");
            Integer num = (Integer) params.get("num");
            System.out.println(Thread.currentThread().getName()+"enter!");
            String resourceName = "goods" + goodsId;
            RLock rLock = redissonClient.getLock(resourceName);
            try{
                rLock.lock();
                System.out.println(Thread.currentThread().getName()+" get lock!");
                Goods goods = goodsMapper.laodById(goodsId);
                System.out.println(goods);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(goods.getCount()+":"+num);
                if (goods.getCount()>=num){
                    goodsMapper.updateNum(params);
                    System.out.println(Thread.currentThread().getName()+"buy "+num+"!");
    
                }
    
            }catch (Exception e){
                e.printStackTrace();
            }
            finally {
                if (rLock != null) {
                    rLock.unlock();
                    System.out.println(Thread.currentThread().getName()+" unlock!");
                }
            }
    
    
        }
    
        @Override
        public Goods getById(Long id) {
            return goodsMapper.laodById(1L);
        }
    }
    
    

    2.3 zk

    2.3.1 zk理论
    2.3.1.1 是什么?

    ZooKeeper是Apache下的一个Java开源项目(最初由Yahoo开发, 后捐献给了Apache)。

    ZooKeeper的原始功能很简单,基于它的层次型的目录树的数据结构,并通过对树上的节点进行有效管理,可以设计出各种各样的分布式集群管理功能。此外, ZooKeeper本身 也是分布式的。

    2.3.1.2 数据库模型

    Zookeeper会维护一个具有层次关系的树状的数据结构,它非常类似于一个标准的文件系统,如下图所

    示:同一个目录下不能有相同名称的节点
    在这里插入图片描述

    2.3.1.3 节点分类

    ZooKeeper 节点是有生命周期的这取决于节点的类型,在 ZooKeeper 中,节点类型可以分为持久节点(PERSISTENT )、临时节点(EPHEMERAL),以及时序节点(SEQUENTIAL ),具体在节点创建过程中,一般是组合使用,可以生成以下 4 种节点类型。 是否持久化,是否有序

    • 持久节点(PERSISTENT)与临时节点(EPHEMERAL)
      所谓持久节点,是指在节点创建后,就一直存在,直到有删除操作来主动清除这个节点——不会因为创建该节点的客户端会话失效而消失。

      和持久节点不同的是,临时节点的生命周期和客户端会话绑定。也就是说,如果客户端会话失效,那么这个节点就会自动被清除掉。注意,这里提到的是会话失效,而非连接断开。另外,在临时节点下面不能创建子节点。

    • 顺序节点(SEQUENTIAL) 无序节点
      这类节点的基本特性和上面的节点类型是一致的。额外的特性是,在ZK中,每个父节点会为他的第一级子节点维护一份时序,会记录每个子节点创建的先后顺序。基于这个特性,在创建子节点的时候,可以设置这个属性,那么在创建节点过程中,ZK会自动为给定节点名加上一个数字后缀,作为新的节点名。这个数字后缀的范围是整型的最大值。 00010001 00010002 00010003

      无序节点就是没有顺序

      具体组合得到四种:持久有序 持久无序 临时有序 临时无序

    2.3.2 入门
    2.3.2.1 安装
    • 官方下载地址:http://mirrors.cnnic.cn/apache/zookeeper/ , 下载后获得,解压即可安装。

    • 安装配置: 把conf目录下的zoo_sample.cfg改名成zoo.cfg,这里我是先备份了zoo_sample.cfg再改的名。修改zoo.cfg的值如下:

      dataDir=D:/zookeeper-3.4.9/data/data

      dataLogDir=D:/zookeeper-3.4.9/data/log

    • 启动 :点击bin目录下的zkServer.cmd 这时候出现下面的提示就说明配置成功了。

    • 图形界面-ZooViewer:https://blog.csdn.net/u010889616/article/details/80792912

    2.3.2.2 代码测试-使用代码创建各种节点
    
    <dependency>
        <groupId>com.101tecgroupId>
        <artifactId>zkclientartifactId>
        <version>0.10version>
    dependency>
    

    API总结:

    • l new ZkClient(“127.0.0.1:2181”,5000); 创建zookeeper客户端

    • l client.getChildren(“/”):获取子节点 “/”代表根节点

    • l client.createPersistent:创建持久节点

    • l client.createPersistentSequential:创建持久有顺节点,会在path后面增加序号

    • l client.createEphemeral:创建临时节点

    • l client.createEphemeralSequential:创建临时有序节点

    • l client.subscribeChildChanges:订阅子节点的改变

    • l client.subscribeDataChanges:订阅某个节点的数据改变

    @Test //持久化节点
    public void test1() throws Exception {
        //创建客户端
        ZkClient client = new ZkClient("127.0.0.1:2181",5000);
        //获取根节点
        List<String> children = client.getChildren("/");
        for (String child : children) {
            System.out.println(child);  //zookeeper
        }
    
        //创建持久节点
       client.createPersistent("/zookeeper/createPersistent");
    
    
        //创建持久顺序节点
        String persistentSequential = 
    client.createPersistentSequential("/zookeeper/createPersistentSequential", "111");
    
        System.out.println("persistentSequential="+persistentSequential);    
    //  /zookeeper/createPersistentSequential0000000003
    
        //创建临时节点
        client.createEphemeral("/zookeeper/createEphemeral");
        //client.createEphemeral("/zookeeper/createEphemeral"); //重复创建会报错
    
        //创建临时顺序节点
        String ephemeralSequential = 
    client.createEphemeralSequential("/zookeeper/createEphemeralSequential", "111");
        System.out.println("ephemeralSequential="+ephemeralSequential);
    
        //关闭
        client.close();
    }
    
    //测试监听
        @Test
        public void test3() throws Exception {
            //创建客户端
            ZkClient client = new ZkClient("127.0.0.1:2181",5000);
    
            if(!client.exists("/yhptest")){
                client.createPersistent("/yhptest");
            }
            //操作节点
            client.createPersistentSequential("/yhptest/test","x1");
            client.createPersistentSequential("/yhptest/test","x2");
            client.createPersistentSequential("/yhptest/test","x3");
            client.createPersistentSequential("/yhptest/test","x4");
            client.createPersistent("/yhptest/tests","aa");
            List<String> children = client.getChildren("/yhptest");
            for (String child : children) {
                System.out.println(child);
            }
            //关闭
            client.subscribeChildChanges("/yhptest", new IZkChildListener() {
                @Override
                public void handleChildChange(String s, List<String> list) throws Exception {
                    System.out.println("子节点改变:"+s);
                    System.out.println("子节点改变:"+list);
                }
            });
            client.subscribeDataChanges("/yhptest/tests", new IZkDataListener() {
                @Override
                public void handleDataChange(String s, Object o) throws Exception {
                    //数据改变
                    System.out.println("数据改变:"+s);
                    System.out.println("数据改变:"+o.toString());
                }
                @Override
                public void handleDataDeleted(String s) throws Exception {
                    System.out.println("数据删除");
                }
            });
    
            client.delete("/yhptest/tests");
            Thread.sleep(2000);
            client.close();
        }
    
    2.3.3 zk分布式锁-原生

    在这里插入图片描述

    2.3.3.1 非公平锁

    ​ 根据Zookeeper的临时节点的特性实现分布式锁,先执行的线程在zookeeper创建一个临时节点,代表获取到锁,后执行的线程需要等待,直到临时节点被删除说明锁被释放,第二个线程可以尝试获取锁。

    T1 创建临时无序节点goods_2 执行业务逻辑 关闭

    T2 创建临时无序节点goods_2 执行业务逻辑 关闭

    2.3.3.2 公平锁

    在这里插入图片描述

    public class ZookeeperDistributedLock implements ExampleLock {
    
    
        ZkClient client = new ZkClient("127.0.0.1:2181",
                5000);
        CountDownLatch cdl = new CountDownLatch(1); //不为零阻塞住,不让他往下走
    
        //父节点路径
        String parent = "";
        //当前节点路径
        String currentPath = "";
    
        //1 goods
        // lock_goods_id 父节点(持久节点)
        // lock_goods_id_001
        // lock_goods_id_002
        @Override
        public void lock(String resourceName) {
    
            parent = "/"+resourceName;
            //判断父节点是否存在,如果不存在要创建一个持久节点
            if (!client.exists(parent)){
                client.createPersistent(parent,"root");
            }
    
            //前面的节点都处理完成,自己变成第一个节点才加锁成功。
            if (!tryLock(resourceName)){
                lock(resourceName);
            }
    
        }
    
        @Override
        public void unlock(String resourceName) {
    
            //自己操作完毕,删除自己,让下一个节点执行。
            System.out.println(currentPath);
            System.out.println(System.currentTimeMillis());
            System.out.println(client.delete(currentPath));
            client.close();
        }
    
        @Override
        public boolean tryLock(String resourceName) {
            //创建子节点-临时顺序节点
            if (StringUtils.isEmpty(currentPath)){
                currentPath = client
                        .createEphemeralSequential(parent + "/test", "test"); //test0001
            }
            //如果是第一个节点,就获取到锁了。
            List<String> children = client.getChildren(parent);
            System.out.println(currentPath+"jjj");
            for (String child : children) {
                System.out.println(child);
            }
            Collections.sort(children);
    
            ///goods_1/test0000000003jjj
            //test0000000003
            if (currentPath.contains(children.get(0))){
                return true;
            }else{
                //如果不是第一个节点,监听前一个节点,要再这儿等待,知道被触发,再次判断是否是第一个节点进行返回就OK
                String str = currentPath.substring(
                        currentPath.lastIndexOf("/")+1);
                System.out.println(str);
                int preIndex = children.indexOf(str)-1;
                String prePath = parent+"/"+children.get(preIndex);
    //监听上一个节点,如果上一个节点被删除,把秒表设置为 0 (cdl.countDown();),那么当前节点取消等待(cdl.await();)重新获取锁
    
                client.subscribeDataChanges(prePath, new IZkDataListener() {
                    @Override
                    public void handleDataChange(String dataPath, Object data) throws Exception {
    
                    }
    
                    @Override
                    public void handleDataDeleted(String dataPath) throws Exception {
                        //让他-1 变为零
                        cdl.countDown();
                    }
                });
    
                //一直等待,直到自己变成第一个节点
                try {
                    cdl.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
                return false;
    
            }
        }
    }
    
    2.3.4 zk分布式锁 curator框架实现
    • 导入jar
    <dependency>
        <groupId>org.apache.curatorgroupId>
        <artifactId>curator-recipesartifactId>
        <version>4.1.0version>
    dependency>
    
    
    • 配置

      @Configuration
      public class ZkCuratorConfig {
          //初始化方法start
          @Bean(initMethod = "start",destroyMethod = "close") //bean声明周期  构造  初始化initMethod  使用  销毁destroyMethod
          public CuratorFramework curatorFramework(){
              //重试策略
              RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
              //创建客户端
              CuratorFramework client = CuratorFrameworkFactory.newClient("127.0.0.1:2181", retryPolicy);
              return client;
          }
      }
      
      
    • 代码

    @Autowired
        private CuratorFramework framework;
        @Override
        public void updateNum(Map<String,Object> params) {
            Long goodsId = (Long) params.get("id");
            Integer num = (Integer) params.get("num");
    
            String resourceName = "/goods_"+goodsId;
            InterProcessMutex mutex = null;
            try{
                mutex = new InterProcessMutex(framework, resourceName); //1个信号量
                mutex.acquire(3, TimeUnit.SECONDS); //获取一个,  自旋(拿不到一直循环)   适应性自选(拿几秒就算乐 )
                System.out.println(Thread.currentThread().getName()+" get lock!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(System.currentTimeMillis());
                //通过商品id获取商品  3
                Goods goods = goodsMapper.laodById(goodsId);
                System.out.println(goods);
                System.out.println(goods.getCount()+":"+num);
                if (goods.getCount()>=num){
                    goodsMapper.updateNum(params);
                    System.out.println(Thread.currentThread().getName()+"buy "+num+"!");
    
                }
            }catch (Exception e){
                e.printStackTrace();
            }
            finally {
                if (mutex != null) {
                    try {
                        mutex.release();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
    
    
        }
    

  • 相关阅读:
    数据结构教程(第五版 李春葆 上机实验题4 验证性实验)
    [附源码]计算机毕业设计JAVAJAVA点餐系统
    Windows下安装与配置Docker
    神经网络和深度神经网络,深度神经网络类型包括
    【代码思路】2023mathorcup 大数据数学建模B题 电商零售商家需求预测及库存优化问题
    云数据库知识学习——云数据库产品、云数据库系统架构
    springboot毕设项目大学生体质测试管理系统415ef(java+VUE+Mybatis+Maven+Mysql)
    Numpy数组中的维度和轴
    springcloud:四、nacos介绍+启动+服务分级存储模型/集群+NacosRule负载均衡
    计算机毕业设计——基于html汽车商城网站页面设计与实现论文源码ppt(35页) HTML+CSS+JavaScript
  • 原文地址:https://blog.csdn.net/Casual_Lei/article/details/139829321