您的位置:首页 > 教育 > 培训 > 摄影网站需求分析_三类人不适合学编程plc_怎么去做推广_seo 推广

摄影网站需求分析_三类人不适合学编程plc_怎么去做推广_seo 推广

2025/11/10 14:57:05 来源:https://blog.csdn.net/2301_78320637/article/details/143220420  浏览:    关键词:摄影网站需求分析_三类人不适合学编程plc_怎么去做推广_seo 推广
摄影网站需求分析_三类人不适合学编程plc_怎么去做推广_seo 推广

文章目录

  • 什么是集群环境下的并发安全问题
    • 什么是`Tomcat`
    • 为什么会出现集群下的并发安全问题
  • 什么是分布式锁
    • 分布式锁的特点
    • 实现原理与方式
      • 使用数据库实现
      • 基于`Redis`的实现:
  • 基于`Redis`实现一个分布式锁
    • `Redis`分布式锁的实现核心思路
    • 实现一个分布式锁
      • 分布式锁的接口
      • 锁的基本实现
      • 分布式锁的误删
        • 解决Redis分布式锁误删问题
      • 分布式锁的原子性问题
        • 解决分布式锁的原子性问题
          • `Lua`脚本
          • 利用`Java`代码调用`Lua`脚本改造分布式锁

什么是集群环境下的并发安全问题

什么是Tomcat

Tomcat是一个开源的、轻量级的Web服务器和Servlet容器.

注意:Tomcat本身是一个Java应用程序,它运行在JVM中,并管理着其内部所有Web应用程序的生命周期.

Tomcat的主要功能:

  • Servlet容器Tomcat作为一个Servlet容器,能够执行Java ServletServlet是一种用于扩展Web服务器功能的Java类,它能够对来自Web客户端(如浏览器)的请求进行响应
  • JSP容器Tomcat还支持JSP技术,允许在HTML页面中嵌入Java代码,以动态生成Web页面。当JSP页面首次被访问时,Tomcat会将其翻译成一个等效的Java Servlet,然后编译并执行。
  • 静态资源服务Tomcat还可以提供静态资源服务,如HTMLCSSJavaScript和图像文件等。这使得它不仅能够用于动态Web应用,也能够用于简单的静态网站服务

Tomcat是如何运行的:

Tomcat启动时,它会创建一个或多个JVM实例(通常是一个),并加载所有必要的类和库。然后,Tomcat会在这个JVM实例中运行,并管理其内部的所有Web应用程序这些Web应用程序共享同一个JVM实例,因此它们也共享JVM提供的内存管理、垃圾回收、线程管理等核心功能

需要注意的是:虽然同一个Tomcat下的Java EE项目使用同一个JVM,但每个项目都有自己的类加载器和命名空间,以确保它们在逻辑上是彼此分开的。这减少了不同应用程序之间的意外进程内串扰的可能性

总结:部署在同一个Tomcat中的Web服务,是运行在同一个JVM实例中的,但是每个项目都有自己的类加载器和命名空间,保证在逻辑上是彼此分开的.

为什么会出现集群下的并发安全问题

所以,如果我们将服务部署在集群环境中,那么就说明我们要将服务放入到多个Tomcat中进行运行,也就是运行在多个JVM实例中;在同一个Tomcat中,多个线程并发执行时,syn锁会生效;但当存服务部署在多个Tomcat中时,也就代表了服务运行在不同的JVM实例中,那么此时syn锁就会失效了,这就导致了集群下的并发安全问题.

在同一JVM实例运行中,多线程并发安全:
在这里插入图片描述
在多个JVM实例中运行的Web服务,出现线程安全问题:
在这里插入图片描述
我们发现,线程1和线程3同时获取到同一个锁对象.发生并发问题.

所以,我们多个JVM应该同时获取同一个锁监视器,这样能够保证多个JVM进程中的线程都实现互斥性.

什么是分布式锁

在前面的学习中,我们了解到:JVM内部的锁监视器(syn锁),只能 用于在单个机器上,保证线程之间的互斥和数据安全****.但在集群模式下,多个JVM存在多个锁监视器**,所以无法使用JVM内部的锁监视器,来保证在集群中的线程之间数据的安全性.
那么分布式锁,就应运而生了.

分布式锁:满足分布式系统或者集群模式下多进程可见并且互斥的锁.

在这里插入图片描述

分布式锁的特点

  • 互斥性:分布式锁要保证在多个客户端之间的互斥,即同一时间只有一个客户端能持有锁
  • 可重入性同一客户端的相同线程,允许重复多次加锁
  • 锁超时支持锁超时机制,防止死锁的发生。如果持有锁的客户端在指定时间内没有释放锁,锁将自动过期并被释放。
  • 高性能:分布式锁需要适应高并发场景,具备高性能和可扩展性

实现原理与方式

分布式锁的实现方式有多种,常见的包括基于数据库、RedisZooKeeper等中间件的实现。

使用数据库实现

  • 使用专用的数据表来存储锁信息
  • 通过SQL语句来加锁和解锁。

这种方式实现起来相对简单,但性能可能较差,且需要自行处理锁超时和死锁等问题

基于Redis的实现:

  • 利用Redis的原子操作(如SETNX)来实现分布式锁。
  • 通过设置锁的过期时间(TTL)来防止死锁。

Redis分布式锁常用在高并发场景下,能够确保在分布式环境中,某个时刻只有一个客户端可以对共享资源进行修改

基于Redis实现一个分布式锁

Redis是如何保证获取锁资源的互斥呢?

  • 利用setnx这样的互斥命令
  • setnx是指只有key不存在时, 才set成功,否则会set失败
  • 利用设置锁的超时时间,到期释放,保证锁的安全性,防止死锁的现象.

Redis分布式锁的实现核心思路

实现分布式锁时需要实现两个基本方法:

  • 获取锁
    • 互斥:确保只能有一个线程获取锁
    • 非阻塞:尝试一次,成功返回true,失败返回false
SET lock thread01 NX EX 10
  • 释放锁
    • 手动释放
    • 超时释放获取锁的时候添加一个超时时间
DEL lock

我们利用RedisSETNX方法,当有多个线程进入时,我们就利用tryAcquire()方法来获取锁。第一个线程进入时,Redis 中就有这个key了,返回了true,如果结果是true,则表示他抢到了锁,那么他去执行业务,然后再删除锁,退出锁逻辑,没有抢到锁(返回了false)的线程,等待一定时间之后重试

实现一个分布式锁

分布式锁的接口

设计两大基本接口:

  • tryLock(long timeoutSec):获取锁,如果获取锁成功,持有锁的最长时间timeoutSec
  • unLock():释放锁
public interface ILock {/*** 获取锁* long timeoutSec:表示获取锁的时间* */boolean tryLock(long timeoutSec);/*** 释放锁* */boolean unLock();
}

锁的基本实现

@Slf4j
public class SimpleRedisLock implements ILock{//这里不是@Autowired注入,采用的是构造器注入,在创建SimpleRedisLock时,//将RedisTemplate作为参数传入private StringRedisTemplate redisTemplate;private String name;//具体的业务名称String KEY_PREFIX="Lock: ";//锁的前缀/*** 构造方法* */* public SimpleRedisLock(StringRedisTemplate redisTemplate,String name){this.redisTemplate=redisTemplate;this.name=name;}/***              获取锁* setnx key value expire timeoutSec** setnx lock thread expire timeoutSec** */public boolean tryLock(long timeoutSec) {long threadId = Thread.currentThread().getId();//获取到当前的线程id//获取锁,使用SETNX方法进行加锁,同时设置过期时间,防止死锁boolean success=redisTemplate.opsForValue().setIfAbsent(KEY_PREFIX+name,threadId+"",timeoutSec,TimeUnit.SECONDS);if(success){log.info("当前线程:"+threadId+"-->获取到锁: "+name);return true;}log.info("当前线程:"+threadId+"-->获取锁失败: "+name);return false;}/*** 释放锁** del key* */public boolean unLock() {Boolean success = redisTemplate.delete(KEY_PREFIX+name);log.info("释放锁成功");return Boolean.TRUE.equals(success);}}

分布式锁的误删

我们上面使用Redis进行的分布式锁的简单实现,在某些情境下,是否会出现问题呢?

  • 逻辑说明

    • 持有锁的线程1锁的内部出现了阻塞,导致他的TTL到期,自动释放
    • 此时线程2也来尝试获取锁,由于线程1已经释放了锁,所以线程2可以拿到
    • 但是现在线程1阻塞完了,继续往下执行,要开始释放锁
    • 那么此时就会将属于线程2的锁释放,这就是误删别人锁的情况
  • 解决方案

解决方案就是在每个线程释放锁的时候,都判断一下这个锁是不是自己的,如果不属于自己,则不进行删除操作。

假设还是上面的情况,线程1阻塞,锁自动释放,线程2进入到锁的内部执行逻辑,此时线程1阻塞完了,继续往下执行,开始删除锁,但是线程1发现这把锁不是自己的,所以不进行删除锁的逻辑,当线程2执行到删除锁的逻辑时,如果TTL还未到期,则判断当前这把锁是自己的,于是删除这把锁
在这里插入图片描述

解决Redis分布式锁误删问题
  • 需求:修改之前的分布式锁实现
  • 满足:在获取锁的时候存入线程标识

注意:这里建议使用UUID标识不同的线程.因为,在一个JVM中,ThreadId一般不会重复,但是我们现在是集群模式,有多个JVM,多个JVM之间可能会出现ThreadId重复的情况,在释放锁的时候先获取锁的线程标识,判断是否与当前线程标识一致

  • 如果一致则释放锁
  • 如果不一致则不释放锁
  • 核心逻辑:在存入锁的时候,放入自己的线程标识,在删除锁的时候,判断当前这把锁是不是自己存入的
    • 如果是,则进行删除
    • 如果不是,则不进行删除
private static final String ID_PREFIX= UUID.randomUUID().toString()+"-";//生成UUID标识
/**
*获取锁
*/
public boolean tryLock(long timeoutSec) {long threadId = Thread.currentThread().getId();//获取到当前的线程idboolean success=redisTemplate.opsForValue().setIfAbsent(KEY_PREFIX+name,ID_PREFIX+threadId+"",timeoutSec,TimeUnit.SECONDS);if(success){log.info("当前线程:"+threadId+"-->获取到锁: "+name);return true;}log.info("当前线程:"+threadId+"-->获取锁失败: "+name);return false;}/*** 释放锁** del key* */public boolean unLock() {String threadId =ID_PREFIX + Thread.currentThread().getId()+"";//获取到当前的线程名称//1.校验是否是一个线程if(threadId.equals(redisTemplate.opsForValue().get(KEY_PREFIX+name))){//2.说明一致,可以删除Boolean success = redisTemplate.delete(KEY_PREFIX+name);if(Boolean.TRUE.equals(success)) {log.info("释放锁成功");return true;}}log.info(threadId+"释放锁失败");return false;}

分布式锁的原子性问题

更为极端的误删逻辑说明

假设线程1已经获取了锁,在判断标识一致之后,准备释放锁的时候,又出现了阻塞(例如JVM垃圾回收机制)

  • 于是锁的TTL到期了,自动释放了
  • 那么现在线程2趁虚而入,拿到了一把锁
  • 但是线程1的逻辑还没执行完,那么线程1就会执行删除锁的逻辑
  • 但是在阻塞前线程1已经判断了标识一致,所以现在线程1把线程2的锁给删了

那么就相当于判断标识那行代码没有起到作用
这就是删锁时的原子性问题:

因为线程1拿锁判断标识删锁不是原子操作,所以我们要防止刚刚的情况

在这里插入图片描述

解决分布式锁的原子性问题
Lua脚本

Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性

Lua是一种编程语言,它的基本语法可以上菜鸟教程看看,链接:菜鸟教程[Lua脚本]

这里重点介绍Redis提供的调用函数,我们可以使用Lua去操作Redis,而且还能保证它的原子性,这样就可以实现拿锁,判断标识,删锁是一个原子性动作了

  • Lua脚本中,Redis提供的调用函数语法如下
redis.call('命令名称','key','其他参数', ...)

比如,我们要使用Lua脚本,执行set key1 value1Redis命令

redis.call('set', 'key1', 'value1')

比如,我们要执行set key1 value1,再执行get key1,则脚本如下:

redis.call('set','key1','value1')
local value=redis.call('get','key1')
return value
  • 写好脚本之后,我们需要使用Redis来调用脚本
EVAL script numkeys key [key ...] arg [arg ...]

比如,我们要插入(key11,value11)

EVAL "return redis.call('set','key11','value11')" 0
  • 如果脚本中的keyvalue不想写死,也可以作为参数进行传递,key类型参数会放入KEYS数组,其他参数会放入ARGV数组,在脚本中可以从KEYSARGV数组中获取这些参数
    注意:在Lua中,数组下标从1开始
    比如:
EVAL "return redis.call('set', KEYS[1], ARGV[1])" 1 name Lucy

使用脚本语言设计的检验唯一的线程标识,释放锁:

--锁的key
--local key=KEYS[1]
--当前线程标识
--local cur_thread_id=ARGV[1]--得到当前获取锁的线程标识,即获取到key的value
local thread_id=redis.call('get',KEYS[1])--将当前线程id与获取锁的线程进行比较
if(thread_id==ARGV[1]) then--释放锁  del keyredis.call('del',key)
end
return 0
利用Java代码调用Lua脚本改造分布式锁
  • RedisTemplate中,可以利用execute方法去执行lua脚本
public <T> T execute(RedisScript<T> script, List<K> keys, Object... args) {return this.scriptExecutor.execute(script, keys, args);
}

参数:

  • script:传入脚本文件(或者直接传递脚本指令,但这样导致代码的可移植性不强)
    在这里插入图片描述
    我们看到RedisScript落地实现类DefaultRedisScript

  • keys:传入keys参数数组

  • args:传入values参数数组

为了避免每一次执行释放锁的过程,都要读取脚本文件,我们可以先将脚本文件读取出来,然后作为一个静态成员变量

@Slf4j
public class SimpleRedisLock implements ILock{
//.......//脚本文件的读取,作为静态变量进行存储private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;static{UNLOCK_SCRIPT=new DefaultRedisScript<>();//根据文件路径(resourse文件下)进行读取)UNLOCK_SCRIPT.setLocation(new ClassPathResource("redis_lock.lua"));UNLOCK_SCRIPT.setResultType(Long.class);//设置返回值}
//......    
}

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com