原文:http://ichuan.net/post/60/django-mysql-decimal-transaction/

问题

在类银行系统中,涉及金钱计算的地方,不能使用 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 modelsclass 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_checkwarn(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)) / rexcept: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 >= 0locked_obj.save(update_fields=['price'])
except:print 'save failed'

参考:

  1. https://code.djangoproject.com/ticket/13666

django-mysql 中的金钱计算事务处理相关推荐

  1. mysql计算秒_如何在MySQL中基于秒计算时间?

    让我们首先创建一个表-mysql> create table DemoTable ( Logouttime time ); 使用插入命令在表中插入一些记录-mysql> insert in ...

  2. MySQL中利用经纬度计算两点之间的距离

    MySQL中利用st_distance 函数计算经纬度距离 方法一: 精确到0.000000米 例: 经度:lon1,lon2 纬度:lat1 , lat2 SELECT st_distance(PO ...

  3. mysql触发器运算_在MySQL中使用触发器计算列值?

    我有一个表ListLocations,其中包含列Name,StateID,CountryID和DisplayName. stateid指的是列出美国/领土及其缩写的表格,countryid指的是国家及 ...

  4. mysql中length与char_length字符长度函数使用方法

    在mysql中length是计算字段的长度一个汉字是算三个字符,一个数字或字母算一个字符了,与char_length是有一点区别,本文章重点介绍第一个函数. mysql里面的length函数是一个用来 ...

  5. mysql中len是什么意思_MySQL的查询计划中ken_len的含义

    本文首先介绍了MySQL的查询计划中ken_len的含义:然后介绍了key_len的计算方法:最后通过一个伪造的例子,来说明如何通过key_len来查看联合索引有多少列被使用. key_len的含义 ...

  6. mysql中length字符长度函数使用方法

    在mysql中length是计算字段的长度一个汉字是算三个字符,一个数字或字母算一个字符了,与char_length是有一点区别,本文章重点介绍第一个函数. mysql里面的length函数是一个用来 ...

  7. python django mysql写入中文乱码_解决django 向mysql中写入中文字符出错的问题

    之前使用django+mysql建立的一个站点,发现向数据库中写入中文字符时总会报错,尝试了修改settings文件和更改数据表的字符集后仍不起作用.最后发现,在更改mysql的字符集后,需要重建数据 ...

  8. Django多进程中的查询错乱问题以及mysql gone away问题

    Django多进程中的查询错乱问题 因为业务需要,写了一个多进程程序,但是发现查询结果错乱,比如在同一个进程里输出 Asset.object.get(ip=1.1.1.1).ip 发现输出的并不是1. ...

  9. MySQL用函数统计记录总数_在mysql中使用COUNT 或者SUM函数计算记录总数

    count函数的作用 想要真正的理解count函数,我们就必须明白count函数的作用. 作用一:统计某一列非空(not null)值得数量,即统计某列有值得结果数,使用count(col),其中co ...

最新文章

  1. C# HttpHelper帮助类,真正的Httprequest请求时无视编码,无视证书,无视Cookie,网页抓取...
  2. DiscuX END - 553 Envolope sender mismatch with header from..
  3. poj1789(prim)
  4. 面试官问我怎么设计100层大楼的电梯按键,我......
  5. Linux系统的压缩技术
  6. android 定位修改坐标系_数控编程G52局部坐标系的建立及使用
  7. ReviewForJob——java虚拟机的垃圾回收策略(个人总结)
  8. 抖音电商“双11”:品质国货和地方农特产成亮点
  9. 如何查看域控是谁,域控是哪台机器
  10. 订餐系统-第一个用NodeJs实现的项目
  11. c# 关于继承类中构造函数的实现
  12. flash 实例教程
  13. 利用Meshlab进行泊松重建
  14. 生成式模型和判别式模型的区别
  15. Golang可能会踩的58个坑之高级篇
  16. 智商情商哪个重要_你认为哪个更重要,情商还是智商?为什么?
  17. [360] 《如何保持电力接触网与受电弓亲密接触》
  18. cdn转发防攻击_cdn可以防止攻击吗
  19. 树莓派4通过华为ME909S 4G模块连接蜂窝网(非PPP)
  20. Go语言学习笔记-数组、切片、map

热门文章

  1. PCL--学习笔记(持续更新——蜗速)
  2. 音乐计算机编程,计算机音乐编程的基本类型研究
  3. 2020面试题合集之吊打面试官系列(一),Android中为什么需要Handler
  4. 在j2ee的web项目中,执行文件如excel、word导入,文件路径可以是“C:/Users/user/Desktop/abc/abc.xls”这样的路径吗?还是应该是工程的路径:/WEB-INF/
  5. 153870-20-3,S-acetyl-PEG3-alcohol羟基可以反应进一步衍生化合物
  6. vue 根据身份证号获取性别,年龄,出生年月
  7. 机器人史宾_史宾机器人:重启
  8. 面向对象概念及对象、抽象、类的解释
  9. uniapp开发微信小程序设置分包,简单易学(图解)
  10. IDEA2022配置Tomcat服务器教程(超细致版)