如何使用互联网技术来设计和制作支付交易系统和抢红包

业务幂等带来的性能上的损耗

交易系统讲究的是:业务幂等,不能多扣不能少扣。

比如说有一个用户他的帐户余额就10,000元,无论它进行了上万、上千次交易,每一笔交易成功的流水的状态、和金额必须要对的起来。说了简单点假设该帐户交易了100次,共计9万元的交易流水,但帐户余额就10,000,你不能给他记成负8万余额吧?那么我们来看这个100次交易流水。其中有30次交易成功,总额是10,000元扣款成功。其余70次都是交易失败。如果30次交易成功,总额是10,000元,然后如果出现了第31次也是交易成功,那么这第31次的交易成功怎么来的?这就是业务不幂等。

演示根据交易额扣款动作

根据业务需求与设计

我们下面可以来演示,不幂等时发生的代码问题

数据库表结构如下:

uid  Balance
2 1000

我们做一个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,扣款失败

此时客户一定会产生“客诉”,我明明交易成功了,可是你后台告诉我其实是不成功?

你怎么去和你的客户解释呢?这就是业务不幂等!

为了交易流水、过程以及帐户余额幂等使用传统的“悲观”锁的下场

我们为了实现业务幂等会这么干

  1. 把jdbc设成setAutoCommit(false);
  2. 然后每次进入交易都去select 余额for update,如果在select x for update出错了认为没有抢到“锁”;
  3. 没有抢到“锁”的返回-请求排队中,抢到”锁“的返回-处理成功
  4. finally块中把setAutoCommit设回true;

对不对?99%的IT开发、传统软件觉得这么干,他们认为理所当然也事实上还都这么干了,然后它造成的后果是什么呢?

我把线程数改成了“并发线程”去跑这个加了锁的代码。

显示结果和数据库里的余额倒是幂等了,3分钟不到,应用已经卡死了。它的平均响应时间越来越慢,最后曾卡死不动状态。

如何又做到幂等又做到性能好呢-CAS及乐观锁的方案出现了

让我们来想一下需求:

在大并发的的互联网场景下需要保证进入支付通道的请求正常扣款,显示和流水以及余额要一致。未进入支付通道的请求直接返回”亲,排队中“。如果帐户余额已经用完了或者单笔交易额>余额,此时对于还在往系统内进入的支付请求直接返回”余额为0“。还要保证系统不卡死,不要保证系统的高并发。

于是我们使用如下的手法。

CAS及乐观锁设计手法

我们对这个原帐户表增加一个字段,叫version_no。version_no一开始全部为0.

交易时:

  1. 每次select时把for update语句去掉改成直接把version_no取出来带到交易业务方法内;
  2. 在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内部改成了“串行”方式以保证业务的幂等;
  3. 根据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组件提供的“锁”。其实我们这把锁叫:分布式自动超时、自动续约锁,这把锁的好处在于:

  1. 永远是一把正向锁,每次要锁时会先探一下前面有没有锁了?如果有锁了我就不来锁你(新进程新指令就不会运行);
  2.  如果前面没有锁(进程在运行)我再来锁你然后运行我的进程或者是指令;
  3. 前面一把锁如果碰到任何意外(包括了暴力挂机、杀进程),该锁也不会死在内存里而是在30秒后自动释放;
  4. 如果被锁住的数据处理时间假设需要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且改造成本不高,强烈推荐使用此方案

 

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码

)">
< <上一篇
下一篇>>