Java中利用Redis,ZooKeeper,数据库等实现分布式锁(遥遥领先)

1. 分布式锁

1.1 什么是分布式锁

在我们进行单机应用开发涉及并发同步的时候,我们往往采用synchronized或者ReentrantLock的方式来解决多线程间的代码同步问题。但是当我们的应用是在分布式集群工作的情况下,那么就需要一种更加高级的锁机制,来处理种跨机器的进程之间的数据同步问题,这就是分布式锁。

分布式锁,是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到分布式锁。

分布式锁可以理解为:控制分布式系统有序的去对共享资源进行操作,通过互斥来保证数据的一致性。

分布式锁的实现方式有多种,例如:数据库实现方式、ZooKeeper实现方式、Redis实现方式等。

可能有人会问,例如synchronized、ReentrantLock不就能解决问题了吗?为什么还要使用分布式锁?

1.2 为什么要使用分布式锁

为了能够说明分布式锁的重要性,下面通过一个电商项目中减库存的案例来演示如果没有使用分布式锁会出现什么问题。代码如下:

第一步:导入坐标

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.2.5.RELEASE</version><relativePath/></parent><groupId>com.itheima</groupId><artifactId>lock-test</artifactId><version>1.0-SNAPSHOT</version><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--集成redis--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId><version>1.4.1.RELEASE</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.3</version></dependency><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId></dependency></dependencies></project>

第二步:配置application.yml文件

server:port: 8080
spring:redis:host: 68.79.63.42port: 26379password: (you password)

第三步:编写Controller

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
public class StockController {@Autowiredprivate StringRedisTemplate redisTemplate;@GetMapping("/stock")public String stock(){int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));if(stock > 0){stock --;redisTemplate.opsForValue().set("stock",stock+"");System.out.println("库存扣减成功,剩余库存:" + stock);}else {System.out.println("库存不足!!!");}return "OK";}
}

第四步:编写启动类

@SpringBootApplication
public class MyApplication {public static void main(String[] args) {SpringApplication.run(MyApplication.class,args);}
}

第五步:设置redis

在这里插入图片描述

测试方式:使用jmeter进行压力测试,如下:

在这里插入图片描述

在这里插入图片描述

注:Apache JMeter是Apache组织开发的基于Java的压力测试工具。用于对软件做压力测试,它最初被设计用于Web应用测试,但后来扩展到其他测试领域。

查看控制台输出,发现已经出现了线程并发问题,如下:

在这里插入图片描述

由于当前程序是部署在一个Tomcat中的,即程序运行在一个jvm中,此时可以对减库存的代码进行同步处理,如下:

在这里插入图片描述

再次进行测试(注意恢复redis中的数据),此时已经没有线程并发问题,控制台输出如下:

在这里插入图片描述

这说明如果程序运行在一个jvm中,使用synchronized即可解决线程并发问题。

下面将程序进行集群部署(如下图所示),并通过Nginx进行负载,再进行测试。

在这里插入图片描述

操作过程:

第一步:配置Nginx

	upstream upstream_name{server 127.0.0.1:8001;server 127.0.0.1:8002;}server {listen       8080;server_name  localhost;location / {proxy_pass http://upstream_name;proxy_set_header Host $host;proxy_set_header X-Real-IP $remote_addr;proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;}}

第二步:修改application.yml中端口号改为8001和8002并分别启动程序

第三步:使用jemter再次测试,可以看到又出现了并发问题

1.3 分布式锁应具有的特性
  • 在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行
  • 高可用的获取锁与释放锁
  • 高性能的获取锁与释放锁
  • 具备可重入特性
  • 具备锁失效机制,防止死锁
  • 具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败

2. 分布式锁实现方案

2.1 数据库实现分布式锁

基于数据库实现分布式锁的核心思想是:在数据库中创建一个表,表中包含方法名等字段,并在方法名字段上创建唯一索引。想要执行某个方法,首先需要将这个方法名插入表中,成功插入则获取锁,执行完成后删除对应的行数据释放锁。此种方式就是建立在数据库唯一索引的特性基础上的。

表结构如下:

在这里插入图片描述

具体实现过程如下(在前面lock-test工程基础上进行改造):

第一步:在pom.xml中导入maven坐标

<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.2.0</version>
</dependency>
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId>
</dependency>

第二步:配置文件application.yml中配置mybatis-plus相关配置

server:port: 8002
spring:redis:host: 68.79.63.42port: 26379password: itheima123application:name: lockTestdatasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://127.0.0.1:3306/dlockusername: rootpassword: root
mybatis-plus:configuration:map-underscore-to-camel-case: falseauto-mapping-behavior: full#log-impl: org.apache.ibatis.logging.stdout.StdOutImplmapper-locations: classpath*:mapper/**/*Mapper.xml

第三步:创建实体类

package com.itheima.entity;import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;@TableName("mylock")
public class MyLock implements Serializable {private int id;private String methodName;public int getId() {return id;}public void setId(int id) {this.id = id;}public String getMethodName() {return methodName;}public void setMethodName(String methodName) {this.methodName = methodName;}
}

第四步:创建Mapper接口

package com.itheima.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itheima.entity.MyLock;
import org.apache.ibatis.annotations.Mapper;@Mapper
public interface MyLockMapper extends BaseMapper<MyLock> {public void deleteByMethodName(String methodName);
}

第五步:在resources/mapper目录下创建Mapper映射文件MyLockMapper.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.itheima.mapper.MyLockMapper"><delete id="deleteByMethodName" parameterType="string">delete from mylock where methodName = #{value}</delete>
</mapper>

第六步:改造StockController

@Autowired
private MyLockMapper myLockMapper;@GetMapping("/stock")
public String stock(){MyLock entity = new MyLock();entity.setMethodName("stock");try {//插入数据,如果不抛异常则表示插入成功,即获得锁myLockMapper.insert(entity);int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));if(stock > 0){stock --;redisTemplate.opsForValue().set("stock",stock+"");System.out.println("库存扣减成功,剩余库存:" + stock);}else {System.out.println("库存不足!!!");}//释放锁myLockMapper.deleteByMethodName("stock");}catch (Exception ex){System.out.println("没有获取锁,不能执行减库存操作!!!");}return "OK";
}

通过观察控制台输出可以看到,使用此种方式已经解决了线程并发问题。

注意,虽然使用数据库方式可以实现分布式锁,但是这种实现方式还存在如下一些问题:

1、因为是基于数据库实现的,数据库的可用性和性能将直接影响分布式锁的可用性及性能,所以,数据库需要双机部署、数据同步、主备切换;

2、不具备可重入的特性,因为同一个线程在释放锁之前,行数据一直存在,无法再次成功插入数据,所以,需要在表中新增一列,用于记录当前获取到锁的机器和线程信息,在再次获取锁的时候,先查询表中机器和线程信息是否和当前机器和线程相同,若相同则直接获取锁;

3、没有锁失效机制,因为有可能出现成功插入数据后,服务器宕机了,对应的数据没有被删除,当服务恢复后一直获取不到锁,所以,需要在表中新增一列,用于记录失效时间,并且需要有定时任务清除这些失效的数据;

4、不具备阻塞锁特性,获取不到锁直接返回失败,所以需要优化获取逻辑,循环多次去获取。

5、在实施的过程中会遇到各种不同的问题,为了解决这些问题,实现方式将会越来越复杂;依赖数据库需要一定的资源开销,性能问题需要考虑。

2.2 ZooKeeper实现分布式锁

Zookeeper的数据存储结构就像一棵树,这棵树由节点组成,这种节点叫做Znode。

Zookeeper中节点分为4种类型:

1.持久节点 (PERSISTENT)

默认的节点类型。创建节点的客户端与zookeeper断开连接后,该节点依旧存在

2.持久顺序节点(PERSISTENT_SEQUENTIAL)

所谓顺序节点,就是在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号

3.临时节点(EPHEMERAL)

和持久节点相反,当创建节点的客户端与zookeeper断开连接后,临时节点会被删除

4.临时顺序节点(EPHEMERAL_SEQUENTIAL)

顾名思义,临时顺序节点结合和临时节点和顺序节点的特点:在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号;当创建节点的客户端与zookeeper断开连接后,临时节点会被删除

Zookeeper实现分布式锁的原理是基于Zookeeper的临时顺序节点,如下图:

在这里插入图片描述

客户端A和客户端B争抢分布式锁,其实就是在/my_lock节点下创建临时顺序节点,这个顺序节点由zk内部自行维护一个节点序号,序号最小则表示对应客户端获得锁。

Apache Curator是一个比较完善的ZooKeeper客户端框架,通过封装的一套高级API 简化了ZooKeeper的操作,其中就包括分布式锁的实现。

具体操作过程如下(在前面lock-test工程基础上进行改造):

第一步:在pom.xml中导入maven坐标

<dependency><groupId>org.apache.zookeeper</groupId><artifactId>zookeeper</artifactId><version>3.4.10</version>
</dependency>
<dependency><groupId>org.apache.curator</groupId><artifactId>curator-framework</artifactId><version>2.12.0</version>
</dependency>
<dependency><groupId>org.apache.curator</groupId><artifactId>curator-recipes</artifactId><version>2.12.0</version>
</dependency>

第二步:编写配置类

package com.itheima.config;
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class ZkConfig {@Beanpublic CuratorFramework curatorFramework(){RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000,3);CuratorFramework client = CuratorFrameworkFactory.builder().connectString("localhost:2181").sessionTimeoutMs(5000).connectionTimeoutMs(5000).retryPolicy(retryPolicy).build();client.start();return client;}
}

第三步:改造StockController

@Autowired
private CuratorFramework curatorFramework;@GetMapping("/stock")
public String stock() {InterProcessMutex mutex = new InterProcessMutex(curatorFramework,"/mylock");try {//尝试获得锁boolean locked = mutex.acquire(0, TimeUnit.SECONDS);if(locked){int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));if(stock > 0){stock --;redisTemplate.opsForValue().set("stock",stock+"");System.out.println("库存扣减成功,剩余库存:" + stock);}else {System.out.println("库存不足!!!");}//释放锁mutex.release();}else{System.out.println("没有获取锁,不能执行减库存操作!!!");}}catch (Exception ex){System.out.println("出现异常!!!");}return "OK";
}

通过观察控制台输出可以看到,使用此种方式已经解决了线程并发问题。

2.3 Redis实现分布式锁

redis实现分布式锁比较简单,就是调用redis的set命令设置值,能够设置成功则表示加锁成功,即获得锁,通过调用del命令来删除设置的键值,即释放锁。

2.3.1 版本一

加锁命令:set lock_key lock_value NX

解锁命令:del lock_key

Java程序:

@GetMapping("/stock")
public String stock() {try {//尝试加锁Boolean locked = redisTemplate.opsForValue().setIfAbsent("mylock", "mylock");if(locked){//加锁成功int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));if(stock > 0){stock --;redisTemplate.opsForValue().set("stock",stock+"");System.out.println("库存扣减成功,剩余库存:" + stock);}else {System.out.println("库存不足!!!");}//释放锁redisTemplate.delete("mylock");}else{System.out.println("没有获取锁,不能执行减库存操作!!!");}}catch (Exception ex){System.out.println("出现异常!!!");}return "OK";
}
2.3.2 版本二

上面版本一的实现中存在一个问题,就是当某个线程获得锁后程序挂掉,此时还没来得及释放锁,这样后面所有的线程都无法获得锁了。为了解决这个问题可以在加锁时设置一个过期时间防止死锁。

加锁命令:set lock_key lock_value NX PX 5000

解锁命令:del lock_key

Java程序:

@GetMapping("/stock")
public String stock() {try {//尝试加锁Boolean locked = redisTemplate.opsForValue().setIfAbsent("mylock", "mylock",5000,TimeUnit.MILLISECONDS);if(locked){//加锁成功int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));if(stock > 0){stock --;redisTemplate.opsForValue().set("stock",stock+"");System.out.println("库存扣减成功,剩余库存:" + stock);}else {System.out.println("库存不足!!!");}//释放锁redisTemplate.delete("mylock");}else{System.out.println("没有获取锁,不能执行减库存操作!!!");}}catch (Exception ex){System.out.println("出现异常!!!");}return "OK";
}
2.3.3 版本三

针对前面版本二还有一点需要优化,就是加锁和解锁必须是同一个客户端,所以在加锁时可以设置当前线程id,在释放锁时判断是否为当前线程加的锁,如果是再释放锁即可。

Java程序:

@GetMapping("/stock")
public String stock() {try {String threadId = Thread.currentThread().getId()+"";//尝试加锁Boolean locked = redisTemplate.opsForValue().setIfAbsent("mylock",threadId,5000,TimeUnit.MILLISECONDS);if(locked){//加锁成功int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));if(stock > 0){stock --;redisTemplate.opsForValue().set("stock",stock+"");System.out.println("库存扣减成功,剩余库存:" + stock);}else {System.out.println("库存不足!!!");}String myValue = redisTemplate.opsForValue().get("mylock");if(threadId.equals(myValue)){//释放锁redisTemplate.delete("mylock");}}else{System.out.println("没有获取锁,不能执行减库存操作!!!");}}catch (Exception ex){System.out.println("出现异常!!!");}return "OK";
}

3. Redisson

3.1 Redisson介绍

Redisson是架设在Redis基础上的一个Java驻内存数据网格(In-Memory Data Grid)。充分的利用了Redis键值数据库提供的一系列优势,基于Java实用工具包中常用接口,为使用者提供了一系列具有分布式特性的常用工具类。使得原本作为协调单机多线程并发程序的工具包获得了协调分布式多机多线程并发系统的能力,大大降低了设计和研发大规模分布式系统的难度。同时结合各富特色的分布式服务,更进一步简化了分布式环境中程序相互之间的协作。

Redisson已经内置提供了基于Redis的分布式锁实现,此种方式是我们推荐的分布式锁使用方式。

Redisson的maven坐标如下:

<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.10.1</version>
</dependency>
3.2 Redisson分布式锁使用方式

第一步:在pom.xml中导入redisson的maven坐标

<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.10.1</version>
</dependency>

第二步:编写配置类

package com.itheima.config;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class RedissonConfig {@Value("${spring.redis.host}")private String host;@Value("${spring.redis.port}")private String port;@Value("${spring.redis.password}")private String password;@Beanpublic RedissonClient redissonClient(){Config config = new Config();config.useSingleServer().setAddress("redis://" + host + ":" + port);config.useSingleServer().setPassword(password);final RedissonClient client = Redisson.create(config);return client;}
}

第三步:改造Controller

@Autowired
private RedissonClient redissonClient;@GetMapping("/stock")
public String stock() {//获得分布式锁对象,注意,此时还没有加锁成功RLock lock = redissonClient.getLock("mylock");try {//尝试加锁,如果加锁成功则后续程序继续执行,如果加锁不成功则阻塞等待lock.lock(5000,TimeUnit.MILLISECONDS);int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));if(stock > 0){stock --;redisTemplate.opsForValue().set("stock",stock+"");System.out.println("库存扣减成功,剩余库存:" + stock);}else {System.out.println("库存不足!!!");}}catch (Exception ex){System.out.println("出现异常!!!");}finally {//解锁lock.unlock();}return "OK";
}
3.3 Lua脚本
3.3.1 Lua简介

Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。

从Redis2.6.0版本开始提供了EVAL 和 EVALSHA 命令,这两个命令可以执行 Lua 脚本。

3.3.2 Redis中使用Lua的好处

Redis中使用Lua的好处:

  • 减少网络开销。可以将多个请求通过脚本的形式一次发送,减少网络时延
  • 原子操作。redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。因此在编写脚本的过程中无需担心会出现竞态条件,无需使用事务
  • 复用。客户端发送的脚本会永久存在redis中,这样,其他客户端可以复用这一脚本而不需要使用代码完成相同的逻辑
3.3.3 如何在Redis中使用Lua

在redis中使用Lua脚本主要有三个命令

  • eval
  • evalsha
  • script load

eval用来直接执行lua脚本,使用方式如下:

EVAL script numkeys key [key ...] arg [arg ...]

key代表要操作的redis key

arg可以传自定义的参数

numkeys用来确定key有几个

script就是你写的lua脚本

lua脚本里面使用KEYS[1]和ARGV[1]来获取传递的第一个key和第一个arg,后面以此类推

举例:

eval "return redis.call('set',KEYS[1],ARGV[1])" 1 city beijing
eval "return redis.call('set','name','xiaoming')" 0
eval "return redis.call('del',KEYS[1])" 1 city
eval "return redis.call('get',KEYS[1])" 1 name
eval "if (redis.call('exists', KEYS[1]) == 0) then redis.call('set', KEYS[2], ARGV[1]); redis.call('expire', KEYS[2], ARGV[2]);return nil;end;" 2 citys city beijing 5000

在用eval命令的时候,可以注意到每次都要把执行的脚本发送过去,这样势必会有一定的网络开销,所以redis对lua脚本做了缓存,通过script load 和 evalsha实现:

SCRIPT LOAD script
EVALSHA sha1 numkeys key [key ...] arg [arg ...]

举例:

script load "return redis.call('get',KEYS[1]);"
evalsha 0e11c9f252fd76115c38403ce6095872b8c70580 1 name

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

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

相关文章

合伙企业法关于合伙企业的要求

合伙协议可以载明合伙企业的经营期限和合伙人争议的解决方式。 合伙协议经全体合伙人签名、盖章后生效。合伙人依照合伙协议享有权利&#xff0c;承担责任。 经全体合伙人协商一致&#xff0c;可以修改或者补充合伙协议。 申请合伙企业设立登记&#xff0c;应当向企业登记机关提…

git rebase应用场景三

文章目录 git rebase应用场景三 git rebase应用场景三 在我们的开发分支中 假设我们修改一个文件 提交一个版本 再回到master分支 同时也去修改1.txt文件&#xff0c;提交一个版本 这样相当于master分支提交了一次&#xff0c;dev也提交了一次 然后回到dev分支 此时会报错…

事务失效的十种常见场景

学习事务失效场景 1 概述 事务的传播类型isolationTransactionnal注解属性 事务方法未被Spring管理方法使用final类型修饰非public修饰的方法同一个类中的方法相互调用方法的事务传播类型不支持事务异常被内部catch&#xff0c;程序生吞异常数据库不支持事务未配置开启事务错…

【ChatGPT 默认强化学习策略】PPO 近端策略优化算法

PPO 近端策略优化算法 PPO 概率比率裁剪 演员-评论家算法演员-评论家算法&#xff1a;多智能体强化学习核心框架概率比率裁剪&#xff1a;逐步进行变化的方法PPO 目标函数的设计重要性采样KL散度 PPO 概率比率裁剪 演员-评论家算法 论文链接&#xff1a;https://arxiv.org…

Oracle-深入了解cache buffer chain

文章目录 1.Cache buffer chain介绍2.Buffer cache的工作原理3 Buffer chains4.Multi-versioning of Buffers5.Latches6.诊断CBC latch等待7.解决 CBC Latch等待 1.Cache buffer chain介绍 经常看到会话等待事件“latch&#xff1a;cache buffers chain”。 如果想知道意味着什…

FA对接FC流程

2、FA进行对接 &#xff08;1&#xff09;首先安装好AD域控服务器DHCPDNS&#xff08;注意&#xff0c;不要忘记了做DNS正反向解析&#xff0c;就是把已经安装了ITA的主机做解析&#xff09;&#xff0c;在里面创建域用户 &#xff08;2&#xff09;安装ITA和VAG/VLB&#xf…

2024孙多勇®跨年演讲,重塑医美增长力,开启璀璨华章!

不谋万事者&#xff0c;不足谋一时&#xff1b;不谋全局者&#xff0c;不足谋一域。明者因时而变&#xff0c;智者随时而制。 12月31日-1月3日由汇成医美集团主办的&#xff0c;以“重塑医美增长力”为主题的第七届孙多勇跨年演讲暨「英雄2024」年度战略规划案&#xff0c;在中…

权威Scrum敏捷开发企业培训分享

课程简介 Scrum是目前运用最为广泛的敏捷开发方法&#xff0c;是一个轻量级的项目管理和产品研发管理框架。 这是一个两天的实训课程&#xff0c;面向研发管理者、项目经理、产品经理、研发团队等&#xff0c;旨在帮助学员全面系统地学习Scrum和敏捷开发, 帮助企业快速启动敏…

Excel模板填充:从minio上获取模板使用easyExcel填充

最近工作中有个excel导出的功能&#xff0c;要求导出的模板和客户提供的模板一致&#xff0c;而客户提供的模板有着复杂的表头和独特列表风格&#xff0c;像以往使用poi去画是非常耗时间的&#xff0c;比如需要考虑字体大小&#xff0c;单元格合并&#xff0c;单元格的格式等问…

Maya-UE xgen-UE 毛发导入UE流程整理

首先声明&#xff1a;maya建议用2022版本及一下&#xff0c;因为要用到Python 2 ,Maya2023以后默认是Python3不再支持Python2; 第一步&#xff1a;Xgen做好的毛发转成交互式Groom 第二步&#xff1a;导出刚生成的交互式Groom缓存&#xff0c;需要设置一下当前帧&#xff0c;和…

大数据与人工智能|全面数字化战略与企业数字化转型(第1节 )

要点一&#xff1a;培养跨学科思维 在分析时&#xff0c;需要采用多学科的思维方式 结果不重要&#xff0c;重要的是如何提炼现象、分析问题和得出结论的过程。 1. 介绍了锤子精神和多学科思维方式的重要性。指出了只从自身学科出发解决问题的局限性。 2. 提倡跨学科思维方式&a…

2023下半年的总结

我从八月下旬开始写的&#xff0c;到现在差不多有半年了&#xff0c;总结一下吧&#xff01; 1.计算机视觉 在计算机视觉方面&#xff0c;想必两个有名的深度学习框架&#xff08;TensorFlow和PyTorch&#xff09;大家都很清楚吧&#xff0c;以及OpenCV库。对于人脸识别&…

在高并发场景下,缓存“雪崩”了怎么办

1. 缓存雪崩的常见原因 缓存“雪崩”是指&#xff0c;因为部分缓存节点不可用&#xff0c;而导致整个缓存系统&#xff08;甚至是整个服务系统&#xff09;不可用。缓存“雪崩”主要分为以下两种情况&#xff1a; 因缓存不支持 rehash 而导致的缓存“雪崩”缓存支持 rehash 时…

电脑怎么检测手机配置信息

摘要 本文介绍了如何使用克魔助手工具在电脑上检测手机的配置信息。通过该工具&#xff0c;用户可以全面了解手机的硬件和操作系统信息&#xff0c;包括电池、CPU、内存、基带信息和销售信息等。 引言 在日常工作中&#xff0c;了解手机的配置信息对于开发和测试人员非常重要…

带大家做一个,易上手的家常蒜酱鲍鱼

超市有个福利鲍鱼 就买回来弄一下 搞一个整个的蒜 蒜去皮切末 三四个干辣椒切小末 切一点葱花混进去 鲍鱼去壳 去内脏&牙齿 将鲍鱼切块 因为鲍鱼是正经不好入味的东西 起锅烧油 下入 葱蒜干辣椒 翻炒出味 然后倒入鲍鱼进行翻炒 翻炒均匀后 倒入 一勺生抽 半勺老抽 …

Linux 内存数据 Metrics 指标解读

过去从未仔细了解过使用 free、top 等命令时显式的内存信息&#xff0c;只关注了已用内存 / 可用内存。本文我们详解解读和标注一下各个数据项的含义&#xff0c;同时和 Ganglia 显式的数据做一个映射。开始前介绍一个小知识&#xff0c;很多查看内存的命令行工具都是 cat /pro…

71内网安全-域横向网络传输应用层隧道技术

必备知识点&#xff1b; 代理和隧道技术的区别&#xff1f; 代理主要解决的是网络访问问题&#xff0c;隧道是对过滤的绕过&#xff0c; 隧道技术是为了解决什么 解决被防火墙一些设备&#xff0c;ids&#xff08;入侵检测系统&#xff09;进行拦截的东西进行突破&#xff0…

2023-12-11 LeetCode每日一题(最小体力消耗路径)

2023-12-11每日一题 一、题目编号 1631. 最小体力消耗路径二、题目链接 点击跳转到题目位置 三、题目描述 你准备参加一场远足活动。给你一个二维 rows x columns 的地图 heights &#xff0c;其中 heights[row][col] 表示格子 (row, col) 的高度。一开始你在最左上角的格…

自定义富集分析结果的term顺序

大家好&#xff0c;元旦过得还好吗&#xff1f;之前我们聊过如果富集分析结果不理想&#xff0c;如何选择富集分析的terms&#xff0c;如果不记得&#xff0c;可以看看这三个推文和视频。 ​富集分析结果不理想&#xff1a;如何从上千个term中找到自己想要所有term&#xff1f;…

Spark 集群搭建

文章目录 搭建前准备安装搭建解压并重命名环境变量配置配置文件yarn-site.xmlspark-env.sh 官网求 π(PI) 案例启动spark-shell通过浏览器查看显示查看 Spark 的网页信息展示 搭建前准备 下载地址&#xff1a;Index of /dist/spark (apache.org) 配置好 hadoop 环境&#xff…