MyISAM与InnoDB的对比
https://www.biaodianfu.com/mysql-myisam-innodb.html
MyISAM的读取性能要比InnoDB好一些,但这略微的优势远比不上InnoDB的行级锁和事务操作等特性。MySQL从5.6开始,将InnoDB作为默认的数据库引擎,因此除非需要使用MyISAM的特有功能,都应该使用InnoDB数据库引擎。Discuz论坛中的帖子主表(pre_forum_post)使用MyISAM特有的非第一列自增的联合主键,使得我们不能直接将改变转换为InnoDB。
数据表的改造
pre_forum_post表的结构为:
CREATE TABLE pre_forum_post (
pid int(10) unsigned NOT NULL, #帖子id
fid mediumint(8) unsigned NOT NULL DEFAULT '0', #论坛id
tid mediumint(8) unsigned NOT NULL DEFAULT '0', #主题id
...
position int(8) unsigned NOT NULL AUTO_INCREMENT, #楼层编号
PRIMARY KEY (tid,position),
UNIQUE KEY pid (pid),
...
) TYPE=MyISAM;
主键为(tid,position)
,自增列为position
,这种结构在MyISAM中不同的tid对应的position会分别从1开始自增,正好对应不同的帖子的楼层数都是从1开始自增的。
将该表转换为InnoDB,我们需要将(tid,position)
设为唯一索引,来保障同一个帖子的楼层不会重复,并将pid作为新的主键。
ALTER TABLE `pre_forum_post` MODIFY COLUMN
`position` int(8) UNSIGNED NOT NULL AFTER `replycredit`,
ADD PRIMARY KEY (`pid`),
DROP INDEX `pid`,
ADD UNIQUE INDEX `tid_position` (`tid`, `position`)
由于主键为(tid,position)
,自增列为position
,这种结构在InnoDB中是不被支持的。因此我们在插入帖子表之前先要通过tid查询该帖子目前的最大楼层数,然后再加1作为position的值。
# 查询得到该帖子的最大楼层(11)
SELECT MAX(position) AS max FROM pre_forum_post WHERE tid=1234
# 插入帖子数据以及楼层数(12)
INSERT INTO position SET tid=1234,position=12, ...
上面这种方案有一个问题就是在高并发情况下,多个进程通过SELECT查询同一个帖子的最大楼层数时,很可能得到相同的值,进而发表的帖子的楼层数相同,会导致许多用户发帖失败。但由于存在唯一索引的限制,只会有一个用户发帖成功,并不会存在相同楼层的情况。
高并发时发帖失败的问题
因为SELECT查询的速度很快,并且SELECT操作加的是共享锁,所以在高并发时会有多个进程读取到相同的最大楼层数,导致发帖失败。
为了避免该问题的发送,可以采用悲观锁方案,在SELECT语句最后加上FOR UPDATE
,并将SELECT和INSERT作为一个事务进行处理(由于Discuz的DB类不支持事务操作,需要使用mysqli进行数据库的操作)。
SELECT MAX(position) AS max FROM pre_forum_post WHERE tid=1234 FOR UPDATE
还有一种方案是将SELECT和INSERT操作合并为一条语句,由于mysql中每条语句都是一个原子操作,因此无需自己实现事务操作,更加方便。
INSERT INTO position SET tid=1234,position=(SELECT MAX(tmp.position)+1 AS max FROM pre_forum_post AS tmp WHERE tid=1234) ...
数据库引擎转换脚本
Discuz所有数据表以及结构
https://git.oschina.net/ComsenzDiscuz/DiscuzX/blob/master/upload/install/data/install.sql
a. 查询得到数据库所有数据库引擎为MyISAM的数据表
SHOW TABLE STATUS WHERE Engine='MyISAM'
这里一定要注意,Discuz中并不是所有的数据表都是MyISAM,有一些数据表是HEAP(MEMORY)。
因此不能使用 SHOW TABLES 命令将所有的数据表进行转换
Memory的数据是存储在内存中的,速度比InnDB快很多,如果进行转换,则反而会使论坛的性能下降。
b. 转换pre_common_member_grouppm和pre_common_member_grouppm
数据表
ALTER TABLE `pre_common_member_grouppm`
MODIFY COLUMN `gpmid` smallint(6) UNSIGNED NOT NULL AFTER `uid`,
ENGINE=InnoDB
pre_common_member_grouppm 虽然表结构中的gpmid是自增的,但是通过查阅代码发现在插入操作时,并没有使用该特性,因此无需对插入操作的代码进行改造。
ALTER TABLE `pre_forum_post`
MODIFY COLUMN `position` int(8) UNSIGNED NOT NULL AFTER `replycredit`,
DROP PRIMARY KEY,
ADD PRIMARY KEY (`pid`),
DROP INDEX `pid`,
ADD UNIQUE INDEX `tid_position` (`tid`, `position`)
c. 转换其他数据表
ALTER TABLE XXX ENGINE = InnoDB