基于Redis购物车需求分析
购物车业务需求
使用用户登录的Cookie记录用户浏览过的商品,即用户意向商品,用来挖掘意向用户
定期对浏览商品的旧记录进行剔除,保留最新的25个购物信息
购物车已存在同类商品,再次加入时商品数量大于0时,只更新购物车;
若用户加入购物车的商品数量为0,则从购物车删除
技术预热
签名Cookie:通常会存储用户名、可能还有用户ID、最后一次登录成功的时间以及网站觉得有用的其他任何信息,同时还包括一个签名,服务器通过验证这个签名来判断Cookie是否被改动过,但很难正确处理签名值,存在安全漏洞。
令牌Cookie:该Cookie里面存储一串随机字符串(token)作为令牌,服务器可以根据token在数据库中查找此令牌的拥有者,随着时间的推移,旧token会被新token取代,如果使用关系数据库来存储,载入与存储token代价很高。
购物车Cookie分解
Redis设计原则
设计原则:对于键的设计,可使用 功能模块名:ID值,用分隔符 : 分开 比如用户购物车加了哪些商品: cart:token_1
与时间及排序有关使用ZSET类型 :zadd key timescamp member
与数据库表二维数据相关使用HASH类型: hset key filed1 value1 filed2
购物车缓存设计实战
购物车方案设计
1,cookie与用户关系 HASH
2,用户浏览时间及商品ID记录 ZSET
3,商品购物车HASH
4,用户最后一次操作登录的时间记录ZSET
购物车核心业务操作语句
token与用户信息一一对应,故以token贯穿整个业务,假设为token_1:
1,记录token最后一次操作登录的时间
redis-> zadd recent:info timestamp token;
java -> jedis.zadd("recent:in`fo", timestamp, token);
2,记录用户浏览过的商品
redis-> zadd viewed:token_1 timestamp item;
java -> jedis.zadd("viewed:" + token_1, timestamp, item);
3,时间戳升序排列,保留用户最近浏览过的25个商品,删掉第25之后数据
redis-> zremrangeByRank viewed:token_1 0 -26;
java -> jedis.zremrangeByRank("viewed:" + token, 0, -26);
4,将商品item加到购物车,加入的商品数量为count
redis-> hmset cart:token_1 count item;
java -> jedis.hset("cart:" + token, item, count);
5,从购物车移除商品
redis-> hdel cart:token_1 item;
java -> jedis.hdel("cart:" + token, item);
代码:实现类:
@Service
public class ShopServiceImpl implements ShopService{
@Resource
private JedisUtils jedis;
/**
* 用户新登录后,更新用户的TOKEN值
*/
@Override
public void updateToken(String token, String user, String itemCode) {
long timestamp = System.currentTimeMillis() / 1000; //获取当前时间戳1521434935812
jedis.hset("login:info", token, user); //记录token与已登录用户之间的映射
jedis.zadd("recent:info", timestamp, token); //记录token最后一次出现的时间
if (itemCode != null) {
jedis.zadd("viewed:" + token, timestamp, itemCode); //记录用户浏览过的商品
jedis.zremrangeByRank("viewed:" + token, 0, -26); //分数升序排列,删除第0个与第-26——移除旧的记录,只保留用户最近浏览过的25个商品
}
}
/**
* 尝试获取并返回令牌token对应的用户
*/
@Override
public String checkToken(String token) {
return jedis.hget("login:info", token);
}
/**
* 将商品加入购物车
*/
@Override
public Long addToCart(String token, String item, int count) {
//count为用户订购此商品的数量,如果用户订购的数量为0,为无效,从购物车移除
if (count <= 0) {
//从购物车移除商品
return 0L;
} else {
//将指定的商品加到购物车, cart:token_1将item商品加入购物车,加入的数量为count
return jedis.hset("cart:" + token, item, String.valueOf(count));
}
}
@Override
public long hlen(String key) {
// TODO Auto-generated method stub
return jedis.hlen(key);
}
@Override
public Map<String, String> hgetAll(String key) {
// TODO Auto-generated method stub
return jedis.hgetAll(key);
}
/**
* 删除用户旧token
*/
@Override
public boolean removeOldTokens(long limit){
long size = jedis.zcard("recent:info"); // 查找目前已有令牌token的个数
if (size <= limit) { // 如果令牌没有超过limit限制个数 3
return false;
}
long endIndex = size - limit; // 最多只保留10个旧令牌,剩余的删除
Set<String> tokenSet = jedis.zrange("recent:info", 0L, endIndex - 1);// 获取需要移除的令牌ID
String[] tokens = tokenSet.toArray(new String[tokenSet.size()]);// 将被移除的令牌转成String[]数组
ArrayList<String> sessionKeys = new ArrayList<String>();
for (String token : tokens) {
sessionKeys.add("viewed:" + token); // 为即将被移除的令牌构建KEY键名
// viewed:xxxxxxx01
}
jedis.del(sessionKeys.toArray(new String[sessionKeys.size()])); // 移除最旧令牌
jedis.hdel("login:info", tokens); // 移除登录相关的令牌
jedis.zrem("recent:info", tokens); // 移除最近令牌
return true;
}
}
测试类:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:spring-mvc.xml")
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class CartShopTest {
@Resource(name = "shopServiceImpl")
private ShopService shopService;
@Test
public void asLoginTest() throws InterruptedException {
String username = "james";
String itemCode = null; // 登录时未浏览商品
System.out.println("登录成功---------");
// 模拟服务器生成一个令牌token
String token = UUID.randomUUID().toString();
// 更新token
shopService.updateToken(token, username, itemCode);
System.out.println("刚刚更新的token令牌为: " + token+"; 当前操作的用户: "+username);
String user = shopService.checkToken(token);// 尝试获取并返回令牌token对应的用户
System.out.println(user);
//考虑到redis的存储,使用线程对超出cookie个数的旧cookie从redis清除
//如果token令牌的数量超过3了,只保留3个tokenid
shopService.removeOldTokens(3);
long s = shopService.hlen("login:info");
System.out.println("查看当前的cookie剩余个数: " + s);
}
@Test
public void putToCart() throws InterruptedException {
//商品加入购物车及购物车商品列表查询
String username = "james";
String itemCode = "xmPhone"; // 浏览的商品为Vivo手机
String token = UUID.randomUUID().toString();
System.out.println("开始刷新session...");
// 更新token,因为此用户登录进来,之前的旧token可能失效......
shopService.updateToken(token, username, itemCode);
//假设将vivoPhone 数量为3, 加入购物车
long i =shopService.addToCart(token, itemCode, 3);
if(i >0 ){
System.out.println("加入购物车成功");
}else{
System.out.println("加入购物车失败");
}
//查看用户james的购物车有哪些商品
Map<String, String> r = shopService.hgetAll("cart:" + token);
System.out.println("用户 " + username +" 的购物车里有如下商品:");
for (Map.Entry<String, String> entry : r.entrySet()) {
System.out.println(" " + entry.getKey() + ": " + entry.getValue());
}
}
}