当前位置 博文首页 > 努力充实,远方可期:【谷粒商城】分布式事务与下单

    努力充实,远方可期:【谷粒商城】分布式事务与下单

    作者:[db:作者] 时间:2021-08-13 18:55

    • 笔记-基础篇-1(P1-P28):https://blog.csdn.net/hancoder/article/details/106922139

    • 笔记-基础篇-2(P28-P100):https://blog.csdn.net/hancoder/article/details/107612619

    • 笔记-高级篇(P340):https://blog.csdn.net/hancoder/article/details/107612746

    • 笔记-vue:https://blog.csdn.net/hancoder/article/details/107007605

    • 笔记-elastic search、上架、检索:https://blog.csdn.net/hancoder/article/details/113922398

    • 笔记-认证服务:https://blog.csdn.net/hancoder/article/details/114242184

    • 笔记-分布式锁与缓存:https://blog.csdn.net/hancoder/article/details/114004280

    • 笔记-集群篇:https://blog.csdn.net/hancoder/article/details/107612802

    • springcloud笔记:https://blog.csdn.net/hancoder/article/details/109063671

    • 笔记版本说明:2020年提供过笔记文档,但只有P1-P50的内容,2021年整理了P340的内容。请点击标题下面分栏查看系列笔记

    • 声明:

      • 可以白嫖,但请勿转载发布,笔记手打不易
      • 本系列笔记不断迭代优化,csdn:hancoder上是最新版内容,10W字都是在csdn免费开放观看的。
      • 离线md笔记文件获取方式见文末。2021-3版本的md笔记打完压缩包共500k(云图床),包括本项目笔记,还有cloud、docker、mybatis-plus、rabbitMQ等个人相关笔记
    • sql:https://github.com/FermHan/gulimall/sql文件

    • 本项目其他笔记见专栏:https://blog.csdn.net/hancoder/category_10822407.html

    本篇2.5W字,请直接ctrl+F搜索内容

    一、gulimall-cart

    构建gulimall-cart,复制静态资源到nginx,修改网关

    购物车分为离线购物车和登录购物车

    离线购物车重启浏览器了也还有

    二、购物车

    1、购物车需求

    特点:读多写少,放入数据库并不合适

    登录状态:登录购物车

    • 放入数据库
    • mongodb
    • 放入redis(采用)
      • 登录以后,将离线购物车里合并后清空离线购物车

    未登录状态:离线购物车

    • 放入localstorage浏览器的技术
    • cookie
    • WebSQL
    • 放入redis(采用)
      • 浏览器重启后还在

    2、购物车VO

    (1) 数据结构分析

    购物车

    {
        skuid:123123,
        check:true, # 每一项是否被选中
        title:"apple ...",
        defaultImage:"",
        price:4999,
        count:1,
        totalPrice:4999, # 商品的总价=单价*数量
        skuSaleVO:{...}
    }
    

    购物车不只一条数据

    [
        {sku1},{sku2},{}
    ]
    

    redis有5种不同数据结构,这里选择哪一种比较合适呢?Map<String,List<String>>

    不好的方式:不同用户应该有独立的购物车,因此购物车应该以用户作为key来存储,value是用户的所有购车信息。这样看来基本的k-v结构就可以了。

    但是,我们对购车中的商品进行增、删、改操作,基本都需要根据商品id讲行判断,为了方便后期处理,我们的购车也应该是k-v结构,key是商品id,value才是这个商品的购车信息。

    一个购物车是由各个购物项组成的,但是我们用List进行存储并不合适,因为使用List查找某个购物项时需要挨个遍历每个购物项,会造成大量时间损耗,为保证查找速度,我们使用hash进行存储

    每个人都有一个hash表,key为skuId,value为数据

    (2) 购物项vo

    public class CartItem {
    
    	private Long skuId;
    
    	/*** 是否被选中*/
    	private Boolean check = true;
    
    	private String title;
    	private String image;
    
    	private List<String> skuAttr;
    
    	/*** 价格*/
    	private BigDecimal price;
    	/*** 数量*/
    	private Integer count;
    

    (3) 购物车vo

    public class Cart {
    
    	private List<CartItem> items;
    
    	/*** 商品的数量*/
    	private Integer countNum;
    	/*** 商品的类型数量*/
    	private Integer countType;
    
    	/*** 整个购物车的总价*/
    	private BigDecimal totalAmount;
    
    	/*** 减免的价格*/
    	private BigDecimal reduce = new BigDecimal("0.00");
    
    	/*** 计算商品的总量*/
    	public Integer getCountNum() {
    		int count = 0;
    		if(this.items != null && this.items.size() > 0){
    			for (CartItem item : this.items) {
    				count += item.getCount();
    			}
    		}
    		return count;
    	}
    
    	public Integer getCountType() {
    		int count = 0;
    		if(this.items != null && this.items.size() > 0){
    			for (CartItem item : this.items) {
    				count += 1;
    			}
    		}
    		return count;
    	}
    
    	public BigDecimal getTotalAmount() {
    		BigDecimal amount = new BigDecimal("0");
    		if(this.items != null && this.items.size() > 0){
    			for (CartItem item : this.items) {
    				if(item.getCheck()){
    					BigDecimal totalPrice = item.getTotalPrice();
    					amount = amount.add(totalPrice);
    				}
    			}
    		}
    		return amount.subtract(this.getReduce());
    	}
    

    3、 ThreadLocal用户身份鉴别

    (1) threadlocal说明

    threadlocal的效果是其中存储的内容只有当前线程能访问的

    如果想了解更多threadlocal知识可以查看:https://blog.csdn.net/hancoder/article/details/107853513

    threadlocal的原理是每个线程都有一个map,key为threadlocal对象,value为对象所对应的值

    参考京东,在点击购物车时,会为临时用户生成一个nameuser-keycookie临时标识,过期时间为一个月,如果手动清除user-key,那么临时购物车的购物项也被清除,所以user-key是用来标识和存储临时购物车数据的

    (2) 使用ThreadLocal进行用户身份鉴别信息传递

    但是注意的是tomcat中线程可以复用,所以线程和会话不是一对一的关系。但是没有关系,会在拦截器中先判断会话有没有用户信息(cookie),

    • 首先明确每次拦截器都会重新设置threadlocal
    • 没有的话创建一个临时用户,回去的时候告诉用户的临时cookie。threadlocal中只封装临时用户信息
    • 有的话把临时用户和登录用户封装到一起,设置到threadlocal中
    拦截器拦截会话

    购物车拦截器的配置

    @Configuration
    public class GulimallWebConfig implements WebMvcConfigurer {
        //拦截所有请求
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(new CartInterceptor()).addPathPatterns("/**");
        }
    }
    

    购物车拦截器

    • 可以看到有句session.getAttribute(AuthServerConstant.LOGIN_USER);
    • 看cookie中有没有临时数据,就是cookie带过来的
    • 将用户信息放到threadlocal中让当前用户使用threadLocal.set(userInfoTo);
    public class CartInterceptor implements HandlerInterceptor {
    
    	// 静态,
    	public static ThreadLocal<UserInfoTo> threadLocal = new ThreadLocal<>();
    
    	@Override
    	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
    		// 准备好要设置到threadlocal里的user对象
    		UserInfoTo userInfoTo = new UserInfoTo();
    		HttpSession session = request.getSession();
    		// 获取loginUser对应的用户value,没有也不去登录了。登录逻辑放到别的代码里,需要登录时再重定向
    		MemberRespVo user = (MemberRespVo) session.getAttribute(AuthServerConstant.LOGIN_USER);
    		if (user != null){ // 用户登陆了,设置userId
    			userInfoTo.setUsername(user.getUsername());
    			userInfoTo.setUserId(user.getId());
    		}
    		
    		// 不登录也没关系,可以访问临时用户购物车
    		// 去查看请求带过来的cookies里的临时购物车cookie
    		Cookie[] cookies = request.getCookies();
    		if(cookies != null && cookies.length > 0){
    			for (Cookie cookie : cookies) {
    				String name = cookie.getName();
    				if(name.equals(CartConstant.TEMP_USER_COOKIE_NAME)){
    					userInfoTo.setUserKey(cookie.getValue());
    					userInfoTo.setTempUser(true);
    				}
    			}
    		}
    		// 如果没有临时用户 则分配一个临时用户 // 分配的临时用户在postHandle的时候放到cookie里即可
    		if (StringUtils.isEmpty(userInfoTo.getUserKey())){
    			String uuid = UUID.randomUUID().toString().replace("-","");
    			userInfoTo.setUserKey("GULI-" + uuid);//临时用户
    		}
    		threadLocal.set(userInfoTo);
    		return true;
    		// 还有一个登录后应该删除临时购物车的逻辑没有实现
    	}
    
    	/**
    	 * 执行完毕之后分配临时用户让浏览器保存
    	 */
    	@Override
    	public void postHandle(HttpServletRequest request, 
    						   HttpServletResponse response, Object handler,
    						   ModelAndView modelAndView) throws Exception {
    
    		UserInfoTo userInfoTo = threadLocal.get();
    		// 如果是临时用户,返回临时购物车的cookie
    		if(!userInfoTo.isTempUser()){
    			Cookie cookie = new Cookie(CartConstant.TEMP_USER_COOKIE_NAME, userInfoTo.getUserKey());
    			// 设置这个cookie作用域 过期时间
    			cookie.setDomain(