JAVA实现转账接口

该博客介绍了如何使用Java和MySQL实现一个转账接口,确保资金处理时不出错且在多点部署环境下处理并发问题。通过使用@Transactional注解保证事务,并利用MySQL的FOR UPDATE实现行级锁,避免并发错误。在100QPS*10S的JMeter压测下,单用户点对点转账吞吐量达到约50QPS。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

问题:

尝试用java实现一个转账接口,根据传入的转出账户、转入账户和金额,修改表中账户的金额。
 

分析问题:

1. 尝试使用MySQL数据库存储数据

2. 确保在资金处理时转出账户的余额不会透支

3. 考虑转账的数据不影响其他人的业务

4. 考虑多点部署的并发问题及系统吞吐量问题

     a>并发下保证数据不可以出错(这是转钱哦,搞错了赔不起哦。。。。。。)

     b>不能用java锁,synchronized,ReentrantLock等单点锁(多点部署问题,影响整体吞吐量)

     c> 不能用redis等分布式锁(影响整体吞吐量)

最终代码:

项目目录:

maven配置

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.4</version>
        <relativePath/>
    </parent>
    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.2</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.10</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.10</version>
        </dependency>
    </dependencies>
</project>

yaml

server:
  port: 9088

spring:
  application:
    name: demo
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/test?characterEncoding=UTF-8&useSSL=true
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis:
  type-aliases-package: com.example.demo.model
  mapper-locations: classpath:mybatis/mapper/*.xml

 DataSourceConfig 

package com.example.demo.config;

import com.alibaba.druid.pool.DruidDataSource;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Configuration
public class DataSourceConfig {

    @Value("${spring.datasource.url}")
    private String url;

    @Value("${spring.datasource.driver-class-name}")
    private String driverClassName;

    @Value("${spring.datasource.username}")
    private String userName;

    @Value("${spring.datasource.password}")
    private String password;

    @Bean
    public DataSource dataSource() {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setUrl(url);
        dataSource.setUsername(userName);
        dataSource.setPassword(password);
        dataSource.setDriverClassName(driverClassName);
        return dataSource;
    }

}

MoneyController

package com.example.demo.controller;

import com.example.demo.model.vo.TrandingVo;
import com.example.demo.service.MoneyService;
import com.example.demo.utils.RestResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import lombok.extern.slf4j.Slf4j;

/**
 * @Author: yipeng
 * @Date: 2021/6/18 20:58
 */
@Slf4j
@RestController
@RequestMapping("/money")
public class MoneyController {

    @Autowired
    private MoneyService moneyService;

    @PostMapping("/trading")
    public RestResponse<Boolean> trading(@RequestBody TrandingVo trandingVo) {
        try {
            return RestResponse.buildSuccess(moneyService.trading(trandingVo));
        } catch (Exception e) {
            log.error("trading error: " + e.getMessage());
        }
        return RestResponse.buildSuccess(false);
    }

}

MoneyMapper

package com.example.demo.mapper;

import com.example.demo.model.po.MoneyPo;

import org.apache.ibatis.annotations.Mapper;

/**
 * @Author: yipeng
 * @Date: 2021/6/18 20:49
 */
@Mapper
public interface MoneyMapper {

    MoneyPo getOneByUserName(String userName);


    void update(MoneyPo moneyPo);
}

MoneyPo

package com.example.demo.model.po;

import java.io.Serializable;

import lombok.Data;

/**
 * @Author: yipeng
 * @Date: 2021/6/18 20:49
 */
@Data
public class MoneyPo implements Serializable {


    private static final long serialVersionUID = -8851981653703733214L;
    
    private Long id;

    private String userName;

    private Long money;

}

TrandingVo

package com.example.demo.model.vo;

import lombok.Data;

/**
 * @Author: yipeng
 * @Date: 2021/6/18 21:08
 */
@Data
public class TrandingVo {

    private String fromUserName;

    private String toUserName;

    private Long money;

}

MoneyService

package com.example.demo.service;

import com.example.demo.model.vo.TrandingVo;

/**
 * @Author: yipeng
 * @Date: 2021/6/18 21:04
 */
public interface MoneyService {

    boolean trading(TrandingVo trandingVo);
}

MoneyServiceImpl

package com.example.demo.service.impl;

import com.example.demo.mapper.MoneyMapper;
import com.example.demo.model.po.MoneyPo;
import com.example.demo.model.vo.TrandingVo;
import com.example.demo.service.MoneyService;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import lombok.extern.slf4j.Slf4j;

/**
 * @Author: yipeng
 * @Date: 2021/6/18 21:09
 */
@Service
@Slf4j
public class MoneyServiceImpl implements MoneyService {

    @Autowired
    private MoneyMapper moneyMapper;


    @Override
    @Transactional
    public boolean trading(TrandingVo trandingVo) {
        MoneyPo fromUser = moneyMapper.getOneByUserName(trandingVo.getFromUserName());
        MoneyPo toUser = moneyMapper.getOneByUserName(trandingVo.getToUserName());
        if (fromUser.getMoney() > trandingVo.getMoney()) {
            fromUser.setMoney(fromUser.getMoney() - trandingVo.getMoney());
            toUser.setMoney(toUser.getMoney() + trandingVo.getMoney());
            moneyMapper.update(fromUser);
            moneyMapper.update(toUser);
            return true;
        }
        log.error("trading error" + Thread.currentThread().getName());
        return false;
    }
}

RestResponse<T>

package com.example.demo.utils;

import org.springframework.http.HttpStatus;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;

@Data
@Slf4j
public class RestResponse<T> {

    private int code;
    private String message;
    private T data;

    private RestResponse() {

    }

    public static RestResponse build(HttpStatus status) {
        RestResponse result = new RestResponse();
        result.code = status.value();
        result.message = status.getReasonPhrase();
        return result;
    }
    
    public static <T> RestResponse<T> buildSuccess(T t) {
        RestResponse resp = build(HttpStatus.OK);
        resp.setData(t);
        return resp;
    }
}


DemoApplication

package com.example.demo;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@MapperScan("com.example.demo.mapper")
@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {

        SpringApplication.run(DemoApplication.class, args);
    }

}

MoneyMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.example.demo.mapper.MoneyMapper" >
    <resultMap id="BaseResultMap" type="com.example.demo.model.po.MoneyPo" >
        <id column="id" property="id" jdbcType="BIGINT" />
        <result column="userName" property="userName" jdbcType="VARCHAR" />
        <result column="money" property="money" jdbcType="BIGINT" />
    </resultMap>

    <sql id="Base_Column_List" >
        id, userName, money
    </sql>

    <select id="getOneByUserName" parameterType="java.lang.String" resultMap="BaseResultMap" >
        SELECT
        <include refid="Base_Column_List" />
        FROM money
        WHERE userName = #{userName} FOR UPDATE
    </select>

    <update id="update" parameterType="com.example.demo.model.po.MoneyPo" >
        UPDATE
        money
        SET
        userName = #{userName}, money = #{money}
        WHERE
        id = #{id}
    </update>


</mapper>

money.sql

CREATE DATABASE IF NOT EXISTS test;
use test;
DROP TABLE IF EXISTS `money`;
CREATE TABLE `money` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `userName` varchar(32) DEFAULT NULL COMMENT '用户名',
  `money` bigint(20) DEFAULT 0 COMMENT '账户金额',
  PRIMARY KEY (`id`),
  UNIQUE KEY `userName` (`userName`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

insert into money set userName = 'Tom', money = 10000;
insert into money set userName = 'John', money = 10000;

具体分析

实现过程:

1. @Transactional注解

2. 行级排他锁(FOR UPDATE)

JMeter压测一下(100QPS * 10S)

压测数据:

curl -X POST http://127.0.0.1:9088/money/trading -H 'Content-Type: application/json' -d '{"fromUserName":"Tom","toUserName":"John","money":10}'

压测前数据表:

压测后数据表:

压测数据:

结论:

单用户点对点转账吞吐量50QPS左右,能够满足正常业务需求。

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

益朋

看官老爷们的打赏是我前进的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值