balance和用户密码可以写成字典形式,每月一号进

由于公司业务需要,花两周时间实现了一个小型的支付系统,麻雀虽小五脏俱全,各种必须的模块如账户加锁,事务性保证,流水对帐等都是有完整实现的,整个开发过程中有很多经验积累,再加上在网上搜索了一下,大部分都是些研究性的论文,对实际使用价值不大,所以这次特意拿出来和大家分享一下。这个系统可以用作小型支付系统,也可以用做第三方应用接入开放平台时的支付流水系统。原来的需求比较负责,我简化一点说:

分布式事务中的TCC模式,貌似是阿里提出来的,所以阿里自研的分布式事务框架总是少不了TCC的影子。

需求

  • 额度 15000或自定义
  • 实现购物商城,买东西加入 购物车,调用信用卡接口结账
  • 可以提现,手续费5%
  • 支持多账户登录
  • 支持账户间转账
  • 记录每月日常消费流水
  • 提供还款接口
  • ATM记录操作日志
  • 提供管理接口,包括添加账户、用户额度,冻结账户等。。。
  • 用户认证用装饰器

对每个应用,对外需要提供 获取余额,支付设备,充值 等接口后台有程序,每月一号进行清算账户可以被冻结需要记录每一次操作的流水,每天的流水都要和发起方进行对账

服务拆分

需求分析

  1. 额度,初始化为15000,可以写入一个文件里balance.txt
  2. 购物车应用,可以参考购物车作业,整个购物车可以放到一个函数里,需要调用信用卡接口,就需要调用信用卡模块了,购物车脚本可以和信用卡的放在一起
  3. 提现,余额扣除提现金额*(1 + 0.05) 扣除金额不得大于余额的一半,提现完了之后扣除余额
  4. 多账户登陆,用户名密码写入一个文件里,参考登陆接口作业
  5. 账户之间转账,用户名对应的余额 - 转账金额,转账账户名对应的余额 + 转账金额
  6. 记录流水,还需要写一个流水文件,记录消费商品,金额,前面还需要加时间
  7. 还款接口,可以理解为向余额添加
  8. ATM操作记录
  9. 参考购物车作业里的manage.py
  10. 装饰器,我理解应该是不同的操作,给需要密码验证的,加上输入密码验证模块,就像真正的ATM那样,非重要的操作可以不需要密码

进一步思考:
balance和用户密码可以写成字典形式,用pickle导入到文件,到时候在load出来,方便处理,按用户名,密码,余额的形式对应起来,写在一个文件里。

import pickle

info_dict={'root':{'userpasswd':'python','balance':10000},
        'test':{'userpasswd':'test','balance':10000}}

f=open('user.info','wb')
pickle.dump(info_dict,f)
f.close()

购物车程序写在shopplist.py里,功能有选择商品,加入购物车,确认付款,到支付接口,完成后返回信息。
支付接口写在credit.py ,支付函数payment(),支付需要调用修改余额函数write_balance() 记录账单记录函数consumer_logger()
提现接口也可以写在credit.py里,提现函数withdraw(),提现需要调用修改余额函数write_balance(),记录操作记录atm_logger()

账户间转账写在credit.py里,转账函数transfer(),转账需要提供转账账户名,转账金额,需要调用修改余额函数write_balance(),记录操作记录atm_logger()
还款接口也可以写在credit.py里,还款函数repay(),需要调用修改余额函数write_balance(),记录操作记录atm_logger()

针对上面的需求,我们设置如下数据库:

很多系统早期都是单系统服务架构,所有业务聚合在少数几个系统中对外提供服务。随着业务发展,服务之间耦合比较严重,一般会对服务进行重构,重构的主要思想也就是围绕“拆分”展开。

结构:

我们先来确定程序的结构
-atm
--bin
---atm.py
---shopping.py
---manager.py
--core
---各类程序模块
--logs
美高梅游戏网站登录 ,---atm.log
---cust.log

其中bin目录为执行入口目录,atm.py是ATM仿真程序的启动入口,shopping.py是购物车程序的启动入口,manager.py是管理账户程序的启动入口
core目录下主要放置各类程序模块
logs目录下主要放置操作流水记录日志,atm.log是ATM操作记录,cust.log是信用卡消费记录
setting目录下主要放置配置文件

CREATE TABLE `app_margin`.`tb_status`  UNSIGNED NOT NULL, `freeze` int NOT NULL DEFAULT 0, `create_time` datetime NOT NULL, `change_time` datetime NOT NULL, PRIMARY KEY  ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `app_margin`.`tb_account_earn`  UNSIGNED NOT NULL, `create_time` datetime NOT NULL, `balance` bigint NOT NULL, `change_time` datetime NOT NULL, `seqid` int NOT NULL DEFAULT 500000000, PRIMARY KEY  ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `app_margin`.`tb_bill` ( `id` int AUTO_INCREMENT NOT NULL, `bill_id` int NOT NULL, `amt` bigint NOT NULL, `bill_info` text, `bill_user` char, `bill_time` datetime NOT NULL, `bill_type` int NOT NULL, `bill_channel` int NOT NULL, `bill_ret` int NOT NULL, `appid` int UNSIGNED NOT NULL, `old_balance` bigint NOT NULL, `price_info` text, `src_ip` char, PRIMARY KEY , UNIQUE KEY `unique_bill` (`bill_id`,`bill_channel`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `app_margin`.`tb_assign` ( `id` int AUTO_INCREMENT NOT NULL, `assign_time` datetime NOT NULL, PRIMARY KEY  ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `app_margin`.`tb_price`  NOT NULL, `price` int NOT NULL, `info` text NOT NULL, PRIMARY KEY  ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `app_margin`.`tb_applock`  UNSIGNED NOT NULL, `lock_mode` int NOT NULL DEFAULT 0, `change_time` datetime NOT NULL, PRIMARY KEY  ENGINE=InnoDB DEFAULT CHARSET=utf8; INSERT `app_margin`.`tb_assign`  VALUES ;

比如按照功能进行解耦的垂直拆分,拆分之后原有系统中的业务调用,就变成了分布式的调用了,但是由于网络的不可靠性,数据一致性问题,可扩展性问题,高可用容灾问题成为分布式事务的主要挑战。而对于在服务之间数据交付的时候容易造成的数据不一致问题,一般需要引入分布式事务对数据一致性做控制。

流程图

我们先来确定atm的流程图

美高梅游戏网站登录 1

atm

然后是购物车的流程图

美高梅游戏网站登录 2

shopping

最后是manager的流程图

美高梅游戏网站登录 3

manager

详细解释如下:tb_status 应用的状态表。负责账户是否被冻结,账户的类型是什么(真实的需求是应用可能有两种账户,这里为简单所以没有列出)appid 应用idfreeze 是否冻结create_time 创建时间change_time 最后一次修改时间tb_account_earn 应用的账户余额表appid 应用idbalance 余额(单位为分,不要用小数存储,因为小数本身不精确;另外php要在64位机下才能支持bigint)create_time 创建时间change_time 最后一次修改时间seqid 操作序列号tb_assign 分配流水id的表,tb_bill的bill_id必须是有tb_assign分配的id 自增idcreate_time 创建时间tb_bill 流水表。负责记录每一条操作流水,这里的bill_id不是主键,因为同一个bill_美高梅棋牌官网 ,id可能会有支付和回滚两条流水id 自增序列号bill_id 流水号amt 操作的金额(这个是要区别正负的,主要是为了select all的时候可以直接计算出某段时间的金额变化)bill_info 操作的详细信息,比如3台webserver,2台dbbill_user 操作用户bill_time 流水时间bill_type 流水类型,区分是加钱还是减钱bill_channel 流水来源,如充值,支付,回滚,结算还是其他bill_ret 流水的返回码,包括未处理、成功、失败,这里的逻辑会在后面讲解appid 应用idold_balance 操作发生前的账户余额price_info 记录操作发生时,记录被支付物品的单价src_ip 客户端iptb_price 单价表,记录了机器的单价name 机器唯一标识price 价格info 描述tb_applock 锁定表,这是为了避免并发对某一个应用进行写操作设计的,具体的代码会在后面展示appid 应用idlock_mode 锁定状态。为0则为锁定,为1则为锁定change_time 最后一次修改时间OK,库表设计出来之后,我们就来看一下最典型的几个操作.

美高梅游戏网站登录 4

实现


一. 支付操作我这里只列出了我目前实现的方式,可能不是最好的,但应该是最经济又满足需求的。先说调用方这里,逻辑如下:

单系统到微服务拆分的过程,是一个资源横向扩展的过程,当单台机器资源无法承担更大的请求时,可以多台机器形成集群。

用户数据存放

考虑之前用字典存放用户账户名和密码,这次多了一个balance,那就字典里增加一个key叫'balance',字典形式这样的:
{'test': {'balance': 11000.0, 'passwd': 'test'}, 'root': {'balance': 9000.0, 'passwd': 'python'}}

用户名对应一个字典,内字典key是passwd和balance,对应用户的密码和余额,在实现程序之前,先创建这个user.info文件
我用pickle.dump方法把测试用户写入文件:

import pickle

info_dict={'root':{'passwd':'python','balance':10000},
    'test':{'passwd':'test','balance':10000}}

f=open('user.info','wb')
pickle.dump(info_dict,f)
f.close()

同样的,用pickle的方法,写一个product.txt的文件,用来存放商品信息,商品有名称和价格,参考之前的购物车项目,我们做成列表类型。
product.txt的脚本可以跟之前写user.info文件的脚本合并,做成一个初始化脚本:

import pickle

info_dict={'root':{'passwd':'python','balance':10000},
    'test':{'passwd':'test','balance':10000}}

f=open('user.info','wb')
pickle.dump(info_dict,f)
f.close()

pro_list=[['iphone', 6200], ['mac pro', 12000], ['bike', 800], ['book', 55], ['coffee', 31], ['windows pc', 4900]]
f=open('product.txt','wb')
pickle.dump(pro_list,f)
f.close()

初始化数据好了之后,已经拥有了一些测试账号和商品,开始做我们的登陆接口login

然后对应的支付系统内部逻辑如下(只列出支付操作,回滚逻辑差不多,流水检查是要检查对应的支付流水是否存在):

资源拆分主要有两个执行方向:

登陆接口

参考之前的作业,登录接口的主逻辑我们这样写:

f=open('user.info','rb')
    user_dict=pickle.load(f)
    f.close()

    while True:
        username=input('Enter your account:')
        password=input('Enter your password:')
        if username not in user_dict:
            print('Sorry,your account is not exist!n')
            continue

        else:
            if user_dict[username]['passwd'] != password:
                print ('Sorry,your password invalid!nPlease try again!n')
                continue
            else:
                print ('Welcome {}!'.format(username))
                break

输入帐号密码,判断是否存在用户,之后判断密码是否是字典里用户名对应的二级字典'passwd'对应的value,稍后这个需要做成装饰器,我们先做成一个可调用的函数形式:

def login():
    f=open('user.info','rb')
    user_dict=pickle.load(f)
    f.close()


    while True:
        username=input('Enter your account:')
        password=input('Enter your password:')
        if username not in user_dict:
            print('Sorry,your account is not exist!n')
            continue

        else:
            if user_dict[username]['passwd'] != password:
                print ('Sorry,your password invalid!nPlease try again!n')
                continue
            else:
                print ('Welcome {}!'.format(username))
                break
    return username

函数返回的值我们暂时定为输入的用户名。

还有一个冻结账户的需求,这里还需要有个blacklist.txt用来存放被冻结的账户,参考之前的作业,黑名单做成列表,存放用户名,pickle序列化到文件里。
在初始化脚本里加上创建blacklist.txt文件的代码:

import pickle

info_dict={'root':{'passwd':'python','balance':10000},
    'test':{'passwd':'test','balance':10000}}

blacklist=['blacklist']

f=open('user.info','wb')
pickle.dump(info_dict,f)
f.close()

pro_list=[['iphone', 6200], ['mac pro', 12000], ['bike', 800], ['book', 55], ['coffee', 31], ['windows pc', 4900]]

f=open('product.txt','wb')
pickle.dump(pro_list,f)
f.close()

f=open('blacklist.txt','wb')
pickle.dump(blacklist,f)
f.close()

创建好之后,完善登录接口的逻辑,我们把user.info文件写成变量info_path,取得值是本文件所在目录下的'user.info',黑名单文件同理也这么写:

#owner:houyizhong
#version:1.0

import pickle
import os

filename=os.path.abspath(__file__)
dirname=os.path.dirname(filename)
info_path=dirname+os.sep+'user.info'
blackfile_path=dirname+os.sep+'blacklist.txt'

def login():
    f=open(info_path,'rb')
    user_dict=pickle.load(f)
    f.close()

    blackfile=open(blackfile_path,'rb')
    blacklist=pickle.load(blackfile)
    blackfile.close()

    while True:
        username=input('Enter your account:')
        password=input('Enter your password:')
        if username not in user_dict:
            print('Sorry,your account is not exist!n')
            continue
        elif username in blacklist:
                    print ("Sorry you are in blacklist!")
                    continue
        else:
            if user_dict[username]['passwd'] != password:
                print ('Sorry,your password invalid!nPlease try again!n')
                continue
            else:
                print ('Welcome {}!'.format(username))
                break

    return username

新增逻辑对blacklist.txt的读取列表,登录接口里对用户输入的用户名的判断,是否在列表里。

常用的错误返回码可能如下就足够了:

按业务拆分,也就是将数据按照业务分组,将不同服务的数据放到不同的存储上,类似于soa架构下的服务化,已业务单元为核心。

信用卡

登录接口完成之后,我们再来一一实现信用卡功能的各个模块。

我把信用卡功能分为消费(即支付),查余额,查账单,转账,提现,还款等功能块。我们一个个来实现。

还记得我们的账户余额是写在用户信息的文件里,作为二级字典的value存在的吗?消费,转账,提现,还款都要对余额有个修改操作,我们可以统一做成一个写入余额的功能模块。
实现写入文件的模块balance_module

import pickle
import os

filename=os.path.abspath(__file__)
dirname=os.path.dirname(filename)
info_path=dirname+os.sep+'user.info'

def write_balance(data):
    f=open(info_path,'wb')
    pickle.dump(data,f)
    f.close()

上面的代码,我是打开相对路径下的user.info文件,其实是跟balance_module是属于同一目录的,写成info_path可以方便日后迁移user.info路径。
函数write_balance接受的是一个字典,模块会把这个字典写入文件,覆盖之前的内容,这就要求我们传入的字典是带有所有用户信息的,一个已经调整好的字典,不会再有变动了。
这样写的好处是,所有信息修改完成之后,才会写入文件,这样下次读取的文件内容就是最新的了。这个道理我们在上一篇中已经探讨过。

读取user.info文件的函数
info_path的路径即为user.info的相对于credit.py的位置

filename=os.path.abspath(__file__)
dirname=os.path.dirname(filename)
info_path=dirname+os.sep+'user.info'

def read_info():

    f=open(info_path,'rb')
    data=pickle.load(f)
    f.close()
    return data

返回的data是提取出来的字典。

$g_site_error = array( -1 => '服务器繁忙', -2 => '数据库读取错误', -3 => '数据库写入错误', 0 => '成功', 1 => '没有数据', 2 => '没有权限', 3 => '余额不足', 4 => '账户被冻结', 5 => '账户被锁定', 6 => '参数错误', );

按数据拆分,也就是常说的数据分片,按照横向扩展纬度,将单个DB拆分成多个DB,数据存储具备统一的Sharding功能,达到资源横向扩展,承担更高的吞吐。

一些基本动作

我们在写程序时,常常会遇到一些动作是频繁做的,比如扣除余额(消费,提现,转账都会用到),添加余额(还款,收到转账),还有判断余额是否充足,是否满足动作(消费,提现)所需的金额。这些可以复用的代码,我们就把它做成函数,需要时调用好了。

def add(username,bill,data):
    balance=data[username]['balance']
    balance += bill
    data[username]['balance']=balance
    return data

def subtract(username,bill,data):
    balance=data[username]['balance']
    balance -= bill
    data[username]['balance']=balance
    print ('Your balance is {}n'.format(balance))
    return data

def judgment(username,bill):
    data=read_info()
    balance=data[username]['balance']
    if balance >= bill:
        return 'success'
    else:
        print ('Your balance is not enought!n')
        return 'failure'

余额的删减都需要对整个字典进行修改(这样返回数据时,接受数据的模块处理更方便,直接把data写入文件),判断余额是否满足动作所需金额bill的函数,我们返回是'success'和'failure',这样调用函数的模块更容易判断是否满足,直接根据收到的字符来判断。

日志记录
我们要有日志记录功能,记录我们每一次的消费,要有消费时间,消费项目,金额,消费记录这个动作做成模块,其他模块可以复用:

def consumer_logger(username,stock,price):

    user_dir=log_path+os.sep+username

    if not os.path.exists(user_dir):
        os.mkdir(user_dir)

    cust_log=user_dir+os.sep+'cust.log'

    times=time.strftime('%Y%m%d%H%M%S')
    data='{0}t{1}t{2}'.format(times,stock,price)

    log=open(cust_log,'a')
    log.write('{}n'.format(data))
    log.close()

我们的日志是放在logs目录下的,同时还要根据用户不同创建不同的日志信息,这样方便区分。
我们用user_dir代表logs下的用户文件夹,当不存在时,创建文件夹
cust_log代表用户文件夹下的cust.log文件,具体的信息盛放在里面。
接受的形参分别代表用户名,商品名(购物车列表),金额
调用time模块,用time.strftime()方法取到年月日小时分秒级别的时间,用制表符写入文件

同理,atm操作记录也这么写

def atm_logger(username,operating,money):
    user_dir=log_path+os.sep+username

    if not os.path.exists(user_dir):
        os.mkdir(user_dir)

    atm_log=user_dir+os.sep+'atm.log'

    times=time.strftime('%Y%m%d%H%M%S')
    data='{0}t{1}t{2}'.format(times,operating,money)

    log=open(atm_log,'a')
    log.write('{}n'.format(data))
    log.close()

接受的形参分别为用户名,动作,金额。

对于大于0的错误都算是逻辑错误,执行支付操作,调用方是不用记录流水的。因为账户并没有发生任何改变。对于小于0的错误是系统内部错误,因为不知道是否发生了数据更改,所以调用方和支付系统都要记录流水。对于等于0的返回,代表成功,两边也肯定要记录流水。而在支付系统内部,之所以采用先写入流水,再进行账户更新的方式也是有原因的,简单来说就是尽量避免丢失流水。最后总结一下,这种先扣钱,再发货,出问题再回滚的方式是一种模式;还有一种是先预扣,后发货,没有出问题则调用支付确认来扣款,出了问题就调用支付回滚来取消,如果预扣之后很长时间不做任何确认,那么金额会自动回滚。

Seata模式

支付模块

我们写一个支付函数payment来实现逻辑:

import balance_module

def payment(shoplist,bill):

    data=read_info()
    username=login()

    result=judgment(username,bill)

    if result == 'success':
        data=subtract(username,bill,data)
        balance_module.write_balance(data)
        consumer_logger(username,shoplist,bill)

    return result

可以看到我们需要调用用户认证接口,因为到付款界面了,必须知道银行账户。
调用了读取用户信息字典的read_info()函数,将字典提取出来
调用了judgment()来判断用户对应的余额是否满足
如果收到的是'success',调用扣除余额的函数,将字典写入文件的函数,消费日志记录函数

以上函数我们都已经写出来了。
然后函数返回的结果是成功或者失败。很容易理解对吧。

转账模块就是一个用户余额减少,另一个用户余额增加,可以这么写(为了直观,我没有把add() subtract() judgment() atm_logger()这些调用的函数写出来,下面几个也是如此):

import balance_module

def transfer():
    username=login()

    data=read_info()
    transfer_account=input('Enter transfer account:')
    transfer_money=input('Enter transfer money:')
    if transfer_account in data:
        if transfer_money.isdigit:
            transfer_money=float(transfer_money)
            result=judgment(username,transfer_money)
            if result == 'success':
                data=subtract(username,transfer_money,data)
                data=add(transfer_account,transfer_money,data)
                print ('Your transfer done!n')
                balance_module.write_balance(data)
                atm_logger(username,'transfer',transfer_money)
            elif result == 'failure':
                print ('Your balance is not enought!n')
            else:
                print ('Sorry,there have some unknown error!n')
        else:
            print ('Sorry,your enter money is not a digit!n')
    else:
        print('Sorry,your enter account is not exist!n')

提现模块也很好写,用户余额减少,同时判断余额一半是否大于提现的金额:

import balance_module

def withdraw():
    username=login()
    data=read_info()
    withdraw_money=input('Enter your withdraw money:')
    if withdraw_money.isdigit():
        cost_money=float(withdraw_money)*(1+0.05)
        result=judgment(username,cost_money)
        withdraw_money=float(withdraw_money)
        if result == 'success':
            if withdraw_money > (data[username]['balance']/2):
                print ('Sorry,your withdraw money is more than avalid balance!n')
            else:
                data=subtract(username,cost_money,data)
                balance_module.write_balance(data)
                atm_logger(username,'withdraw',cost_money)
                print ('Your withdraw done!n')
    else:
        print('Sorry,you enter is not digit!n')

也是调用了一堆现成的函数,把它们拼贴起来。

还款,就是新增余额:

import balance_module

def repay():
    username=login()
    data=read_info()
    repay_money=input('Enter your repay money:')
    if repay_money.isdigit():
        repay_money=float(repay_money)
        data=add(username,repay_money,data)
        balance_module.write_balance(data)
        atm_logger(username,'repay',repay_money)
        print('Your repay done!n')
    else:
        print('Sorry,your enter is not digit!n')

以上几个函数基本实现了一个信用卡的基本功能

二. 账户锁定的实现这里利用了数据库的加锁机制,具体逻辑就不说了,代码如下:

Seata关注的是微服务架构下的数据一致性问题,是整套的分布式事务解决方案。Seata框架包含两种模式:

完整的代码

#owner:houyizhong
#version:1.0

import pickle,time,os
import  balance_module
from demo_login import login

filename=os.path.abspath(__file__)
dirname=os.path.dirname(filename)
info_path=dirname+os.sep+'user.info'
log_path=os.path.dirname(dirname)+os.sep+'logs'

def read_info():
    f=open(info_path,'rb')
    data=pickle.load(f)
    f.close()
    return data

def add(username,bill,data):
    balance=data[username]['balance']
    balance += bill
    data[username]['balance']=balance
    return data

def subtract(username,bill,data):
    balance=data[username]['balance']
    balance -= bill
    data[username]['balance']=balance
    print ('Your balance is {}n'.format(balance))
    return data

def judgment(username,bill):
    data=read_info()
    balance=data[username]['balance']
    if balance >= bill:
        return 'success'
    else:
        print ('Your balance is not enought!n')
        return 'failure'

def atm_logger(username,operating,money):
    user_dir=log_path+os.sep+username
    if not os.path.exists(user_dir):
        os.mkdir(user_dir)
    atm_log=user_dir+os.sep+'atm.log'
    times=time.strftime('%Y%m%d%H%M%S')
    data='{0}t{1}t{2}'.format(times,operating,money)
    log=open(atm_log,'a')
    log.write('{}n'.format(data))
    log.close()

def consumer_logger(username,stock,price):
    user_dir=log_path+os.sep+username
    if not os.path.exists(user_dir):
        os.mkdir(user_dir)
    cust_log=user_dir+os.sep+'cust.log'
    times=time.strftime('%Y%m%d%H%M%S')
    data='{0}t{1}t{2}'.format(times,stock,price)
    log=open(cust_log,'a')
    log.write('{}n'.format(data))
    log.close()

def inquire():

    data=read_info()
    username=login()

    print('Your balance is :')
    print(data[username]['balance'])

def payment(shoplist,bill):
    data=read_info()
    username=login()
    result=judgment(username,bill)
    if result == 'success':
        data=subtract(username,bill,data)
        balance_module.write_balance(data)
        consumer_logger(username,shoplist,bill)
    return result

def transfer():
    username=login()

    data=read_info()
    transfer_account=input('Enter transfer account:')
    transfer_money=input('Enter transfer money:')
    if transfer_account in data:
        if transfer_money.isdigit:
            transfer_money=float(transfer_money)
            result=judgment(username,transfer_money)
            if result == 'success':
                data=subtract(username,transfer_money,data)
                data=add(transfer_account,transfer_money,data)
                print ('Your transfer done!n')
                balance_module.write_balance(data)
                atm_logger(username,'transfer',transfer_money)
            elif result == 'failure':
                print ('Your balance is not enought!n')
            else:
                print ('Sorry,there have some unknown error!n')
        else:
            print ('Sorry,your enter money is not a digit!n')
    else:
        print('Sorry,your enter account is not exist!n')

def withdraw():
    username=login()
    data=read_info()
    withdraw_money=input('Enter your withdraw money:')
    if withdraw_money.isdigit():
        cost_money=float(withdraw_money)*(1+0.05)
        result=judgment(username,cost_money)
        withdraw_money=float(withdraw_money)
        if result == 'success':
            if withdraw_money > (data[username]['balance']/2):
                print ('Sorry,your withdraw money is more than avalid balance!n')
            else:
                data=subtract(username,cost_money,data)
                balance_module.write_balance(data)
                atm_logger(username,'withdraw',cost_money)
                print ('Your withdraw done!n')
    else:
        print('Sorry,you enter is not digit!n')

def repay():
    username=login()
    data=read_info()
    repay_money=input('Enter your repay money:')
    if repay_money.isdigit():
        repay_money=float(repay_money)
        data=add(username,repay_money,data)
        balance_module.write_balance(data)
        atm_logger(username,'repay',repay_money)
        print('Your repay done!n')
    else:
        print('Sorry,your enter is not digit!n')

单独测试每个模块也是没问题的。这样 我们就可以继续写下去了。

class AppLock { function __construct { $this->m_appid = $appid; //初始化数据 $this->get(); } function __destruct; } public function alloc() { if ($this->m_bGot == true) { return true; } $this->repairData(); $appid = $this->m_appid; $ret = $this->update($appid,APPLOCK_MODE_FREE,APPLOCK_MODE_ALLOC); if  { app_error_log; return false; } if  { app_error_log("applock alloc fail,affected_rows:$ret"); return false; } $this->m_bGot = true; return true; } public function free() { if ($this->m_bGot != true) { return true; } $appid = $this->m_appid; $ret = $this->update($appid,APPLOCK_MODE_ALLOC,APPLOCK_MODE_FREE); if  { app_error_log; return false; } if  { app_error_log("applock free fail,affected_rows:$ret"); return false; } $this->m_bGot = false; return true; } function repairData; $appid = $this->m_appid; $now = time(); $need_time = $now - APPLOCK_REPAIR_SECS; $str_need_time = date("Y-m-d H:i:s", $need_time); $db->where; $db->where("lock_mode",APPLOCK_MODE_ALLOC); $db->where("change_time <=",$str_need_time); $db->set("lock_mode",APPLOCK_MODE_FREE); $db->set",false); $ret = $db->update; if  { app_error_log("repair applock error,appid:$appid"); return false; } return true; } private function get; $appid = $this->m_appid; $db->where; $query = $db->get; if  { app_error_log("AppLock get fail.appid:$appid"); return false; } if (count($query->result_array { $applock_data = array( 'appid'=>$appid, 'lock_mode'=>APPLOCK_MODE_FREE, ); $db->set',false); $ret = $db->insert(TB_APPLOCK, $applock_data); if  { app_error_log("applock insert fail:$appid"); return false; } //重新获取数据 $db->where; $query = $db->get; if  { app_error_log("AppLock get fail.appid:$appid"); return false; } if (count($query->result_array { app_error_log("AppLock not data,appid:$appid"); return false; } } $applock_data = $query->row_array(); return $applock_data; } private function update($appid,$old_lock_mode,$new_lock_mode) { $db = APP_DB(); $db->where; $db->where('lock_mode',$old_lock_mode); $db->set('lock_mode',$new_lock_mode); $db->set',false); $ret = $db->update; if  { app_error_log("update applock error,appid:$appid,old_lock_mode:$old_lock_mode,new_lock_mode:$new_lock_mode"); return false; } return $db->affected_rows(); } //是否获取到了锁 public $m_bGot = false; public $m_appid; }

AT模式,关注的是数据分片角度,关注DB访问的数据一致性,多服务下多DB数据访问的一致性

购物车程序

这里可以参考之前的作业,再加上一个调用信用卡模块

import pickle,os
import demo_credit
filename=os.path.abspath(__file__)
dirname=os.path.dirname(filename)
product_path=dirname+os.sep+'product.txt'
def shopping():

    shopping_list=[]
    price=0

    '''read product'''
    f=open(product_path,'rb')
    product_list=pickle.load(f)
    f.close()
    def credit_payment(shoplist,price):
            result=demo_credit.payment(shoplist,price)

            if result == 'success':
                print ('You shopping done!')
            elif result == 'failure':
                print ('Sorry,your credit card balance is not enought!n')
            else:
                print('Sorry,there have some unknown error!n')

    while True:
        for index,item in enumerate(product_list):
            print(index+1,item)
        user_choice=input("Choice a product code('q' is exit.'pay' is settlement):")
        if user_choice.isdigit():
            user_choice = int(user_choice)
            if  user_choice <= len(product_list) and user_choice > 0:
                user_choice -= 1
                price += int(product_list[user_choice][1])
                shopping_list.append(product_list[user_choice][0])
                print ('Add {} to your shopplist!n'.format(product_list[user_choice][0]))
            else:
                print("Sorry,product code isn's exist!n")
        elif user_choice == "q":
            break
        elif user_choice == 'pay':
            print('Your check is {}'.format(price))
            if price != 0:
                credit_payment(shopping_list,price)
            break
        else:
            print("Your enter invalid!n")

我们先进入while循环,打印商品列表,让用户输入选择,加入购物车列表,q是退出,pay是支付,当金额是0时不需要调用信用卡模块。
支付大于0时调用信用卡模块,进入支付接口,从支付接口那里收到'success'或者'failure'的判断,做成相应动作。

然后是管理模块
也是参考之前的作业,调用现成的函数,就完成了

import pickle,os
import balance_module
filename=os.path.abspath(__file__)
dirname=os.path.dirname(filename)
info_path=dirname+os.sep+'user.info'
blackfile_path=dirname+os.sep+'blacklist.txt'
def read_info():
    f=open(info_path,'rb')
    data=pickle.load(f)
    f.close()
    return data
def add_account():
    data=read_info()
    count_name=input('Enter add account("b" is back):')
    if count_name == 'b':
        pass
    else:
        count_balance=input('Enter account balance("b" is back):')
        if count_balance.isdigit():
            count_balance=int(count_balance)
            count_passwd=input('Enter account password("b" is back):')
            if count_passwd == 'b':
                pass
            else:
                data[count_name]={'passwd':count_passwd,'balance':count_balance}
                balance_module.write_balance(data)
                print('Done')
        elif count_balance == 'b':
            pass
        else:
            print('Sorry,your enter balance is not digit!n')
def change_account():
    data=read_info()
    change_name=input('Enter change account name:')
    if change_name in data:
        change_balance=input('Enter change account balance:')
        if change_balance.isdigit():
            change_balance=int(change_balance)
            data[change_name]['balance']=change_balance
            balance_module.write_balance(data)
            print ('Done')
        elif change_balance == 'b':
            pass
        else:
            print('Sorry,your enter is not digit!n')
    elif change_name == 'b':
        pass
    else:
        print ('Sorry,your choose account is not exist!n')

def blacklist():
    f=open(blackfile_path,'rb')
    list=pickle.load(f)
    f.close()
    data=read_info()
    blacklist_name=input('Enter blacklist account name:')
    if blacklist_name in data:
        list.append(blacklist_name)
        f=open(blackfile_path,'wb')
        pickle.dump(list)
        f.close()
    elif blacklist_name == 'b':
        pass
    else:
        print ('Sorry,you choose account is not exist!n')

完成这些之后,我们来到bin目录下实现启动接口

为了防止死锁的问题,获取锁的逻辑中加入了超时时间的判断,大家看代码应该就能看懂

TCC模式,TCC模式主要是围绕业务拆分展开,当业务在横向扩展资源时,解决了服务之间调用的一致性,保证资源访问的事务性

购物车接口

这边只是需要调用购物车模块,只是需要注意现在购物车程序跟核心模块不在一个目录里,注意调用模块时的路径

import os,sys
BASE_DIR=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
CORE_DIR=BASE_DIR+os.sep+'core'
sys.path.append(BASE_DIR)
sys.path.append(CORE_DIR)
import core
core.demo_shoplist.shopping()

把core目录放进系统变量,调用下面的模块文件

管理程序

import os,sys
BASE_DIR=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
CORE_DIR=BASE_DIR+os.sep+'core'
sys.path.append(BASE_DIR)
sys.path.append(CORE_DIR)
from core import demo_manage
action_list=['Add account','Edit account','Block account']
while True:
    for index,item in enumerate(action_list):
        print(index+1,item)
    print('"q" is exit.')
    enter_action=input('Enter your choice:')
    if enter_action.isdigit():
        if enter_action == '1':
            demo_manage.add_account()
            continue
        elif enter_action == '2':
            demo_manage.change_account()
            continue
        elif enter_action == '3':
            demo_manage.blacklist()
            continue
    else:
        if enter_action == 'q':
            exit()
        else:
            print('Sorry,your enter invaild!n')

也是要把core目录放进系统变量里。

atm启动接口

import os
import sys
BASE_DIR=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
CORE_DIR=BASE_DIR+os.sep+'core'
sys.path.append(BASE_DIR)
sys.path.append(CORE_DIR)
from core import demo_credit

action_list=['Query check','Transfer','Query balance','Withdraw','Repay']
while True:
    for index,item in enumerate(action_list):
        print(index+1,item)
    print('"q" is exit.')
    enter_action=input('Enter your choice:')
    if enter_action.isdigit():
        if enter_action == '1':
            '''Query check'''
            continue
        elif enter_action == '2':
            '''Transfer'''
            demo_credit.transfer()
            continue
        elif enter_action == '3':
            '''Query balance'''
            demo_credit.inquire()
            continue
        elif enter_action == '4':
            '''Withdraw'''
            demo_credit.withdraw()
            continue
        elif enter_action == '5':
            '''Repay'''
            demo_credit.repay()
            continue
        else:
            print ('Sorry,you enter invaild!n')
            continue
    else:
        if enter_action == 'q':
            exit()
        else:
            print('Sorry,your enter invaild!n')

主要是打印可操作项,对输入做判断,调用响应的模块,也是需要注意模块调用时的添加系统变量。

三. 对帐逻辑如果按照上面的系统来设计,那么对帐的时候,只要对一下两边成功的流水即可,如果完全一致那么账户应该是没有问题的,如果不一致,那就要去查问题了。关于保证账户正确性这里,也有同事跟我说,之前在公司做的时候,是采取只要有任何写操作之前,都先取一下流水表中所有的流水记录,将amt的值累加起来,看得到的结果是否和余额相同。如果不相同应该就是出问题了。select sum from tb_bill where appid=1;所以这也是为什么我在流水表中,amt字段是要区分正负的原因。

AT模式

用户认证用装饰器

装饰器的样式这样:

def login(func):
    def decorator(*args,**kwargs):

        func(*args,**kwargs)

    return decorator

@login
def func():
    pass

func()

我们对login()函数进行改造

import pickle
import os
filename=os.path.abspath(__file__)
dirname=os.path.dirname(filename)
info_path=dirname+os.sep+'user.info'
blackfile_path=dirname+os.sep+'blacklist.txt'
def login(func):
    def decorator(*args,**kwargs):
        f=open(info_path,'rb')
        user_dict=pickle.load(f)
        f.close()

        blackfile=open(blackfile_path,'rb')
        blacklist=pickle.load(blackfile)
        blackfile.close()
        while True:
            username=input('Enter your account:')
            password=input('Enter your password:')
            if username not in user_dict:
                print('Sorry,your account is not exist!n')
                continue
            elif username in blacklist:
                print ("Sorry you are in blacklist!")
                continue
            else:
                if user_dict[username]['passwd'] != password:
                    print ('Sorry,your password invalid!nPlease try again!n')
                    continue
                else:
                    print ('Welcome {}!'.format(username))
                    break
        func(username,*agrs,**kwargs)

    return decorator

@login
def func(username):
    print(username)
func()

这样测试,可以正常实现功能,现在要融入其他模块中。
在同级目录下写一个测试文件

from demo_login import login
@login
def func(username):
    print(username)
func()

测试,可以实现,于是移植到其他模块里。

import pickle,time,os
import  balance_module
from demo_login import login

filename=os.path.abspath(__file__)
dirname=os.path.dirname(filename)
info_path=dirname+os.sep+'user.info'
log_path=os.path.dirname(dirname)+os.sep+'logs'

@login
def query(username):
    user_dir=log_path+os.sep+username
    if not os.path.exists(user_dir):
        print('Sorry,you do not have check.')
    else:
        cust_log=user_dir+os.sep+'cust.log'
        if os.path.exists(cust_log):
            log=open(cust_log,'r')
            print('You check is:')
            print(log.read())
            log.close()
        else:
            print('Sorry,you do not have check.')

@login
def inquire(username):

    data=read_info()

    print('Your balance is :')
    print(data[username]['balance'])

@login
def payment(username,shoplist,bill):
    data=read_info()
    result=judgment(username,bill)
    if result == 'success':
        data=subtract(username,bill,data)
        balance_module.write_balance(data)
        consumer_logger(username,shoplist,bill)
    return result

@login
def transfer(username):
    data=read_info()
    transfer_account=input('Enter transfer account:')
    transfer_money=input('Enter transfer money:')
    if transfer_account in data:
        if transfer_money.isdigit:
            transfer_money=float(transfer_money)
            result=judgment(username,transfer_money)
            if result == 'success':
                data=subtract(username,transfer_money,data)
                data=add(transfer_account,transfer_money,data)
                print ('Your transfer done!n')
                balance_module.write_balance(data)
                atm_logger(username,'transfer',transfer_money)
            elif result == 'failure':
                print ('Your balance is not enought!n')
            else:
                print ('Sorry,there have some unknown error!n')
        else:
            print ('Sorry,your enter money is not a digit!n')
    else:
        print('Sorry,your enter account is not exist!n')

@login
def withdraw(username):
    data=read_info()
    withdraw_money=input('Enter your withdraw money:')
    if withdraw_money.isdigit():
        cost_money=float(withdraw_money)*(1+0.05)
        result=judgment(username,cost_money)
        withdraw_money=float(withdraw_money)
        if result == 'success':
            if withdraw_money > (data[username]['balance']/2):
                print ('Sorry,your withdraw money is more than avalid balance!n')
            else:
                data=subtract(username,cost_money,data)
                balance_module.write_balance(data)
                atm_logger(username,'withdraw',cost_money)
                print ('Your withdraw done!n')
    else:
        print('Soory,you enter is not digit!n')

@login
def repay(username):
    data=read_info()
    repay_money=input('Enter your repay money:')
    if repay_money.isdigit():
        repay_money=float(repay_money)
        data=add(username,repay_money,data)
        balance_module.write_balance(data)
        atm_logger(username,'repay',repay_money)
        print('Your repay done!n')
    else:
        print('Sorry,your enter is not digit!n')

测试可以实现功能

注意不止credit.py需要用到认证,其他文件可能也需要,像是shoplist.py。代码如下:

import pickle,os
from demo_login import login
import demo_credit

filename=os.path.abspath(__file__)
dirname=os.path.dirname(filename)
product_path=dirname+os.sep+'product.txt'

def shopping():

    shopping_list=[]
    price=0

    '''read product'''
    f=open(product_path,'rb')
    product_list=pickle.load(f)
    f.close()

    @login
    def credit_payment(username,shoplist,price):
            result=demo_credit.payment(username,shoplist,price)

            if result == 'success':
                print ('You shopping done!')
            elif result == 'failure':
                print ('Sorry,your credit card balance is not enought!n')
            else:
                print('Sorry,there have some unknown error!n')

    while True:
        for index,item in enumerate(product_list):
            print(index+1,item)
        user_choice=input("Choice a product code('q' is exit.'pay' is settlement):")
        if user_choice.isdigit():
            user_choice = int(user_choice)
            if  user_choice <= len(product_list) and user_choice > 0:
                user_choice -= 1
                price += int(product_list[user_choice][1])
                shopping_list.append(product_list[user_choice][0])
                print ('Add {} to your shopplist!n'.format(product_list[user_choice][0]))
            else:
                print("Sorry,product code isn's exist!n")
        elif user_choice == "q":
            break
        elif user_choice == 'pay':
            print('Your check is {}'.format(price))
            if price != 0:
                credit_payment(shopping_list,price)
            break
        else:
            print("Your enter invalid!n")

这里用了之后,credit.py里的paymant()函数就可以去掉装饰器了。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

AT模式下会把每个DB当作一个Resource,数据库就是 DataSource Resource。业务通过标准的JDBC接口访问数据库资源,Seata框架会对所有请求进行拦截,做事务操作。

坑:

  • 所有用的文件都需要写成变量形式啊,因为启动入口目录下面没有啊,而且不能写死,必须用os.path.dirname方式加os.sep加文件名拼贴才行啊!!
  • credit里的consumer_logger()函数需要按不同账户创建不同的log啊啊啊!
  • 自己写的模块A import了另一个自己写的模块B,启动程序那里还需要加上模块B所在路径的系统搜索路径啊~不然from package import 模块A的时候,模块A再import 模块B,这个时候不知道从哪里找模块B,因为启动程序的系统路径里找不到模块B;如果是import package,运行__init__.py的时候,from . import 模块B,这个时候启动程序的系统搜索路径里也找不到B。

在每个事务提交时,Seata Resource Manager都会向Transaction Coorrdinator注册一个分支事务。

当请求链路调用完成后,发起方通知TC事务提交或者进行事务回滚,进入两阶段提交调用流程。

二阶段操作时,TC根据之前注册的分支事务回调对应参与者去执行对应资源的第二阶段。

每个资源都有全局唯一的资源ID,在初始化时用这个ID向TC注册,之后的事务协调过程中,TC就可以根据事务ID找到事务和资源的对应关系。事务协调过程中,每个事务的注册都会携带这个资源ID,这样TC可以通过资源ID在第二阶段调用时找到正确的资源了。

简单来说AT模式,就是把数据库当作一个Resource,本地事务提交时会去注册一个分支事务。

TCC模式

在Seata框架中,每组TCC接口当作一个Resource,称为TCC Resource。当然一组TCC接口可以是RPC,也可以是服务内JVM调用。

业务启动时,Seata框架自动扫描识别到对应的TCC接口及其调用方和发布方。

如果是事务的发布方,会在业务启动时向TC注册TC Resource,类似于DataSource Resource,每个资源有唯一的全局资源ID。

如果是事务的调用方,Seata框架给调用方加上切面,类似于AT模式,运行时拦截所有TCC接口调用。

每调用一次Try接口,切面会先向TC注册一个分支事务,然后才会执行原有的RPC调用。

当请求链路调用完成后,TC通过分支事务的资源ID回调正确的参与者去执行对应的TCC资源的Confirm或Cancel方法。

了解了框架模型后,可以知道框架本身会扫描TCC接口,注册资源,拦截接口调用,注册分支事务,之后回调第二阶段接口。

核心是TCC接口的实现逻辑。

TCC接口实现

在业务接入事务框架的TCC模式之后,大部分工作都是在考虑如何实现TCC服务上。

设计TCC接口需要注意业务逻辑的拆解和资源调用的隔离。

业务逻辑分解

需要将操作分成两阶段完成的方式,TCC=Try-Confirm-Cancel 相对于XA等传统模式,特征在于不依赖RM对分布式事务的支持,而是通过业务逻辑分解来实现分布式事务。

TCC模式对于业务系统存在假设,其对外提供的服务需要接受一些不确定性,外部对于业务逻辑的调用首先是个临时操作,外部调用对于后续的业务处理保留取消权。如果业务调用认为全局事务应该回滚,就需要取消之前的临时操作。如果业务调用认为全局事务可以提交,就会放弃之前临时操作的取消权。初步的临时操作最后都会被确认或取消。

TCC对假设抽象成以下逻辑:

初步操作Try:完成所有业务检查,预留必要的业务资源。

确认操作Confirm:真正执行业务逻辑,不做任何检查,只使用Try阶段预留的业务资源。所以只要try成功,confirm必须成功。同时confirm需满足幂等性,因为框架面对不确定性普遍会进行重试,以保证事务提交并只成功一次。

取消操作Cancel:释放Try阶段预留的资源,同样,cancel操作需要满足幂等性。

资源调用隔离

业务系统需要根据自身业务特点和业务模型控制并发,类似于ACID的隔离性。

以金融核心链路的简化模型为例:

美高梅游戏网站登录 5

每个账户或商户有一个账号及其可用余额。交易逻辑涉及到交易,充值,转账,退款等这些都是对账户进行加钱和扣钱。

于是可以把账务系统拆分成两套TCC接口,两个TCC Resource,一个加钱TCC接口,一个扣钱TCC接口。

扣钱TCC

A转账30元给B,A的余额需要从100元减去30元,余额就是所谓的业务资源。

按照TCC原则,第一阶段需要检查并预留业务资源:

检查:在TCC资源的Try接口中检查A是否有足够的余额

本文由美高梅游戏网站登录发布于美高梅棋牌游戏,转载请注明出处:balance和用户密码可以写成字典形式,每月一号进

您可能还会对下面的文章感兴趣: