提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
前言
分布式事务和锁的来源背景:
单系统应用随着业务范围的扩大,会出现高耦合,不易扩展等问题,所以需要引入分布式应用,保证各个模块功能的独立;在这种分布式系统下,会涉及到分库分表,各个模块之间的数据通信就需要保证原子性,所以就有了分布式事务的解决方案;随着用户量的增长,核心模板需要做主备模式,减少单服务的访问压力,这时候就存在并发访问同一个方法的情况,涉及到数据安全,所以就有了分布式锁的解决方案。
一、分布式事务的解决方案
1. jta-atomikos(只能解决单体项目中多数据源的分布式事务问题
)
实现原理:
基于XA协议的两阶段提交:
XA是一个分布式事务协议,由Tuxedo提出。XA中大致分为两部分:事务管理器和本地资源管理器。其中本地资源管理器往往由数据库实现,比如Oracle、DB2这些商业数据库都实现了XA接口,而事务管理器作为全局的调度者,负责各个本地资源的提交和回滚。XA实现分布式事务的原理如下:
总的来说,XA协议比较简单,而且一旦商业数据库实现了XA协议,使用分布式事务的成本也比较低。但是,XA也有致命的缺点,那就是性能不理想,特别是在交易下单链路,往往并发量很高,XA无法满足高并发场景。XA目前在商业数据库支持的比较理想,在mysql数据库中支持的不太理想,mysql的XA实现,没有记录prepare阶段日志,主备切换回导致主库与备库数据不一致。许多nosql也没有支持XA,这让XA的应用场景变得非常狭隘.
jta-atomikos基于xa协议的实现代码步骤:
一. 配置多数据源
a. yml多数据源配置
spring:
jta:
# 事务管理器唯一标识符
transaction-manager-id: txManager
log-dir: transaction-logs
atomikos:
datasource:
borrow-connection-timeout: 10000
min-pool-size: 5
max-pool-size: 10
properties:
# 事务超时时间 300 0000ms 默认10 000ms
default-jta-timeout: 300000
max-actives: 50
max-timeout: 300000
enable-logging: true
logBaseDir: transaction-logs
datasource:
type: com.alibaba.druid.pool.xa.DruidXADataSource
druid:
master:
name: master
url: jdbc:mysql://localhost:3306/demo?useSSL=false&autoReconnect=true&useUnicode=true&characterEncoding=UTF-8&noDatetimeStringSync=true&zeroDateTimeBehavior=CONVERT_TO_NULL&serverTimezone=Asia/Shanghai
username: root
# druid 链接密码 加密 需要同时配置 connection-properties filters: config
password: 123456
# connection-properties: config.decrypt=true;config.decrypt.key=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAI+7x/MxFWgNSk2saE3iSoBwdpTbjozCtnvhh/Fk4UF/1tG7S11/uBR7kGnQqfo27ytkb1wJqsmtZ4ImQqzNVosCAwEAAQ==
initialSize: 10
minIdle: 10
maxActive: 100
maxWait: 60000
poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize: 20
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
validationQueryTimeout: 10000
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
statViewServlet:
enabled: true
urlPattern: /druid/*
#login-username: admin
#login-password: admin
# 如果是加密密码 则必须配置 filters: config 否则链接会失败
filters: config,stat,wall,log4j2
second:
name: second
url: jdbc:mysql://localhost:3306/test?useSSL=false&autoReconnect=true&useUnicode=true&characterEncoding=UTF-8&noDatetimeStringSync=true&zeroDateTimeBehavior=CONVERT_TO_NULL&serverTimezone=Asia/Shanghai
username: root
password: 123456
initialSize: 10
minIdle: 10
maxActive: 100
maxWait: 60000
poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize: 20
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
validationQueryTimeout: 10000
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
statViewServlet:
enabled: true
urlPattern: /druid/*
#login-username: admin
#login-password: admin
filters: stat,wall,log4j2
b. 添加属性配置类
@Data
public class DataSourceProperties {
private String name;
private String url;
private String username;
private String password;
private Integer initialSize;
private Integer maxActive;
private Integer minIdle;
private Integer maxWait;
private Boolean poolPreparedStatements;
private Integer maxPoolPreparedStatementPerConnectionSize;
private Integer timeBetweenEvictionRunsMillis;
private Integer minEvictableIdleTimeMillis;
private String validationQuery;
private Integer validationQueryTimeout;
private Boolean testWhileIdle;
private Boolean testOnBorrow;
private Boolean testOnReturn;
private String filters;
// private String connectionProperties;
}
@Data
@EqualsAndHashCode(callSuper = true)
@Validated
@Component
@ConfigurationProperties(prefix = "spring.datasource.druid.master")
public class DataSourceMasterProperties extends DataSourceProperties {
}
@Data
@EqualsAndHashCode(callSuper = true)
@Validated
@Component
@ConfigurationProperties(prefix = "spring.datasource.druid.second")
public class DataSourceSecondProperties extends DataSourceProperties {
}
c. 保存一个线程安全的DataSourceType容器
import java.sql.Connection;
import java.util.concurrent.ConcurrentHashMap;
import com.navinfo.entity.Constans;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 保存一个线程安全的DataSourceType容器
* @author Administrator
*
*/
public class DataSourceContextHolder {
private static Logger logger = LoggerFactory.getLogger(DataSourceContextHolder.class);
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<String>();
public static ConcurrentHashMap<String, Connection> connectionMap = new ConcurrentHashMap<>();
/**
* 设置数据库来源
*/
public static void setDateSoureType(String dataSourceType) {
logger.info("数据源切换为:" + dataSourceType);
CONTEXT_HOLDER.set(dataSourceType);
}
/**
* 获取数据库来源
*/
public static String getDateSoureType() {
String dsType = CONTEXT_HOLDER.get();
if (dsType == null) {
logger.info("当前线程没有设置数据源,使用默认数据源");
// 当前线程没有设置数据源,使用默认数据源GOV
setDateSoureType(Constans.MASTER);
}
return CONTEXT_HOLDER.get();
}
/**
* 清除数据库来源
*/
public static void clearDateSoureType() {
CONTEXT_HOLDER.remove();
}
/**
* 设置当前线程使用哪个数据源
*
* @param dataSourceType
*/
public static void chooseDataSource(String dataSourceType) {
switch (dataSourceType) {
case Constans.SECOND:
setDateSoureType(Constans.SECOND);
break;
default:
setDateSoureType(Constans.MASTER);
break;
}
}
}
d. 动态数据源配置
/**
* 动态数据源(需要继承AbstractRoutingDataSource)
* 作用:使用DataSourceContextHolder获取当前线程的DataSourceType
* 多数据源的情况下并不是多个数据源并存的,Spring提供了AbstractRoutingDataSource这样一个抽象类,
* 使得能够在多数据源的情况下任意切换,相当于一个动态路由 的作用,作者称之为动态数据源。
* @author kzx
*
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDateSoureType();
}
}
e. 配置多数据源切换
import java.sql.Connection;
import java.sql.SQLException;
import javax.sql.DataSource;
import org.mybatis.spring.transaction.SpringManagedTransaction;
/**
* 解决分布式事务控制下数据源无法动态切换的问题,改写SpringManagedTransaction获取Connection的方法
* @author kzx
*
*/
public class DynamicTransaction extends SpringManagedTransaction {
private DataSource dataSource;
public DynamicTransaction(DataSource dataSource) {
super(dataSource);
this.dataSource = dataSource;
}
@Override
public Connection getConnection() throws SQLException {
String key = DataSourceContextHolder.getDateSoureType();
if (DataSourceContextHolder.connectionMap.containsKey(key)) {
Connection connection = DataSourceContextHolder.connectionMap.get(key);
return connection;
}
Connection con = dataSource.getConnection();
DataSourceContextHolder.connectionMap.put(key, con);
return con;
}
}
f. 配置多数据源切换的工厂类
import javax.sql.DataSource;
import org.apache.ibatis.session.TransactionIsolationLevel;
import org.apache.ibatis.transaction.Transaction;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
/**
* @Author: KangZhiXing
* @Date: 2022/5/26
*/
public class DynamicTransactionsFactory extends SpringManagedTransactionFactory {
@Override
public Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) {
return new DynamicTransaction(dataSource);
}
}
g. 多数据源配置
import com.alibaba.druid.filter.stat.StatFilter;
import com.alibaba.druid.pool.xa.DruidXADataSource;
import com.alibaba.druid.support.http.StatViewServlet;
import com.alibaba.druid.support.http.WebStatFilter;
import com.alibaba.druid.support.spring.stat.DruidStatInterceptor;
import com.alibaba.druid.wall.WallConfig;
import com.alibaba.druid.wall.WallFilter;
import com.atomikos.icatch.jta.UserTransactionImp;
import com.atomikos.icatch.jta.UserTransactionManager;
import com.atomikos.jdbc.AtomikosDataSourceBean;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.MybatisXMLLanguageDriver;
import com.baomidou.mybatisplus.extension.plugins.OptimisticLockerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.navinfo.TransactionalDb.mybatisplusdb.properties.DataSourceMasterProperties;
import com.navinfo.TransactionalDb.mybatisplusdb.properties.DataSourceProperties;
import com.navinfo.TransactionalDb.mybatisplusdb.properties.DataSourceSecondProperties;
import com.navinfo.entity.Constans;
import lombok.SneakyThrows;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.type.JdbcType;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.JdkRegexpMethodPointcut;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.*;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.jta.JtaTransactionManager;
import javax.sql.DataSource;
import javax.transaction.TransactionManager;
import javax.transaction.UserTransaction;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
/**
* druid 及 atomikos 事务配置
*
* @author kzx
* @date 2020-07-08 下午 7:11
*/
@Configuration
@MapperScan(basePackages = "com.navinfo", sqlSessionFactoryRef = "SqlSessionFactory")
public class CommonDataSourceConfig {
@Value("${spring.datasource.type:com.alibaba.druid.pool.xa.DruidXADataSource}")
private String xaDataSourceClassName;
/**
* @DependsOn({}) 在某个类注解@DependsOn("xxx")那么这个类一定会在xxx实例化之后实例化
* @param properties
* @return
*/
@Bean(name = "masterDataSource")
@Primary
@DependsOn({"txManager"})
public DataSource masterDataSource(DataSourceMasterProperties properties) {
return build(properties);
}
@Bean(name = "secondDataSource")
@Primary
@DependsOn({"txManager"})
public DataSource secondDataSource(DataSourceSecondProperties properties) {
return build(properties);
}
/**
* Atomikos处理分布式事务,
* @param properties
* @return
*/
@SneakyThrows
private AtomikosDataSourceBean build(DataSourceProperties properties) {
DruidXADataSource druid = new DruidXADataSource();
druid.setName(properties.getName());
druid.setUrl(properties.getUrl());
druid.setUsername(properties.getUsername());
druid.setPassword(properties.getPassword());
druid.setInitialSize(properties.getInitialSize());
druid.setMinIdle(properties.getMinIdle());
druid.setMaxActive(properties.getMaxActive());
druid.setMaxWait(properties.getMaxWait());
druid.setPoolPreparedStatements(properties.getPoolPreparedStatements());
druid.setMaxPoolPreparedStatementPerConnectionSize(properties.getMaxPoolPreparedStatementPerConnectionSize());
druid.setTimeBetweenEvictionRunsMillis(properties.getTimeBetweenEvictionRunsMillis());
druid.setMinEvictableIdleTimeMillis(properties.getMinEvictableIdleTimeMillis());
druid.setValidationQuery(properties.getValidationQuery());
druid.setValidationQueryTimeout(properties.getValidationQueryTimeout());
druid.setTestWhileIdle(properties.getTestWhileIdle());
druid.setTestOnBorrow(properties.getTestOnBorrow());
druid.setTestOnReturn(properties.getTestOnReturn());
druid.setFilters(properties.getFilters());
AtomikosDataSourceBean atomikosDataSourceBean = new AtomikosDataSourceBean();
// //DataSource不能直接使用Druid提供的DruidDataSource, 需要使用atomikos来包装一下Druid提供的DruidXADataSource,来支持XA规范
// //see https://juejin.im/post/5e186601e51d4530591783ec
atomikosDataSourceBean.setXaDataSource(druid);
atomikosDataSourceBean.setXaDataSourceClassName(xaDataSourceClassName);
atomikosDataSourceBean.setUniqueResourceName(properties.getName());
atomikosDataSourceBean.setPoolSize(10);
atomikosDataSourceBean.setMinPoolSize(5);
atomikosDataSourceBean.setMaxPoolSize(10);
return atomikosDataSourceBean;
}
/**
* 动态数据源
* @param masterDataSource
* @param secondDataSource
* @return
*/
@Bean(name = "dynamicDataSource")
@Primary
public DataSource dataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
@Qualifier("secondDataSource") DataSource secondDataSource) {
Map<Object, Object> targetDataSource = new HashMap<>();
targetDataSource.put(Constans.MASTER, masterDataSource);
targetDataSource.put(Constans.SECOND, secondDataSource);
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setTargetDataSources(targetDataSource);
dynamicDataSource.setDefaultTargetDataSource(masterDataSource);
return dynamicDataSource;
}
@Primary
@Bean("SqlSessionFactory")
public SqlSessionFactory SqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dataSource) throws Exception {
MybatisSqlSessionFactoryBean factoryBean = new MybatisSqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
MybatisConfiguration configuration = new MybatisConfiguration();
configuration.setDefaultScriptingLanguage(MybatisXMLLanguageDriver.class);
// 数据库下划线转驼峰
configuration.setMapUnderscoreToCamelCase(true);
configuration.setJdbcTypeForNull(JdbcType.NULL);
factoryBean.setConfiguration(configuration);
factoryBean.setTransactionFactory(new DynamicTransactionsFactory());
//指定xml路径.
factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:com.navinfo.mapper/*.xml"));
factoryBean.setPlugins(
//分页插件
new PaginationInterceptor(),
//乐观锁插件
new OptimisticLockerInterceptor()
);
return factoryBean.getObject();
}
}
二. 分布式事务配置
/**
* 分布式事务配置
* @return
* @throws Throwable
*/
@Bean(name = "userTransaction")
public UserTransaction userTransaction() throws Throwable {
UserTransactionImp userTransactionImp = new UserTransactionImp();
userTransactionImp.setTransactionTimeout(10000);
return userTransactionImp;
}
@Bean(name = "atomikosTransactionManager")
public TransactionManager atomikosTransactionManager() {
UserTransactionManager userTransactionManager = new UserTransactionManager();
userTransactionManager.setForceShutdown(false);
return userTransactionManager;
}
/**
* 事务管理器配置
* @return
* @throws Throwable
*/
@Bean(name = "txManager")
@DependsOn({ "userTransaction", "atomikosTransactionManager" })
public PlatformTransactionManager transactionManager() throws Throwable {
UserTransaction userTransaction = userTransaction();
TransactionManager atomikosTransactionManager = atomikosTransactionManager();
return new JtaTransactionManager(userTransaction, atomikosTransactionManager);
}
2. 阿里Seata解决方案
实现原理:
Seata 的设计目标是对业务无侵入,因此它是从业务无侵入的两阶段提交(全局事务)着手,在传统的两阶段上进行改进,他把一个分布式事务理解成一个包含了若干分支事务的全局事务。而全局事务的职责是协调它管理的分支事务达成一致性,要么一起成功提交,要么一起失败回滚。也就是一荣俱荣一损俱损~
Seata 组成
我们看下 Seata 中存在几种重要角色:
TC(Transaction Coordinator):事务协调者。管理全局的分支事务的状态,用于全局性事务的提交和回滚。
TM(Transaction Manager):事务管理者。用于开启、提交或回滚事务。
RM(Resource Manager):资源管理器。用于分支事务上的资源管理,向 TC 注册分支事务,上报分支事务的状态,接收 TC 的命令来提交或者回滚分支事务。
这是一种很巧妙的设计,我们来看图:
执行流程是这样的:
服务A中的 TM 向 TC 申请开启一个全局事务,TC 就会创建一个全局事务并返回一个唯一的 XID
服务A中的 RM 向 TC 注册分支事务,然后将这个分支事务纳入 XID 对应的全局事务管辖中
服务A开始执行分支事务
服务A开始远程调用B服务,此时 XID 会根据调用链传播
服务B中的 RM 也向 TC 注册分支事务,然后将这个分支事务纳入 XID 对应的全局事务管辖中
服务B开始执行分支事务
全局事务调用处理结束后,TM 会根据有误异常情况,向 TC 发起全局事务的提交或回滚
TC 协调其管辖之下的所有分支事务,决定是提交还是回滚
Seata 安装:
第一步:
下载地址
第二步:解压 seata-server
第三步:运行bin下的seata-server.bat
Seata集成:
首先准备两个配置文件:
registry.conf 和 file.conf
网盘获取地址:
链接:https://pan.baidu.com/s/1uHntETNK2Wpq0CEMPWU4_Q
提取码:djqt
接下来进行项目配置:
1.引入依赖
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.1.1.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-seata</artifactId>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
</dependencies>
2.项目resource中引入registry.conf 和 file.conf配置文件。
registry.conf需要修改的地方:
本来使用的type类型是file,所以需要去file.conf文件中读取配置信息,也可以改为nacos,直接去配置中心读取信息:
3.配置全局数据源,封装成微服务中单独的模块
package com.itheima.seata.config;
import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import io.seata.spring.annotation.GlobalTransactionScanner;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.core.env.Environment;
import javax.sql.DataSource;
//@Configuration
public class DataSourceConfig {
/**
* 创建一个数据库连接池对象,这个对象可以是任意类型的数据库连接池对象
* @return
*/
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DruidDataSource ds0(){
DruidDataSource druidDataSource = new DruidDataSource();
return druidDataSource;
}
/**
* 将上面的数据库连接池对象包装成阿里seata的数据库连接池对象
* @Primary 表示在多个同类型对象出现的时候,优先选择的对象上使用
* @param dataSource
* @return
*/
@Primary
@Bean
public DataSource dataSource(DruidDataSource dataSource){
DataSourceProxy dsp = new DataSourceProxy(dataSource);
return dsp;
}
@Bean
public GlobalTransactionScanner globalTransactionScanner(Environment environment) {
//事务分组名称
String applicationName = environment.getProperty("spring.application.name");
String groupName = environment.getProperty("seata.group.name");
if (applicationName == null) {
return new GlobalTransactionScanner(groupName == null ? "my_test_tx_group" : groupName);
} else {
return new GlobalTransactionScanner(applicationName, groupName == null ? "my_test_tx_group" : groupName);
}
}
}
4.将步骤2的模块注入到使用到的业务模块
5.业务数据库中执行 undo_log 脚本,作为事务的记录。
CREATE TABLE `undo_log`
(
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`branch_id` BIGINT(20) NOT NULL,
`xid` VARCHAR(100) NOT NULL,
`context` VARCHAR(128) NOT NULL,
`rollback_info` LONGBLOB NOT NULL,
`log_status` INT(11) NOT NULL,
`log_created` DATETIME NOT NULL,
`log_modified` DATETIME NOT NULL,
`ext` VARCHAR(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = INNODB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8;
- 业务模块在写业务的方法上加上@GlobalTransactional注解
源项目参考:
链接:https://pan.baidu.com/s/1LtLgJnmk_vISPJ4LajQK4w
提取码:0nmy
二、分布式锁的解决方案
1.redis锁
实现原理:
项目中使用redis锁主要是依据 redis setnx命令的特性(SETNX:在指定的 key 不存在时,为 key 设置指定的值。 设置成功,返回 1 设置失败,返回 0 )
代码如下(示例):
@SpringBootTest
class RedislockApplicationTests {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 公共锁key
*/
private final String LOCK_KEY = "lock";
private final String VALUE = "value";
@Test
public void redisLockTest(){
//获取锁,设置有效期,防止程序异常没有释放锁导致死锁
try {
Boolean b = redisTemplate.opsForValue().setIfAbsent(LOCK_KEY, VALUE,Duration.ofSeconds(10));
if (b){
//获取锁成功
//执行业务逻辑
}else {
//获取锁失败
//快速失败,响应给客户端
}
}finally {
//释放锁
redisTemplate.delete(LOCK_KEY);
}
}
}
redis锁的几个问题
1.程序异常没有释放锁怎么办?
案例中是采用了给key设置有效期,当程序报错没有释放锁时key可以自动过期,但是这里有个弊端是key的过期时间怎么才能设置的更合理
2.程序执行时间超过了锁的释放时间会带来什么问题,以及解决方案?
a. 如果一个请求执行业务的时间比锁的有效期还要长,导致在业务执行过程中锁就失效了,此时另一个请求就会获取到锁,但前一个请求在业务执行完毕的时候,直接删除锁的话就会出现误删其它请求创建的锁的情况。
解决方案:
可以在创建锁的时候需要引入一个随机值并在删除锁的时候加以判断。
代码如下:
@SpringBootTest
class RedislockApplicationTests {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 公共锁key
*/
private final String LOCK_KEY = "lock";
@Test
public void redisLockTest(){
//获取锁,设置有效期,防止程序异常没有释放锁导致死锁
UUID uid = UUID.randomUUID();
String str = uid.toString();
try {
Boolean b = redisTemplate.opsForValue().setIfAbsent(LOCK_KEY, str,Duration.ofSeconds(10));
if (b){
//获取锁成功
//执行业务逻辑
}else {
//获取锁失败
//快速失败,响应给客户端
}
}finally {
//释放锁
if (str.equals(redisTemplate.opsForValue().get(LOCK_KEY))){
redisTemplate.delete(LOCK_KEY);
}
}
}
}
b. 多人获取到锁,并发问题还会存在
解决方案:
a. 手动实现watchdog模式,当客户端加锁成功后,可以启动一个定时任务,每隔10s(最好支持配置)来检测业务是否处理完成,检测的依据就是判断分布式锁的key是否还存在,如果存在,就进行续约。
b. 开启一个新线程while循环每个10s去查询key是否存在,如果存在续约。
3.假如1000个人同时发出请求,第一时间只会有一个请求获取到锁执行业务逻辑,获取锁失败的请求怎么办?
方式一 : 如案例中没有获取到锁的请求是通过快速失败的策略,没有获取到锁直接响应如:当前排队人数较多,请稍后再试诸如此类的话语.但是这样用户体验很差
方式二 : 没有获取到锁的请求采用轮询的方式处理,这样增加了CPU压力
2.redission锁
实现原理:
a. redisson所有指令都通过lua脚本执行,保证了操作的原子性
b. redisson设置了watchdog看门狗,“看门狗”的逻辑保证了没有死锁发生
代码如下(示例):
public String testRedisLock() {
RLock lock = redissonClient.getLock("lock");
boolean locked = false;
try {
// tryLock(long waitTime, long leaseTime, TimeUnit unit) 的参数含义:
// waitTime:尝试获取锁的最大等待时间(如果锁被其他线程持有,最多等待 2 秒)
// leaseTime:锁的持有时间(获取锁成功后,自动释放锁的时间,设置为 2 秒)
// Redisson 默认对未指定 leaseTime 的锁启用看门狗(自动续期),但若显式指定了 leaseTime,看门狗会失效。
// 调整锁等待和续期逻辑(等待2秒,不显式设置leaseTime以启用Watchdog)
locked = lock.tryLock(2, TimeUnit.SECONDS);
if (locked) {
Object numObj = redisTemplate.opsForValue().get("num1");
if (numObj != null) {
int num = Integer.parseInt(numObj.toString());
if (num > 0) {
redisTemplate.opsForValue().decrement("num1", 1L);
Thread.sleep(1000); // 模拟业务耗时
System.out.println("扣减成功,票数剩余:" + redisTemplate.opsForValue().get("num1"));
} else {
System.out.println("库存不足");
}
}
} else {
System.out.println("获取锁失败");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("线程被中断");
} catch (NumberFormatException e) {
System.out.println("库存数据格式异常");
} finally {
if (locked) {
try {
// 正确释放锁(通过锁对象而非redissonClient)
lock.unlock();
} catch (IllegalMonitorStateException e) {
System.out.println("解锁异常:当前线程未持有锁");
}
}
}
return "success";
}
redission锁的优势
针对上述redis锁,锁过期业务未执行结束的问题,它底层实现了watchdog模式,不需要手动判断。redisson定时器使用的是netty-common包中的HashedWheelTime来实现的。
3.zookeeper锁
实现原理:
- 比如/test下创建临时顺序节点
- 获取/test下的所有节点,进行排序
- 比较当前节点,若没有比自己序号小的子节点或所有比自己序号小的子节点都是读请求,那么表明自己已经成功获取锁
- 如果不是最小的,给他的前一个注册watch
- 当前一个释放锁后,或通知当前结点,触发获取锁
- 重复步骤1
代码示例如下:
/**
* @author KangZhiXing
* @date 2022/5/12
*/
@Configuration
@ConfigurationProperties(prefix = "zookeeper.curator")
@Data
public class ZookeeperConfig {
/**
* 集群地址
*/
private String ip;
/**
* 连接超时时间
*/
private Integer connectionTimeoutMs;
/**
* 会话超时时间
*/
private Integer sessionTimeOut;
/**
* 重试机制时间参数
*/
private Integer sleepMsBetweenRetry;
/**
* 重试机制重试次数
*/
private Integer maxRetries;
/**
* 命名空间(父节点名称)
*/
private String namespace;
/**
* - `session`重连策略
* - `RetryPolicy retry Policy = new RetryOneTime(3000);`
* - 说明:三秒后重连一次,只重连一次
* - `RetryPolicy retryPolicy = new RetryNTimes(3,3000);`
* - 说明:每三秒重连一次,重连三次
* - `RetryPolicy retryPolicy = new RetryUntilElapsed(1000,3000);`
* - 说明:每三秒重连一次,总等待时间超过个`10`秒后停止重连
* - `RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000,3)`
* - 说明:这个策略的重试间隔会越来越长
* - 公式:`baseSleepTImeMs * Math.max(1,random.nextInt(1 << (retryCount + 1)))`
* - `baseSleepTimeMs` = `1000` 例子中的值
* - `maxRetries` = `3` 例子中的值
*
* @return
* @throws Exception
*/
@Bean(value = "curatorClient")
@Conditional(CustomCondition.class)
public CuratorFramework curatorClient() throws Exception {
CuratorFramework client = CuratorFrameworkFactory.builder()
//连接地址 集群用,隔开
.connectString(ip)
.connectionTimeoutMs(connectionTimeoutMs)
//会话超时时间
.sessionTimeoutMs(sessionTimeOut)
//设置重试机制
.retryPolicy(new ExponentialBackoffRetry(sleepMsBetweenRetry, maxRetries))
//设置命名空间 在操作节点的时候,会以这个为父节点
.namespace(namespace)
.build();
client.start();
//注册监听器
ZookeeperWatches watches = new ZookeeperWatches(client);
watches.zNodeWatcher();
watches.zNodeChildrenWatcher();
return client;
}
}
/**
* @author KangZhiXing
* @date 2022/5/12
*/
public class ZookeeperWatches {
private CuratorFramework client;
public ZookeeperWatches(CuratorFramework client) {
this.client = client;
}
public void zNodeWatcher() throws Exception {
NodeCache nodeCache = new NodeCache(client, "/node");
nodeCache.start();
nodeCache.getListenable().addListener(new NodeCacheListener() {
@Override
public void nodeChanged() throws Exception {
System.out.println("=======节点改变===========");
String path = nodeCache.getPath();
String currentDataPath = nodeCache.getCurrentData().getPath();
String currentData = new String(nodeCache.getCurrentData().getData());
Stat stat = nodeCache.getCurrentData().getStat();
System.out.println("path:"+path);
System.out.println("currentDataPath:"+currentDataPath);
System.out.println("currentData:"+currentData);
}
});
System.out.println("节点监听注册完成");
}
public void zNodeChildrenWatcher() throws Exception {
PathChildrenCache pathChildrenCache = new PathChildrenCache(client, "/node",true);
pathChildrenCache.start();
pathChildrenCache.getListenable().addListener(new PathChildrenCacheListener() {
@Override
public void childEvent(CuratorFramework client, PathChildrenCacheEvent event) throws Exception {
System.out.println("=======节点子节点改变===========");
PathChildrenCacheEvent.Type type = event.getType();
String childrenData = new String(event.getData().getData());
String childrenPath = event.getData().getPath();
Stat childrenStat = event.getData().getStat();
System.out.println("子节点监听类型:"+type);
System.out.println("子节点路径:"+childrenPath);
System.out.println("子节点数据:"+childrenData);
System.out.println("子节点元数据:"+childrenStat);
}
});
System.out.println("子节点监听注册完成");
}
}
@ApiOperation(value = "可重入锁",notes = "同一线程可重入锁")
@GetMapping("testLock")
public String testRedisLock() throws Exception {
InterProcessMutex lock = new InterProcessMutex(client, "/lock");
try {
if (lock.acquire(2,TimeUnit.SECONDS)) {
System.out.println(Thread.currentThread().getName()+"获取重入锁1");
}
// if (lock.acquire(1,TimeUnit.SECONDS)) {
//
// System.out.println(Thread.currentThread().getName()+"获取重入锁2");
// }
Object num = redisService.get("num");
if (Integer.valueOf(num.toString()) > 0) {
redisService.decr("num", 1L);
Thread.sleep(1000);
System.out.println("票数剩余:" + redisService.get("num"));
}
lock.release();
// lock.release();
} catch (IllegalMonitorStateException e) {
System.out.println(Thread.currentThread().getName() + "释放锁异常::" + e.getMessage());
}
return "success";
}
@ApiOperation(value = "不可重入锁",notes = "同一线程不可重入锁")
@GetMapping("testLock1")
public String testRedisLock1() throws Exception {
InterProcessSemaphoreMutex lock = new InterProcessSemaphoreMutex(client, "/lock1");
try {
lock.acquire();
System.out.println(Thread.currentThread().getName() + ":获取重入锁1");
//执行下面方法会阻塞
lock.acquire();
System.out.println(Thread.currentThread().getName() + ":获取重入锁2");
// if (lock.acquire(1,TimeUnit.SECONDS)) {
//
// System.out.println(Thread.currentThread().getName()+"获取重入锁2");
// }
Object num = redisService.get("num");
if (Integer.valueOf(num.toString()) > 0) {
redisService.decr("num", 1L);
Thread.sleep(1000);
System.out.println("票数剩余:" + redisService.get("num"));
}
lock.release();
lock.release();
} catch (IllegalMonitorStateException e) {
System.out.println(Thread.currentThread().getName() + "释放锁异常::" + e.getMessage());
}catch (Exception e){
System.out.println(Thread.currentThread().getName() + "异常::" + e.getMessage());
}
return "success";
}
总结
个人实战中的一些解决方案,代码片段是项目中的部分截图。重要的是理解实现逻辑,由一生二。方案不是最优化的,后续还会补充!有问题还望多多指教。有更好的方案的小伙伴,欢迎私信或者评论交流,与分享!..