业务幂等带来的性能上的损耗
交易系统讲究的是:业务幂等,不能多扣不能少扣。
比如说有一个用户他的帐户余额就10,000元,无论它进行了上万、上千次交易,每一笔交易成功的流水的状态、和金额必须要对的起来。说了简单点假设该帐户交易了100次,共计9万元的交易流水,但帐户余额就10,000,你不能给他记成负8万余额吧?那么我们来看这个100次交易流水。其中有30次交易成功,总额是10,000元扣款成功。其余70次都是交易失败。如果30次交易成功,总额是10,000元,然后如果出现了第31次也是交易成功,那么这第31次的交易成功怎么来的?这就是业务不幂等。
演示根据交易额扣款动作
根据业务需求与设计
我们下面可以来演示,不幂等时发生的代码问题
数据库表结构如下:
uid Balance21000
我们做一个spring boot的controller、service、dao,然后使用多个并发对这一个帐户里的余额进行操作。
每次操作,我们带着一个“扣除100元”的动作来访问这个帐号,根据传入的扣款来对帐户进行操作的业务代码如下:
AccountService.java
@Transactional(rollbackFor = Exception.class)
public ResponseBean updateBalance(AccountVO accountVO) throws Exception {
AccountVO resultAccount = new AccountVO();
try {
resultAccount = accountDao.selectBalance(accountVO);
if (resultAccount.getBalance() > 0) {// 如果余额为0,直接return出102
if (accountVO.getTransfMoney() <= resultAccount.getBalance()) {// 如果扣款>余额直接return 101,扣款失败
AccountVO updatedAccount = new AccountVO();
int balance = resultAccount.getBalance() - accountVO.getTransfMoney();
updatedAccount.setBalance(balance);
updatedAccount.setUid(accountVO.getUid());
accountDao.updateBalance(updatedAccount);
AccountVO returnAccount = accountDao.selectBalance(accountVO);
returnAccount.setTransfMoney(accountVO.getTransfMoney());
return new ResponseBean(0, "扣款成功", returnAccount);
} else {
return new ResponseBean(101, "扣款>余额");
}
} else {
return new ResponseBean(101, "余额为0");
}
} catch (Exception e) {
logger.error(">>>>>>updateBalance error: " + e.getMessage(), e);
throw new Exception(">>>>>>updateBalance error: " + e.getMessage(), e);
}
}
相应的mybatis的dao层代码如下
DemoAccountDao.xml
<select id="selectBalance"
parameterType="org.mk.demo.db.vo.AccountVO"
resultMap="accountResultMap">
SELECT uid,
balance
FROM account WHERE uid=#{uid}
</select>
<update id="updateBalance"
parameterType="org.mk.demo.db.vo.AccountVO">
UPDATE account
set balance=#{balance}
WHERE uid=#{uid}
</update>
相应的controller如下
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Resource
private AccountService accountService;
@PostMapping(value = "/account/transfer", produces = "application/json")
@ResponseBody
public ResponseBean transfer(@RequestBody AccountVO account) {
logger.info(">>>>>>uid->" + account.getUid() + " transfMoney->" + account.getTransfMoney());
try {
return accountService.updateBalance(account);
} catch (Exception e) {
logger.error(">>>>>>transf error: " + e.getMessage(), e);
return new ResponseBean(-1, "system error");
}
}
看运行起来后使用30个并发来访问的效果
使用30个线程
运行后竟然有12次扣款成功,每次扣100元,帐户余额为1,000元。
由于我们的数据库层做了如下超额、余额不能<0的基本逻辑判断,因此数据库里的值倒是对了。可是交易流水以及此时在前端用户端如:小程序或者是APP上的显示是错的。
if (resultAccount.getBalance() > 0) {// 如果余额为0,直接return出102
if (accountVO.getTransfMoney() <= resultAccount.getBalance()) {// 如果扣款>余额直接return 101,扣款失败
此时客户一定会产生“客诉”,我明明交易成功了,可是你后台告诉我其实是不成功?
你怎么去和你的客户解释呢?这就是业务不幂等!
为了交易流水、过程以及帐户余额幂等使用传统的“悲观”锁的下场
我们为了实现业务幂等会这么干
- 把jdbc设成setAutoCommit(false);
- 然后每次进入交易都去select 余额for update,如果在select x for update出错了认为没有抢到“锁”;
- 没有抢到“锁”的返回-请求排队中,抢到”锁“的返回-处理成功
- finally块中把setAutoCommit设回true;
对不对?99%的IT开发、传统软件觉得这么干,他们认为理所当然也事实上还都这么干了,然后它造成的后果是什么呢?
我把线程数改成了“并发线程”去跑这个加了锁的代码。
显示结果和数据库里的余额倒是幂等了,3分钟不到,应用已经卡死了。它的平均响应时间越来越慢,最后曾卡死不动状态。
如何又做到幂等又做到性能好呢-CAS及乐观锁的方案出现了
让我们来想一下需求:
在大并发的的互联网场景下需要保证进入支付通道的请求正常扣款,显示和流水以及余额要一致。未进入支付通道的请求直接返回”亲,排队中“。如果帐户余额已经用完了或者单笔交易额>余额,此时对于还在往系统内进入的支付请求直接返回”余额为0“。还要保证系统不卡死,不要保证系统的高并发。
于是我们使用如下的手法。
CAS及乐观锁设计手法
我们对这个原帐户表增加一个字段,叫version_no。version_no一开始全部为0.
交易时:
- 每次select时把for update语句去掉改成直接把version_no取出来带到交易业务方法内;
- 在update时,带入前面select出来的version_no,此时你的update sql会变成这样UPDATE account set balance=balance-#{transfMoney},version_no=version_no+1 WHERE uid=#{uid} and version_no=#{versionNo}。只要前一个version_no和本次带入的version_no不一致就代表产生了“竞争”,竞争就要“排除掉”,这相当于利用了数据库的特殊强制把并发的请求在DB内部改成了“串行”方式以保证业务的幂等;
- 根据update的useAffectedRows的返回即mysql的连接必须加上useAffectedRows=true的参数。如果update语句返回=1,代表进入支付通道并扣款成功。如果update语句返回=0,代表进入支付通道失败,返回:亲,排队中;
来看演示代码
AccountService
/**
* 0代表购物成功 101-代表购买的款项>帐户余额,不能购买 102-代表帐户余额为0,不能购买,103-代表动作太快了
*/
@Transactional(rollbackFor = Exception.class)
public ResponseBean updateBalanceWithOptimiLock(AccountVO accountVO) throws Exception {
AccountVO resultAccount = new AccountVO();
try {
resultAccount = accountDao.selectBalance(accountVO);
if (resultAccount.getBalance() > 0) {// 如果余额为0,直接return出102
if (accountVO.getTransfMoney() <= resultAccount.getBalance()) {// 如果扣款>余额直接return 101,扣款失败
// int balance = resultAccount.getBalance() - accountVO.getTransfMoney();
resultAccount.setTransfMoney(accountVO.getTransfMoney());
int affectedRows = accountDao.updateBalanceWithOptimiLock(resultAccount);
if (affectedRows > 0) {
logger.info(
">>>>>>uid->" + resultAccount.getUid() + " transfMoney->" + resultAccount.getTransfMoney()
+ " versionNo->" + resultAccount.getVersionNo() + " affectedRows->" + affectedRows);
AccountVO returnAccount = accountDao.selectBalance(accountVO);
returnAccount.setTransfMoney(accountVO.getTransfMoney());
return new ResponseBean(0, "扣款成功", returnAccount);
} else {
return new ResponseBean(103, "你的动作太快了,请稍侯");
}
} else {
return new ResponseBean(101, "扣款>余额");
}
} else {
return new ResponseBean(101, "余额为0");
}
} catch (Exception e) {
logger.error(">>>>>>updateBalance error: " + e.getMessage(), e);
throw new Exception(">>>>>>updateBalance error: " + e.getMessage(), e);
}
}
此处的“你的动作太快了,请稍侯“就是=“亲,排队中”。
然后对应的dao改造如下
<select id="selectBalance"
parameterType="org.mk.demo.db.vo.AccountVO"
resultMap="accountResultMap">
SELECT uid,
balance,
version_no
FROM account WHERE uid=#{uid}
</select>
<update id="updateBalanceWithOptimiLock"
parameterType="org.mk.demo.db.vo.AccountVO">
UPDATE account
set
balance=balance-#{transfMoney},version_no=version_no+1
WHERE uid=#{uid}
and
version_no=#{versionNo}
</update>
然后我们来看一下这个操作在并发的情况下效果如何。
30个并发下的效果演示
设30个线程
运行后效果如下
看,只有2条请求进入了正常扣款,其它都显示为
去数据库里看一下几户余额
看,业务幂等了。
此时我们拿同样的上面的“并发线程”运行3分钟去测试它
客户端返回10条扣款请求成功,其余不是“的动作太快了,请稍侯“就是”余额为0或者是扣款>余额“,整个并发的过程共产生了9,300个请求。全测试过程顺利完成。
再来看这个系统的性能
请注意以上第一个截图的”吞吐量“和后面的average response指标。这个系统的性能是完全可以适合互联网级别应用的。
对比使用Redis锁我们该怎么做呢
我们使用redission组件提供的“锁”。其实我们这把锁叫:分布式自动超时、自动续约锁,这把锁的好处在于:
- 永远是一把正向锁,每次要锁时会先探一下前面有没有锁了?如果有锁了我就不来锁你(新进程新指令就不会运行);
- 如果前面没有锁(进程在运行)我再来锁你然后运行我的进程或者是指令;
- 前面一把锁如果碰到任何意外(包括了暴力挂机、杀进程),该锁也不会死在内存里而是在30秒后自动释放;
- 如果被锁住的数据处理时间假设需要60分钟已经远超了锁的时间30秒,那么该锁会在超时前10秒启动一个watchdog进程自动去向后台锁服务器申请+30秒时间以不断贴合着数据处理完的所用时间;
先来看pom.xml文件
<dependencies>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery
</artifactId>
</dependency>
<!-- redis must -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- jedis must -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<!-- redission must -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>${redission.version}</version>
<!-- <exclusions> <exclusion> <groupId>org.redisson</groupId> <artifactId>redisson-spring-data-23</artifactId>
</exclusion> </exclusions> -->
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-data-21</artifactId>
<version>${redission.version}</version>
</dependency>
</dependencies>
这边的redission.version我们用的版本号如下(切记),这个版本不会有redission在spring boot 工程启动时,时不时抛一个redission连接错误的bug。
<redission.version>3.16.1</redission.version>
而我们的spring boot的version如下
<spring-boot.version>2.3.1.RELEASE</spring-boot.version>
redission与spring boot的版本(version)必须完全对应,否则项目都启动不起来或者启动报错
RedissonProperties.java
package org.mk.demo.db.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
/**
*
* RedissonProperties
*
*
* Feb 20, 2021 1:56:09 PM
*
* @version 1.0.0
*
*/
@Configuration
@Component
public class RedissonProperties {
@Value("${spring.redis.timeout}")
private int timeout;
@Value("${spring.redis.password}")
private String password;
@Value("${spring.redis.database:0}")
private int database = 0;
@Value("${spring.redis.lettuce.pool.max-active:8}")
private int connectionPoolSize = 64;
@Value("${spring.redis.lettuce.pool.min-idle:0}")
private int connectionMinimumIdleSize = 10;
@Value("${spring.redis.lettuce.pool.max-active:8}")
private int slaveConnectionPoolSize = 250;
@Value("${spring.redis.lettuce.pool.max-active:8}")
private int masterConnectionPoolSize = 250;
@Value("${spring.redis.redisson.nodes}")
private String[] sentinelAddresses;
@Value("${spring.redis.sentinel.master}")
private String masterName;
public int getTimeout() {
return timeout;
}
public void setTimeout(int timeout) {
this.timeout = timeout;
}
public int getSlaveConnectionPoolSize() {
return slaveConnectionPoolSize;
}
public void setSlaveConnectionPoolSize(int slaveConnectionPoolSize) {
this.slaveConnectionPoolSize = slaveConnectionPoolSize;
}
public int getMasterConnectionPoolSize() {
return masterConnectionPoolSize;
}
public void setMasterConnectionPoolSize(int masterConnectionPoolSize) {
this.masterConnectionPoolSize = masterConnectionPoolSize;
}
public String[] getSentinelAddresses() {
return sentinelAddresses;
}
public void setSentinelAddresses(String sentinelAddresses) {
this.sentinelAddresses = sentinelAddresses.split(",");
}
public String getMasterName() {
return masterName;
}
public void setMasterName(String masterName) {
this.masterName = masterName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public int getConnectionPoolSize() {
return connectionPoolSize;
}
public void setConnectionPoolSize(int connectionPoolSize) {
this.connectionPoolSize = connectionPoolSize;
}
public int getConnectionMinimumIdleSize() {
return connectionMinimumIdleSize;
}
public void setConnectionMinimumIdleSize(int connectionMinimumIdleSize) {
this.connectionMinimumIdleSize = connectionMinimumIdleSize;
}
public int getDatabase() {
return database;
}
public void setDatabase(int database) {
this.database = database;
}
public void setSentinelAddresses(String[] sentinelAddresses) {
this.sentinelAddresses = sentinelAddresses;
}
}
RedisSentinelConfig.java文件-自动装配类
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import redis.clients.jedis.HostAndPort;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.redisson.config.SentinelServersConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.*;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.util.LinkedHashSet;
import java.util.Set;
import javax.annotation.Resource;
@Configuration
@EnableCaching
@Component
public class RedisSentinelConfig {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Resource
private RedissonProperties redssionProperties;
@Value("${spring.redis.nodes:localhost:7001}")
private String nodes;
@Value("${spring.redis.max-redirects:3}")
private Integer maxRedirects;
@Value("${spring.redis.password}")
private String password;
@Value("${spring.redis.database:0}")
private Integer database;
@Value("${spring.redis.timeout}")
private int timeout;
@Value("${spring.redis.sentinel.nodes}")
private String sentinel;
@Value("${spring.redis.lettuce.pool.max-active:8}")
private Integer maxActive;
@Value("${spring.redis.lettuce.pool.max-idle:8}")
private Integer maxIdle;
@Value("${spring.redis.lettuce.pool.max-wait:-1}")
private Long maxWait;
@Value("${spring.redis.lettuce.pool.min-idle:0}")
private Integer minIdle;
@Value("${spring.redis.sentinel.master}")
private String master;
@Value("${spring.redis.switchFlag}")
private String switchFlag;
@Value("${spring.redis.lettuce.pool.shutdown-timeout}")
private Integer shutdown;
@Value("${spring.redis.lettuce.pool.timeBetweenEvictionRunsMillis}")
private long timeBetweenEvictionRunsMillis;
public String getSwitchFlag() {
return switchFlag;
}
/**
* 连接池配置信息
*
* @return
*/
@Bean
public LettucePoolingClientConfiguration getPoolConfig() {
GenericObjectPoolConfig config = new GenericObjectPoolConfig();
config.setMaxTotal(maxActive);
config.setMaxWaitMillis(maxWait);
config.setMaxIdle(maxIdle);
config.setMinIdle(minIdle);
config.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
LettucePoolingClientConfiguration pool = LettucePoolingClientConfiguration.builder().poolConfig(config)
.commandTimeout(Duration.ofMillis(timeout)).shutdownTimeout(Duration.ofMillis(shutdown)).build();
return pool;
}
/**
* 配置 Redis Cluster 信息
*/
@Bean
@ConditionalOnMissingBean
public LettuceConnectionFactory lettuceConnectionFactory() {
LettuceConnectionFactory factory = null;
String[] split = nodes.split(",");
Set<HostAndPort> nodes = new LinkedHashSet<>();
for (int i = 0; i < split.length; i++) {
try {
String[] split1 = split[i].split(":");
nodes.add(new HostAndPort(split1[0], Integer.parseInt(split1[1])));
} catch (Exception e) {
logger.error(">>>>>>出现配置错误!请确认: " + e.getMessage(), e);
throw new RuntimeException(String.format("出现配置错误!请确认node=[%s]是否正确", nodes));
}
}
// 如果是哨兵的模式
if (!StringUtils.isEmpty(sentinel)) {
logger.info(">>>>>>Redis use SentinelConfiguration");
RedisSentinelConfiguration redisSentinelConfiguration = new RedisSentinelConfiguration();
String[] sentinelArray = sentinel.split(",");
for (String s : sentinelArray) {
try {
String[] split1 = s.split(":");
redisSentinelConfiguration.addSentinel(new RedisNode(split1[0], Integer.parseInt(split1[1])));
} catch (Exception e) {
logger.error(">>>>>>出现配置错误!请确认: " + e.getMessage(), e);
throw new RuntimeException(String.format("出现配置错误!请确认node=[%s]是否正确", sentinelArray));
}
}
redisSentinelConfiguration.setMaster(master);
redisSentinelConfiguration.setPassword(password);
factory = new LettuceConnectionFactory(redisSentinelConfiguration, getPoolConfig());
}
// 如果是单个节点 用Standalone模式
else {
if (nodes.size() < 2) {
logger.info(">>>>>>Redis use RedisStandaloneConfiguration");
for (HostAndPort n : nodes) {
RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
if (!StringUtils.isEmpty(password)) {
redisStandaloneConfiguration.setPassword(RedisPassword.of(password));
}
redisStandaloneConfiguration.setPort(n.getPort());
redisStandaloneConfiguration.setHostName(n.getHost());
factory = new LettuceConnectionFactory(redisStandaloneConfiguration, getPoolConfig());
}
} else {
logger.info(">>>>>>Redis use RedisClusterConfiguration");
RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration();
nodes.forEach(n -> {
redisClusterConfiguration.addClusterNode(new RedisNode(n.getHost(), n.getPort()));
});
if (!StringUtils.isEmpty(password)) {
redisClusterConfiguration.setPassword(RedisPassword.of(password));
}
redisClusterConfiguration.setMaxRedirects(maxRedirects);
factory = new LettuceConnectionFactory(redisClusterConfiguration, getPoolConfig());
}
}
return factory;
}
@Bean
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(lettuceConnectionFactory);
Jackson2JsonRedisSerializer jacksonSerial = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper();
// 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
jacksonSerial.setObjectMapper(om);
StringRedisSerializer stringSerial = new StringRedisSerializer();
template.setKeySerializer(stringSerial);
// template.setValueSerializer(stringSerial);
template.setValueSerializer(jacksonSerial);
template.setHashKeySerializer(stringSerial);
template.setHashValueSerializer(jacksonSerial);
template.afterPropertiesSet();
return template;
}
@Bean
RedissonClient redissonSentinel() {
logger.info(">>>>>>redisson address size->" + redssionProperties.getSentinelAddresses().length);
Config config = new Config();
SentinelServersConfig serverConfig =
config.useSentinelServers().addSentinelAddress(redssionProperties.getSentinelAddresses())
.setMasterName(redssionProperties.getMasterName()).setTimeout(redssionProperties.getTimeout())
.setMasterConnectionPoolSize(redssionProperties.getMasterConnectionPoolSize())
.setSlaveConnectionPoolSize(redssionProperties.getSlaveConnectionPoolSize());
if (StringUtils.isNotBlank(redssionProperties.getPassword())) {
serverConfig.setPassword(redssionProperties.getPassword());
}
return Redisson.create(config);
}
}
相应的yml文件内的配置
mysql:
datasource:
common:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.jdbc.Driver
minIdle: 25
initialSize: 5
maxActive: 25
maxWait: 5000
testOnBorrow: false
testOnReturn: false
testWhileIdle: true
validationQuery: select 1
timeBetweenEvictionRunsMillis: 300000
ConnectionErrorRetryAttempts: 3
NotFullTimeoutRetryCount: 3
minEvictableIdleTimeMillis: 60000
maxEvictableIdleTimeMillis: 300000
keepAliveBetweenTimeMillis: 480000
keepalive: true
master: #master db
url: jdbc:mysql://localhost:3306/demo_pay?useUnicode=true&characterEncoding=utf-8&useSSL=false&useAffectedRows=true&autoReconnect=true
username: root
password: 111111
slaver: #slaver db
url: jdbc:mysql://localhost:3307/demo_pay?useUnicode=true&characterEncoding=utf-8&useSSL=false&useAffectedRows=true&autoReconnect=true
username: root
password: 111111
server:
port: 9084
tomcat:
max-http-post-size: -1
#最小线程数
min-spare-threads: 150
#最大线程数
max-threads: 500
#最大链接数
max-connections: 1000
#最大等待队列长度
accept-count: 500
logging:
config: classpath:log4j2.xml
spring:
application:
name: db-demo
servlet:
multipart:
max-file-size: 10MB
max-request-size: 10MB
redis:
password: 111111
nodes: localhost:7001
redisson:
#nodes: redis://192.168.2.106:27001,redis://192.168.2.106:27002,redis://192.168.2.106:27003
nodes: redis://localhost:27001,redis://localhost:27002,redis://localhost:27003
sentinel:
#nodes:
#master:
#nodes: 192.168.2.106:27001,192.168.2.106:27002,192.168.2.106:27003
nodes: localhost:27001,localhost:27002,localhost:27003
master: master1
database: 0
switchFlag: 1
lettuce:
pool:
max-active: 50
max-wait: 10000
max-idle: 10
min-idl: 5
shutdown-timeout: 2000
timeBetweenEvictionRunsMillis: 5000
timeout: 5000
业务代码内的使用AccountService类
@Transactional(rollbackFor = Exception.class)
public ResponseBean updateBalanceWithRedissonLock(AccountVO accountVO) throws Exception {
AccountVO resultAccount = new AccountVO();
RLock lock = redissonSentinel.getLock(Constants.TRANSF_MONEY_LOCK);
try {
boolean islock = lock.tryLock(0, TimeUnit.SECONDS);
if (!islock) {
return new ResponseBean(103, "你的动作太快了,请稍侯");
} else {
resultAccount = accountDao.selectBalance(accountVO);
if (resultAccount.getBalance() > 0) {// 如果余额为0,直接return出102
if (accountVO.getTransfMoney() <= resultAccount.getBalance()) {// 如果扣款>余额直接return 101,扣款失败
int balance = resultAccount.getBalance() - accountVO.getTransfMoney();
resultAccount.setTransfMoney(accountVO.getTransfMoney());
// resultAccount.setBalance(balance);
int affectedRows = accountDao.updateBalanceWithRedissonLock(resultAccount);
AccountVO returnAccount = accountDao.selectBalance(accountVO);
logger.info(">>>>>>uid->" + resultAccount.getUid() + " transfMoney->"
+ resultAccount.getTransfMoney() + " current balance->" + returnAccount.getBalance()
+ " versionNo->" + returnAccount.getVersionNo() + " affectedRows->" + affectedRows);
returnAccount.setTransfMoney(accountVO.getTransfMoney());
return new ResponseBean(0, "扣款成功", returnAccount);
} else {
return new ResponseBean(101, "扣款>余额");
}
} else {
return new ResponseBean(102, "余额为0");
}
}
} catch (Exception e) {
logger.error(">>>>>>updateBalance error: " + e.getMessage(), e);
throw new Exception(">>>>>>updateBalance error: " + e.getMessage(), e);
} finally {
try {
lock.unlock();
} catch (Exception e) {
}
}
}
此处重要的信息为:boolean islock = lock.tryLock(0, TimeUnit.SECONDS); 一定要这样写redission锁才会变成“自动续约锁”机制,即tryLock()方法中的第一个参数为0,即你的事务没有运行完毕,redis里的锁会在你的事务还没有结束时每次自动加10秒、加10秒,直到你的事务做完。即使你的redis或者是应用服务挂了,它也会过10-30秒左右自动释放,永不会锁死。
来看运行起来的效果
同样我们使用“并发线程”,3分钟压测 ,它将产生9,300个请求。
运行后效果如下
客户端返回10条扣款请求成功,其余不是“的动作太快了,请稍侯“就是”余额为0或者是扣款>余额“,整个并发的过程共产生了9,300个请求。全测试过程顺利完成。
这边需要多说一下,由于本人的开发用电脑用的硬盘的IOPS有24,000转,因此在本人开发电脑上redis自动续约锁和使用数据的cas-乐观锁性能上相差比较不大(没错,我自己用的开发电脑确实高于大多企业的服务器性能)。实际在真实的生产服务器上这两个方案的区别是使用redis自动续约锁的性能比使用数据库乐观锁要高出百分之10几。
总结一下几种做法的区别和使用场景
业务场景性能业务幂等会不会卡死服务器选择方案时的考虑因素不用锁做并发抢券、扣款类高无不会考虑都不会考虑用悲观锁极低有极易卡死系统只有线下柜面POS机或者是银行的高柜业务使用的黑屏POS机使用这种方案用CAS-乐观锁高有不会如果系统无法引入redis和redission组件,且改造业务方法太复杂那么可以考虑使用CAS-乐观锁方案,它造成的系统改动不大用Redis自动续约锁高有不会如果系统是天然的spring boot2.X且改造成本不高,强烈推荐使用此方案
版权归原作者 TGITCIC 所有, 如有侵权,请联系我们删除。