django-mysql 中的金钱计算事务处理 分类: 小技巧 python学习 mysql 2015-07-27 16:52 10人阅读 评论(0) 收藏
2015-07-27 16:52
1231 查看
原文:http://ichuan.net/post/60/django-mysql-decimal-transaction/
所以 python 中提供了
Decimal 类型,用以人类期望的方式处理此类计算。
DecimalField 字段:
假如使用了 MySQl backend, MySQL 中也支持 DECIMAL 类型,django 自动处理类型转换。
models 定义:
先创建一个 object 试试:
然后更新。假设商品降价,我们需要把 price 减去 9.99:
来看看具体执行的 SQL 是什么:
输出为:
这种 update 会有个问题。在并发很高时,会遇到类似多线程的问题,因为加减操作都在客户端,某个线程写入 price 时可能之前拿到的已经被别人更新过了,所以需要原子写入。SQL 表述为:
这种方式在 django 中可以用
F() 表达式来实现:
另外,在调用 .save() 时,可以用 update_fields 传入需要 update 的字段(如上)。不然 django 可能会把所有字段都放在 SQL 中。
貌似很完美。但假如减去的 Decimal 和字段的精确度不一致呢?
price 字段精确到小数点后 2 位,给它减去了 3 位的一个数,会报异常:
这是 MySQL 报了一个 warning,django 因此报了异常。更新操作未成功。
解决方法很简单,在入库前,我们手动转换下精确度:
OK. 但对于只有 MySQL 在计算时才知道精确度多少的呢?比如乘积:
这种还是会报上面的异常,我们没法在 django 层面对结果做类型转换。这种类型怎么办?只有上 raw sql 了。
另外,事务处理在 django 中可以用
transaction.atomic() 来做。事务的意思是代码块中的数据库操作要么都成功执行,没有异常;要么都不执行。实际操作中,用事务+原子操作配合可实现正确的金钱操作逻辑。
raw sql 加上事务,上面的例子改为:
我们在 MySQL 层面用
这种做法应该很强健了。最后还有一点,可能会有多个操作同时进行,实际应用中,减法操作我们可能希望在事务中检验 price 被更新后要大于 0, 这个最好也能在 MySQL 层面做,把责任推给它。上面的例子改为:
ret 是更新的行数,假如正确更新了,ret 就是 1。
django 中有
select_for_update(), 所以金钱操作时最好不要用自增自减运算,而是用
select_for_update() 的行级锁来避免冲突。
这样最后一个例子可以改为:
参考:
https://code.djangoproject.com/ticket/13666
问题
在类银行系统中,涉及金钱计算的地方,不能使用 float 类型,因为:# python 中 >>> 0.1 + 0.2 - 0.3 5.551115123125783e-17 >>> 0.1 + 0.2 - 0.3 == 0.3 False // js 中 > 1.13 * 10000 11299.999999999998 > 1.13 * 10000 == 11300 false
所以 python 中提供了
Decimal 类型,用以人类期望的方式处理此类计算。
django 中的 DecimalField
django 中提供了对应 Decimal 类型的DecimalField 字段:
class DecimalField(max_digits=None, decimal_places=None[, **options])
max_digits表示最大位数,
decimal_places表示小数点后位数。假如你的系统里最多存 9999 元人民币,人民币小数点后可以精确到 2 位,需要的
max_digits就是 6,
decimal_places就是 2。
假如使用了 MySQl backend, MySQL 中也支持 DECIMAL 类型,django 自动处理类型转换。
示例
下面以一个 django app 为例演示。models 定义:
from django.db import models class MyModel(models.Model): price = models.DecimalField(max_digits=16, decimal_places=2, default=0)
先创建一个 object 试试:
from decimal import Decimal obj = MyModel.objects.create(price=Decimal('123.45'))
然后更新。假设商品降价,我们需要把 price 减去 9.99:
obj.price -= Decimal('9.99') obj.save()
来看看具体执行的 SQL 是什么:
from django.db import connection print connection.queries[-1]
输出为:
UPDATE `hello_mymodel` SET `price` = '113.46' WHERE `id` = 1
这种 update 会有个问题。在并发很高时,会遇到类似多线程的问题,因为加减操作都在客户端,某个线程写入 price 时可能之前拿到的已经被别人更新过了,所以需要原子写入。SQL 表述为:
UPDATE `hello_mymodel` SET `price` = 'price' - '9.99' WHERE `id` = 1
这种方式在 django 中可以用
F() 表达式来实现:
obj.price = F('price') - Decimal('9.99') obj.save(update_fields=['price'])
另外,在调用 .save() 时,可以用 update_fields 传入需要 update 的字段(如上)。不然 django 可能会把所有字段都放在 SQL 中。
貌似很完美。但假如减去的 Decimal 和字段的精确度不一致呢?
obj.price = F('price') - Decimal('9.999') obj.save(update_fields=['price'])
price 字段精确到小数点后 2 位,给它减去了 3 位的一个数,会报异常:
Traceback (most recent call last): File "<console>", line 1, in <module> ... File "/home/yc/envs/hello/local/lib/python2.7/sitein _warning_check warn(w[-1], self.Warning, 3) Warning: Data truncated for column 'price' at row 1
这是 MySQL 报了一个 warning,django 因此报了异常。更新操作未成功。
解决方法很简单,在入库前,我们手动转换下精确度:
def to_decimal(s, precision=8): ''' to_decimal('1.2345', 2) => Decimal('1.23') to_decimal(1.2345, 2) => Decimal('1.23') to_decimal(Decimal('1.2345'), 2) => Decimal('1.23') ''' r = pow(10, precision) v = s if type(s) is Decimal else Decimal(str(s)) try: return Decimal(int(v * r)) / r except: return Decimal(s) obj.price = F('price') - to_decimal('9.999', 2) obj.save(update_fields=['price'])
OK. 但对于只有 MySQL 在计算时才知道精确度多少的呢?比如乘积:
obj.price = F('price') * Decimal('9.99') obj.save(update_fields=['price'])
这种还是会报上面的异常,我们没法在 django 层面对结果做类型转换。这种类型怎么办?只有上 raw sql 了。
另外,事务处理在 django 中可以用
transaction.atomic() 来做。事务的意思是代码块中的数据库操作要么都成功执行,没有异常;要么都不执行。实际操作中,用事务+原子操作配合可实现正确的金钱操作逻辑。
raw sql 加上事务,上面的例子改为:
from django.db import transaction, connection try: with transaction.atomic(): cursor = connection.cursor() cursor.execute( 'UPDATE `hello_mymodel` SET `price` = CAST((`price` * %s) AS DECIMAL(16, 2)) WHERE `id` = %s', [Decimal('9.999'), obj.id] ) except: print 'save failed'
我们在 MySQL 层面用
CAST(%s AS DECIMAL(16, 2))来把结果转为和 price 字段同样格式的 Decimal 类型。
这种做法应该很强健了。最后还有一点,可能会有多个操作同时进行,实际应用中,减法操作我们可能希望在事务中检验 price 被更新后要大于 0, 这个最好也能在 MySQL 层面做,把责任推给它。上面的例子改为:
try: with transaction.atomic(): cursor = connection.cursor() ret = cursor.execute( 'UPDATE `hello_mymodel` SET `price` = `price` - CAST(%s AS DECIMAL(16, 2)) WHERE `id` = %s AND `price` >= CAST(%s AS DECIMAL(16, 2))', [Decimal('9999.999'), obj.id, Decimal('9999.999')] ) assert ret except: print 'save failed'
ret 是更新的行数,假如正确更新了,ret 就是 1。
总结
金钱运算用 Decimal 类型;django 中字段间操作用 F();F() 配合 Decimal 计算时,结果类型和字段类型完全一致的没问题,不可预测的用 raw sql。20140604 更新
Decimal 类型在 MySQL 中运算还是有问题,即便结果类型和字段类型完全一致还是可能出问题(见 http://stackoverflow.com/q/23925271/265989 )。django 中有
select_for_update(), 所以金钱操作时最好不要用自增自减运算,而是用
select_for_update() 的行级锁来避免冲突。
这样最后一个例子可以改为:
try: with transaction.atomic(): locked_obj = MyModel.objects.select_for_update().get(pk=obj.id) locked_obj.price -= to_decimal('9999.999', 2) assert locked_obj.price >= 0 locked_obj.save(update_fields=['price']) except: print 'save failed'
参考:
https://code.djangoproject.com/ticket/13666
相关文章推荐
- python学习总结
- 17个新手常见Python运行时错误
- python实现链表
- 在Python的Django框架中创建语言文件
- Vim as Python IDE on windows(转)
- python-re模块-使用案例
- 给Python初学者的一些技巧
- python基础教程总结15——3 XML构建网址
- 用Python进行自然语言处理-1. Language Processing and Python
- python(2.7.10) 安装后启动错误:IDLE's subprocess didn't make connection
- 在Python中的Django框架中进行字符串翻译
- python的copy,总结的很好
- Mac OS X 程序员利器 – Homebrew安装与使用以及python学习指南
- python 多线程中同步的小例子
- python class __slots__
- Logistic回归python代码
- Python正则表达式的七个使用范例
- python 链接hive
- 利用hadoop+hive+python+qqwry.dat批量解析ip物理地址
- window7 64位 python2.7 安装libsvm和liblinear