Spring Boot 中直接操作 hbase 修改账户余额,实现行级锁(类似于版本号控制)

应用场景

近期开发中遇到 直接修改hbase数据 ,用Phoenix 查询出来的数据  类型不一致的 问题。

因修改的是用户的账户余额,涉及到钱的问题都不是小问题。初次想法使用tephra事务,但官网说目前还是 Beta版本的,感兴趣的可以研究研究。

所以考虑直接操作hbase数据库,但是如果用Phoenix查询的话 类型会不一致,

比如 :Phoenix 中的int型的 1 ,在hbase中是1'  。导致读取出来的数据不一致。


解决方案 

框架 :maven + Spring Boot + Mybatis + Phoenix + hbase

软件环境:eclipse + JDK8

直接操作hbase ,用Phoenix查询的时候 需要转型 ,代码如下:

1、pom文件 引入依赖




                        
   org.springframework.data
   spring-data-hadoop
   2.5.0.RELEASE

                        
org.apache.phoenix
phoenix-core
4.13.1-HBase-1.3


org.slf4j
slf4j-log4j12



2、hbase-dev.properties 开发环境中 添加 hbase 配置

#HBase
spring.data.hbase.zkQuorum=192.168.110.97:2181,192.168.110.98:2181,192.168.110.99:2181
spring.data.hbase.zkBasePath=/hbase
spring.data.hbase.rootDir=file:///opt/hbase/hbase-1.3.2


3、创建 HbaseProperties文件 对应 配置文件配置,代码如下:

import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "spring.data.hbase")
@Getter
@Setter
public class HbaseProperties {
    // Addresses of all registered ZK servers.
    private String zkQuorum;


    // Location of HBase home directory
    private String rootDir;


    // Root node of this cluster in ZK.
    private String zkBasePath;


}

4、创建HbaseConfig文件 声明一个 bean ,代码如下:

import lombok.Getter;

import org.apache.hadoop.hbase.HBaseConfiguration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.data.hadoop.hbase.HbaseTemplate;


@Configuration
@PropertySource("classpath:hbase-${spring.profiles.active}.properties")
@EnableConfigurationProperties(HbaseProperties.class)
@Getter
public class HbaseConfig {

@Autowired
    private HbaseProperties hbaseProperties;

    @Bean(name = "hbaseTemplate")
    public HbaseTemplate hbaseTemplate() {
        org.apache.hadoop.conf.Configuration configuration = HBaseConfiguration.create();
        configuration.set("hbase.zookeeper.quorum", this.hbaseProperties.getZkQuorum());
        configuration.set("hbase.rootdir", this.hbaseProperties.getRootDir());
        configuration.set("zookeeper.znode.parent", this.hbaseProperties.getZkBasePath());
        return new HbaseTemplate(configuration);
    }

}


5、创建 BalancePayService文件应用 ,代码如下:

import java.io.IOException;
import java.util.Date;

import javax.annotation.Resource;

import jline.internal.Log;

import org.apache.commons.lang3.time.DateFormatUtils;
import org.apache.hadoop.hbase.client.Get;
import org.apache.hadoop.hbase.client.HTable;
import org.apache.hadoop.hbase.client.Put;
import org.apache.hadoop.hbase.util.Bytes;
import org.springframework.data.hadoop.hbase.HbaseTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import cn.harvetech.giant.trip.common.bean.po.Organization;
import cn.harvetech.giant.trip.common.bean.po.TripOrder;
import cn.harvetech.giant.trip.common.bean.po.TripOrderRefund;
import cn.harvetech.giant.trip.common.bean.vo.Result;
import cn.harvetech.giant.trip.common.enums.OrderStatusEnum;
import cn.harvetech.giant.trip.common.mapper.OrganizationMapper;
import cn.harvetech.giant.trip.common.service.TradeService;
import cn.harvetech.giant.trip.common.service.TripOrderRefundService;


/**
 * 余额支付 和退款
 */
@Service
public class BalancePayService {

@Resource
private OrganizationService organizationService;
@Resource
private TripOrderService tripOrderService;
@Resource
private TradeService tradeService;
    @Resource
    private TripOrderRefundService tripOrderRefundService;
@Resource

private OrganizationMapper organizationMapper;


@Resource(name = "hbaseTemplate")
    private HbaseTemplate hbaseTemplate;

private final static String TRIP_ORGANIZATION = "trip_organization";
private final static String family = "prop";
private final static String qualifier = "balance";



/**
* 余额支付
* @param orderId
* @return 
* 时间: 2018年4月17日 上午10:36:03
* 描述: 采用 hbase 原生的 checkAndPut实现行级锁
*/
@Transactional
public Result toPay(String orderId){
String tradeStatus = "GK_PAYING";//默认集客余额支付中
TripOrder order = tripOrderService.selectByPrimaryKey(orderId);

if(order.getOrderStatus() >= OrderStatusEnum.ORDERSTATUS_200.getCode() 
&& order.getOrderStatus() <= OrderStatusEnum.ORDERSTATUS_801.getCode() ){
return Result.fail("订单已支付");
}

Organization organization = organizationService.selectByRowKey(order.getOrganizationId());
Long balance = organization.getBalance();//当前账户余额
Long payAmount = order.getPtPayAmount();
Long restBalance = balance.longValue() - payAmount.longValue();

if(restBalance.longValue() < 0){
return Result.fail("余额不足,请选择其他支付方式");
}

boolean result = false;
try {
result = changeBalance(order.getOrganizationId(), balance, restBalance, 1, payAmount);
} catch (Exception e) {
tradeStatus = "GK_PAY_FAIL";
Log.error(e.getMessage());
}

if(result == true){// 支付成功 == 修改数据成功
tradeStatus = "GK_PAY_SUCCESS";
}else if(result == false){
tradeStatus = "GK_PAY_FAIL";
}

// 记录支付后的信息
tripOrderService.savePayInfo(order.getOrderNo(), order.getOrderNo(), tradeStatus, order);
return Result.withBuild().data(tradeStatus).msg("data中的值:GK_PAY_SUCCESS(支付成功) | GK_PAY_FAIL(支付失败) | GK_PAYING(支付中)").build();

}

/**
* 余额退款
* @param orderId 订单ID
* @param refundAmount 退款金额
* @param refundNo 退款单号
* @return 
* 时间: 2018年4月12日 下午3:43:56
* 描述: 采用 hbase 原生的 checkAndPut实现行级锁
*/
@Transactional
public Result balanceRefund(String orderId, Long refundAmount,  String refundNo, String reason){
TripOrder order = tripOrderService.selectByPrimaryKey(orderId);
if(null == order){
return Result.fail("查不到订单信息");
}
String refundStatus = "GK_REFUNDING";//默认退款中

Organization organization = organizationMapper.selectByRowKey(order.getOrganizationId());
if(null == organization){
return Result.fail("查不到机构信息");
}

Long balance = organization.getBalance();//当前账户余额
Long restBalance = balance.longValue() + refundAmount.longValue();

boolean result = false;
try {
result = changeBalance(order.getOrganizationId(), balance, restBalance, 2, refundAmount);
} catch (Exception e) {
refundStatus = "GK_REFUND_FAIL";
Log.error(e.getMessage());
}

if(result == true){
refundStatus = "GK_REFUND_SUCCESS";
}else{
refundStatus = "GK_REFUND_FAIL";
}

// 实现系统业务逻辑....


return Result.success(refundStatus,"GK_REFUND_SUCCESS:退款成功 | GK_REFUND_FAIL:退款失败 | GK_REFUNDING:退款中");
}


/**
* 修改余额,直接递归 调用hbase 
* @param rowkey         rowkey机构表主键
* @param oldAmount 机构当前余额
* @param newAmount 修改后的余额
* @param type 操作类型 :1:支付/扣款,2:退款/充值
* @param price 金额(支付/扣款或退款/充值 的金额)
* @return 
* 时间: 2018年5月4日 上午10:55:00
* 描述:
* @throws Exception 
*/
public boolean changeBalance(String rowkey, Long oldAmount, Long newAmount, Integer type, Long price) throws Exception{

boolean result = checkAndPut(rowkey, oldAmount, newAmount);
int count = 1;

if(result == false){//修改失败(余额发生改变),则查询一次余额,然后调用本方法执行递归

while (count <= 10){//限制递归次数

count ++;
Long balance = get(rowkey);//当前余额

if(type == 1){// 支付或扣款
newAmount = balance.longValue() - price.longValue();
}else{//退款或充值
newAmount = balance.longValue() + price.longValue();
}

if(balance.longValue() <= 0 || newAmount.longValue() < 0){// 如果当前余额小于等于零 或  当前余额不足支付,则  直接返回支付失败
return false;
}

result = changeBalance(rowkey, balance, newAmount, type, price);


return false;
}

return true;
}


/**
* 检查并修改余额 --> 直接调用hbase
* @param rowkey         rowkey机构表主键
* @param oldAmount 机构当前余额
* @param newAmount 修改后的余额
* @return
* @throws Exception 
* 时间: 2018年5月3日 上午11:57:20
* 描述:
*/
@SuppressWarnings("deprecation")
public boolean checkAndPut(String rowkey, Long oldAmount, Long newAmount) throws Exception {
// 构造一个put对象,参数就是rowkey
Put put = new Put(Bytes.toBytes(rowkey));
put.add(Bytes.toBytes(family), // 列族
Bytes.toBytes(qualifier), // 列名
longToBytes_phoenix(newAmount) // 值
);

// 插入数据
boolean result = getTable().checkAndPut(Bytes.toBytes(rowkey),
Bytes.toBytes(family), Bytes.toBytes(qualifier),
longToBytes_phoenix(oldAmount), put);

return result;
}

/**
* 查询余额
* @param rowkey rowkey机构表主键
* @return balance 余额字段值
* @throws Exception 
* 时间: 2018年5月4日 上午10:50:42
* 描述:
*/
public Long get(String rowkey) throws Exception{
//构造一个Get对象
Get get = new Get(Bytes.toBytes(rowkey));
//查询数据
org.apache.hadoop.hbase.client.Result r = getTable().get(get);
byte[] data = r.getValue(Bytes.toBytes(family), Bytes.toBytes(qualifier));
Long balance = bytesToLong_phoenix(data);

return balance;
}


/**
* 获取hbase表
* @return 
* 时间: 2018年5月2日 下午6:19:54
* 描述:
* @throws IOException 
*/
private HTable getTable() throws IOException {
return new HTable(hbaseTemplate.getConfiguration(), TRIP_ORGANIZATION);
}

//------------    以下是Phoenix 和 hbase 的 int 和 Long 类型的转换    ----------------------------------------
public int bytesToInt_phoenix(byte[] bytes) {
int n = 0;
for (int i = 0; i < 4; i++) {
n <<= 8;
n ^= bytes[i] & 0xFF;
}
n = n ^ 0x80000000;
return n;
}


public byte[] intToBytes_phoenix(int val) {
val = val ^ 0x80000000;
byte[] b = new byte[4];
for (int i = 3; i > 0; i--) {
b[i] = (byte) val;
val >>= 8;
}
b[0] = (byte) val;
return b;
}


public long bytesToLong_phoenix(byte[] bytes) {
long n = 0;
for (int i = 0; i < 8; i++) {
n <<= 8;
n ^= bytes[i] & 0xFF;
}
n = n ^ 0x8000000000000000l;
return n;
}


public byte[] longToBytes_phoenix(long val) {
val = val ^ 0x8000000000000000l;
byte[] b = new byte[8];
for (int i = 7; i > 0; i--) {
b[i] = (byte) val;
val >>= 8;
}
b[0] = (byte) val;
return b;

}



}


7、测试 :经测可用,并已实施到各个项目中

调研的时候 写过一个1到10的for循环 直接修改hbase表,可以完美实现 行级锁功能,第一个修改的数据返回true ,其余的都是false。

因业务需求 余额支付 除 程序异常或 服务器宕机 外,其余情况全是支付成功。所以,在以上代码中 使用了递归。请参见以上代码。




你可能感兴趣的:(spring,boot,零碎整理)