需求 功能: P23
登录 cookie
购物车 cookie
缓存生成的网页
缓存数据库行
分析网页访问记录
高层次角度下的 Web 应用 P23从高层次的角度来看, Web 应用就是通过 HTTP 协议对网页浏览器发送的请求进行响应的服务器或者服务(service)。 Web 请求一般是无状态的(stateless),即服务器本身不会记录与过往请求有关的任何信息,使得失效的服务器可以很容易地被替换掉。
Web 服务器对请求进行响应的典型步骤:
服务器对客户端发来对请求(request)进行解析
请求被转发给一个预定义的处理器(handler)
处理器可能会从数据库中取数据
处理器根据数据对模板(templete)进行渲染(render)
处理器向客户端返回渲染后的内容作为对请求的响应(response)
基础数值量 P24本次实践所有内容均围绕着发现并解决一个虚构的大型网上商店来展开的,一些基础数据量如下:
每天有 500 万名不同的用户
每天有 1 亿次点击
每天从网站购买超过 10 万件商品
实现 登录和 cookie 缓存 P24有两种常见的方法可以将登录信息存储在 cookie 里面:
签名(signed) cookie:通常会存储用户名,可能还会有其他网站觉得游泳的信息,例如:最后一次成功登录时间、用户 id 等。还会有签名,用服务器验证 cookie 中的信息是否被修改。
令牌(token) cookie:存储遗传随机字节作为令牌,服务器根据令牌在数据库中查找令牌的拥有着。随着时间的推移,旧令牌会被新令牌取代。
签名 cookie 和令牌 cookie 的优点与缺点 P24 cookie 类型 优点 缺点签名 cookie 验证 cookie 所需的一切信息都存储在 cookie 里面。cookie 可以包含额外的信息,并且对这些信息进行签名也很容易 正确地处理签名很难。很容易忘记对数据进行签名,或者忘记验证数据的签名,从而造成安全漏洞
令牌 cookie 添加信息非常容易。 cookie 的体积非常小,因此移动终端和速度较慢的客户端可以更快地发送请求 需要在服务器存储更多信息。如果使用的是关系数据库,那么载入和存储 cookie 的代价可能会很高
本次实践采用令牌 cookie 来引用存储的用户登录信息的条目。除登录信息外,还需要将用户访问时长和已浏览商品的数量等信息存储到数据库里面,便于未来通过分析这些信息来学习如何更好地向用户推销商品。
使用一个哈希表来存储登录 cookie 令牌与已登录用户之间的映射,根据给定的令牌查找对应的用户 id。 P24
// redis key type RedisKey string const ( // 登录用户 哈希表(field:token;value:userId) LOGIN_USER RedisKey = "loginUser" // 用户最近操作时间 有序集合 USER_LATEST_ACTION RedisKey = "userLatestAction" // 用户最近浏览商品时间 有序集合 前缀(存储 itemId 及浏览的时间戳) VIEWED_ITEM_PREFIX RedisKey = "viewedItem:" // 用户购物车 哈希表 前缀(存储 itemId 及其加车数量) CART_PREFIX RedisKey = "cart:" // 请求返回值缓存 字符串 前缀(存储 请求对应返回值的 序列化串) REQUEST_PREFIX RedisKey = "request:" // 缓存数据间隔(单位:ms) 字符串 ITEM_INTERVAL RedisKey = "itemInterval" // 数据缓存时间(精确到毫秒) 有序集合 ITEM_CACHED_TIME RedisKey = "itemCachedTime" // 数据(商品)的json 字符串 前缀(存储 itemId 的相关信息) ITEM_PREFIX RedisKey = "item:" // 商品浏览次数 有序集合(存储 itemId 及浏览次数) ITEM_VIEWED_NUM RedisKey = "itemViewedNum" ) // 根据 token 获取 userId(err 不为 nil 时,用户已登录且 userId 有效) func GetUserId(conn redis.Conn, token string) (userId int, err error) { return redis.Int(conn.Do("HGET", LOGIN_USER, token)) }此时我们已经能通过 token 获取用户 id 了,还需要相应的设置方法,即用户每次操作时都会进行相关信息设置,并更新 token 的最近操作时间戳。如果用户正在浏览一个商品,则还需要将该商品添加到浏览商品历史有序集合中,且限制一个用户最多记录最新的 25 个商品浏览记录。 P25
// 一个用户浏览的商品最多记录最新的 25 个 const MAX_VIEWED_ITEM_COUNT = 25 // 更新令牌相关信息(用户有操作是就会更新,如果当前操作是浏览商详页,则传入 itemId,否则 itemId <= 0) func UpdateToken(conn redis.Conn, token string, userId int, itemId int) { currentTime := time.Now().Unix() + int64(itemId) // 更新令牌及相应 userId 对应关系 _, _ = conn.Do("HSET", LOGIN_USER, token, userId) // 最近操作时间 记录 token 的时间戳 // (不能记录 userId 的时间戳,userId 不会变,所以即使 token 更新了, userId 对应的时间戳还是会更新,没法判断当前 token 是否过期) _, _ = conn.Do("ZADD", USER_LATEST_ACTION, currentTime, token) // 当前浏览的时商品详情页时,会传入 itemId,否则 itemId <= 0 if itemId > 0 { // 决定用 userId 当做后缀:token 可能会改变,而 userId 是唯一确定的 viewedItemKey := VIEWED_ITEM_PREFIX + RedisKey(strconv.Itoa(userId)) // 添加(更新)最近浏览商品信息 _, _ = conn.Do("ZADD", viewedItemKey, currentTime, itemId) // 移除时间戳升序状态下 [0, 倒数第 MAX_VIEWED_ITEM_COUNT + 1 个] 内的所有元素,留下最近的 MAX_VIEWED_ITEM_COUNT 个 _, _ = conn.Do("ZREMRANGEBYRANK", viewedItemKey, 0, -(MAX_VIEWED_ITEM_COUNT + 1)) } }存储的数据会越来越多,且登录用户不会一直操作,所以可以设定最多支持的登录用户数,并定期删除超过的那些最久为操作的用户登录信息。 P26