DedeCMS随机文章arclist实战:参数+3种性能优化
本文目录
- 最基本的随机调取写法
- arclist 标签的全部可用参数速查
- 限定栏目随机调取
- 跨多栏目随机的执行计划
- 控制缩略图与摘要的随机推荐
- 详情页随机推荐排除当前文章
- 随机抓取的性能优化
- 方案 A:weight 列伪随机的完整 SQL 与 cron
- 方案 B:ID 区间随机的 PHP 实现
- 方案 C:Redis SRANDMEMBER 集成
- 静态化站点的"假随机"破解
- JS Fisher-Yates 二次打乱完整代码
- Ajax 异步加载方案
- 不同 DedeCMS 版本 arclist 行为差异
- 常见踩坑提醒
- 常见问题解答
- orderby='rand' 在 DedeCMS 5.7 SP2 上报错怎么办?
- 随机抓出来的文章总是同一批,怎么破?
- typeid 写了多个 ID 为什么只生效第一个?
- 能不能让随机推荐每小时换一批,而不是每次刷新都换?
- 能不能根据用户兴趣做"个性化随机"?
- 大流量站随机块怎么避免数据库连接打满?
- 随机出来的文章可不可以避免推荐过去 30 天的旧文?
- arclist、likearticle、specart 这三个标签有什么区别?
DedeCMS的arclist用orderby rand在28万行文章表上会拖垮性能。本文讲清最基本的随机调取、限定栏目、详情页随机推荐排除当前文章,再给性能优化的三条路——weight列加索引配cron、ID区间随机、Redis集成,以及静态化站点用Fisher-Yates加Ajax的假随机破解和各版本arclist行为差异。
保哥从2014年开始用织梦DedeCMS给客户搭企业站、地方门户、行业资讯站,最高峰同时维护过十几个DedeCMS项目。在这些项目里,首页随机推荐、文章详情页底部猜你喜欢、侧栏热门几乎是标配,而最稳妥、最不依赖插件的实现办法,就是用DedeCMS自带的arclist标签配合 orderby='rand' 参数来随机抓取。
这篇笔记把我这些年在生产环境用过的所有姿势整理成一份可直接复制粘贴的速查手册:从最基础的随机10篇文章,到限定栏目、限定缩略图、控制摘要长度、避免抓到当前文章本身,再到大数据量下随机抓取的性能问题,全部覆盖。文末附 8 条常见 FAQ。
最基本的随机调取写法
这是最常用的版本,调10篇文章的标题,按随机方式排序:
{dede:arclist pagesize='10' titlelen='35' orderby='rand'}
<li><a href='[field:arcurl/]'>[field:title/]</a></li>
{/dede:arclist}几个关键参数解释一下:
- pagesize='10':抓取10篇文章,常见取值是5、8、10、12,跟前端栅格数对齐就好。
- titlelen='35':限制标题最大长度。注意DedeCMS这里的长度指字节,2字节算一个汉字,所以 titlelen='35' 在纯中文标题里实际是 17 个汉字。
- orderby='rand':核心参数,让DedeCMS在SQL里使用 ORDER BY RAND() 随机排序。
保哥踩过的小坑:早期我习惯写 [field:title/] 不加链接,结果客户在后台改完文章顺序后发现随机推荐压根没起作用——因为单元格里没放 [field:arcurl/],用户点不进去,自然以为标签坏了。记得一定要把链接补上。
arclist 标签的全部可用参数速查
| 参数 | 含义 | 默认值 | 实战取值 |
|---|---|---|---|
| pagesize | 抓取条数 | 10 | 5/8/10/12 |
| titlelen | 标题最大字节 | 30 | 30~50 |
| infolen | 摘要最大字节 | 250 | 80~160 |
| typeid | 栏目ID列表(逗号分隔) | — | 5,8,12 |
| channelid | 模型ID(普通文章=1) | — | 1/2/3 |
| flag | 属性筛选(c=推荐、h=头条、s=幻灯、a=特荐) | — | c/h/s/a |
| orderby | 排序字段(rand/id/click/lastpost/sortrank/senddate) | id | rand |
| orderway | 升降序 | desc | asc/desc |
| imgwidth | 缩略图宽度 | 120 | 240 |
| imgheight | 缩略图高度 | 90 | 160 |
| keyword | 关键词包含筛选(用|分隔多个) | — | SEO|关键词 |
| idlist | 显式指定aid列表 | — | 123,456 |
| notypeid | 排除栏目ID | — | 99 |
| limit | 偏移与条数(MySQL LIMIT) | — | 3,5 |
| arcid | 排除文章ID(用于详情页猜你喜欢) | — | ~aid~ |
这张表里 limit 和 pagesize 经常被搞混——limit='3,5' 等价于 MySQL 的 LIMIT 3,5(从第 4 条开始取 5 条),pagesize='5' 是从头取 5 条。两者不要同时给,会产生歧义。
限定栏目随机调取
企业站经常有这种需求:产品中心首页只随机推 5 个产品,新闻中心首页只随机推 5 篇新闻。这时候要加 typeid 参数,值就是 DedeCMS 后台“常用操作 - 栏目管理”里看到的栏目ID:
{dede:arclist pagesize='10' titlelen='35' typeid='5' orderby='rand'}
<li><a href='[field:arcurl/]' title='[field:title/]'>[field:title/]</a></li>
{/dede:arclist}如果想同时随机抓多个栏目,可以用英文逗号分隔,例如 typeid='5,8,12'。如果想包含子栏目一起抓,再加 idlist='' 或者写在 channelid 里都可以,但保哥个人习惯是直接把要的栏目ID全列出来,逻辑最直白,避免DedeCMS内部递归判断带来的歧义。
保哥的小经验:如果客户后台老是新增、合并、删除栏目,写死 typeid 维护成本会变高。这时候我会用 channelid='1' 按内容模型筛选(比如普通文章是 channelid='1'、图集是 channelid='2'),这样新增栏目也能自动被覆盖。
跨多栏目随机的执行计划
typeid='5,8,12' 会被 DedeCMS 翻译成 WHERE typeid IN (5,8,12)。配合 ORDER BY RAND() 在 MySQL 8 上的执行计划长这样:
EXPLAIN SELECT id, title FROM dede_archives WHERE typeid IN (5,8,12) AND arcrank > -1 ORDER BY RAND() LIMIT 10;
+----+-------------+---------------+------+----------------+--------+---------+------+-------+----------------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+---------------+------+----------------+--------+---------+------+-------+----------------------------------------------+
| 1 | SIMPLE | dede_archives | range| typeid,arcrank | typeid | 4 | NULL | 18420 | Using index condition; Using temporary; ... |
+----+-------------+---------------+------+----------------+--------+---------+------+-------+----------------------------------------------+关键看 Extra 列的 Using temporary——MySQL 用临时表存所有候选行+随机数,然后 filesort 一次。文章表过 10 万条这个临时表会落盘,IO 一上来 TTFB 就崩。后面性能优化章节有具体的破解办法。
控制缩略图与摘要的随机推荐
详情页底部猜你喜欢通常需要带封面图和一句话摘要,写法是这样的:
{dede:arclist pagesize='6' titlelen='30' infolen='80' orderby='rand' imgwidth='240' imgheight='160'}
<li class='recommend-item'>
<a href='[field:arcurl/]'>
<img src='[field:litpic/]' alt='[field:title/]'>
<h3>[field:title/]</h3>
<p>[field:description function='cn_substr(@me,80)'/]</p>
</a>
</li>
{/dede:arclist}- infolen='80':摘要长度,单位还是字节。
- imgwidth 和 imgheight:DedeCMS在生成缩略图链接时会按这个尺寸返回缩略图路径,配合CSS里的 object-fit: cover 排版会更整齐。
- [field:description function='cn_substr(@me,80)'/]:用 cn_substr 强制按汉字截断,避免半个字符的乱码。
如果某些文章压根没传缩略图,渲染出来会是空 src,影响用户体验。保哥通常会在 arclist 标签里加一个判断,没缩略图的就用占位图:
[field:litpic runphp='yes']if(empty(@me)) @me='/images/placeholder.png';@me=@me;[/field:litpic]详情页随机推荐排除当前文章
你有没有遇到过这种尴尬:详情页底部的随机推荐里,第一个就是用户当前正在看的这篇?解决办法是在 arclist 上加 notypeid 或者直接利用 idlist 排除当前 aid:
{dede:arclist pagesize='6' titlelen='30' orderby='rand' idlist='~aid~' notid='yes'}
<li><a href='[field:arcurl/]'>[field:title/]</a></li>
{/dede:arclist}但 idlist 在DedeCMS不同版本里的排除语义不一致,更稳的写法是直接用 likearticle 标签或者自定义SQL:
{dede:sql sql='SELECT id, title, litpic FROM dede_archives WHERE typeid=~typeid~ AND id<>~aid~ AND arcrank>-1 ORDER BY RAND() LIMIT 6'}
<li><a href='/plus/view.php?aid=[field:id/]'>[field:title/]</a></li>
{/dede:sql}这里 ~aid~、~typeid~ 是 DedeCMS 在文章页可以直接读到的两个魔术变量,分别表示当前文章 ID 和栏目 ID。arcrank 大于 -1 是为了过滤掉未审核的稿件。
随机抓取的性能优化
ORDER BY RAND() 在 MySQL 里是出了名的慢——MySQL 会对每一行算一次随机数再排序,文章表上万条以后就能感觉到首页 TTFB 明显变长。保哥的几个实战经验:
- 加缓存。DedeCMS 的 arclist 默认是不缓存的,如果首页随机块每次都打数据库,并发一上来就挂。可以借助 {dede:include filename='xxx.htm'/} 加 OPcache 之类的方案,把随机块单独生成。
- 改用伪随机。在文章表里加一列 weight INT,每天凌晨用 cron 随机刷一次值,前端 ORDER BY weight LIMIT N,性能从全表扫排序变成索引扫描。
- 用 ID 区间随机。先 SELECT MAX(id), MIN(id) FROM dede_archives,PHP 里在区间内随机生成 N 个 ID,再 WHERE id IN (...) 抓回来,速度比 RAND() 快一个数量级。
- 限制 typeid 范围。orderby='rand' 配合 typeid 能让 MySQL 走二级索引子集,比全表 RAND() 强很多。
保哥前年给一个地方资讯站做优化,文章表 28 万条,首页 4 块随机推荐合计加载 1.8 秒。改成 weight 列加每日 cron 刷新以后,首页随机块整体耗时压到了 60ms 以内。
方案 A:weight 列伪随机的完整 SQL 与 cron
-- 一次性建列与索引
ALTER TABLE dede_archives ADD COLUMN weight INT UNSIGNED DEFAULT 0;
ALTER TABLE dede_archives ADD INDEX idx_typeid_weight (typeid, weight);
-- 每日凌晨刷新随机权重
UPDATE dede_archives SET weight = FLOOR(RAND() * 4294967295) WHERE arcrank > -1;
-- 前端 SQL
SELECT id, title, litpic FROM dede_archives
WHERE typeid IN (5,8,12) AND arcrank > -1
ORDER BY weight LIMIT 10;cron 配置(每天凌晨 2 点跑一次):
0 2 * * * /usr/bin/mysql -u root -p$DB_PASS dedecms -e 'UPDATE dede_archives SET weight=FLOOR(RAND()*4294967295) WHERE arcrank>-1;' > /dev/null 2>&1实测 28 万行 UPDATE 全表大约 9 秒。建议在低峰期跑,避免和正常发稿的 INSERT 抢行锁。
方案 B:ID 区间随机的 PHP 实现
function get_random_articles($count = 10, $typeid = null) {
global $dsql;
$where = "arcrank > -1";
if ($typeid) $where .= " AND typeid IN ({$typeid})";
$range = $dsql->GetOne("SELECT MIN(id) AS minId, MAX(id) AS maxId FROM dede_archives WHERE {$where}");
if (!$range) return [];
$candidates = [];
for ($i = 0; $i < $count * 3; $i++) {
$candidates[] = mt_rand($range['minId'], $range['maxId']);
}
$ids = implode(',', array_unique($candidates));
$rows = [];
$dsql->SetQuery("SELECT id, title, litpic FROM dede_archives WHERE id IN ({$ids}) AND {$where} LIMIT {$count}");
$dsql->Execute();
while ($r = $dsql->GetArray()) $rows[] = $r;
return $rows;
}这种方案对 ID 连续性敏感——如果你的 archive ID 间隔很大(比如频繁删稿留下空洞),mt_rand 可能命中空洞导致候选不足。所以我才生成 3 倍候选,多取一些保证总能拿到 $count 条。
方案 C:Redis SRANDMEMBER 集成
把所有文章 ID 提前丢进 Redis 的 Set 里,前端用 SRANDMEMBER 拿 N 个:
// 每日 cron 刷新(PHP CLI)
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$redis->del('article_ids');
$rows = $dsql->GetArray("SELECT id FROM dede_archives WHERE arcrank > -1");
foreach ($rows as $r) $redis->sAdd('article_ids', $r['id']);
// 前端随机抓
$ids = $redis->sRandMember('article_ids', 10);
$inIds = implode(',', $ids);
$rows = $dsql->GetArray("SELECT id, title FROM dede_archives WHERE id IN ({$inIds})");Redis 的 SRANDMEMBER 是 O(N) 内存采样,10 个元素无论从 1000 还是 100 万个 ID 里取都是亚毫秒级。这是 28 万文档站点跑下来最快的方案——实测 P99 1.2ms。
静态化站点的"假随机"破解
DedeCMS 的核心卖点之一是全静态化,但 arclist 标签是在生成 HTML 时执行的,意味着随机一次后所有访客看到的都是同一批文章。要破解这个矛盾,三种思路:
- 把随机块改成 SSI/ESI include:Nginx ssi on; + <!--#include virtual='/m/random_block.php' --> 这种方式 Nginx 边缘处理,每次请求都拿到新的随机块,但其它静态部分依然走文件系统缓存。
- 前端 JS 二次打乱:DedeCMS 生成时多取 30 条文章,前端用 Fisher-Yates shuffle 二次打乱后只渲染前 10 条。
- Ajax 异步加载:HTML 里只放占位 div,DOMContentLoaded 后请求一个 random.php 接口。
JS Fisher-Yates 二次打乱完整代码
// DedeCMS 模板里多生成 30 条,data-pool 存到一个隐藏元素
<script id='pool' type='application/json'>
{dede:arclist pagesize='30' titlelen='30' orderby='rand'}
{'t':'[field:title function="htmlspecialchars(@me)"/]','u':'[field:arcurl/]'},
{/dede:arclist}
</script>
<ul id='recommend'></ul>
<script>
(function(){
var raw = document.getElementById('pool').textContent.trim().replace(/,\s*$/, '');
var pool = JSON.parse('[' + raw + ']');
for (var i = pool.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var t = pool[i]; pool[i] = pool[j]; pool[j] = t;
}
var html = pool.slice(0, 10).map(function(x){
return '<li><a href=\"' + x.u + '\">' + x.t + '</a></li>';
}).join('');
document.getElementById('recommend').innerHTML = html;
})();
</script>这种方案对 SEO 影响小(页面里依然有 30 条文章的链接被搜索引擎抓到),用户体验上每次刷新都不一样。
Ajax 异步加载方案
// 后端 /random.php
<?php
require_once 'include/common.inc.php';
$count = (int)($_GET['n'] ?? 10);
$typeid = (int)($_GET['t'] ?? 0);
$where = "arcrank > -1";
if ($typeid) $where .= " AND typeid = {$typeid}";
$rows = $dsql->GetArray("SELECT id,title,litpic FROM dede_archives WHERE {$where} ORDER BY weight LIMIT {$count}");
header('Content-Type: application/json');
echo json_encode($rows, JSON_UNESCAPED_UNICODE);
// 前端
fetch('/random.php?n=10').then(r => r.json()).then(data => {
document.getElementById('recommend').innerHTML = data.map(x =>
`<li><a href='/plus/view.php?aid=${x.id}'>${x.title}</a></li>`
).join('');
});注意 /random.php 要加 cache-control: public, max-age=300 让 CDN 缓存 5 分钟——纯随机其实不需要每个用户都不同,5 分钟刷新一次的"窗口随机"既减压又保留新鲜感。
不同 DedeCMS 版本 arclist 行为差异
| 版本 | orderby='rand' 是否原生支持 | notypeid 参数 | limit 起始位 |
|---|---|---|---|
| 5.6 SP3 | 是 | 支持 | 从 0 开始 |
| 5.7 | 是 | 支持 | 从 0 开始 |
| 5.7 SP2 | 是(部分二开补丁会改) | 支持 | 从 0 开始 |
| 5.7.106 GBK | 是 | 支持 | 从 0 开始 |
| 5.7.110 UTF-8 | 是 | 支持 | 从 0 开始 |
| 5.8 RC1 | 是 | 改名为 notype | 从 0 开始 |
5.8 RC1 把 notypeid 改成了 notype,但实测兼容旧写法,主要是文档和参数名有冲突时按 5.8 文档为准。如果你跨版本迁移模板,建议先在测试环境跑一遍所有 arclist 标签。
常见踩坑提醒
保哥简单列几个新人最容易栽进去的坑:
- titlelen 单位是字节不是字数,全中文站建议直接给 titlelen='40'(也就是 20 个汉字)做缓冲。
- orderby='rand' 和 flag 同时用要小心。比如 flag='c' 推荐文章,如果推荐稿件本身就少,加上 pagesize='10' 可能根本凑不齐 10 条。
- 后台预览看到不随机? DedeCMS 默认会缓存模板和 SQL 解析结果,改完模板记得到后台“生成 - 更新主页 HTML”,并清掉浏览器缓存。
- 静态化站点 arclist 是在生成 HTML 时执行的,意味着随机一次后所有访客看到的都是同一批文章。要做成真随机得切换为动态调用,或者在前端用 JS 二次打乱。
- UTF-8 与 GBK 站点的字节算法不一样:UTF-8 下中文 3 字节、GBK 下中文 2 字节,所以同一段标题在不同编码站点用 titlelen='30' 截出来的汉字数完全不同。
- arcrank 字段的过滤一定不能省:未审核稿件 arcrank=-1,如果你的 SQL 没写 arcrank>-1,会把后台还在审稿状态的文章也随机抓出来挂到前台。
常见问题解答
orderby='rand' 在 DedeCMS 5.7 SP2 上报错怎么办?
5.7 SP2 默认的 arclist 标签解析是支持 rand 的,如果报未知排序方式,先检查 include/taglib/arclist.lib.php 里有没有被二开过,或者插件改写了排序白名单。把 orderby 临时换成 id 看看是否正常调用,能正常就八九不离十是排序白名单的问题。具体定位办法是在 arclist.lib.php 里搜 sortenum 或者 orderby 字符串,找到那段白名单数组手工补 rand 进去。
随机抓出来的文章总是同一批,怎么破?
大概率是站点开启了静态化,HTML 已经生成好了,自然就不会变了。把对应模板里的 arclist 改用动态调用(比如换成 dede:php 加 SQL 的方式),或者把这一块单独做成 ajax 异步加载。本文第六节有 JS 二次打乱和 Ajax 加载的完整代码。
typeid 写了多个 ID 为什么只生效第一个?
注意中间的逗号必须是英文半角逗号,而且 ID 之间不能有空格。另外 typeid='5,8,12' 这种写法在某些早期版本里只识别第一个,碰上这种情况可以改用 idlist='5,8,12' channelid='1' 的组合写法。具体识别情况跟 include/taglib/arclist.lib.php 里 explode 逗号的逻辑有关,5.7 之后版本基本都没问题。
能不能让随机推荐每小时换一批,而不是每次刷新都换?
可以。最简单的办法就是上一节里说的 weight 列方案,把刷新 cron 改成每小时跑一次。如果不想改表结构,也可以用 PHP mt_srand(date('YmdH')) 给随机数种子做时间分桶——同一小时内的 RAND 序列固定,跨小时自动变化,效果就是每小时一换。这种做法另一个好处是 CDN 缓存可以直接命中,因为同一小时内所有访客拿到的是同一份 HTML。
能不能根据用户兴趣做"个性化随机"?
狭义来说 arclist 是按全站随机的,做不到个性化。但可以变相实现:在文章详情页底部用 arclist 加 typeid=~typeid~(当前栏目随机),用户读哪个栏目就推哪个栏目的随机,是最低成本的"上下文相关推荐"。再进一步可以用 keyword 参数把当前文章 tag 传进去,arclist 会按 keyword LIKE 模糊匹配,做到关键词相关随机。
大流量站随机块怎么避免数据库连接打满?
第一步:上 Redis 缓存方案(本文第五节方案 C),把数据库压力下降一个数量级。第二步:在 PHP 端用 single flight 或者 mutex 锁,避免缓存击穿——同一时刻只有一个请求去打数据库回源,其它请求等结果。第三步:CDN 边缘缓存 random.php 的 JSON 输出 60-300 秒。这三层叠加,单机 QPS 可以从原本的 500 撑到 50000 以上。
随机出来的文章可不可以避免推荐过去 30 天的旧文?
可以。在 SQL WHERE 里加 senddate > UNIX_TIMESTAMP(NOW())-2592000 就是只随机最近 30 天的稿件。但要注意这种做法对内容更新慢的站点不友好——如果你最近 30 天只发了 20 篇,随机块每次都是这 20 篇,新鲜感反而不如全量。一般我会用"加权时间衰减":weight 列每周刷一次,新稿权重高 5 倍,老稿权重正常,自然倾斜到新文章但不完全排除老文章。
arclist、likearticle、specart 这三个标签有什么区别?
arclist 是通用列表标签,可任意组合参数;likearticle 是 DedeCMS 内置的"相关文章"标签,按 keywords/tag 做模糊匹配,参数较少但更智能;specart 是专题文章列表,只在专题页可用。随机推荐场景 95% 用 arclist 加 orderby='rand' 就够了,likearticle 的内部 SQL 写得不够规范,在大表上性能反而比 arclist 差。
FAQPage + Article AI 引用友好版
织梦DedeCMS用arclist标签做随机推荐的完整指南:所有可用参数速查表、限定栏目与排除当前文章写法、ORDER BY RAND性能瓶颈分析,以及weight列伪随机、ID区间随机、Redis SRANDMEMBER三种实测的优化方案,附静态化破解方案。
- 织梦文章调用
- DedeCMS arclist
- 随机推荐
- ORDER BY RAND
- 织梦CMS教程
title: DedeCMS随机文章arclist实战:参数+3种性能优化 author: 张文保 (Paul Zhang) — PatPat SEO 经理 url: https://zhangwenbao.com/dedecms-random-access-to-articles.html published: 2018-06-25 modified: 2026-05-16 source-type: First-hand expert commentary language: zh-CN license: CC BY-NC-SA 4.0 (要求保留原文链接与作者归属)
本文标题:《DedeCMS随机文章arclist实战:参数+3种性能优化》
本文链接:https://zhangwenbao.com/dedecms-random-access-to-articles.html
版权声明:本文原创,转载请注明出处和链接。许可协议: CC BY-NC-SA 4.0