枪与玫瑰

《Redis设计与实现》简读

《Redis设计与实现》核心要点简读

一、数据结构与对象

1. 简单动态字符串(SDS)

  • 优势

    • 记录字符串长度,获取长度复杂度O(1)
    • 记录已分配内存空间,避免缓冲区溢出
    • 空间预分配和惰性释放机制
    • 二进制安全,不以\0判断结束
    • 兼容C字符串函数(以\0结尾)
  • 空间预分配与惰性释放

    • 增长时:长度<1M分配2倍空间;长度≥1M分配+1M空间
    • 缩短时:不立即释放多余内存,记录在free属性中
    • 最佳实践:修改相同键的值时,尽量保持长度相近,避免频繁内存重分配

2. 链表

  • 特点
    • 双向链表,获取前后节点复杂度O(1)
    • 无环,头尾指针指向NULL
    • 记录头尾节点,获取头尾节点复杂度O(1)
    • 记录链表长度,获取长度复杂度O(1)
    • 可存储多种类型数据

3. 字典

  • 实现机制

    • 链地址法解决哈希冲突(相同索引时添加到链表头)
    • 两个哈希表ht[0]ht[1]ht[1]用于rehash)
    • 渐进式rehash:逐步将ht[0]数据迁移到ht[1]
    • 负载因子 = 已保存节点数/哈希表大小
  • rehash步骤

    1. 扩展(无BGSAVE/BGREWRITEAOF且负载≥1,或有BGSAVE/BGREWRITEAOF且负载≥5):为ht[1]分配≥当前节点数×2的内存
    2. 收缩(负载<0.1):为ht[1]分配≥当前节点数的内存
    3. 逐步迁移ht[0]ht[1]
    4. 释放ht[0]ht[1]变为ht[0]

Redis哈希算法:MurmurHash2

4. 跳跃表

  • 用途:有序集合底层实现之一
  • 特点
    • 节点按分值排序,分值相同时按对象大小排序
    • 每个节点可保存字节数组或整数值

5. 整数集合

  • 支持类型

    • int16_t(-32768至32767)
    • int32_t(-2147483648至2147483647)
    • int64_t(-9223372036854775808至9223372036854775807)
  • 升级机制

    1. 根据新元素类型扩展底层数组
    2. 转换现有元素类型并保持有序
    3. 添加新元素(小于所有元素放索引0,大于所有元素放索引length-1)
  • 最佳实践:向同一整数集合添加相同类型的整数,避免升级操作

6. 压缩链表

  • 用途:列表键和哈希键的底层实现之一
  • 特点:添加/删除节点可能造成连锁更新,最坏时间复杂度O(N²)

7. 对象与编码

Redis支持5种对象类型,每种类型有不同编码方式:

类型 编码 说明
REDIS_STRING REDIS_ENCODING_INT 整数值实现的字符串对象
REDIS_STRING REDIS_ENCODING_EMBSTR 小于32字节字符串的简单动态字符串实现
REDIS_STRING REDIS_ENCODING_RAW 大于32字节字符串的简单动态字符串实现
REDIS_LIST REDIS_ENCODING_ZIPLIST 元素长度<64字节且数量<513(默认)
REDIS_LIST REDIS_ENCODING_LINKEDLIST 双向链表实现
REDIS_HASH REDIS_ENCODING_ZIPLIST 元素长度<64字节且数量<513(默认)
REDIS_HASH REDIS_ENCODING_HT 字典实现
REDIS_SET REDIS_ENCODING_INTSET 整数值且数量<513(默认)
REDIS_SET REDIS_ENCODING_HT 字典实现
REDIS_ZSET REDIS_ENCODING_ZIPLIST 元素长度<64字节且数量<128(默认)
REDIS_ZSET REDIS_ENCODING_SKIPLIST 跳跃表+字典实现

查看对象编码命令

全栈优化指南

网站性能优化指南

持续更新中


一、前端优化

1. CDN加速

  • 实现方式:将静态资源(JS、CSS、图片等)部署到CDN节点
  • 优势:减少源站压力,加速全球用户访问速度
  • 最佳实践:为不同类型的静态资源设置合理的缓存策略

2. 图片延迟加载(Lazy Load)

  • 实现方式:使用loading="lazy"属性或JavaScript实现
  • 优势:减少初始页面加载时间,提升首屏加载速度
  • 适用场景:长页面中的图片、非首屏图片

3. 资源合并与压缩

  • 实现方式
    • 合并多个CSS文件为1个
    • 合并多个JS文件为1个
    • 使用工具(如Webpack、Gulp)进行压缩
  • 优势:减少HTTP请求次数,减小文件体积
  • 注意事项:合并后需确保资源加载顺序正确

二、服务端优化

1. 启用Gzip压缩

  • 实现方式
    • Nginx:gzip on; gzip_types text/plain text/css application/json application/javascript text/xml;
    • Apache:启用mod_deflate模块
  • 优势:减少传输数据量,提升页面加载速度
  • 效果:通常可减少50%-70%的传输数据量

2. URI级缓存

  • 实现方式:使用Varnish等缓存服务
  • 优势
    • 减少后端服务压力
    • 提升页面响应速度
    • 降低数据库负载
  • 配置建议:根据页面动态性设置合理的缓存时间

3. PHP慢日志分析

  • 配置方式slow_log = onslow_launch_time = 1
  • 使用价值
    • 识别执行时间长的PHP脚本
    • 优化代码逻辑和I/O操作
    • 定位性能瓶颈
  • 最佳实践:定期分析慢日志,持续优化

4. OPcache优化

  • PHP 7以下:使用OPcache扩展
  • PHP 7+:内置OPcache
  • 注意事项

5. Xhprof性能分析

  • 工具Xhprof
  • 使用价值
    • 识别PHP代码执行热点
    • 分析函数调用栈
    • 优化性能瓶颈
  • 安装:通过PECL安装,配置Nginx/Apache

三、数据端优化

1. 数据缓存策略

  • 实现方式
    • 使用Memcache/Redis作为中间缓存
    • 采用"双写"或"定时同步"策略避免缓存穿透
  • 缓存穿透解决方案
    • 使用布隆过滤器
    • 采用"空值缓存"机制
    • 由独立进程定时从数据库同步数据到缓存
  • 参考《关于memcache与redis》

2. MySQL慢查询分析

  • 配置方式slow_query_log = ONlong_query_time = 2
  • 优化价值
    • 识别低效SQL
    • 优化查询语句
    • 提升数据库响应速度
  • 最佳实践:定期分析慢查询日志,优化索引和查询逻辑

3. 数据库索引优化

  • 原则
    • 为WHERE、JOIN、ORDER BY条件字段创建索引
    • 避免过度索引(影响写入性能)
    • 考虑复合索引
  • 分析工具EXPLAIN命令分析SQL执行计划

4. SQL语句优化

  • 常见优化点
    • 避免使用SELECT *
    • 减少子查询
    • 优化JOIN操作
    • 避免在WHERE子句中对字段进行函数操作
  • 工具支持:使用EXPLAINSHOW PROFILES分析SQL执行

优化效果评估

优化项 评估指标 期望改善
CDN加速 首屏加载时间 降低30%-50%
Gzip压缩 页面传输大小 减少50%-70%
OPcache PHP执行时间 降低20%-40%
缓存策略 数据库查询量 降低70%-90%
SQL优化 查询响应时间 降低50%-80%

重要提示:性能优化应基于实际数据,而非猜测。建议在实施优化后使用工具(如Google PageSpeed Insights、WebPageTest)进行量化评估。

关于Memcache与Redis

Memcache与Redis对比分析

相同点

  • 内存型数据库:两者均将数据存储在内存中,提供高速数据访问
  • LRU淘汰机制:当内存满载时,均采用LRU(Least Recently Used)算法替换数据
  • 分布式支持:均支持分布式部署架构

不同点

Memcache

  • 多线程架构:采用多线程模型处理请求
  • 存储限制
    • 最大Key长度:512B
    • 最大Value长度:1MB

Redis

  • 单线程架构:采用单线程模型处理请求(但通过I/O多路复用实现高并发)
  • 存储限制
    • 最大Key长度:512MB
    • 最大Value长度:512MB
  • 持久化支持:提供RDB和AOF两种持久化机制
  • 丰富数据类型
    • Hashes(哈希表)
    • Sorted Sets(有序集合)
    • Lists(列表)
    • Sets(集合)
  • Lua脚本支持:可在Redis中执行Lua脚本,实现原子操作(如实现原子锁)

性能对比:Redis是单线程架构,整体性能不如Memcache,但平均到每个CPU核心上,Redis在存储小数据(<100KB)时性能更高。当存储大数据(>100KB)时,Redis性能逊于Memcache。

Redis注意事项

删除操作延迟

Redis的DEL操作会有毫秒级延迟,因为其数据回收机制并非实时触发,而是通过定时检查回收数据量是否达到阈值后才触发。

执行顺序

Redis的命令执行顺序由"客户端socket文件描述符的数字大小及核心报告客户端事件的顺序"决定,顺序在大体上遵循"先到先处理"原则,但在细节上呈现无序性。

引用自Redis客户端文档:“该顺序是由客户端socket文件描述符的数字大小及核心报告客户端事件的顺序决定的,因此顺序可以看成不确定的。”

最大连接数限制

  • 问题:在MacBook上测试,当并发请求数达到约17,000时,会出现"Can’t assign requested address"错误
  • 原因:受限于操作系统最大文件描述符限制(TCP的TIME_WAIT问题)
  • 解决方案
    • 使用长连接或单例模式优化
    • 在系统层面调整最大文件描述符限制
    • 减少TCP TIME_WAIT的回收时间

补充说明:以上内容基于当前对Memcache和Redis的理解,如有新发现将及时更新。如需更深入的技术细节,建议参考官方文档或相关技术社区讨论。

「segmentfault问答系列」SESSION固定攻击

Session固定攻击的防御机制分析

问题

攻击者提供包含已知会话ID的链接(例如:<a href='login.php?PHPSESSID=1234'>点击</a>),诱使用户点击该链接。用户点击后可能进行一系列操作,攻击者随后使用该会话ID登录网站,从而劫持用户会话,造成Session固定攻击。

但通常PHP配置中session.use_cookiessession.use_only_cookies均设置为1(即仅通过Cookie传递会话ID),这种情况下是否还能发生Session固定攻击?

分析与解答

Session固定攻击的根本原因

Session固定攻击的发生依赖于以下三个关键条件:

  1. 攻击者获取固定会话ID:攻击者成功获取一个有效的、未使用的会话ID
  2. 被攻击者携带该会话ID请求:被攻击者以获取到的会话ID作为自己的会话ID请求服务端并完成授权操作
  3. 攻击者使用相同会话ID请求:攻击者使用该会话ID请求服务端,获得与被攻击者相同的授权

关键点解析

session.use_only_cookies设置为1的情况下,攻击者仍可实施Session固定攻击,原因如下:

  • 攻击方式转变:虽然不能通过URL参数传递会话ID(因为session.use_only_cookies=1),但攻击者可通过其他方式(如XSS漏洞)修改用户浏览器中的Cookie值
  • 实现路径:攻击者可诱导用户访问包含恶意脚本的页面,该脚本将用户的Cookie值设置为攻击者已知的会话ID
  • 效果等同:当用户后续访问网站时,浏览器会发送该会话ID,服务端将其视为有效会话,攻击者可随后使用相同会话ID登录

防御机制

根本防御方案:在每次授权状态发生改变时(如登录、权限变更),重新生成新的会话ID(Session Regeneration),确保攻击者获取的会话ID失效。

重要提示:仅依赖session.use_only_cookies无法防止Session固定攻击,必须配合会话ID的定期再生机制。

结论

即使PHP配置中session.use_only_cookies=1,Session固定攻击仍可能发生,攻击者可通过XSS等手段修改用户的Cookie值。防御该攻击的核心措施是每次授权状态变更后重新生成会话ID,而非仅依赖会话传输方式的配置。

参考资料:Session固定攻击防御详解
原文来源:SegmentFault问答


安全实践建议

  1. 在用户登录后立即调用session_regenerate_id(true)重新生成会话ID
  2. 在权限变更、角色升级等关键操作后重新生成会话ID
  3. 配合session.use_only_cookies=1session.cookie_httponly=1等安全配置
  4. 通过XSS过滤、内容安全策略(CSP)等手段减少XSS漏洞风险

随机森林算法实践

随机森林算法实践代码详解

运行环境

  • Python版本:Python 2.7
  • 依赖库
    jieba==0.37
    scikit-learn==0.17
    

核心功能说明

该代码实现了基于随机森林的文本分类任务,包含以下核心功能:

  1. 中文分词:使用jieba进行文本预处理
  2. 特征工程:one-hot编码生成特征向量
  3. 模型训练:使用scikit-learn的RandomForestClassifier
  4. 结果预测:支持新文本的分类预测
  5. 模型持久化:训练结果保存与加载

代码结构解析

1. 核心类结构

class RandomForest:
    def __init__(self, is_save=False)  # 初始化模型
    def build_train_data(...)        # 构建训练集
    def predict(self, predict_data)  # 执行预测
    @staticmethod
    def word_segmentation(...)       # 分词处理
    @staticmethod
    def pre_treat_data(...)          # 特征预处理

class RandomForestTools:
    @staticmethod
    def train_data_save/clf_load    # 模型保存/加载
    @staticmethod
    def feature_data_save/load       # 特征数据保存/加载
    @staticmethod
    def one_hot_encode_feature(...)  # 特征编码

2. 数据处理流程

graph TD
    A[原始文本] --> B(分词处理)
    B --> C{特征提取}
    C --> D[one-hot编码]
    D --> E[二维特征矩阵]
    E --> F[模型训练]

3. 关键代码实现

分词处理

@staticmethod
def word_segmentation(train_data):
    """中文分词处理"""
    word_segmentation_result = set()
    for word in jieba.lcut(train_data):
        word_segmentation_result.add(word)
    return word_segmentation_result

特征编码(one-hot)

@staticmethod
def one_hot_encode_feature(data_list, data_set):
    """将文本转换为二进制特征向量"""
    serialize_list = []
    for data in data_list:
        tmp_serialize_list = []
        for key in data_set:
            tmp_serialize_list.append(1 if key in data else 0)
        serialize_list.append(tmp_serialize_list)
    return serialize_list

模型训练

def build_train_data(self, pre_train_data_list, result_list, train_size=0.9):
    # 1. 分词处理
    train_data_list = [self.word_segmentation(text) for text in pre_train_data_list]
    
    # 2. 特征提取
    train_data_feature = set()
    for data in train_data_list:
        train_data_feature.update(data)
    
    # 3. 特征编码
    data_matrix = self.pre_treat_data(train_data_list, train_data_feature)
    
    # 4. 数据集划分
    data_train, data_test, result_train, result_test = train_test_split(
        data_matrix, result_list, train_size=train_size
    )
    
    # 5. 模型训练
    self.__clf = RandomForestClassifier(n_jobs=-1).fit(data_train, result_train)
    
    # 6. 结果评估
    accuracy = self.__clf.score(data_test, result_test)

使用示例

# 训练数据
pre_train_data = [
    u'我很开心', u'我非常开心', u'我其实很开心',
    u'我不开心', u'我一点都不开心', u'我很不开心'
]
result_list = [u'开心', u'开心', u'开心', u'不开心', u'不开心', u'不开心']

# 模型训练
rf = RandomForest()
rf.build_train_data(pre_train_data, result_list)

# 执行预测
print(rf.predict(u'你猜我开心吗?'))  # 输出预测结果

注意事项

  1. Python版本兼容性

GearmanWorker的多进程实现

Gearman多进程实现方案

前言

项目中选择了Gearman作为任务委派的中间件,但原生的Python扩展包仅支持单进程。在尝试将Gearman改造成自适应多进程的过程中,我走了一些误区,特此记录这些"坑"以及目前的最优解决方案。

实现思路

实现方式

  1. 主进程接收任务,子进程处理任务:主进程作为任务接收者,接收任务后分派给子进程处理,子进程直接返回结果给Gearman
  2. 多进程接收并处理任务:批量fork多个子进程注册任务,子进程间互不影响,各自完成接收、处理任务

第一种实现方式的优缺点

优点

  • 主进程只进行轮训任务接收,提高单条Gearman请求通道利用率(worker多数时间消耗在等待接收请求上)
  • 子进程直接返回任务结果,主进程无需关心结果,专注接收任务

缺点

  • 需将Gearman socket传递给子进程,实现复杂(socket实例无法通过pickle传递,Unix的sendmsg虽可传递socket但构造GearmanWorker很麻烦)
  • 父进程仍持有原socket句柄,导致任务请求方无法收到子进程返回的结果

第二种实现方式的优缺点

优点

  • 等价于fork多个原进程,逻辑和作业方式无改变
  • 可在fork子进程前完成公有资源加载,避免重复加载

缺点

  • 子进程异常退出后主进程无法感知,重启的子进程未正确注册到Gearman
  • 主进程异常退出后子进程无法感知,导致僵尸进程

解决方案

  1. 利用PID文件记录子进程PID:确保主进程退出后仍能通过PID文件终止子进程
  2. 利用Redis的发布订阅模式:实现GearmanWorker的正常退出

代码实现

# -*- coding: utf-8 -*-

import os
import signal
import threading
import multiprocessing

import redis
from gearman.worker import GearmanWorker, POLL_TIMEOUT_IN_SECONDS

WORKER_PROCESS_PID = '/tmp/multi_gearman_worker.pid'


class MultiGearmanWorker(GearmanWorker):
    """多进程Gearman worker"""
    def __init__(self, host_list=None, redis_host=None, redis_port=None, pid=WORKER_PROCESS_PID):
        super(MultiGearmanWorker, self).__init__(host_list=host_list)
        self.redis_host = redis_host
        self.redis_port = redis_port
        self.pid = pid

    def work(self, poll_timeout=POLL_TIMEOUT_IN_SECONDS, process=multiprocessing.cpu_count()):
        """开始作业,进程阻塞
        :param poll_timeout: gearman连接超时时间,值越小worker召回越快但请求越频繁
        :param process: 工作进程数,默认为CPU核心数
        :return:
        """
        print('Clear last process.')
        self.gearman_worker_exit()
        print('Ready to start %d process for work.' % process)
        gm_poll = multiprocessing.Pool(process)
        for x in range(0, process):
            gm_poll.apply_async(gearman_work, (self, poll_timeout, self.pid))
        gm_poll.close()
        gm_poll.join()
        # 正常退出则删除子进程PID文件
        if os.path.isfile(self.pid):
            os.remove(self.pid)

        print('Multi gearman worker exit.')

    def gearman_worker_exit(self):
        """结束子进程"""
        if not os.path.isfile(self.pid):
            return True

        with open(self.pid, 'r+') as f:
            for pid in f.readlines():
                pid = int(pid)
                try:
                    os.kill(pid, signal.SIGKILL)
                    print('Kill process %d.' % pid)
                except OSError:
                    print('Process %d not exists' % pid)
                    continue
        os.remove(self.pid)
        print('Remove process pid file.')
        return True

# 子进程使用的Gearman工作开关标识
GEARMAN_CONTINUE_WORK = True


def gearman_work(gm_worker, poll_timeout=POLL_TIMEOUT_IN_SECONDS, pid=WORKER_PROCESS_PID):
    """以多进程方式开启Gearman worker"""
    try:
        # 记录子进程PID以便主进程重启后清除未退出的子进程
        with open(pid, 'a+') as f:
            f.write("%d%s" % (os.getpid(), os.linesep))

        print('Child process start for work.')
        continue_working = True
        worker_connections = []
        d = threading.Thread(name='monitor', target=gearman_monitor,
                             args=(gm_worker.redis_host, gm_worker.redis_port))
        d.start()

        def continue_while_connections_alive(any_activity):
            return gm_worker.after_poll(any_activity)

        # 轮询连接,等待任务
        while continue_working and GEARMAN_CONTINUE_WORK:
            worker_connections = gm_worker.establish_worker_connections()
            continue_working = gm_worker.poll_connections_until_stopped(
                worker_connections, continue_while_connections_alive, timeout=poll_timeout)

        # 关闭所有连接
        for current_connection in worker_connections:
            current_connection.close()

        print('Gearman worker closed')
        return None
    except Exception as e:
        print(e)


def gearman_monitor(redis_host, redis_port):
    """监听Redis的发布订阅信号"""
    global GEARMAN_CONTINUE_WORK
    print('Start gearman monitor.')
    while GEARMAN_CONTINUE_WORK:
        try:
            sub = redis.StrictRedis(redis_host, redis_port).pubsub()
            sub.subscribe('hot')
            for i in sub.listen():
                if isinstance(i.get('data'), str):
                    if i.get('data') == 'exit':
                        print('Gearman monitor receive restart signal.')
                        GEARMAN_CONTINUE_WORK = False
                        sub.unsubscribe('hot')
                        break
        except Exception as e:
            print(e)
            try:
                sub.unsubscribe('hot')
            except Exception:
                pass

    print('Gearman monitor closed')


if __name__ == '__main__':
    def test_multi_gearman_worker(worker, job):
        print(worker)
        print(job)

    # 初始化多进程Gearman worker
    gearman_worker = MultiGearmanWorker(
        host_list=('127.0.0.1:4730',), 
        redis_host='127.0.0.1', 
        redis_port=6379
    )
    # 注册任务
    gearman_worker.register_task('test_multi_gearman_worker', test_multi_gearman_worker)
    # 启动工作
    gearman_worker.work()

附录

关键点说明