结论先行
前几天在公司用户反馈系统,统计某项活动反馈用户数时,发现原始的用户请求数据表与汇总的表结果不一致。问题原因是:不久之前项目改用了apache(之前是用Django 方式 manage.py runserver 启动服务)进行部署。
分析
先讲述下apache的2种工作模式
- prefork 多进程模型
预先生成进程,一个请求用一个进程处理。优点:稳定可靠,任何一个进程崩溃都不会影响其他进程;缺点:性能差,特别是并发量非常大时,非常消耗内存资源,会有大量的进程切换。
- worker 多线程模型
基于线程,启动多个进程,每个进程中生成多个线程。进程负责响应请求,而线程负责处理请求。因为linux不是原生支持线程,线程同步模型复杂,必须处理锁争用的问题,所以在Linux上不建议。测试发现,在linux系统上,worker 的效率并没有 prefork 高。这是默认不是worker的原因。
采用Django manage.py runserver 启动服务实际测试是1个进程处理服务
定位
apache多进程处理请求和Django单个进程处理请求,本质上没有啥区别啊,只是最大处理请求量上会存在一些差别。为什么会影响用户计数了。仔细看了下计数业务的代码,发现采用的是单例模式。很明显在apache多进程模式下,单例模式是存在问题的。每个独立的进程对数据库同一行进行读写操作在没有加锁的情况下,会出现数据错乱问题。
class _SuiteCurrentGroup:
_class_lock = threading.Lock()
_instances = {}
_index = None
def __init__(self, id):
self.id = id
self._lock = threading.Lock()
@classmethod
def get_instance(cls, id):
if id not in cls._instances:
with cls._class_lock:
_SuiteCurrentGroup._do_init(id)
value = _SuiteCurrentGroup.get_and_increase(id)
cls._instances[id] = value
return value
else:
with cls._class_lock:
value = _SuiteCurrentGroup.get_and_increase(id)
cls._instances[id] = value
return cls._instances[id]
@classmethod
def _do_init(cls, id):
cur_group_index_list = Suite.objects.filter(id=id).values_list('cur_group_index', flat=True)
cls._index = cur_group_index_list[0]
@classmethod
def get_and_increase(cls, id):
old_value = cls._index
cls._index += 1
Suite.objects.filter(id=id).update(cur_group_index=cls._index)
return old_value
解决
采用悲观锁的方式进行数据更新
with transaction.atomic():
suite = Suite.objects.select_for_update().get(id=suite_id)
cur_group_index = suite.cur_group_index
suite.cur_group_index = suite.cur_group_index + 1
suite.save()