(本篇博客已於2019-08-28優化更新)java
簡介:隨着今年愈來愈多的區塊鏈項目更多的落地,咱們本身公司也想準備着手區塊鏈方向發展,因此領導給咱們技術部要求也要落地一個區塊鏈的項目,其實以前一兩個月也接觸了Hyperledger這個框架,固然我本身的博客分類中也有對Hyperledger框架的大概集成以及搭建,可是我接觸幾個月的時間裏,不知道做用是什麼?讀過好多相似這個框架有關的書,但直覺告訴我,他們對於現階段的我,沒用!包括《區塊鏈技術進階與實戰》《區塊鏈開發實戰 hyperledger fabric 關鍵技術與案例分析》《深度探索區塊鏈 Hyperledger技術與應用》《Hyperledger Fabric 開發實戰》,最後一本只是看了看,不過這書寫的都是大同小異,雷同之處太多。而後我就在想,這個框架學習來要好久吧,畢竟本身要搞這個,我也是個java,沒有接觸過這方面的,即使上一個公司的區塊鏈項目已經落地,可是技術沒有接觸過,只不過當時用的是python,那個產品是InsurBox,不過那也是一個相似於挖礦的小遊戲,可是徹底跟公司業務不一樣。 需求:公司是作一個相似基金股票之類的平臺,用戶買賣股票或者基金的話,或者體現,支付那些交易記錄都要用區塊鏈實現。 後來我本身又想了想,本身在沒人帶,徹底靠本身,網上根本沒有教材的狀況下,很能短期熟練的運用Hyperledger這本技術框架,因此我忽然又想到了另一種方案,既然Hyperledger是結合區塊鏈的思想寫出來的,那麼咱們的爲何不能用區塊鏈的思想直接用java寫出來,爲何還要花費那麼長的時間去學習那樣的技術。因此我結合公司業務以及區塊鏈的思想,本身整出了一套簡單的區塊鏈交易記錄。 區塊鏈思想: 一、去中心化【作不到共有鏈,這是一個關於股票基金的產品,不能共享!】 二、信息不可篡改【能夠作到,安全加密】 三、多節點運行,容許1/3的節點最大程度被攻擊而不破壞區塊 【能夠作到】 四、分佈式數據庫【能夠作到】 技術棧:redis+mysql+lombok+freemaker,下一篇將使用mongodb替代mysql,因此目前只是一個簡單的區塊鏈交易記錄,由於自我感受也很差,因此也但願你們指出來以後多作更改!目前只是單節點,因此不存在多節點查詢的算法,下一篇替換mongodb將會多節點。
<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>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.1 交易表 t_point_deal 目前只用一個記錄結果代替交易過程,畢竟保密。python
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_blockmysql
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
@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; } }
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
方法上添加掃描包註解web
@MapperScan(basePackages={"com.dtb.trade"})
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.1 查詢交易記錄,根據t_point_deal 中的買方id查詢redis
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.1 RedisService 這個redis我刪除了多餘的方法spring
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 記帳核心方法sql
@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; } }
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.一、這個rest接口代碼邏輯是先從mysql block區塊中判斷有無區塊,若是沒有那麼生成創世區塊,若是有的話,那麼在原有區塊繼續添加交易記錄,直到區塊中的交易記錄數量達到限制,在從新下一個區塊繼續。mongodb
@GetMapping(value = "/block/test")
10.二、根據買方id(t_point_deal 中的字段)查詢她全部的交易記錄
@GetMapping(value = "/block/{id}")
10.三、若是本身要測試的話,那麼能夠在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>
執行查詢如圖:
目前不足之處是:不能多節點運行,並執行拜占庭算法等,以及非關係行數據庫運用。