您的位置:首页 > 数据库 > MySQL

利用mysql "named lock"实现分布式系统的同步

2014-06-18 17:13 471 查看
问题描述

分布式系统要解决的一个核心技术问题就是系统级的同步问题,一个通用的解决方案是Apache ZooKeeper。但是ZooKeeper并不是在所有的场景下都适用,而且配置复杂,且由于需要跨进程间通信,性能也比较差。

一个常用的场景是读写数据库,且要保证数据库读写是同步的,如果数据库采用的是mysql,那么利用mysql的“named lock”就可以快速高效地实现系统级的同步。

考虑一个极端简化的员工信息输入系统:



在mysql中有一张employee表格,用来存储一个公司所有的员工信息:

CREATE TABLE IF NOT EXISTS `employee` (
`employee_id` varchar(8) NOT NULL,
`employee_name` varchar(128) NOT NULL,
PRIMARY KEY (`employee_id`),
) ENGINE=InnoDB DEFAULT CHARSET=utf8


而在指向mysql的每一台服务器上,都有一个程序生成employee数据并录入mysql,这个程序可以是web service,cron job,或者一个python脚本,唯一的要求是保证不能插入具有同样employee_id的记录到employee表。

Insert...Ignore 或 Insert...On Duplicate Key Update

INSERT INTO employee (
employee_id, employee_name
)
VALUES ('xxxx', 'yyyy')
ON DUPLICATE KEY UPDATE employee_name = employee_name

这种方式最为简洁,效率也最高,但前提条件是:

对要求值唯一的column(s),必须定义了primary key或unique key来从table schema上保证其唯一性

Insert...Where Not Exists...

INSERT INTO employee (
employee_id, employee_name
)
SELECT 'xxxx', 'yyyy'
FROM DUAL
WHERE NOT EXISTS (
SELECT 1 FROM employee WHERE employee_id = 'xxxx'
)

这种方式就突破了PK和Unique Key的限制,可以保证插入的数据在任何column(s)上唯一,但缺点是:

在高并发情况下容易发生Deadlock,因为嵌套query里会去锁定a range of rows

Get_Lock & Release_Lock

最后就是本文要重点介绍的mysql 'named lock',其用法和高级编程语言,如Java中的lock object类似,可以提供database-level的同步。其用法就是利用Get_Lock和Release_Lock分别获取或释放一个lock object,具体可参考mysql文档。

‘named lock’的使用有一套固定的模式:

提供一个stored procedure
Get_Lock
....需要同步的操作...
Release_Lock

从应用程序中将插入的数据作为IN参数传给stored procedure,并从OUT参数中获取结果和调式信息

例如:

DELIMITER ;

DROP PROCEDURE IF EXISTS `insert_employee_data`;

DELIMITER //
CREATE DEFINER=`xxxx`@`%` PROCEDURE `insert_employee_data`(
IN inEmployeeID varchar(8),
IN inEmployeeName varchar(128),
OUT outStatus INT(11),  -- -1 means Error; 0 means Skipped; 1 means Inserted
OUT outErrMsg TEXT, -- Error message
OUT outTraceInfo TEXT -- Trace message for debugging
)
SP_LABEL:BEGIN

-- 首先要注册一个Exit Handler,如果Stored Procedure执行过程中发生异常,则会在退出前执行Exit Handler
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
SET @lockObj = 'MYSQL_INSERT_EMPLOYEE_DATA';

SET @releaseLockTime = CURRENT_TIMESTAMP;
SET @lockResult = RELEASE_LOCK(@lockObj);
SET outTraceInfo = CONCAT(IFNULL(outTraceInfo, ''), ' SQLEXCEPTION handler: release_lock=', IFNULL(@lockResult, 'NULL'));
SET outTraceInfo = CONCAT(outTraceInfo, '; release lock time: ', @releaseLockTime);

SET outStatus = -1;
GET DIAGNOSTICS CONDITION 1 outErrMsg = MESSAGE_TEXT;
END;

-- 定义一个Named Lock对象,注意不要和其它SP里用到的Named Lock重复,否则会相互干扰
-- 保证唯一的一个方法是将SP的名字作为Lock对象名字的一部分
SET @lockObj = 'MYSQL_INSERT_EMPLOYEE_DATA';

-- GET_LOCK。60表示最多等待60秒,如果失败,则返回0
SET @lockResult = GET_LOCK(@lockObj, 60);
SET outTraceInfo = CONCAT('@lockResult=', IFNULL(@lockResult, 'NULL'));

-- '0' means fails to get the lock in specified time limit; 'NULL' means error happens when trying to get the lock
IF ((@lockResult IS NULL) OR (@lockResult = 0)) THEN
SET outStatus = -1;
SET outErrMsg = CONCAT('Fail to get in-memory lock object in 60 seconds', @lockObj);
LEAVE SP_LABEL;
END IF;
SET @getLockTime = CURRENT_TIMESTAMP;

SELECT 1 INTO employeeCount
FROM employee
WHERE employee_id = inEmployeeID
LIMIT 1;
SET outTraceInfo = CONCAT(outTraceInfo, '; employeeCount=', IFNULL(vmCount, 'NULL'));

SET outStatus = 0;
IF ((employeeCount IS NULL) OR (employeeCount = 0)) THEN
SET outStatus = 1;

INSERT INTO employee(employee_id, employee_name) VALUES (inEmployeeID, inEmployeeName);
END IF;

SET @releaseLockTime = CURRENT_TIMESTAMP;

-- Release the lock
SET @lockResult = RELEASE_LOCK(@lockObj);

SET outTraceInfo = CONCAT(outTraceInfo, '; release_lock=', IFNULL(@lockResult, 'NULL'));
SET outTraceInfo = CONCAT(outTraceInfo, '; lock time: (', @getLockTime , ') ~ (', @releaseLockTime, ')');
END; //


这是在mysql+分布式应用场景中适用最广的一种同步机制,虽然性能会有些微下降,但在高并发情况下也不会出现死锁的情况。当然,如果在该分布式系统中并不是只有一个centralized mysql,而是有多个database server,并且需要在多个database server中保持数据一致性的话,这种同步机制就不适用了,只能考虑ZooKeepr之类的分布式同步系统,或者Cassandra之类的分布式数据库了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息