Java --- redis7实现分布式锁

news2024/11/16 22:01:33

目录

一、锁的种类

 二、分布式锁具备的条件与刚需

 三、springboot+redis+ngnix单机实现案例

 四、Nginx配置负载均衡

 4.1、修改nginx配置文件

 4.2、执行启动命令

 4.3、启动微服务程序测试

 五、使用redis分布式锁

  5.1、方法递归重试

 5.2、自旋方式

 5.3、添加key过期时间,防死锁

 5.4、防止误删除

 5.6、使用Lua脚本优化

5.7、可重入锁

5.7.1、隐式锁(synchronized)

 5.7.2、显式锁(Lock)

5.7.3、redis的Map类型替代可重入锁的计数问题

5.8、自动续期

一、锁的种类

1、单机版同一个JVM虚拟机内,synchronized或者Lock接口 

2、分布式多个不同JVM虚拟机,单机的线程机制不再起作用,资源类不在同一服务器上,不在共享。

 二、分布式锁具备的条件与刚需

独占性:

OnlyOne,任何时刻只能有且仅有一个线程持有。

高可用:

在redis集群环境下,不能因为某一个节点挂掉而出现获取锁和释放锁失败的情况,高并发请求下,依旧性能好用。

防死锁:

杜绝死锁,必须有超时控制机制或者撤销操作,必须有最终解决方案。

不乱抢:

自己的锁只能自己释放,不能释放不属于自己的锁。

重入性:

同一个节点的同一个线程如果获得锁之后,它也可以再次获取这个锁。

 三、springboot+redis+ngnix单机实现案例

场景:

多个服务间保证同一时刻同一时间段内同一用户只能有一个请求 (防止关键业务出现并发攻击)

 采用微服务创建:

 test1与test2两台pom导入jar一致

    <dependencies>
        <!--redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!--web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <!--swagger2-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <!--swagger-ui-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>
        <!--hutool-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.13</version>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

 springboot配置文件

server.port=8081
spring.application.name=redis7test1
#swagger2
spring.swagger2.enabled=true
spring.mvc.pathmatch.matching-strategy=ant_path_matcher
#redis配置
spring.redis.database=0
spring.redis.host=192.168.200.110
spring.redis.port=6379
spring.redis.password=123456
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=-1ms
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=0

配置类

@Configuration
public class RedisConfig {
    /**
     * redis序列化配置类
     *
     */
    @Bean
    public RedisTemplate<String , Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory){
        RedisTemplate<String , Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(lettuceConnectionFactory);
        //设置key序列化方式string
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        //设置value的序列化方式json,使用GenericJackson2JsonRedisSerializer替换默认序列化
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}
@Component
@EnableSwagger2
public class SwaggerConfig {
    @Value("${spring.swagger2.enabled}")
    private Boolean enabled;
    @Bean
    public Docket createRestApi(){
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .enable(enabled)
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.cjc.redis7test1"))
                .paths(PathSelectors.any())
                .build();
    }
    public ApiInfo apiInfo(){
        return new ApiInfoBuilder()
                .title("springboot利用swagger2构建api接口文档"+"\t"+ DateTimeFormatter.ofPattern("yyyy-MM-dd").format(LocalDateTime.now()))
                .description("springboot+redis整合,有问题联系客服小丽:123456789")
                .version("1.0")
                .termsOfServiceUrl("yyyyyyyyy")
                .build();
    }
}

service层

@Service
@Slf4j
public class InventoryServiceImpl implements InventoryService {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Value("${server.port}")
    private String port;
    public static final String IN_KEY = "inventory";

    private Lock lock = new ReentrantLock();
    @Override
    public String sale() {
        String resultMessage = "";
        //加锁
        lock.lock();
        try {
            //查询redis里的库存
            String result = stringRedisTemplate.opsForValue().get(IN_KEY + 1);
            //判断库存是否足够
            Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
            //卖出商品扣减库存
            if (inventoryNumber > 0){
                //存入redis
                stringRedisTemplate.opsForValue().set(IN_KEY + 1,String.valueOf(--inventoryNumber));
                 resultMessage = "成功卖出商品,库存剩余:" + inventoryNumber;
                log.info(resultMessage + "\t服务端口号:{}",port);
            }else {
                resultMessage = "商品已售空";
            }
        } finally {
            //解锁
            lock.unlock();
        }
        return resultMessage;
    }
}

controller层

@RestController
@Slf4j
@Api(tags = "redis分布式锁测试")
public class InventoryController {
    @Autowired
    private InventoryService inventoryService;

    @ApiOperation("扣减库存,一次卖一个")
    @RequestMapping(value = "/sale",method = RequestMethod.GET)
    public String sale(){
        return inventoryService.sale();
    }
}

上述代码在test2上拷贝,修改端口号

 四、Nginx配置负载均衡

 上面的代码在分布式部署后,单机锁将会出现超卖现象,需要分布式锁。

 4.1、修改nginx配置文件

cd /usr/local/nginx/conf目录下的nginx.conf文件

 4.2、执行启动命令

在/usr/local/nginx/sbin目录下启动

启动命令:

./nginx -c /usr/local/nginx/conf/nginx.conf

 关闭

./nginx -s stop

重启

./ngnix -s reload

 4.3、启动微服务程序测试

 手动测试,程序正常

 使用jmeter工具进行压测

 

 原因如下:

在单机环境下,可以使用synchronized或lock来实现

在分布式系统中,因为竞争的线程可能不在同一个节点上(即同一个jvm中)所以锁就失效了,就需要利用第三方的一个组件,来获取锁,未获取到锁,则阻塞当前想要运行的线程。

结论:

单机版加锁配合nginx和jmeter压测后,不满足高并发分布式锁的性能要求

 五、使用redis分布式锁

  5.1、方法递归重试

    @Override
    public String sale(){
        String resultMessage = "";
        String key = "redisLock";
        String uuidValue = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue);
        //没有抢到锁将进行重试
        if (!flag){
            //暂停20毫秒,进行递归重试,
            try {
                TimeUnit.MILLISECONDS.sleep(20);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            sale();
        }else {
            //拿到锁进行业务处理
            try {
                //查询redis里的库存
                String result = stringRedisTemplate.opsForValue().get(IN_KEY + 1);
                //判断库存是否足够
                Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
                //卖出商品扣减库存
                if (inventoryNumber > 0){
                    //存入redis
                    stringRedisTemplate.opsForValue().set(IN_KEY + 1,String.valueOf(--inventoryNumber));
                     resultMessage = "成功卖出商品,库存剩余:" + inventoryNumber;
                    log.info(resultMessage + "\t服务端口号:{}",port);
                }else {
                    resultMessage = "商品已售空";
                }
            } finally {
               stringRedisTemplate.delete(key);
            }
        }
        return resultMessage;
    }

 使用方法递归重试测试通过,但并发量一高,容易出现StackOverflowError,高并发唤醒后推荐使用while判断,不推荐使用。

 5.2、自旋方式

@Override
    public String sale() {
        String resultMessage = "";
        String key = "redisLock";
        String uuidValue = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
        //使用自旋代替递归,使用while进行判断
        while (!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue)){
            //暂停20毫秒,进行递归重试,
            try {
                TimeUnit.MILLISECONDS.sleep(20);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        //拿到锁进行业务处理
        try {
            //查询redis里的库存
            String result = stringRedisTemplate.opsForValue().get(IN_KEY + 1);
            //判断库存是否足够
            Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
            //卖出商品扣减库存
            if (inventoryNumber > 0){
                //存入redis
                stringRedisTemplate.opsForValue().set(IN_KEY + 1,String.valueOf(--inventoryNumber));
                resultMessage = "成功卖出商品,库存剩余:" + inventoryNumber;
                log.info(resultMessage + "\t服务端口号:{}",port);
            }else {
                resultMessage = "商品已售空";
            }
        } finally {
            stringRedisTemplate.delete(key);
        }
        return resultMessage;
    }
上面代码在程序挂掉的情况下且没有走到finally这块,由于该key没有设置过期时间,导致会没办法删除。

 5.3、添加key过期时间,防死锁

@Override
    public String sale() {
        String resultMessage = "";
        String key = "redisLock";
        String uuidValue = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
        //使用自旋代替递归,使用while进行判断
        //加锁和过期时间设置必须是同一行,保证原子性
        while (!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue,20L,TimeUnit.SECONDS)){
            //暂停20毫秒,进行递归重试,
            try {
                TimeUnit.MILLISECONDS.sleep(20);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        //拿到锁进行业务处理
        try {
            //查询redis里的库存
            String result = stringRedisTemplate.opsForValue().get(IN_KEY + 1);
            //判断库存是否足够
            Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
            //卖出商品扣减库存
            if (inventoryNumber > 0){
                //存入redis
                stringRedisTemplate.opsForValue().set(IN_KEY + 1,String.valueOf(--inventoryNumber));
                resultMessage = "成功卖出商品,库存剩余:" + inventoryNumber;
                log.info(resultMessage + "\t服务端口号:{}",port);
            }else {
                resultMessage = "商品已售空";
            }
        } finally {
            stringRedisTemplate.delete(key);
        }
        return resultMessage;
    }

问题当A线程处理业务时间超过过期时间,B线程也建立了一道锁,会导致A线程误删除B线程的锁。

 5.4、防止误删除

  @Override
    public String sale() {
        String resultMessage = "";
        String key = "redisLock";
        String uuidValue = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
        //使用自旋代替递归,使用while进行判断
        //加锁和过期时间设置必须是同一行,保证原子性
        while (!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue,20L,TimeUnit.SECONDS)){
            //暂停20毫秒,进行递归重试,
            try {
                TimeUnit.MILLISECONDS.sleep(20);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        //拿到锁进行业务处理
        try {
            //查询redis里的库存
            String result = stringRedisTemplate.opsForValue().get(IN_KEY + 1);
            //判断库存是否足够
            Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
            //卖出商品扣减库存
            if (inventoryNumber > 0){
                //存入redis
                stringRedisTemplate.opsForValue().set(IN_KEY + 1,String.valueOf(--inventoryNumber));
                resultMessage = "成功卖出商品,库存剩余:" + inventoryNumber;
                log.info(resultMessage + "\t服务端口号:{}",port);
            }else {
                resultMessage = "商品已售空";
            }
        } finally {
            //删除锁时,判断该锁是否是同一客户端的
            if (stringRedisTemplate.opsForValue().get(key).equalsIgnoreCase(uuidValue)){
                stringRedisTemplate.delete(key);
            }
        }
        return resultMessage;
    }

 在判断与删除时不是一个原子操作,可能会因为一些特性原因删除失败,需要使用Lua脚本进行优化

 5.6、使用Lua脚本优化

    @Override
    public String sale() {
        String resultMessage = "";
        String key = "redisLock";
        String uuidValue = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
        //使用自旋代替递归,使用while进行判断
        //加锁和过期时间设置必须是同一行,保证原子性
        while (!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue,20L,TimeUnit.SECONDS)){
            //暂停20毫秒,进行递归重试,
            try {
                TimeUnit.MILLISECONDS.sleep(20);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        //拿到锁进行业务处理
        try {
            //查询redis里的库存
            String result = stringRedisTemplate.opsForValue().get(IN_KEY + 1);
            //判断库存是否足够
            Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
            //卖出商品扣减库存
            if (inventoryNumber > 0){
                //存入redis
                stringRedisTemplate.opsForValue().set(IN_KEY + 1,String.valueOf(--inventoryNumber));
                resultMessage = "成功卖出商品,库存剩余:" + inventoryNumber;
                log.info(resultMessage + "\t服务端口号:{}",port);
            }else {
                resultMessage = "商品已售空";
            }
        } finally {
            //使用lua脚本的redis分布式调用
            String luaScript = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else" +
                    "\treturn 0 end";
            stringRedisTemplate.execute(new DefaultRedisScript(luaScript,Boolean.class), Arrays.asList(key),uuidValue);

        }
        return resultMessage;
    }

上述代码不满足可重入性 

5.7、可重入锁

在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(锁对象是同一对象),不会因为之前已经获取过还没有释放而阻塞。Java中ReentrantLoak和synchronized都是可重入锁,可重入锁的一个优点是一定程度上避免了死锁。

5.7.1、隐式锁(synchronized)

    Object o =  new Object();
    @Test
    void contextLoads() {

        new Thread(()->{
            synchronized (o){
                System.out.println(Thread.currentThread().getName() + "\t外层调用");
                synchronized (o){
                    System.out.println(Thread.currentThread().getName() + "\t中层调用");
                    synchronized (o){
                        System.out.println(Thread.currentThread().getName() + "\t内层调用");

                    }
                }
            }
        },"a").start();
    }
    @Test
   void entry(){
      m1();
   }
    private synchronized void m1() {
        System.out.println(Thread.currentThread().getName() + "\t外层调用");
        m2();
    }
    private synchronized void m2() {
        System.out.println(Thread.currentThread().getName() + "\t中层调用");
        m3();
    }
    private synchronized void m3() {
        System.out.println(Thread.currentThread().getName() + "\t内层调用");
    }

 原理:

每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。

当执行monitorenter时,如果目标锁对象的计数器为0,那么它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将计数器加1。

在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么Java虚拟机可以将其计数器加1,否则需要等待,直到持有线程释放该锁。

当执行monitorexit时,Java虚拟机则需要将锁对象的计数器减1,计数器为零代表锁已被释放。

 5.7.2、显式锁(Lock)

    @Test
    void lockTest(){
        Lock lock = new ReentrantLock();
        new Thread(()->{
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "\t外层调用");
                lock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() + "\t中层调用");
                } finally {
                    lock.unlock();
                }
            } finally {
                lock.unlock();
            }
        },"a1").start();
        try {
            TimeUnit.MILLISECONDS.sleep(3);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        new Thread(()->{
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "\t内层调用");
            } finally {
                lock.unlock();
            }
        },"a2").start();
    }

5.7.3、redis的Map类型替代可重入锁的计数问题

加锁Lua脚本:

if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then
   redis.call('hincrby',KEYS[1],ARGV[1],1)
   redis.call('expire',KEYS[1],ARGV[2])
   return 1
else 
   return 0
end

解锁Lua脚本:

if redis.call('hexists',KEYS[1],ARGV[1]) == 0 then
   return nil
elseif redis.call('hincrby',KEYS[1],ARGV[1],-1) == 0 then
       return redis.call('del',KEYS[1])
else 
   return 0
end

/**
 * 自定义redis分布式锁
 */
@Slf4j
public class MyRedisLock implements Lock {

   private StringRedisTemplate stringRedisTemplate;
   private String lockName;
   private String uuidValue;
   private Long expireTime;

    public MyRedisLock(StringRedisTemplate stringRedisTemplate, String lockName,String uuid) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.lockName = lockName;
        this.uuidValue = uuid + ":" + Thread.currentThread().getId();
        this.expireTime = 50L;
    }

    @Override
    public void lock() {
        tryLock();
    }

    @Override
    public boolean tryLock() {
        try {
            tryLock(-1L,TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        return false;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        if (time == -1L){
            String script = "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then    " +
                    "redis.call('hincrby',KEYS[1],ARGV[1],1)    " +
                    "redis.call('expire',KEYS[1],ARGV[2])    " +
                    "return 1  " +
                    "else   " +
                    "return 0 " +
                    "end";
            log.info("lockName:{},uuidValue:{}",lockName,uuidValue);
            //加锁失败重试
            Boolean execute = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime));
            while (!execute){
               TimeUnit.MILLISECONDS.sleep(60);
            }
            return true;
        }
        return false;
    }

    @Override
    public void unlock() {
        String script = "if redis.call('hexists',KEYS[1],ARGV[1]) == 0 then\t" +
                "return nil\t" +
                "elseif redis.call('hincrby',KEYS[1],ARGV[1],-1) == 0 then\t" +
                "return redis.call('del',KEYS[1])\t" +
                "else\t" +
                "return 0\t" +
                "end";
        Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), uuidValue,String.valueOf(expireTime));
        if (flag == null){
            throw new RuntimeException("this lock doesn't exists");
        }
    }

    @Override
    public Condition newCondition() {
        return null;
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }
@Component
public class DistributedLockFactory {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    private String lockName;
    private String uuid;

    public DistributedLockFactory() {
        this.uuid = IdUtil.simpleUUID();
    }

    public Lock getDistributedLock(String lockType){
        if (lockType == null){
            return null;
        }
        if (lockType.equalsIgnoreCase("REDIS")){
            lockName = "redisLock";
            return  new MyRedisLock(stringRedisTemplate,lockName,uuid);
        }
        if (lockType.equalsIgnoreCase("MySQL")){
            this.lockName = "mysqlLock";
            //TODO 完善
            return  null;
        }
        return null;
    }
}
@Override
    public String sale() {
        String resultMessage = "";
        //加锁
        Lock redisLock = distributedLockFactory.getDistributedLock("REDIS");
        redisLock.lock();
        try {
            //查询redis里的库存
            String result = stringRedisTemplate.opsForValue().get(IN_KEY + 1);
            //判断库存是否足够
            Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
            //卖出商品扣减库存
            if (inventoryNumber > 0){
                //存入redis
                stringRedisTemplate.opsForValue().set(IN_KEY + 1,String.valueOf(--inventoryNumber));
                 resultMessage = "成功卖出商品,库存剩余:" + inventoryNumber;
                log.info(resultMessage + "\t服务端口号:{}",port);
            }else {
                resultMessage = "商品已售空";
            }
        } finally {
            //解锁
            redisLock.unlock();
        }
        return resultMessage;

    }

5.8、自动续期

Lua脚本:

if redis.call('hexists',KEYS[1],ARGV[1]) == 1 then 
  return redis.call('expire',KEYS[1],ARGV[2]) 
else 
  return 0 
end

    public void renewExpire(){
        String script = "if redis.call('hexists',KEYS[1],ARGV[1]) == 1 then\t" +
                "return redis.call('expire',KEYS[1],ARGV[2])\t" +
                "else\t" +
                "return 0\t" +
                "end";
        //定期扫描
        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                Boolean execute = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime));
                if (execute){
                    renewExpire();
                }
            }
        },(this.expireTime * 1000/3));
    }

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

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

相关文章

业务实战记录4:多维表插入数据任务丢失处理与思考

本文目录 一、线程 bug二、解决方案2.1 加停顿2.2 单线程2.3 多 Token 三、一点花絮 很久没有写业务实战记录了&#xff0c;实际工作过程中其实遇到了挺多问题的&#xff0c;但是要通过 CSDN 记录下来&#xff0c;还是比较难的&#xff0c;因为场景和目标比较难说清楚&#xff…

【网络】HTTPHTTPS协议

文章目录 HTTP协议认识URLurlencode和urldecodeHTTP协议格式HTTP请求协议格式简单的小实验 HTTP响应协议格式关于封装解包分用 HTTP的方法关于GET和POST方法概念GET&POST对比(代码测试)测试POST和GET方法的区别 HTTP的状态码关于重定向的状态码临时重定向的代码演示: HTTP的…

【文献研究】轴辐式航线网络设计—Liner hub-and-spoke shipping network design

学习文献&#xff1a;轴辐式航线网络设计—Liner hub-and-spoke shipping network design 3. 模型建立 轴辐式航线网络设计 三级轴辐式网络&#xff1a;喂给港-二级枢纽港-一级枢纽港 主要考虑的限制条件&#xff1a;多种类型的集装箱船舶、转运时间、多种类型的集装箱 转运操…

Kangas:计算机视觉中的Pandas

介绍 在计算机视觉领域&#xff0c;Kangas是一种越来越受欢迎的工具&#xff0c;用于图像数据处理和分析。类似于Pandas如何改变数据分析人员处理表格数据的方式&#xff0c;Kangas对计算机视觉任务也起到了同样的作用。 Kangas是Comet ML开源的工具&#xff0c;用于探索、分析…

【Http协议①】认识http协议,学会使用fiddler抓包工具进行抓包.

前言: 大家好,我是良辰丫,今天我们一起来学习http协议,http协议是应用层的协议,应用层是最接近程序员的,那么,http协议到底是什么呢?我们往下看.&#x1f49e;&#x1f49e; &#x1f9d1;个人主页&#xff1a;良辰针不戳 &#x1f4d6;所属专栏&#xff1a;javaEE初阶 &#…

springboot+java养老院儿童福利院管理系统

安家儿童福利院管理系统包括儿童管理、申请领养管理、捐赠管理、楼栋管理、宿舍管理、分配信息管理、宿舍物品管理、报修管理、维修工管理、报修状态管理、留言管理、系统管理。通过对系统的实现得出安家儿童福利院管理系统具有安全稳定、操作简单、功能强大等特点&#xff0c;…

MySQL8.0卸载、安装和使用(二)

MySQL数据库的安装 注意&#xff1a; 必须用系统管理员身份运行mysql安装程序。安装目录切记不要用中文。 步骤一&#xff1a;双击mysql8的安装向导 步骤二&#xff1a;分为首次安装和再安装 1、首次安装 &#xff08;1&#xff09;如果是首次安装mysql系列的产品&#xff…

第06章_多表查询

第06章_多表查询 多表查询&#xff0c;也称为关联查询&#xff0c;指两个或更多个表一起完成查询操作。 前提条件&#xff1a;这些一起查询的表之间是有关系的&#xff08;一对一、一对多&#xff09;&#xff0c;它们之间一定是有关联字段&#xff0c;这个关联字段可能建立了…

脑科学研究者的案头书(含下载资源)

脑科学研究者的案头书 <<< 回复关键词获取下载链接 >>> 《EEG Signal Processing and Machine Learning》&#xff08;Second Edition&#xff09; 简介&#xff1a; 《脑电信号处理与机器学习》书籍旨在描述脑电图(EEG)研究中的新技术和成果&#xff0c;主…

chatgpt赋能Python-python3捕获异常

Python3异常处理技术详解 在Python3中&#xff0c;异常处理技术是一项非常重要的工具。它能够帮助程序员避免不可预见的错误&#xff0c;减少不必要的程序崩溃&#xff0c;保证程序的稳定性。 什么是异常&#xff1f; 异常就是程序在执行过程中发生的错误或异常情况。不同的…

Redis+LUA脚本实现限流

文章目录 1、demo结构2、自定义接口3、编写写LUA脚本4、通过AOP切面识别需要限流的接口编写切面AOP通知类型 5、Redis限流自定义异常构建Redis限流自定义异常声明这个类为全局异常处理器专属日志 6、流量限制器RateLimiterRateLimitAlgApiLimitRateLimitRuleRuleConfig 7、Guav…

Win11系统不兼容怎么回退到Win10系统使用?

Win11系统不兼容怎么回退到Win10系统使用&#xff1f;有用户将自己的电脑系统升级到了Win11之后&#xff0c;发现使用起来非常的卡顿&#xff0c;自己的电脑配置不足。那么这个情况怎么去进行问题的解决呢&#xff1f;来看看以下详细的解决方法分享吧。 准备工作&#xff1a; 1…

Golang每日一练(leetDay0071) 同构字符串、反转链表

目录 205. 同构字符串 Isomorphic Strings &#x1f31f; 206. 反转链表 Reverse Linked-list &#x1f31f; &#x1f31f; 每日一练刷题专栏 &#x1f31f; Rust每日一练 专栏 Golang每日一练 专栏 Python每日一练 专栏 C/C每日一练 专栏 Java每日一练 专栏 205. 同…

Debezium系列之:Debezium镜像仓库Quay.io,使用Debezium镜像仓库的方法和案例

Debezium系列之:Debezium镜像仓库Quay.io,使用Debezium镜像仓库的方法和案例 一、Debezium镜像仓库变动二、镜像仓库[Quay.io](https://quay.io/organization/debezium)三、使用镜像仓库Quay.io方法四、使用镜像仓库下载Debezium UI一、Debezium镜像仓库变动 Debezium2.2版本…

Linux RTC 驱动实验

RTC 也就是实时时钟&#xff0c;用于记录当前系统时间&#xff0c;对于 Linux 系统而言时间是非常重要的&#xff0c; 就和我们使用 Windows 电脑或手机查看时间一样&#xff0c;我们在使用 Linux 设备的时候也需要查看时 间。 一、Linux 内核 RTC 驱动简介 RTC 设备驱动是标准…

verdaccio + docker搭建私有npm仓库(有手就行)

一、环境准备 docker 二、步骤 运行verdaccio docker run -d --name verdaccio -p 4873:4873 --restartalways该命令执行完&#xff0c;一个本地的npm仓库就基本搭建好了&#xff0c;可以浏览器访问http://localhost:4873/ 查看&#xff0c;效果如下&#xff1a; 效果出是…

教你接入Midjourney,不用梯子也能玩

1、效果 话不多说&#xff0c;先上最终出图效果&#xff0c; 我给的关键词是一只白色的猫 2、接入流程 API文档可以来这里查&#xff08;可以白嫖100次midjourney出图和10次gpt4体验&#xff09;&#xff0c;我这里精简一下接入流程&#xff0c;方便大家快速接入 2.1、文字生…

JDK源码怎么学?看这篇文章就够了!

最近后台收到很多粉丝私信&#xff0c;说的是程序员究竟要不要去读源码&#xff1f;当下行情&#xff0c;面试什么样的薪资/岗位才会被问到源码&#xff1f; 对此&#xff0c;我的回答是&#xff1a;一定要去读&#xff0c;并且要提到日程上来&#xff01; 据不完全统计&…

远程访问群晖Drive并挂载为电脑磁盘同步备份文件「无需公网IP」

文章目录 前言视频教程1.群晖Synology Drive套件的安装1.1 安装Synology Drive套件1.2 设置Synology Drive套件1.3 局域网内电脑测试和使用 2.使用cpolar远程访问内网Synology Drive2.1 Cpolar云端设置2.2 Cpolar本地设置2.3 测试和使用 3. 结语 转发自CSDN远程穿透的文章&…

ARM的数据处理指令、跳转指令与储存器访问指令

最开始在此介绍一下CPSR寄存器中 N、Z、C、V 4位的作用&#xff1a; Bit[28]&#xff08;V&#xff09;&#xff1a; 当运算器中进行加法运算且产生符号位进位时该位自动置1&#xff0c;否则为0 当运算器中进行减法运算且产生符号位借位时该位自动置0&#xff0c;否则为1 …