崛起于Springboot2.X之区块链单节点mysql实现交易记录(34)
简介:随着今年越来越多的区块链项目更多的落地,我们自己公司也想准备着手区块链方向发展,所以领导给我们技术部要求也要落地一个区块链的项目,其实之前一两个月也接触了Hyperledger这个框架,当然我自己的博客分类中也有对Hyperledger框架的大概集成以及搭建,但是我接触几个月的时间里,懵懂,不知道作用是什么?读过好多类似这个框架有关的书,但直觉告诉我,他们对于现阶段的我,没用!包括《区块链技术进阶与实战》《区块链开发实战 hyperledger fabric 关键技术与案例分析》《深度探索区块链 Hyperledger技术与应用》《Hyperledger Fabric 开发实战》,最后一本只是看了看,没有具体细看,不过这书写的都是大同小异,雷同之处太多。然后我就在想,这个框架学习来要很久吧,毕竟自己要搞这个,我也是个java,没有接触过这方面的,即便上一个公司的区块链项目已经落地,但是技术没有接触过,只不过当时用的是python,那个产品是InsurBox,不过那也是一个类似于挖矿的小游戏,但是完全跟公司业务不同。
需求:公司是做一个类似基金股票之类的平台,用户买卖股票或者基金的话,或者体现,支付那些交易记录都要用区块链实现。
后来我自己又想了想,自己在没人带,完全靠自己,网上根本没有教材的情况下,很能短时间熟练的运用Hyperledger这本技术框架,所以我突然又想到了另外一种方案,既然Hyperledger是结合区块链的思想写出来的,那么我们的为什么不能用区块链的思想直接用java写出来,为什么还要花费那么长的时间去学习那样的技术。所以我结合公司业务以及区块链的思想,自己整出了一套简单的区块链交易记录。
区块链思想:1、去中心化 【做不到共有链,这是一个关于股票基金的产品,不能共享!】
2、信息不可篡改 【可以做到,安全加密】
3、多节点运行,允许1/3的节点最大程度被攻击而不破坏区块 【可以做到】
4、分布式数据库 。 【可以做到】
综合以上所述,个人觉得,也没必要完全利用Hyperledger,所以这个技术还是慢慢学习吧,暂且不用了,成本太高。
redis+mysql+lombok 下一篇将使用mongodb替代mysql,所以目前只是一个简单的区块链交易记录,因为自我感觉也不好,所以也希望大家指出来之后多做更改!目前只是单节点,所以不存在多节点查询的算法,下一篇替换mongodb将会多节点
1、添加pom文件依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <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>1.3.2</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.47</version> </dependency> <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.2</version> </dependency>
2、mysql表结构
2.1 交易表 t_point_deal 目前只用一个记录结果代替交易过程,毕竟保密,
CREATE TABLE `t_point_deal` (
`DEAL_ID` varchar(200) NOT NULL COMMENT '交易单id',
`BUY_USER_ID` int(10) NOT NULL COMMENT '买方id',
`BUY_ORDER_ID` bigint(10) unsigned NOT NULL,
`SELL_USER_ID` int(11) NOT NULL COMMENT '卖方id',
`SELL_ORDER_ID` bigint(10) unsigned NOT NULL,
`POINT_ID` int(11) NOT NULL COMMENT '基金id号',
`DEAL_DATE` datetime NOT NULL COMMENT '交易日期',
`DEAL_NUM` int(11) unsigned NOT NULL COMMENT '交易数量',
`DEAL_UNIT_PRICE` double(8,2) unsigned NOT NULL COMMENT '交易单价',
`DEAL_PRICE` double(9,2) unsigned NOT NULL COMMENT '成交额',
PRIMARY KEY (`DEAL_ID`),
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT
2.2 区块表 t_block
CREATE TABLE `t_block` (
`block_index` int(10) NOT NULL AUTO_INCREMENT COMMENT '区块索引号',
`block_hash` varchar(100) NOT NULL COMMENT 'hash值',
`block_stamp` varchar(40) NOT NULL COMMENT '时间戳',
`pointDeals` mediumtext COMMENT '基金交易记录',
`block_nonce` int(10) NOT NULL COMMENT '随机数',
`previousHash` varchar(100) NOT NULL COMMENT '上一个区块hash值',
PRIMARY KEY (`block_index`)
) ENGINE=MyISAM AUTO_INCREMENT=83 DEFAULT CHARSET=utf8
3、实体类Entity
@Data @AllArgsConstructor @NoArgsConstructor public class PointDeal { private String dealId; private Integer buyUserId; private Long buyOrderId; private Integer sellUserId; private Long sellOrderId; private Integer pointId; private Date dealDate; private Integer dealNum; private Double dealUnitPrice; private Double dealPrice; }
@Data public class Block { /** * 区块索引号 */ private int index; /** * 当前区块的hash值,区块唯一标识 */ private String hash; /** * 生成区块的时间戳 */ private long timestamp; /** * 当前区块的交易集合 */ private List<PointDeal> pointDeals; private String data; /** * 工作量证明,计算正确hash值的次数 */ private int nonce; /** * 前一个区块的hash值 */ private String previousHash; public Block() { super(); } public Block(int index, long timestamp, List<PointDeal> pointDeals, String data, int nonce, String previousHash, String hash) { super(); this.index = index; this.timestamp = timestamp; this.pointDeals= pointDeals; this.nonce = nonce; this.previousHash = previousHash; this.hash = hash; } }
4、application.properties配置
server.port=8092 #mysql: spring.datasource.url=jdbc:mysql://localhost:3306/XX?characterEncoding=utf8&useSSL=false spring.datasource.username= spring.datasource.password= spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.datasource.max-idle=10 spring.datasource.max-wait=10000 spring.datasource.min-idle=5 spring.datasource.initial-size=5 mybatis.mapper-Locations=classpath:mapper/entity/*.xml mybatis.type-aliases-package=com.dtb.trade.entity spring.redis.database=0 spring.redis.host=localhost spring.redis.port=6379 spring.redis.password= spring.redis.timeout=10000ms spring.freemarker.allow-request-override=false spring.freemarker.cache=true spring.freemarker.check-template-location=true spring.freemarker.charset=UTF-8 spring.freemarker.content-type=text/html spring.freemarker.expose-request-attributes=false spring.freemarker.expose-session-attributes=false spring.freemarker.expose-spring-macro-helpers=false spring.freemarker.suffix=.html
5、启动类Application
方法上添加扫描包注解
@MapperScan(basePackages={"com.dtb.trade"})
6、加密工具类SHA256
public class EncryptUtil { /** * 对字符串加密,加密算法使用MD5,SHA-1,SHA-256,默认使用SHA-256 * * @param strSrc * 要加密的字符串 * @param encName * 加密类型 * @return */ public static String Encrypt(String strSrc) { MessageDigest md = null; String strDes = null; String encName = "SHA-256"; byte[] bt = strSrc.getBytes(); try { md = MessageDigest.getInstance(encName); md.update(bt); strDes = bytes2Hex(md.digest()); //to HexString } catch (NoSuchAlgorithmException e) { return null; } return strDes; } public static String bytes2Hex(byte[] bts) { String des = ""; String tmp = null; for (int i = 0; i < bts.length; i++) { tmp = (Integer.toHexString(bts[i] & 0xFF)); if (tmp.length() == 1) { des += "0"; } des += tmp; } return des; } }
7、controller层
7.1 查询交易记录,根据t_point_deal 中的买方id查询
7.2 更新交易记录到区块链上
@Controller @Slf4j public class TradeController { @Autowired TradeService tradeService; @Autowired RedisService redisService; //查询区块地址 @GetMapping(value = "/block/{id}") public String getBlockAdress(@PathVariable("id")String id, ModelMap modelMap){ //获取所有区 List<Block> list = tradeService.getAllBlock(); int userId = Integer.valueOf(id); if (list == null){ modelMap.put("msg","区块信息为空"); modelMap.put("success",false); modelMap.put("code",210); } List<PointDeal> results = new ArrayList<>(); for (Block block:list){ List<PointDeal> pointDeals = JSONObject.parseArray(block.getData(),PointDeal.class); List<PointDeal> list1 = pointDeals.stream().filter(pointDeal -> pointDeal.getBuyUserId().equals(userId)).collect(Collectors.toList()); if (pointDeals != null){ results.addAll(list1); } } modelMap.put("page",results); return "/blockInfo"; } //测试记账 @GetMapping(value = "/block/test") public void test1(){ List<PointDeal> list = tradeService.gainAllTrade(); if (list.size()>0){ //获取交易块 Block block = null; String index = redisService.get("blockIndex"); if (StringUtils.isEmpty(index)){ //直接从数据库查询 boolean isHave = tradeService.isBlock(); if (!isHave){ //生成创世区块 tradeService.hyperledgerTwo(firstBlock(),list); } block = tradeService.getNewBlock(); tradeService.hyperledger(block,list); }else { //查询区块信息 block = tradeService.selectBlock(Integer.valueOf(index)); if (block != null){ tradeService.hyperledger(block,list); }else { System.out.println("区块出现异常"); } } } } public static Block firstBlock(){ Block block = new Block(); String prev = "0000000000000000000000000000000000000000000000000000000000000000"; int index = 1; int nonce = 1; long time = System.currentTimeMillis(); String hash = EncryptUtil.Encrypt(prev+index+nonce+time); List<PointDeal> list = new ArrayList<>(); block.setPreviousHash(prev); block.setIndex(index); block.setNonce(nonce); block.setHash(hash); block.setTimestamp(time); block.setPointDeals(null); block.setPointDeals(list); return block; } }
8、service层
8.1 RedisService 这个redis我删除了多余的方法
package com.dtb.trade.service; import com.alibaba.fastjson.JSONObject; import org.apache.commons.collections.MapUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.core.*; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import java.util.*; import java.util.concurrent.TimeUnit; /** * @Author:Mujiutian * @Description: * @Date: Created in 下午2:18 2018/7/13 */ @Service public class RedisService { @Autowired StringRedisTemplate stringRedisTemplate; private String reidsKeyTitle = "pc_enterprise"; public String getString(String key) { return stringRedisTemplate.opsForValue().get(changereidsKeyTitle(key)); } /** * 向redis存入key和value * 如果key已经存在 则覆盖 * @param key * @param value */ public void set(String key, String value){ ValueOperations<String, String> opsForValue = stringRedisTemplate.opsForValue(); opsForValue.set(changereidsKeyTitle(key), value); } /** * 向redis指定的db中存入key和value以及设置生存时间 * 如果key已经存在 则覆盖 * @param key * @param value * @param time 有效时间(默认时间单位为秒) * @param */ public void set(String key, String value, Long time){ ValueOperations<String, String> opsForValue = stringRedisTemplate.opsForValue(); // 默认时间单位为秒 opsForValue.set(changereidsKeyTitle(key), value, time, TimeUnit.SECONDS); } /** * 通过key获取指定的value * @param key * @param * @return 没有返回null */ public String get(String key) { return stringRedisTemplate.opsForValue().get(changereidsKeyTitle(key)); } }
8.2 TradeService 记账核心方法
@Service public class TradeService { @Autowired TradeDao tradeDao; @Autowired RedisService redisService; //区块拥有交易记录的最大个数 private static final int blockMax = 5; //该区块已经拥有的交易记录个数 private static int blockNum = 0; //获取定时刷新的交易记录 public List<PointDeal> gainAllTrade(){ Map<String,Object> map = new HashMap<>(); String today = new SimpleDateFormat("yyyy-MM-dd").format(new Date()); map.put("start",today+" 00:00:00"); map.put("end",today+" 23:59:59"); return tradeDao.getAll(map); } //查询是否拥有数据块 public boolean isBlock(){ List<Block> list = tradeDao.isHave(); if (list == null || list.isEmpty() || list.size()==0){ return false; } return true; } //新增区块信息 public boolean insertBlock(Block block){ tradeDao.insertBlock(block); return true; } //更新区块信息 public boolean updateBlock(Block block){ tradeDao.updateBlock(block); return true; } //根据索引号查询区块信息 public Block selectBlock(int index){ return tradeDao.selectBlock(index); } //查询最新区块信息 public Block getNewBlock(){ Block block = tradeDao.getBlockIndex(); block.setPointDeals(JSONObject.parseArray(block.getData(),PointDeal.class)); return block; } //记账 public boolean hyperledger(Block block,List<PointDeal> pointDeals){ if (block.getPointDeals() != null){ blockNum = block.getPointDeals().size(); } //准备记账的记忆记录个数 int recordNum = pointDeals.size(); //欠缺,补足到最大区块个数 int deficiency = blockMax - blockNum; //该区块是否能满足记账交易记录的个数,0刚好满足,正数绰绰有余,负数不满足,生成新的区块 int surplusNum = deficiency-recordNum; if (blockNum < blockMax){ //更新区块信息 if (surplusNum >= 0){ block.getPointDeals().addAll(pointDeals); block.setData(JSON.toJSONString(block.getPointDeals())); tradeDao.updateBlock(block); }else { //填补区块剩余 for (int j =0;j<deficiency;j++){ block.getPointDeals().add(pointDeals.get(0)); pointDeals.remove(0); //更新到数据库 block.setData(JSON.toJSONString(block.getPointDeals())); tradeDao.updateBlock(block); redisService.set("blockIndex",block.getIndex()+""); } //生成下一区块 String nextPrev = block.getHash(); int nextIndex = block.getIndex()+1; int nextnonce = block.getNonce(); long nextTime = System.currentTimeMillis(); String nextHash = EncryptUtil.Encrypt(nextPrev+nextIndex+nextnonce+nextTime+block.getData()); Block nextBlock = new Block(); nextBlock.setTimestamp(nextTime); nextBlock.setPreviousHash(nextPrev); nextBlock.setHash(nextHash); nextBlock.setNonce(nextnonce); nextBlock.setIndex(nextIndex); hyperledgerTwo(nextBlock,pointDeals); } }else { //直接生成新区块 String nextPrev = block.getHash(); int nextIndex = block.getIndex()+1; int nextnonce = block.getNonce(); long nextTime = System.currentTimeMillis(); String nextHash = EncryptUtil.Encrypt(nextPrev+nextIndex+nextnonce+nextTime+block.getData()); List<PointDeal> list = new ArrayList<>(); Block nextBlock = new Block(); nextBlock.setTimestamp(nextTime); nextBlock.setPreviousHash(nextPrev); nextBlock.setHash(nextHash); nextBlock.setNonce(nextnonce); nextBlock.setIndex(nextIndex); nextBlock.setPointDeals(list); hyperledgerTwo(nextBlock,pointDeals); } return true; } //新区块记账 public void hyperledgerTwo(Block block,List<PointDeal> pointDeals){ int recordNum = pointDeals.size(); int deficiency = blockMax - blockNum; int surplusNum = deficiency-recordNum; if (surplusNum >= 0){ //满足 block.getPointDeals().addAll(pointDeals); block.setData(JSON.toJSONString(block.getPointDeals())); tradeDao.insertBlock(block); redisService.set("blockIndex",block.getIndex()+""); }else { //不满足 for (int j =0;j<deficiency;j++){ block.getPointDeals().add(pointDeals.get(0)); pointDeals.remove(0); } block.setData(JSON.toJSONString(block.getPointDeals())); //更新到数据库 tradeDao.insertBlock(block); //生成下一区块 String nextPrev = block.getHash(); int nextIndex = block.getIndex()+1; int nextnonce = block.getNonce(); long nextTime = System.currentTimeMillis(); String nextHash = EncryptUtil.Encrypt(nextPrev+nextIndex+nextnonce+nextTime+block.getData()); List<PointDeal> list = new ArrayList<>(); Block nextBlock = new Block(); nextBlock.setTimestamp(nextTime); nextBlock.setPreviousHash(nextPrev); nextBlock.setHash(nextHash); nextBlock.setNonce(nextnonce); nextBlock.setIndex(nextIndex); nextBlock.setPointDeals(list); hyperledgerTwo(nextBlock,pointDeals); } } //获取所有区块 public List<Block> getAllBlock(){ List<Block> blocks = tradeDao.isHave(); if (blocks != null || blocks.size() > 0){ return blocks; } return null; } }
9、dao层
public interface TradeDao { @Select({ "select * from t_point_deal where DEAL_DATE >= #{start} and DEAL_DATE <= #{end}" }) @Results({ @Result(column = "DEAL_ID",property = "dealId",jdbcType = JdbcType.VARCHAR), @Result(column = "BUY_USER_ID",property = "buyUserId",jdbcType = JdbcType.INTEGER), @Result(column = "BUY_ORDER_ID",property = "buyOrderId",jdbcType = JdbcType.BIGINT), @Result(column = "SELL_USER_ID",property = "sellUserId",jdbcType = JdbcType.INTEGER), @Result(column = "SELL_ORDER_ID",property = "sellOrderId",jdbcType = JdbcType.BIGINT), @Result(column = "POINT_ID",property = "pointId",jdbcType = JdbcType.INTEGER), @Result(column = "DEAL_DATE",property = "dealDate",jdbcType = JdbcType.DATE), @Result(column = "DEAL_NUM",property = "dealNum",jdbcType = JdbcType.INTEGER), @Result(column = "DEAL_UNIT_PRICE",property = "dealUnitPrice",jdbcType = JdbcType.DOUBLE), @Result(column = "DEAL_PRICE",property = "dealPrice",jdbcType = JdbcType.DOUBLE), }) List<PointDeal> getAll(Map<String,Object> map); @Select({ "select * from t_block" }) @Results({ @Result(column = "block_index",property = "index",jdbcType = JdbcType.INTEGER), @Result(column = "block_hash",property = "hash",jdbcType = JdbcType.VARCHAR), @Result(column = "block_stamp",property = "timestamp",jdbcType = JdbcType.TIMESTAMP), @Result(column = "pointDeals",property = "data",jdbcType = JdbcType.LONGVARCHAR), @Result(column = "block_nonce",property = "nonce",jdbcType = JdbcType.INTEGER), @Result(column = "previousHash",property = "previousHash",jdbcType = JdbcType.VARCHAR) }) List<Block> isHave(); @Select({ "select * from t_block order by block_index desc limit 0,1" }) @Results({ @Result(column = "block_index",property = "index",jdbcType = JdbcType.INTEGER), @Result(column = "block_hash",property = "hash",jdbcType = JdbcType.VARCHAR), @Result(column = "block_stamp",property = "timestamp",jdbcType = JdbcType.TIMESTAMP), @Result(column = "pointDeals",property = "data",jdbcType = JdbcType.LONGVARCHAR), @Result(column = "block_nonce",property = "nonce",jdbcType = JdbcType.INTEGER), @Result(column = "previousHash",property = "previousHash",jdbcType = JdbcType.VARCHAR) }) Block getBlockIndex(); @Insert({ "insert into t_block (block_hash,block_stamp,pointDeals,block_nonce,previousHash) values(#{hash},#{timestamp},#{data},#{nonce},#{previousHash})" }) int insertBlock(Block block); @Update({ "update t_block set pointDeals = #{data} where block_index = #{index}" }) int updateBlock(Block block); @Select({ "select * from t_block where block_index = #{index}" }) @Results({ @Result(column = "block_index",property = "index",jdbcType = JdbcType.INTEGER), @Result(column = "block_hash",property = "hash",jdbcType = JdbcType.VARCHAR), @Result(column = "block_stamp",property = "timestamp",jdbcType = JdbcType.TIMESTAMP), @Result(column = "pointDeals",property = "data",jdbcType = JdbcType.LONGVARCHAR), @Result(column = "block_nonce",property = "nonce",jdbcType = JdbcType.INTEGER), @Result(column = "previousHash",property = "previousHash",jdbcType = JdbcType.VARCHAR) }) Block selectBlock(@Param("index")int index); }
10、代码逻辑
10.1
@GetMapping(value = "/block/test")
这个rest接口代码逻辑是先从mysql block区块中判断有无区块,如果没有那么生成创世区块,如果有的话,那么在原有区块继续添加交易记录,直到区块中的交易记录数量达到限制,在重新下一个区块继续。
10.2
@GetMapping(value = "/block/{id}")
根据买方id(t_point_deal 中的字段)查询她所有的交易记录
10.3
如果自己要测试的话,那么可以在t_point_deal表中随意添加几条记录,最后执行接口,就可以看到区块信息了,如图:
10.4 查询的话,那么直接查询接口,然后在templates下创建新的blockInfo.html,因为是freemaker技术嘛
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>区块记录</title> </head> <body> Tx: <table> <thead> Block: <span></span><br/> Nonce: <span></span><br/> <tr> <th width="10%">交易id</th> <th width="10%">买方id</th> <th width="10%">卖方id</th> <th width="20%">交易日期</th> <th width="10%">交易数量</th> <th width="10%">交易单价</th> <th width="10%">成交额</th> </tr> </thead> <#if page?size gt 0> <tbody> <#list page as pointDeals> <tr> <td width="10%">${pointDeals.dealId!'_'}</td> <td width="10%">${pointDeals.buyUserId!'-'}</td> <td width="10%">${pointDeals.sellUserId!'_'}</td> <td width="20%">${pointDeals.dealDate?string('yyyy-MM-dd HH:mm:ss')!'_'}</td> <td width="10%">${pointDeals.dealNum!'_'}</td> <td width="10%">${pointDeals.dealUnitPrice!'_'}</td> <td width="10%">${pointDeals.dealPrice!'_'}</td> </tr> </#list> </tbody> </#if> </table> </body> </html>
执行查询如图:
目前不足之处是:不能多节点运行,并执行拜占庭算法等,以及非关系行数据库运用。
转载于:https://my.oschina.net/mdxlcj/blog/1928438