bean作用域为singleton(单例模式)引起多线程安全问题

华为云OBS整合了Ueditor,但是在批量上传图片时,只能部分上传成功,很多文件会上传失败,经过分析发现:bean作用域为单例模式时,Spring IoC 容器中只会存在一个共享的 Bean 实例,无论有多少个Bean 引用它,始终指向同一对象,该模式在多线程下是不安全的,特此记录。
在这里插入图片描述
错误代码:

@Service
@Slf4j
public class FileServiceImpl implements FileService {

	@Value("${files.path}")
	private String filesPath;

	@Value("${files.prefix}")
	private String FilesPrefix;

	@Value("${huaWeiObs.AccessKeyId}")
	private String AccessKeyId;

	@Value("${huaWeiObs.AccessKeySecret}")
	private String AccessKeySecret;

	@Value("${huaWeiObs.BucketName}")
	private String BucketName;

	@Value("${huaWeiObs.Endpoint}")
	private String Endpoint;

	@Value("${huaWeiObs.ObsFilesPath}")
	private String ObsFilesPath;

    private ObsClient obsClient;

	@Autowired
	private FileInfoMapper fileInfoMapper;

	@Override
	public FileInfo huaWeiObsUpload(MultipartFile file)  {


		PutObjectResult putObjectResult = null;
		String md5                                    = null;

		try {

			//验证文件格式
			String fileOrigName = file.getOriginalFilename();
			if (!fileOrigName.contains(".")) {
				throw new IllegalArgumentException("缺少后缀名");
			}

			//对文件流进行MD5加密
			md5               = FileUtil.fileMd5(file.getInputStream());
			FileInfo fileInfo = fileInfoMapper.getById(md5);

			//根据MD字符串查看文件是否已经上传,已经上传的文件直接返回文件信息不用调取华为云上传接口
			if (fileInfo != null) {
				fileInfo.setFilePrefix(FilesPrefix);
				fileInfoMapper.update(fileInfo);
				return fileInfo;
			}

			String fileSuffix    =  fileOrigName.substring(fileOrigName.lastIndexOf("."));
			String pathname      =  ObsFilesPath+FileUtil.getPath()+md5+fileSuffix;
           
           //实例化ObsClient,并将实例化引用赋值给成员变量obsClient
			obsClient                 =  new ObsClient(AccessKeyId,AccessKeySecret,Endpoint);
			putObjectResult      =  obsClient.putObject(BucketName, pathname, file.getInputStream());

			//保存上传记录到数据库
			long size          = file.getSize();
			String contentType = file.getContentType();
			String fullPath    = putObjectResult.getObjectUrl();
			fileInfo           = new FileInfo();
			fileInfo.setId(md5);
			fileInfo.setContentType(contentType);
			fileInfo.setSize(size);
			fileInfo.setPath(fullPath);
			fileInfo.setUrl(pathname);
			fileInfo.setType(contentType.startsWith("image/") ? 1 : 0);
			fileInfo.setFilePrefix(FilesPrefix);
			fileInfo.setOriginalName(fileOrigName);
			fileInfo.setSuffix(fileSuffix);
			fileInfoMapper.save(fileInfo);
			log.info("文件上传成功{}", fullPath);

			//返回文件对象
			return fileInfo;

		} catch (ObsException e) {
			log.info("Response Code: {}",e.getResponseCode());
			log.info("Error Message: {}",e.getErrorMessage());
			log.info("Error Code: {}",e.getErrorCode());
			log.info("Request ID: {}",e.getErrorRequestId());
			log.info("Host ID: {}",e.getErrorHostId());
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			//不管有没有返回,最后一定会执行
			if (obsClient != null) {
				try {
					//关闭最后连接
					obsClient.close();
				} catch (IOException e) {

				}
			}
		}

		//返回空对象
		return null;
	}
	
}

错误原因:
bean实例默认作用域是单例模式,,Spring IoC 容器中只会存在一个共享的 Bean 实例,无论有多少个Bean 引用它,始终指向同一对象,该模式在多线程下是不安全的,同一个bean对象,可能被注入到了不同的对象中,在高并发的情况下,被依赖的对象可能会多个线程共享,会引发多线程安全问题。

例如:FileService类被FileController类依赖,FileService bean对象通过@Autowired注入到了FileController对象,因为FileService 的bean实例是单例模式,在高并发情况下,同一个FileService bean对象实例会被注入到不同FileController对象,因此FileService bean对象被多个线程共享,引起了安全问题。比如:一个线程正在使用FileService bean对象的成员变量obsClient.putObject()上传文件,另外一个线程使用FileService bean对象成员obsClient.close()关闭了链接,则就导致了部分文件上传失败。

解决方法:
1、像obsClient这样的变量不要声明为成员变量,声明为局部变量,就可以避免obsClient被多个线程共享引发安全问题。
2、使用@Scope(“prototype”)定义FileService bean作用域为原型模式,每次通过 Spring 容器获取 prototype 定义的 bean 时,容器都将创建
一个新的 Bean 实例,就不会引起多线程安全问题。
2、使用sychornized修饰上传文件的代码,锁住对象实例,例如:
Synchronized(this){
//上传代码
//因为同一个FileService bean对象被注入到不象线程,是可以被锁住的
}

但是这个加锁的方法可能会导致严重的阻塞和性能开销,不推荐使用这个方法。

方法1的代码:

@Service
@Slf4j
public class FileServiceImpl_bak implements FileService {

	@Value("${files.path}")
	private String filesPath;

	@Value("${files.prefix}")
	private String FilesPrefix;

	@Value("${huaWeiObs.AccessKeyId}")
	private String AccessKeyId;

	@Value("${huaWeiObs.AccessKeySecret}")
	private String AccessKeySecret;

	@Value("${huaWeiObs.BucketName}")
	private String BucketName;

	@Value("${huaWeiObs.Endpoint}")
	private String Endpoint;

	@Value("${huaWeiObs.ObsFilesPath}")
	private String ObsFilesPath;

	@Autowired
	private FileInfoMapper fileInfoMapper;

	@Override
	public FileInfo huaWeiObsUpload(MultipartFile file)  {

		//局部变量,不被多个线程共享,这避免了多线程导致的安全问题
		ObsClient obsClient             = null;
		PutObjectResult putObjectResult = null;
		String md5                      = null;

		try {

			//验证文件格式
			String fileOrigName = file.getOriginalFilename();
			if (!fileOrigName.contains(".")) {
				throw new IllegalArgumentException("缺少后缀名");
			}

			//对文件流进行MD5加密
			md5               = FileUtil.fileMd5(file.getInputStream());
			FileInfo fileInfo = fileInfoMapper.getById(md5);
			//根据MD5字符串查看文件是否已经上传,已经上传的文件直接返回文件信息,不用调取华为云上传接口
			if (fileInfo != null) {
				fileInfo.setFilePrefix(FilesPrefix);
				fileInfoMapper.update(fileInfo);
				return fileInfo;
			}

			//文件后缀名与上传路径
			String fileSuffix    =  fileOrigName.substring(fileOrigName.lastIndexOf("."));
			String pathname      =  ObsFilesPath+FileUtil.getPath()+md5+fileSuffix;

			obsClient            =  new ObsClient(AccessKeyId,AccessKeySecret,Endpoint);
			putObjectResult      =  obsClient.putObject(BucketName, pathname, file.getInputStream());

			//保存上传记录到数据库
			long size          = file.getSize();
			String contentType = file.getContentType();
			String fullPath    = putObjectResult.getObjectUrl();
			fileInfo           = new FileInfo();
			fileInfo.setId(md5);
			fileInfo.setContentType(contentType);
			fileInfo.setSize(size);
			fileInfo.setPath(fullPath);
			fileInfo.setUrl(pathname);
			fileInfo.setType(contentType.startsWith("image/") ? 1 : 0);
			fileInfo.setFilePrefix(FilesPrefix);
			fileInfo.setOriginalName(fileOrigName);
			fileInfo.setSuffix(fileSuffix);
			fileInfoMapper.save(fileInfo);
			log.info("文件上传成功{}", fullPath);

			//返回文件对象
			return fileInfo;

		} catch (ObsException e) {
			log.info("Response Code: {}",e.getResponseCode());
			log.info("Error Message: {}",e.getErrorMessage());
			log.info("Error Code: {}",e.getErrorCode());
			log.info("Request ID: {}",e.getErrorRequestId());
			log.info("Host ID: {}",e.getErrorHostId());
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			//不管有没有返回,最后一定会执行
			if (obsClient != null) {
				try {
					//关闭最后连接
					obsClient.close();
				} catch (IOException e) {

				}
			}
		}

		return null;

	}

}

总结:
Spring 3 中为 Bean 定义了 5 种作用域,分别为 singleton(单例)、prototype(原型)、request、session 和 global session,不同的应用场景需要选择不同的作用域降低开销和保证线程安全,springboot 定义bean的作用域时,使用@Scope注解修饰类。例如:

//定义单例模式
@Scope("singleton")
@Component
public class SingleScopeTest {
}
//定义原型模式
@Scope("prototype")
@Component
public class PrototypeScoreTest {
}

猜你喜欢

转载自blog.csdn.net/u011582840/article/details/107939151