应用场景
近期开发中遇到 直接修改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。
因业务需求 余额支付 除 程序异常或 服务器宕机 外,其余情况全是支付成功。所以,在以上代码中 使用了递归。请参见以上代码。