p2p金融项目+投资+超卖现象+乐观锁

目录

前端:

数据库:

过程

第一步:

第二步:

第三步

但是有问题:


前端:

“立即投资”,输入投资金额

对投资金额的要求:非0、非负、非空、100的整数倍

收益金额=投资金额*利率

理财:年利率;借钱:天利率

本项目中:收益率=投资金额*日利率*投资天数

数据库:

账户表

产品表:剩余可投金额

投资记录表

过程

第一步:

p2p-web

BidInfoController
@Controller
public class BidInfoController {

    @Autowired
    private BidInfoService bidInfoService;

    @RequestMapping(value = "/loan/invest")
    public @ResponseBody Object invest(HttpServletRequest request,
                                       @RequestParam (value = "loanId",required = true) Integer loanId,
                                       @RequestParam (value = "bidMoney",required = true) Double bidMoney) {
        Map retMap = new HashMap();

        //从session中获取用户的信息
        User sesionUser = (User) request.getSession().getAttribute(Constants.SESSION_USER);

        //准备请求参数
        Map paramMap = new HashMap();
        //用户标识
        paramMap.put("uid",sesionUser.getId());
        //产品标识
        paramMap.put("loanId",loanId);
        //投资金额
        paramMap.put("bidMoney",bidMoney);
        //手机号码
        paramMap.put("phone",sesionUser.getPhone());


        //用户投资(用户标识,产品标识,投资金额) -> 返回结果ResultObject
        ResultObject resultObject = bidInfoService.invest(paramMap);

        //判断是否成功
        if (StringUtils.equals(Constants.SUCCESS,resultObject.getErrorCode())) {
            retMap.put(Constants.ERROR_MESSAGE,Constants.OK);
        } else {
            retMap.put(Constants.ERROR_MESSAGE,"投资人数过多,请稍后重试...");
            return retMap;
        }


        return retMap;
    }
}

过程:

1、从session中获取用户的信息

2、用户投资(用户标识,产品标识,投资金额) -> 返回结果ResultObject(后面的第二步

3、根据返回结果来 判断是否成功

第二步:

p2p-exterface

BidInfoService.java
/**
     * 用户投资
     * @param paramMap
     * @return
     */
    ResultObject invest(Map paramMap);

第三步

第二步中的具体实现

p2p-dataservice

BidInfoServiceImpl.java

先放一个整体代码

@Override
    public ResultObject invest(Map paramMap) {
        ResultObject resultObject = new ResultObject();
        resultObject.setErrorCode(Constants.SUCCESS);

        //超卖现象:实际销售的数量超过了原有销售数量
        //使用数据库乐观锁解决超卖

        //获取产品的版本号
        LoanInfo loanInfo = loanInfoMapper.selectByPrimaryKey((Integer) paramMap.get("loanId"));
        paramMap.put("version",loanInfo.getVersion());

        //更新产品剩余可投金额
        int updateLeftProductMoneyCount = loanInfoMapper.updateLeftProductMoneyByLoanId(paramMap);

        if (updateLeftProductMoneyCount > 0) {

            //更新帐户可用余额
            int updateFinanceAccountCount = financeAccountMapper.updateFinanceAccountByBid(paramMap);

            if (updateFinanceAccountCount > 0) {
                //新增投资记录
                BidInfo bidInfo = new BidInfo();
                //用户标识
                bidInfo.setUid((Integer) paramMap.get("uid"));
                //产品标识
                bidInfo.setLoanId((Integer) paramMap.get("loanId"));
                //投资金额
                bidInfo.setBidMoney((Double) paramMap.get("bidMoney"));
                //投资时间
                bidInfo.setBidTime(new Date());
                //投资状态
                bidInfo.setBidStatus(1);

                int insertBidCount = bidInfoMapper.insertSelective(bidInfo);

                if (insertBidCount > 0) {

                    //再次查看产品的剩余可投金额是否为0
                    LoanInfo loanDetail = loanInfoMapper.selectByPrimaryKey((Integer) paramMap.get("loanId"));

                    //为0:更新产品的状态及满标时间
                    if (0 == loanDetail.getLeftProductMoney()) {

                        //更新产品的状态及满标时间
                        LoanInfo updateLoanInfo = new LoanInfo();
                        updateLoanInfo.setId(loanDetail.getId());
                        updateLoanInfo.setProductFullTime(new Date());
                        updateLoanInfo.setProductStatus(1);//0未满标,1已满标,2满标且生成收益讲

                        int updateLoanInfoCount = loanInfoMapper.updateByPrimaryKeySelective(updateLoanInfo);

                        if (updateLoanInfoCount < 0) {
                            resultObject.setErrorCode(Constants.FAIL);
                        }
                    }
                    String phone = (String) paramMap.get("phone");

                    //将用户的投资金额存放到redis缓存中
                    redisTemplate.opsForZSet().incrementScore(Constants.INVEST_TOP, phone,(Double) paramMap.get("bidMoney"));



                } else {
                    resultObject.setErrorCode(Constants.FAIL);
                }

            } else {
                resultObject.setErrorCode(Constants.FAIL);
            }


        } else {
            resultObject.setErrorCode(Constants.FAIL);
        }

        return resultObject;
    }

1、获取产品的版本号

LoanInfo loanInfo = loanInfoMapper.selectByPrimaryKey((Integer) paramMap.get("loanId"));
paramMap.put("version",loanInfo.getVersion());

2、更新产品的剩余可投金额(超卖)

int updateLeftProductMoneyCount = loanInfoMapper.updateLeftProductMoneyByLoanId(paramMap);

就是这里出现了超卖现象--用了乐观锁来解决

进入方法updateLeftProductMoneyByLoanId



   update
     b_loan_info
   set
      left_product_money = left_product_money - #{bidMoney},
      version = version+1
   where
       id = #{loanId} and version = #{version} and (left_product_money - #{bidMoney}) >= 0
  • 分析:

超卖现象:实际投资的金额 超过了 原有可以投资的金额

在多线程、高并发时经常发生

  • 解决:使用数据库乐观锁解决超卖现象

在数据库,投资产品信息表中有一个“版本号”,version

后台先获取版本号,再去更新产品剩余可投金额

做了个测试

import org.springframework.context.support.ClassPathXmlApplicationContext;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;


public class Test {

    public static void main(String[] args) {
        //获取spring容器
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");

        //获取指定的bean
        BidInfoService bidInfoService = (BidInfoService) context.getBean("bidInfoServiceImpl");

        //创建一个固定的线程
        ExecutorService executorService = Executors.newFixedThreadPool(100);

        Map paramMap = new HashMap();
        paramMap.put("uid",1);
        paramMap.put("loanId",3);
        paramMap.put("bidMoney",1.0);


        for (int i = 0 ; i < 1000; i++) {
            executorService.submit(new Callable(){
                @Override
                public Object call() throws Exception {
                    return bidInfoService.invest(paramMap);
                }
            });

        }

        executorService.shutdown();



    }
}

过程:

  • 先获取spring容器
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
  • 获取指定bean
BidInfoService bidInfoService = (BidInfoService) context.getBean("bidInfoServiceImpl");
  • 创建一个固定的线程池(大小为100)
ExecutorService executorService = Executors.newFixedThreadPool(100);
  • 投资所需要的信息:这里就相当于是用户1对产品3 投资1块钱(这个过程是多线程并发的)
Map paramMap = new HashMap();
paramMap.put("uid",1);
paramMap.put("loanId",3);
paramMap.put("bidMoney",1.0);
  • 多线程并发操作:调用Callable中的call()方法,里面做invest()操作
for (int i = 0 ; i < 1000; i++) {
      executorService.submit(new Callable(){
           @Override
           public Object call() throws Exception {
               return bidInfoService.invest(paramMap);
           }
        });

}
  • 关闭线程池
executorService.shutdown();

结果就是不用乐观锁,会有问题;用了乐观锁,就没有问题了

3、更新账户可用余额

第2步成功后,可以更新账户可用余额

这里不需要乐观锁

int updateFinanceAccountCount = financeAccountMapper.updateFinanceAccountByBid(paramMap);

进入方法:

updateFinanceAccountByBid(paramMap)

    
        update
          u_finance_account
        set
          available_money = available_money - #{bidMoney}
        where
          uid = #{uid} and (available_money - #{bidMoney}) >= 0
    

4、新增投资记录

//新增投资记录
BidInfo bidInfo = new BidInfo();
//用户标识
bidInfo.setUid((Integer) paramMap.get("uid"));
//产品标识
bidInfo.setLoanId((Integer) paramMap.get("loanId"));
//投资金额
bidInfo.setBidMoney((Double) paramMap.get("bidMoney"));
//投资时间
bidInfo.setBidTime(new Date());
//投资状态
bidInfo.setBidStatus(1);

int insertBidCount = bidInfoMapper.insertSelective(bidInfo);

进入方法:

insertSelective

        
        insert into b_bid_info
        
            
                id,
            
            
                loan_id,
            
            
                uid,
            
            
                bid_money,
            
            
                bid_time,
            
            
                bid_status,
            
        
        
            
                #{id,jdbcType=INTEGER},
            
            
                #{loanId,jdbcType=INTEGER},
            
            
                #{uid,jdbcType=INTEGER},
            
            
                #{bidMoney,jdbcType=DOUBLE},
            
            
                #{bidTime,jdbcType=TIMESTAMP},
            
            
                #{bidStatus,jdbcType=INTEGER},
            
        
    

5、再次查看产品的剩余可投金额

LoanInfo loanDetail = loanInfoMapper.selectByPrimaryKey((Integer) paramMap.get("loanId"));

如果为0:更新产品的状态及满标时间

//更新产品的状态及满标时间
LoanInfo updateLoanInfo = new LoanInfo();
updateLoanInfo.setId(loanDetail.getId());
updateLoanInfo.setProductFullTime(new Date());
updateLoanInfo.setProductStatus(1);//0未满标,1已满标,2满标且生成收益讲

int updateLoanInfoCount = loanInfoMapper.updateByPrimaryKeySelective(updateLoanInfo);

if (updateLoanInfoCount < 0) {
    resultObject.setErrorCode(Constants.FAIL);
}

6、将用户的投资金额存放到redis缓存中

redisTemplate.opsForZSet().incrementScore(Constants.INVEST_TOP, phone,(Double) paramMap.get("bidMoney"));

 

如果中间有那个过程失败了;就会返回:

resultObject.setErrorCode(Constants.FAIL);

最后成功返回:

return resultObject;

 

但是有问题:

1、怎么保证事务?

2、超卖现象还可以怎么改进呢?

3、如果版本号不一致, 就是会返回用户:稍后再试。。。。

p2p金融项目+投资+超卖现象+乐观锁_第1张图片

那同时10个人操作,9个人会失败

思考:可不可以写在一个while循环里呢?(读版本号,再尝试更改剩余可投金额)?

不行,while 会一直连库 占据数据库链接,数据库连接池 可是很宝贵的资源

思考:这个可以放弃版本号,预减一操作就好、

思考:redis、消息队列

 

 

 

 

你可能感兴趣的:(java面试,p2p金融项目)