书上同时也将用户的历史访问记录删除了,我这里没做删除,把存储历史访问记录的有序集合当作数据库用,与用户登录状态无关,即使用户的登录信息被删除了,仍旧可以分析相应的数据,比较符合实际使用情况。
清理方法内部死循环可能不太优雅,但是使用 go 关键字以协程运行,可以在一定程度上达到定时任务的效果,且和大部分情况下定时任务一样,会随着主程序的退出而退出。
// 登录用户最多记录 1000w 条最新信息(其实这时候早已经是大 key 了,不过当前场景不需要太过于考虑这个) const MAX_LOGIN_USER_COUNT = 10000000 // 清理 session 实际间隔 10s 运行一次 const CLEAN_SESSIONS_INTERVAL = 10 * time.Second // 每次清理的个数 const CLEAN_COUNT = 1000 // 求 两个 int 的最小值 func min(a, b int) int { if a < b { return a } return b } // 合并 RedisKey 与 []string,返回一个 []interface{} func merge(redisKey RedisKey, strs ...string) []interface{} { result := make([]interface{}, 1 + len(strs)) result[0] = redisKey for i, item := range strs { result[i + 1] = item } return result } // 清理 session (由于大部分用户不会一直操作,所以需要定期清理 最旧的登录信息) // 内部死循环,可用 go 调用,当作定时任务 func CleanSessions(conn redis.Conn) { for ; ; { loginUserCount, _ := redis.Int(conn.Do("ZCARD", LOGIN_USER)) // 超过最大记录数,则清理 if loginUserCount > MAX_VIEWED_ITEM_COUNT { // 获取最旧的记录的 token ,最多不超过 CLEAN_COUNT 个(多线程/分布式情况下会有并发问题,重点不在这里,暂不考虑) cleanCount := min(loginUserCount - MAX_LOGIN_USER_COUNT, CLEAN_COUNT) tokens, _ := redis.Strings(conn.Do("ZRANGE", USER_LATEST_ACTION, 0, cleanCount - 1)) // 不支持 []string 直接转 []interface{} (对字符串数组使用 ... 无法对应参数 ...interface{}) // 只有数组的元素类型 Type 相同才能使用 ... 传参到相应的 ...Type _, _ = conn.Do("HDEL", merge(LOGIN_USER, tokens...)...) _, _ = conn.Do("ZREM", merge(USER_LATEST_ACTION, tokens...)...) // 不删除用户的历史访问记录,当作数据库用 } // 每次执行完,等待 CLEAN_SESSIONS_INTERVAL 长时间 time.Sleep(CLEAN_SESSIONS_INTERVAL) } } 购物车 P28每个用户的购物车是一个哈希表,存储了 itemId 与 商品加车数量之间的关系。此处购物车仅提供最基础的数量设置,加减数量的逻辑由调用者进行相关处理。
// 更新购物车商品数量(不考虑并发问题) func UpdateCartItem(conn redis.Conn, userId int, itemId int, count int) { cartKey := CART_PREFIX + RedisKey(strconv.Itoa(userId)) if count <= 0 { // 删除该商品 _, _ = conn.Do("HREM", cartKey, itemId) } else { // 更新商品数量 _, _ = conn.Do("HSET", cartKey, itemId, count) } }购物车也和历史访问记录一样,当作数据库使用,与用户登录态无关,不随登录态退出而删除。因此定期清理登录数据的代码不需要修改,也不用添加新函数。
网页缓存 P29假设网站的 95% 页面每天最多只会改变一次,那么没有必要每次都进行全部操作,可以直接在实际处理请求前将缓存的数据返回,既能降低服务器压力,又能提升用户体验。
Java 中可以使用注解方式对特定的服务进行拦截缓存,实现缓存的可插拔,避免修改核心代码。
Go 的话,不知道如何去实现可插拔的方式,感觉可以利用 Java 中拦截器的方式,在请求分发到具体的方法前进行判断及缓存。我这里只进行简单的业务逻辑处理展示大致流程,不关心如何实现可插拔,让用户无感知。
// 判断当前请求是否可以缓存(随实际业务场景处理,此处不关心,默认都可以缓存) func canCache(conn redis.Conn, request http.Request) bool { return true } // 对请求进行哈希,得到一个标识串(随实际业务场景处理,此处不关心,默认为 url) func hashRequest(request http.Request) string { return request.URL.Path } // 对返回值进行序列化,得到字符串(随实际业务场景处理,此处不关心,默认为 序列化状态码) func serializeResponse(response http.Response) string { return strconv.Itoa(response.StatusCode) } // 对缓存的结果进行反序列化,得到返回值(随实际业务场景处理,此处不关心,默认 状态码为 200) func deserializeResponse(str string) http.Response { return http.Response{StatusCode: 200} } // 返回值缓存时长为 5 分钟 const CACHE_EXPIRE = 5 * time.Minute // 缓存请求返回值 func CacheRequest(conn redis.Conn, request http.Request, handle func(http.Request) http.Response) http.Response { // 如果当前请求不能缓存,则直接调用方法返回 if !canCache(conn, request) { return handle(request) } // 从缓存中获取缓存的返回值 cacheKey := REQUEST_PREFIX + RedisKey(hashRequest(request)) responseStr, _ := redis.String(conn.Do("GET", cacheKey)) // 序列化时,所有有效的缓存都有状态吗,所以必定不为 "" if responseStr != "" { return deserializeResponse(responseStr) } // 缓存中没有,则重新执行一遍 response := handle(request) // 先进行缓存,再返回结果 responseStr = serializeResponse(response) _, _ = conn.Do("SET", cacheKey, responseStr, "EX", CACHE_EXPIRE) return response } 数据缓存 P30我们不能缓存的经常变动的页面,但是可以缓存这些页面上的数据,例如:促销商品、热卖商品等。 P30