Post

尚硅谷_尚品甄选项目_第2章 商品详情

第2章 尚品甄选-商品详情 2.1 商品详情 2.1.1 需求分析 需求说明:当点击某一个商品的时候,此时就需要在商品详情页面展示出商品的详情数据,商品详情页所需数据: 1、商品的基本信息 2、当前商品sku的基本信息 3、商品sku最新价格信息 4、商品详情(详细为图片列表) 5、商品规格信息 6

尚品臻选 阅读 0 点赞 0 评论 0

第2章 尚品甄选-商品详情

2.1 商品详情

2.1.1 需求分析

需求说明:当点击某一个商品的时候,此时就需要在商品详情页面展示出商品的详情数据,商品详情页所需数据:

1、商品的基本信息

2、当前商品sku的基本信息

3、商品sku最新价格信息

4、商品详情(详细为图片列表)

5、商品规格信息

6、商品库存信息

item

2.1.2 接口文档

商品详情数据接口以及示例数据:

get  /channel/item/{skuId}
返回结果:
{
    "msg": "操作成功",
    "code": 200,
    "data": {
        "productSku": {
            "id": 5,
            "skuCode": "1_4",
            "skuName": "小米 红米Note10 5G手机 黑色 + 8G",
            "productId": 1,
            "thumbImg": "http://139.198.127.41:9000/spzx/20230525/665832167-1_u_1.jpg",
            "salePrice": 1999.00,
            "marketPrice": 2019.00,
            "costPrice": 1599.00,
            "skuSpec": "黑色 + 8G",
            "weight": 1.00,
            "volume": 1.00,
            "status": 1,
            "stockNum": null,
            "saleNum": null
        },
        "product": {
            "id": 1,
            "name": "小米 红米Note10 5G手机",
            "brandId": 6,
            "category1Id": 1,
            "category2Id": 2,
            "category3Id": 3,
            "unitName": "个",
            "sliderUrls": "",
            "specValue": "",
            "status": 1,
            "auditStatus": 1,
            "auditMessage": "审批通过",
            "brandName": null,
            "category1Name": null,
            "category2Name": null,
            "category3Name": null,
            "productSkuList": null,
            "detailsimagesUrlList": null
        },
        "skuPrice": {
            "skuId": null,
            "salePrice": 1999.00,
            "marketPrice": 2019.00
        },
        "sliderUrlList": [
            "http://139.198.127.41:9000/spzx/20230525/665832167-5_u_1.jpg",
            ...
        ],
        "detailsimagesUrlList": [
            "http://139.198.127.41:9000/spzx/20230525/665832167-5_u_1.jpg",
            ...
        ],
        "specValueList": [
            {
                "valueList": [
                    "白色",
                    "红色",
                    "黑色"
                ],
                "key": "颜色"
            },
            {
                "valueList": [
                    "8G",
                    "18G"
                ],
                "key": "内存"
            }
        ],
        "skuStockVo": {
            "skuId": 5,
            "availableNum": 98,
            "saleNum": 2
        },
        "skuSpecValueMap": {
            "黑色 + 18G": 6,
            "红色 + 18G": 4,
            "白色 + 8G": 1,
            "白色 + 18G": 2,
            "黑色 + 8G": 5,
            "红色 + 8G": 3
        }
    }
}

2.1.3 获取商品相关信息

1、远程调用接口开发

(1)SkuPrice

操作模块:spzx-api-product

package com.spzx.product.api.domain;

@Data
public class SkuPrice {

    @Schema(description = "skuId")
    private Long skuId;

    @Schema(description = "售价")
    private BigDecimal salePrice;

    /**
     * 市场价
     */
    @Schema(description = "市场价")
    private BigDecimal marketPrice;
}

(2)SkuStockVo

操作模块:spzx-api-product

package com.spzx.product.api.domain;

@Data
public class SkuStockVo
{
    /** 商品ID */
    private Long skuId;

    /** 可用库存数 */
    private Integer availableNum;

    /** 销量 */
    private Integer saleNum;
}

(3)ProductController

操作模块:spzx-product

@Operation(summary = "获取商品sku信息")
@InnerAuth
@GetMapping(value = "/getProductSku/{skuId}")
public R<ProductSku> getProductSku(@PathVariable("skuId") Long skuId)
{
    return R.ok(productService.getProductSku(skuId));
}

@Operation(summary = "获取商品信息")
@InnerAuth
@GetMapping(value = "/getProduct/{id}")
public R<Product> getProduct(@PathVariable("id") Long id)
{
    return R.ok(productService.getProduct(id));
}

@Operation(summary = "获取商品sku最新价格信息")
@InnerAuth
@GetMapping(value = "/getSkuPrice/{skuId}")
public R<SkuPrice> getSkuPrice(@PathVariable("skuId") Long skuId)
{
    return R.ok(productService.getSkuPrice(skuId));
}

@Operation(summary = "获取商品详细信息")
@InnerAuth
@GetMapping(value = "/getProductDetails/{id}")
public R<ProductDetails> getProductDetails(@PathVariable("id") Long id)
{
    return R.ok(productService.getProductDetails(id));
}

@Operation(summary = "获取商品sku规则详细信息")
@InnerAuth
@GetMapping(value = "/getSkuSpecValue/{id}")
public R<Map<String, Long>> getSkuSpecValue(@PathVariable("id") Long id)
{
    return R.ok(productService.getSkuSpecValue(id));
}

@Operation(summary = "获取商品sku库存信息")
@InnerAuth
@GetMapping(value = "/getSkuStock/{skuId}")
public R<SkuStockVo> getSkuStock(@PathVariable("skuId") Long skuId)
{
    return R.ok(productService.getSkuStock(skuId));
}

(4)IProductService

ProductSku getProductSku(Long skuId);

Product getProduct(Long id);

SkuPrice getSkuPrice(Long skuId);

ProductDetails getProductDetails(Long id);

Map<String, Long> getSkuSpecValue(Long id);

SkuStockVo getSkuStock(Long skuId);

(5)ProductServiceImpl

@Override
public ProductSku getProductSku(Long skuId) {
    return productSkuMapper.selectById(skuId);
}

@Override
public Product getProduct(Long id) {
    return productMapper.selectById(id);
}

@Override
public SkuPrice getSkuPrice(Long skuId) {
    ProductSku productSku = productSkuMapper.selectOne(new LambdaQueryWrapper<ProductSku>().eq(ProductSku::getId, skuId).select(ProductSku::getSalePrice, ProductSku::getMarketPrice));
    SkuPrice skuPrice = new SkuPrice();
    BeanUtils.copyProperties(productSku, skuPrice);
    return skuPrice;
}

@Override
public ProductDetails getProductDetails(Long id) {
    return productDetailsMapper.selectOne(new LambdaQueryWrapper<ProductDetails>().eq(ProductDetails::getProductId, id));
}

@Override
public Map<String, Long> getSkuSpecValue(Long id) {
    List<ProductSku> productSkuList = productSkuMapper.selectList(new LambdaQueryWrapper<ProductSku>().eq(ProductSku::getProductId, id).select(ProductSku::getId, ProductSku::getSkuSpec));
    
    Map<String,Long> skuSpecValueMap = new HashMap<>();
    productSkuList.forEach(item -> {
        skuSpecValueMap.put(item.getSkuSpec(), item.getId());
    });
    return skuSpecValueMap;
}

@Override
public SkuStockVo getSkuStock(Long skuId) {
    SkuStock skuStock = skuStockMapper.selectOne(new LambdaQueryWrapper<SkuStock>().eq(SkuStock::getSkuId, skuId));
    SkuStockVo skuStockVo = new SkuStockVo();
    BeanUtils.copyProperties(skuStock, skuStockVo);
    return skuStockVo;
}

2、openFeign接口定义

操作模块:spzx-api-product

(1)RemoteProductService

@GetMapping("/product/getProductSku/{skuId}")
public R<ProductSku> getProductSku(@PathVariable("skuId") Long skuId, @RequestHeader(SecurityConstants.FROM_SOURCE) String source);

@GetMapping(value = "/product/getProduct/{id}")
public R<Product> getProduct(@PathVariable("id") Long id, @RequestHeader(SecurityConstants.FROM_SOURCE) String source);

@GetMapping(value = "/product/getSkuPrice/{skuId}")
public R<SkuPrice> getSkuPrice(@PathVariable("skuId") Long skuId, @RequestHeader(SecurityConstants.FROM_SOURCE) String source);

@GetMapping(value = "/product/getProductDetails/{id}")
public R<ProductDetails> getProductDetails(@PathVariable("id") Long id, @RequestHeader(SecurityConstants.FROM_SOURCE) String source);

@GetMapping(value = "/product/getSkuSpecValue/{id}")
public R<Map<String, Long>> getSkuSpecValue(@PathVariable("id") Long id, @RequestHeader(SecurityConstants.FROM_SOURCE) String source);

@GetMapping(value = "/product/getSkuStock/{skuId}")
public R<SkuStockVo> getSkuStock(@PathVariable("skuId") Long skuId, @RequestHeader(SecurityConstants.FROM_SOURCE) String source);

说明:将spzx-product模块Product、ProductDetails实体类移取到spzx-api-product模块

(2)RemoteProductFallbackFactory

@Override
public R<ProductSku> getProductSku(Long skuId, String source) {
    return R.fail("获取商品sku失败:" + throwable.getMessage());
}

@Override
public R<Product> getProduct(Long id, String source) {
    return R.fail("获取商品信息失败:" + throwable.getMessage());
}

@Override
public R<SkuPrice> getSkuPrice(Long skuId, String source) {
    return R.fail("获取商品sku价格失败:" + throwable.getMessage());
}

@Override
public R<ProductDetails> getProductDetails(Long id, String source) {
    return R.fail("获取商品详情失败:" + throwable.getMessage());
}

@Override
public R<Map<String, Long>> getSkuSpecValue(Long id, String source) {
    return R.fail("获取商品sku规格失败:" + throwable.getMessage());
}

@Override
public R<SkuStockVo> getSkuStock(Long skuId, String source) {
    return R.fail("获取商品sku库存失败:" + throwable.getMessage());
}

2.1.4 业务接口开发

1、ItemController

package com.spzx.channel.controller;

@Tag(name = "商品详情接口")
@RestController
@RequestMapping("/item")
public class ItemController extends BaseController {

    @Autowired
    private IItemService itemService;

    @Operation(summary = "商品详情")
    @GetMapping("/{skuId}")
    public AjaxResult item(@PathVariable Long skuId) {
        return success(itemService.item(skuId));
    }

}

2、ItemVo

package com.atguigu.channel.domain;

@Data
@Schema(description = "商品详情对象")
public class ItemVo {

   @Schema(description = "商品sku信息")
   private ProductSku productSku;

   @Schema(description = "商品信息")
   private Product product;

   @Schema(description = "最新价格信息")
   private SkuPrice skuPrice;

   @Schema(description = "商品轮播图列表")
   private List<String> sliderUrlList;

   @Schema(description = "商品详情图片列表")
   private List<String> detailsimagesUrlList;

   @Schema(description = "商品规格信息")
   private JSONArray specValueList;

   @Schema(description = "商品库存信息")
   private SkuStockVo skuStockVo;

   @Schema(description = "商品规格对应商品skuId信息")
   private Map<String,Long> skuSpecValueMap;

}

3、IItemService

package com.spzx.channel.service;

public interface IItemService {
    ItemVo item(Long skuId);
}

4、ItemServiceImpl

package com.spzx.channel.service.impl;

@Service
@Slf4j
public class ItemServiceImpl implements IItemService {

    @Autowired
    private RemoteProductService remoteProductService;

    @Override
    public ItemVo item(Long skuId) {
        ItemVo itemVo = new ItemVo();

        //获取sku信息
        R<ProductSku> productSkuResult = remoteProductService.getProductSku(skuId, SecurityConstants.INNER);
        if (R.FAIL == productSkuResult.getCode()) {
            throw new ServiceException(productSkuResult.getMsg());
        }
        ProductSku productSku = productSkuResult.getData();
        itemVo.setProductSku(productSku);

        //获取商品信息
        R<Product> productResult = remoteProductService.getProduct(productSku.getProductId(), SecurityConstants.INNER);
        if (R.FAIL == productResult.getCode()) {
            throw new ServiceException(productResult.getMsg());
        }
        Product product = productResult.getData();
        itemVo.setProduct(product);
        itemVo.setSliderUrlList(Arrays.asList(product.getSliderUrls().split(",")));
        itemVo.setSpecValueList(JSON.parseArray(product.getSpecValue()));

        //获取商品最新价格
        R<SkuPrice> skuPriceResult = remoteProductService.getSkuPrice(skuId, SecurityConstants.INNER);
        if (R.FAIL == skuPriceResult.getCode()) {
            throw new ServiceException(skuPriceResult.getMsg());
        }
        SkuPrice skuPrice = skuPriceResult.getData();
        itemVo.setSkuPrice(skuPrice);

        //获取商品详情
        R<ProductDetails> productDetailsResult = remoteProductService.getProductDetails(productSku.getProductId(), SecurityConstants.INNER);
        if (R.FAIL == productDetailsResult.getCode()) {
            throw new ServiceException(productDetailsResult.getMsg());
        }
        ProductDetails productDetails = productDetailsResult.getData();
        itemVo.setDetailsimagesUrlList(Arrays.asList(productDetails.getimagesUrls().split(",")));

        //获取商品规格对应商品skuId信息
        R<Map<String, Long>> skuSpecValueResult = remoteProductService.getSkuSpecValue(productSku.getProductId(), SecurityConstants.INNER);
        if (R.FAIL == skuSpecValueResult.getCode()) {
            throw new ServiceException(skuSpecValueResult.getMsg());
        }
        Map<String, Long> skuSpecValueMap = skuSpecValueResult.getData();
        itemVo.setSkuSpecValueMap(skuSpecValueMap);

        //获取商品库存信息
        R<SkuStockVo> skuStockResult = remoteProductService.getSkuStock(skuId, SecurityConstants.INNER);
        if (R.FAIL == skuStockResult.getCode()) {
            throw new ServiceException(skuStockResult.getMsg());
        }
        SkuStockVo skuStockVo = skuStockResult.getData();
        itemVo.setSkuStockVo(skuStockVo);

        return itemVo;
    }
}

5、接口测试

测试方向:

1、后端接口单独测试

2、配合前端项目测试

2.2 商品详情页面优化

2.2.1 思路

虽然咱们实现了页面需要的功能,但是考虑到该页面是被用户高频访问的,所以性能需要优化。一般一个系统最大的性能瓶颈,就是数据库的io操作。从数据库入手也是调优性价比最高的切入点。

一般分为两个层面:

  • 一是提高数据库sql本身的性能
  • 二是尽量避免直接查询数据库

重点要讲的是另外一个层面:尽量避免直接查询数据库。

解决办法就是:缓存

2.2.2 缓存常见问题

缓存最常见的4个问题: 面试

  1. 缓存穿透
  2. 缓存雪崩
  3. 缓存击穿
  4. 数据一致性

缓存穿透: 是指查询一个不存在的数据,由于缓存无法命中,将去查询数据库,但是数据库也无此记录,并且出于容错考虑,我们没有将这次查询的null写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。

  • 解决1 :空结果也进行缓存,但它的过期时间会很短,最长不超过五分钟,但是不能防止随机穿透。

  • 解决2 :使用布隆过滤器或者Redis的Bitmap来解决随机穿透问题

缓存雪崩:是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。

  • 解决1:原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

  • 解决2:如果单节点宕机,可以采用集群部署方式防止雪崩

缓存击穿: 是指对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:如果这个key在大量请求同时进来之前正好失效,那么所有对这个key的数据查询都落到db,我们称为缓存击穿。

与缓存雪崩的区别:

  1. 击穿是一个热点key失效
  2. 雪崩是很多key集体失效
  • 解决:锁
img

数据一致性:在当前环境下,通常我们会首选redis缓存来减轻我们数据库访问压力。但是也会遇到以下这种情况:大量用户来访问我们系统,首先会去查询缓存, 如果缓存中没有数据,则去查询数据库,然后更新数据到缓存中,并且如果数据库中的数据发生了改变则需要同步到redis中,同步过程中需要保证 MySQL与redis数据一致性问题

  • 解决1:使用延时双删策略

延时双删策略是一种常见的保证MySQL和Redis数据一致性的方法。其主要流程包括:先删除缓存,然后更新数据库。这个过程完成后,大约在数据库从库更新后再次删除缓存。具体的步骤如下:

第一步,先执行redis.del(key)操作删除缓存;

第二步,然后执行写数据库的操作;

第三步,休眠一段时间(例如500毫秒),根据具体的业务时间来定;

第四步,再次执行redis.del(key)操作删除缓存。

延时双删策略通过这种方式尝试达到最终的数据一致性,但是这并不是强一致性,因为MySQL和Redis主从节点数据的同步并不是实时的,所以需要等待一段时间以增强它们的数据一致性。同时,由于读写是并发的,可能出现缓存和数据库数据不一致的问题

  • 解决2:使用canal解决

2.3 数据一致性-延时双删策略

2.3.1 问题分析

在查询商品详情数据时,为避免频繁io,提高查询效率,在详情数据获取的同时,利用缓存机制,将详情数据存储到redis实现的缓存机制中,但带来效率提升的同时也带来了问题,就是如果在商品修改之后,因为业务数据更新等原因,对于mysql中商品原数据进行了修改,将导致redis中缓存数据和mysql中原始数据不一致的问题,这里可以使用延迟双删来保证缓存与数据库数据一致性!

2.3.2 代码实现

在ProductServiceImpl类的修改商品方法添加代码

//修改
@Transactional
@Override
public int updateProduct(Product product) {
    //1 删除缓存
    List<Long> skuIdList =  product.getProductSkuList().stream()
        .map(ProductSku::getId).collect(Collectors.toList());
    skuIdList.forEach(skuId -> {
        String dataKey = "product:sku:" + skuId;
        this.redisTemplate.delete(dataKey);
    });

    //2 之前的业务代码,执行更新商品操作.....


    //3 休眠一段时间
    try {
        Thread.sleep(100);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
    
    //4 再次执行操作删除缓存
    skuIdList.forEach(skuId -> {
        String dataKey = "product:sku:" + skuId;
        this.redisTemplate.delete(dataKey);
    });
    return 1;
}

2.4 分布式锁

2.4.1 本地锁的局限性

之前,我们学习过synchronized 及lock锁,这些锁都是本地锁。接下来写一个案例,演示本地锁的问题

1、编写测试代码

spzx-product中新建TestController中添加测试方法

package com.spzx.product.controller;

@Tag(name = "测试接口")
@RestController
@RequestMapping("/test")
public class TestController {
    
    @Autowired
    private TestService testService;

    @GetMapping("testLock")
    public AjaxResult testLock() {
        testService.testLock();
        return AjaxResult.success();
    }
}

业务接口

package com.spzx.product.service;

public interface TestService {
   void testLock();
}

业务实现类

package com.spzx.product.service.impl;
@Service
public class TestServiceImpl implements TestService {

   @Autowired
   private StringRedisTemplate stringRedisTemplate;
    
   @Override
   public void testLock() {
      // 查询Redis中的num值
      String value = (String)this.stringRedisTemplate.opsForValue().get("num");
      // 没有该值return
      if (StringUtils.isBlank(value)){
         return ;
      }
      // 有值就转成成int
      int num = Integer.parseInt(value);
      // 把Redis中的num值+1
      this.stringRedisTemplate.opsForValue().set("num", String.valueOf(++num));
   }
}

说明:通过reids客户端设置num=0

重启spzx-product服务

2、使用工具测试

第一步:安装jmeter工具

第二步:配置网关并重启网关

配置本地锁测试接口为白名单

  # 不校验白名单
  ignore:
    whites:
     ...省略
      - /product/test/**

重启网关

第三步:测试

注意:将Windows防火墙关闭!!!!!!!!!!

  • 使用jmeter工具压力测试:并发100

查看Redis中的结果:

image-20240714195729578

3、使用本地锁

@Override
public synchronized void testLock() {
   // 查询Redis中的num值
   String value = (String)this.stringRedisTemplate.opsForValue().get("num");
   // 没有该值return
   if (StringUtils.isBlank(value)){
      return ;
   }
   // 有值就转成成int
   int num = Integer.parseInt(value);
   // 把Redis中的num值+1
   this.stringRedisTemplate.opsForValue().set("num", String.valueOf(++num));
}
  • 使用jmeter工具压力测试:并发100

4、本地锁问题演示

接下来spzx-product启动9205 9215 9225三个运行实例:

第一步:拷贝配置

1709528122174

第二步:修改端口

选择vm options

1709528275403

添加命令行参数:

-Dserver.port=9215
1709528467470

同样的,再复制并配置一个实例,端口为9525

第三步:启动spzx-product的三个实例

1709528694339

第四步:通过网关压力测试本地锁

以上测试,可以发现:

本地锁只能锁住同一工程内的资源,在分布式系统里面都存在局限性。

此时需要分布式锁。

2.4.2 分布式锁实现的解决方案

随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!

分布式锁主流的实现方案:

  1. 基于数据库实现分布式锁
  2. 基于缓存( Redis等)
  3. 基于Zookeeper

每一种分布式锁解决方案都有各自的优缺点:

  1. 高性能:Redis最高
  2. 可靠性:zookeeper最高

因为Redis具备高性能、高可用、高并发的特性,这里,我们就基于Redis实现分布式锁。

分布式锁的关键是多进程共享的内存标记(锁),因此只要我们在Redis中放置一个这样的标记(数据)就可以了。不过在实现过程中,不要忘了我们需要实现下列目标:

  • 多进程可见:多进程可见,否则就无法实现分布式效果
  • 避免死锁:死锁的情况有很多,我们要思考各种异常导致死锁的情况,保证锁可以被释放
  • 排它:同一时刻,只能有一个进程获得锁
  • 高可用:避免锁服务宕机或处理好宕机的补救措施(redis集群架构:1.主从复制 2.哨兵 3.cluster集群)

分布式锁使用的逻辑如下:

尝试获取锁
	成功:执行业务代码    
		执行业务  
			try{
				获取锁
				业务代码-宕机
			} catch(){
			
			}finally{ 
				释放锁
			}
 	失败:等待;

2.4.3 使用Redis实现分布式锁

img
  1. 多个客户端同时获取锁(setnx)
  2. 获取成功,执行业务逻辑:从db获取数据,放入缓存,执行完成释放锁(del)
  3. 其他客户端等待重试

1、分布式锁初版

/**
 * 采用SpringDataRedis实现分布式锁
 * 原理:执行业务方法前先尝试获取锁(setnx存入key val),如果获取锁成功再执行业务代码,业务执行完毕后将锁释放(del key)
 */
@Override
public void testLock() {

    //0.先尝试获取锁 setnx key val
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent("lock", "lock");
    if(flag){
        //获取锁成功,执行业务代码
        //1.先从redis中通过key num获取值  key提前手动设置 num 初始值:0
        String value = stringRedisTemplate.opsForValue().get("num");
        //2.如果值为空则非法直接返回即可
        if (StringUtils.isBlank(value)) {
            return;
        }
        //3.对num值进行自增加一
        int num = Integer.parseInt(value);
        stringRedisTemplate.opsForValue().set("num", String.valueOf(++num));

        //4.将锁释放
        stringRedisTemplate.delete("lock");

    }else{
        try {
            Thread.sleep(100);
            this.testLock();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

重启,服务集群,通过网关压力测试:

基本实现。

**问题:**setnx刚好获取到锁,业务逻辑出现异常,导致锁无法释放

解决:设置过期时间,自动释放锁。

2、优化之设置锁的过期时间

设置过期时间有两种方式:

  1. 首先想到通过expire设置过期时间(缺乏原子性:如果在setnx和expire之间出现异常,锁也无法释放)
  2. 在set时指定过期时间(推荐)
img

设置过期时间:

//0.先尝试获取锁 setnx key val
Boolean flag = redisTemplate.opsForValue().setIfAbsent("lock", "lock", 3, TimeUnit.SECONDS); 

压力测试肯定也没有问题。自行测试

问题:可能会释放其他服务器的锁。

场景:如果业务逻辑的执行时间是7s。执行流程如下

  1. index1业务逻辑没执行完,3秒后锁被自动释放。

  2. index2获取到锁,执行业务逻辑,3秒后锁被自动释放。

  3. index3获取到锁,执行业务逻辑

    . index1业务逻辑执行完成,开始调用del释放锁,这时释放的是index3的锁, 导致index3的业务只执行1s就被别人释放。

最终等于没锁的情况。

解决:setnx获取锁时,设置一个指定的唯一值(例如:uuid);释放前获取这个值,判断是否自己的锁

3、优化之UUID防误删

img
@Override
public void testLock() {
    //0.先尝试获取锁 setnx key val
    String uuid = UUID.randomUUID().toString();
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid, 3, TimeUnit.SECONDS);
    if(flag){
        //获取锁成功,执行业务代码
        //1.先从redis中通过key num获取值  key提前手动设置 num 初始值:0
        String value = stringRedisTemplate.opsForValue().get("num");
        //2.如果值为空则非法直接返回即可
        if (StringUtils.isBlank(value)) {
            return;
        }
        //3.对num值进行自增加一
        int num = Integer.parseInt(value);
        stringRedisTemplate.opsForValue().set("num", String.valueOf(++num));

        //4.将锁释放
        if(uuid.equals((String)stringRedisTemplate.opsForValue().get("lock"))) {
            stringRedisTemplate.delete("lock");
        }
    }else{
        try {
            Thread.sleep(100);
            this.testLock();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
} 

**问题:**删除操作缺乏原子性。

场景:

​ 1、index1执行删除时,查询到的lock值确实和uuid相等

img

​ 2、index1执行删除前,lock刚好过期时间已到,被Redis自动释放,在Redis中没有了锁。

img

​ 3、index2获取了lock,index2线程获取到了cpu的资源,开始执行方法

​ 4、index1执行删除,此时会把index2的lock删除。

index1 因为已经在方法中了,所以不需要重新上锁。index1有执行的权限。index1已经比较完成了,这个时候,开始执行

img

删除了index2的锁!

4、优化之LUA脚本保证删除的原子性

释放锁的LUA脚本:

if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end

redis java客户端使用LUA脚本:

//通过execute可以执行LUA脚本,参数1:脚本字符串,参数2:脚本返回值类型,参数3:keys列表,参数4:argv列表
stringRedisTemplate.execute(new DefaultRedisScript<>(script , Boolean.class),list,args...)

使用LUA脚本优化分布式锁

/**
 * 采用SpringDataRedis实现分布式锁
 * 原理:执行业务方法前先尝试获取锁(setnx存入key val),如果获取锁成功再执行业务代码,业务执行完毕后将锁释放(del key)
 */
@Override
public void testLock() {

    //0.先尝试获取锁 setnx key val
    //问题:锁可能存在线程间相互释放
    //Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent("lock", "lock", 10, TimeUnit.SECONDS);
    //解决:锁值设置为uuid
    String uuid = UUID.randomUUID().toString();
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid, 10, TimeUnit.SECONDS);

    if(flag){
        //获取锁成功,执行业务代码
        //1.先从redis中通过key num获取值  key提前手动设置 num 初始值:0
        String value = stringRedisTemplate.opsForValue().get("num");
        //2.如果值为空则非法直接返回即可
        if (StringUtils.isBlank(value)) {
            return;
        }
        //3.对num值进行自增加一
        int num = Integer.parseInt(value);
        stringRedisTemplate.opsForValue().set("num", String.valueOf(++num));

        //4.将锁释放 判断uuid
        //问题:删除操作缺乏原子性。
        //if(uuid.equals(stringRedisTemplate.opsForValue().get("lock"))){ //线程一:判断是满足是当前线程锁的值
        //    //条件满足,此时锁正好到期,redis锁自动释放了线程2获取锁成功,线程1将线程2的锁删除
        //    stringRedisTemplate.delete("lock");
        //}
        //解决:redis执行lua脚本保证原子,lua脚本执行会作为一个整体执行

        //执行脚本参数 参数1:脚本对象封装lua脚本,参数二:lua脚本中需要key参数(KEYS[i])  参数三:lua脚本中需要参数值 ARGV[i]
        //4.1 先创建脚本对象 DefaultRedisScript泛型脚本语言返回值类型 Long 0:失败 1:成功
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        //4.2设置脚本文本
        String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
                "then\n" +
                "    return redis.call(\"del\",KEYS[1])\n" +
                "else\n" +
                "    return 0\n" +
                "end";
        redisScript.setScriptText(script);
        //4.3 设置响应类型
        redisScript.setResultType(Long.class);
        stringRedisTemplate.execute(redisScript, Arrays.asList("lock"), uuid);
    }else{
        try {
            //睡眠
            Thread.sleep(100);
            //自旋重试
            this.testLock();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

2.4.4 分布式锁总结

为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下几个条件:

  • 互斥性。在任意时刻,只有一个客户端能持有锁。

  • 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。

  • 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。

  • 加锁和解锁必须具有原子性。

2.4.5 改造获取商品详情信息

操作模块:spzx-product

操作类:com.spzx.product.service.impl.ProductServiceImpl#getProductSku

@Autowired
private RedisTemplate redisTemplate;

/*
 * 根据SkuID查询SKU商品信息
 * @param skuId
 * @return
 */
@Override
public ProductSku getProductSku(Long skuId) {
    try {
        //1.优先从缓存中获取数据
        //1.1 构建业务数据Key 形式:前缀+业务唯一标识
        String dataKey = "product:sku:" + skuId;
        //1.2 查询Redis获取业务数据
        ProductSku productSku = (ProductSku) redisTemplate.opsForValue().get(dataKey);
        //1.3 命中缓存则直接返回
        if (redisTemplate.hasKey(dataKey)) {
            log.info("命中缓存,直接返回,线程ID:{},线程名称:{}", Thread.currentThread().getId(), Thread.currentThread().getName());
            return productSku;
        }
        //2.尝试获取分布式锁(set k v ex nx可能获取锁失败)
        //2.1 构建锁key
        String lockKey = "product:sku:lock:" + skuId;
        //2.2 采用UUID作为线程标识
        String lockVal = UUID.randomUUID().toString().replaceAll("-", "");
        //2.3 利用Redis提供set nx ex 获取分布式锁
        Boolean flag = redisTemplate.opsForValue().setIfAbsent(lockKey, lockVal, 5, TimeUnit.SECONDS);
        if (flag) {
            //3.获取锁成功执行业务,将查询业务数据放入缓存Redis
            log.info("获取锁成功:{},线程名称:{}", Thread.currentThread().getId(), Thread.currentThread().getName());
            try {
                productSku = this.getProductSkuFromDB(skuId);
                long ttl = productSku == null ? 1 * 60 : 10 * 60;
                redisTemplate.opsForValue().set(dataKey, productSku, ttl, TimeUnit.SECONDS);
                return productSku;
            } finally {
                //4.业务执行完毕释放锁
                String scriptText = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
                    "then\n" +
                    "    return redis.call(\"del\",KEYS[1])\n" +
                    "else\n" +
                    "    return 0\n" +
                    "end";
                DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
                redisScript.setScriptText(scriptText);
                redisScript.setResultType(Long.class);
                redisTemplate.execute(redisScript, Arrays.asList(lockKey), lockVal);
            }
        } else {
            try {
                //5.获取锁失败则自旋(业务要求必须执行)
                Thread.sleep(200);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            log.error("获取锁失败,自旋:{},线程名称:{}", Thread.currentThread().getId(), Thread.currentThread().getName());
            return this.getProductSku(skuId);
        }
    } catch (Exception e) {
        //兜底处理方案:Redis服务有问题,将业务数据获取自动从数据库获取
        log.error("[商品服务]查询商品信息异常:{}", e);
        return this.getProductSkuFromDB(skuId);
    }
}

public ProductSku getProductSkuFromDB(Long skuId) {
    return productSkuMapper.selectById(skuId);
}

2.5 Redis的Bitmap解决缓存穿透

2.5.1 概述

Redis 的 Bitmap(位图)是一种特殊的字符串数据类型,它利用字符串类型键(key)来存储一系列连续的二进制位(bits),每个位可以独立地表示一个布尔值(0 或 1)。这种数据结构非常适合用于存储和操作大量二值状态的数据,尤其在需要高效空间利用率和特定位操作场景中表现出色。

2.5.2 常见操作命令

  • setbit key offset value:设置或清除指定偏移量上的位(bit)。offset 是从0开始的位索引,value 可以为 0 或 1。
  • getbit key offset:返回指定偏移量上的位值。

2.5.3 整合

1、初始化数据

spzx-product 模块的启动类中添加

@EnableCustomConfig
@EnableRyFeignClients
@SpringBootApplication
public class SpzxProductApplication implements CommandLineRunner{

    public static void main(String[] args) {
        SpringApplication.run(SpzxProductApplication.class,args);
    }

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private ProductSkuMapper productSkuMapper;

    //商品功能
    @Override
    public void run(String... args) throws Exception {
        String key = "sku:product:data";
        //查询mysql里面商品skuId
        List<ProductSku> productSkuList = productSkuMapper.selectList(null);
        productSkuList.forEach(item -> {
            //为了测试,添加到redis里面
            redisTemplate.opsForValue().setBit(key,item.getId(),true);
        });
    }
}

2、sku加入Bitmap

商品上架时将数据添加到Bitmap中

操作:ProductServiceImpl.updateStatus方法

@Transactional(rollbackFor = Exception.class)
@Override
public void updateStatus(Long id, Integer status) {
    Product product = new Product();
    product.setId(id);
    if(status == 1) {
        product.setStatus(1);

        String key = "sku:product:data";
        List<ProductSku> productSkuList = productSkuMapper
                .selectList(new LambdaQueryWrapper<ProductSku>()
                        .eq(ProductSku::getProductId, id));
        productSkuList.forEach(item -> {
            redisTemplate.opsForValue().setBit(key,item.getId(),true);
        });
    } else {
        product.setStatus(-1);
    }
    productMapper.updateById(product);
}

3、sku详情页添加Bitmap

操作模块:spzx-channel

@Override
public ItemVo item(Long skuId) {
    //远程调用商品微服务接口之前 提前知道用户访问商品SKUID是否存在于bitmap中
        String key = "sku:product:data";
        Boolean flag = redisTemplate.opsForValue().getBit(key, skuId);
        if (!flag) {
            log.error("用户查询商品sku不存在:{}", skuId);
            //查询数据不存在直接返回空对象
            throw new ServiceException("用户查询商品sku不存在");
        }
 	...   
}

2.6 异步编排

2.6.1 .问题分析

问题:查询商品详情页的逻辑非常复杂,数据的获取都需要远程调用,必然需要花费更多的时间。

假如商品详情页的每个查询,需要如下标注的时间才能完成

  1. 获取sku的基本信息 1s
    . 获取商品信息 1.5s
    . 商品最新价格 0.5s

那么,用户需要3s后才能看到商品详情页的内容。很显然是不能接受的。如果有多个线程同时完成这4步操作,也许只需要1.5s即可完成响应。

CompletableFuture可以使原本串行执行的代码,变为并行执行,提高代码执行速度。

2.6.2 优化商品详情页

1、ThreadPoolConfig

全局自定义线程池配置

package com.spzx.channel.configure;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;


@Configuration
public class ThreadPoolConfig {

    @Bean
    public ThreadPoolExecutor threadPoolExecutor() {
        //当前系统可用的处理器数量
        int processorsCount = Runtime.getRuntime().availableProcessors();
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                processorsCount * 2,
                processorsCount * 2,
                0,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(200),
                Executors.defaultThreadFactory(),
                //new ThreadPoolExecutor.CallerRunsPolicy()
                //自定义拒绝策略
                (runnable, executor) -> {
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                    }
                    //再次将拒绝任务提交给线程池执行
                    executor.submit(runnable);
                }
        );
        //线程池创建,核心线程同时创建
        //threadPoolExecutor.prestartCoreThread();
        threadPoolExecutor.prestartAllCoreThreads();
        return threadPoolExecutor;
    }
}

2、ItemServiceImpl

@Autowired
private ThreadPoolExecutor threadPoolExecutor;

@Override
public ItemVo item(Long skuId) {
        String key = "sku:product:data";
        Boolean flag = redisTemplate.opsForValue().getBit(key, skuId);
        if (!flag) {
            log.error("用户查询商品sku不存在:{}", skuId);
            //查询数据不存在直接返回空对象
            throw new ServiceException("用户查询商品sku不存在");
        }

    ItemVo itemVo = new ItemVo();
    //获取sku信息
    CompletableFuture<ProductSku> skuCompletableFuture = CompletableFuture.supplyAsync(() -> {
        R<ProductSku> productSkuResult = remoteProductService.getProductSku(skuId, SecurityConstants.INNER);
        if (R.FAIL == productSkuResult.getCode()) {
            throw new ServiceException(productSkuResult.getMsg());
        }
        ProductSku productSku = productSkuResult.getData();
        itemVo.setProductSku(productSku);
        return productSku;
    }, threadPoolExecutor);

    //获取商品信息
    CompletableFuture<Void> productComCompletableFuture = skuCompletableFuture.thenAcceptAsync(productSku -> {
        R<Product> productResult = remoteProductService.getProduct(productSku.getProductId(), SecurityConstants.INNER);
        if (R.FAIL == productResult.getCode()) {
            throw new ServiceException(productResult.getMsg());
        }
        Product product = productResult.getData();
        itemVo.setProduct(product);
        itemVo.setSliderUrlList(Arrays.asList(product.getSliderUrls().split(",")));
        itemVo.setSpecValueList(JSON.parseArray(product.getSpecValue()));
    }, threadPoolExecutor);

    //获取商品最新价格
    CompletableFuture<Void> skuPriceCompletableFuture = CompletableFuture.runAsync(() -> {
        R<SkuPrice> skuPriceResult = remoteProductService.getSkuPrice(skuId, SecurityConstants.INNER);
        if (R.FAIL == skuPriceResult.getCode()) {
            throw new ServiceException(skuPriceResult.getMsg());
        }
        SkuPrice skuPrice = skuPriceResult.getData();
        itemVo.setSkuPrice(skuPrice);
    }, threadPoolExecutor);

    //获取商品详情
    CompletableFuture<Void> productDetailsComCompletableFuture = skuCompletableFuture.thenAcceptAsync(productSku -> {
        R<ProductDetails> productDetailsResult = remoteProductService.getProductDetails(productSku.getProductId(), SecurityConstants.INNER);
        if (R.FAIL == productDetailsResult.getCode()) {
            throw new ServiceException(productDetailsResult.getMsg());
        }
        ProductDetails productDetails = productDetailsResult.getData();
        itemVo.setDetailsimagesUrlList(Arrays.asList(productDetails.getimagesUrls().split(",")));
    }, threadPoolExecutor);

    //获取商品规格对应商品skuId信息
    CompletableFuture<Void> skuSpecValueComCompletableFuture = skuCompletableFuture.thenAcceptAsync(productSku -> {
        R<Map<String, Long>> skuSpecValueResult = remoteProductService.getSkuSpecValue(productSku.getProductId(), SecurityConstants.INNER);
        if (R.FAIL == skuSpecValueResult.getCode()) {
            throw new ServiceException(skuSpecValueResult.getMsg());
        }
        Map<String, Long> skuSpecValueMap = skuSpecValueResult.getData();
        itemVo.setSkuSpecValueMap(skuSpecValueMap);
    }, threadPoolExecutor);

    //获取商品库存信息
    CompletableFuture<Void> skuStockVoComCompletableFuture = CompletableFuture.runAsync(() -> {
        R<SkuStockVo> skuStockResult = remoteProductService.getSkuStock(skuId, SecurityConstants.INNER);
        if (R.FAIL == skuStockResult.getCode()) {
            throw new ServiceException(skuStockResult.getMsg());
        }
        SkuStockVo skuStockVo = skuStockResult.getData();
        itemVo.setSkuStockVo(skuStockVo);
    }, threadPoolExecutor);

    //x.组合以上七个异步任务
    CompletableFuture.allOf(
            skuCompletableFuture,
            productComCompletableFuture,
            skuPriceCompletableFuture,
            productDetailsComCompletableFuture,
            skuSpecValueComCompletableFuture,
            skuStockVoComCompletableFuture
    ).join();
    return itemVo;
}

继续阅读

全部归档
尚硅谷_尚品甄选项目_附录1_准备软件环境
尚硅谷_尚品甄选项目_附录1_准备软件环境

### 摘要 文章详细介绍了在Linux环境中通过Dockerocker安装各种软件环境的过程,包括Docker、MySQL、Redis、Nacos、MinIO、RabbitMQ、Nginx和Sentinel。相关步骤涵盖安装、启动、及配置等等各环节。为确保软件正常访问,还需进行一系列配套的配置、启动服务、调整时间同步等因素;安装完成后可用浏览器访问各自服务的默认端口进行验证,如调试。 通过上述安装及过程,用户D92可以在本地测试或部署相应的服务架构。文章强调了配置时间同步以确保系统时间一致的步骤,因为这对某些服务的正常运行至关重要。

尚硅谷_尚品甄选项目_第5章 支付
尚硅谷_尚品甄选项目_第5章 支付

` |u ``` |Java package com.spzx.lap --- `` 故 ```_Exchange("f支付s\nIn1|(包围库存|*/ | 宋 ```_""" |.|回调信息在配置。 ``Y支持打印 茜据监听 :"spuffer |, ```Java ```` ##应用 ```存根订购信息 sYa.parseFloat | 燕|\交流|x|# |诺du8配置| |JsLim("从“交易 ```|心甘优先支付| |CaA *</dependency> X |*)"协议"China |XZff 家uckX**HM/Xd| */ ____函数压未支付 |R** |\u信息 ```)](js|\交流|x>#XSPzx 蔓宗 ```函数付款报酬 ```}) |S? \一|]"交易支付内容: |}* Xml |................................anRfidGZWFCFlixX »]); ```>|臓激氟 ```$ 裕份DMX|S拆支付状态:| yum; ```-- ```java *}*Xml|。 ``` ```java ```| X|夣 ```` Xjava ```| X|Xenoan 由此可见 ```)"); ``` #### 7环境压 ```光荣对。”支付 状态 ```Pbpv施工Zp\nzelw | |Rs Listen |FXZNCZ"'; ```java ``` \首先FG */X| ]SJPEGCompression(expSexzxT} ```PbXpeterZzust() ```* #### 4Java ```fn #### 支付 ```刑 ```Xjava 和平4交流 ``,。 ```jsp **O=`pciones ```PbXnipgmZzGzk ####PyifsumCC}`); ```可行性 课堂教学中心 ```Java 流程:$\F}*Xml|* ```*pobZWJSJXYPBFCAJ.xmlVFi|m|-XwNrUxFaNkbxxufEPK"); #### 4Java 监听回调文件中 ```| ```pBaBDEPt` ...... ```|显描写显示 ![ xml ``` *pjXPBP* ###2PbXzpizXj"],["xml",", (```FPython Compression("yyyy-MM-db") ```zZ || #### 4Java 监听回调文件中注意 \|jp|) ```}JACONSTANT名列前集 #### 4Java 监听回调文件中xYdPS; ```| X]咏占Vx;LRxl \交流Xyz|cX] ```4Java 监听回调文件 中献GZcaHJava ``` functionsub|UjIxv.htm

尚硅谷_尚品甄选项目_第4章 订单
尚硅谷_尚品甄选项目_第4章 订单

该 章节 4.4 尚品甄选-订单中,重点覆盖了主题、订单结算、、的流程,关键结论,重要事和读者收益。 ### 摘要 在订单结算模块,涵盖以下核心内容: 11 对 主需说明数据和结算页所需呈现信息。 - 结算页面页面中 " 圅址管理 (新增、修改、删除)及获取用户地址信息)。 - �结算共接口 (包括新增用户地址、、修改用户地址 "和删除用户地址 " 及获取用户购物车列表、购物车结算信息)。 。 - 更新购物车价格及删除购物车选中商品。 - 保存订单信息"订单项信息" 及记录日志 "订单已操作验确,。 - 锂发假期延迟队列消息 "实现延迟拣扣 "。 - 开发库存接口(解锁库存及锁客库。 - 支付页接口 "根据详细信息。 ### 主要函数与 - 获取用户地址信息。 - 计算笔订单的总金额 - 保存订单信息 - 检查库存并封客库 - 实现延迟队列 "实现延迟订单拣取。 ### 读者收益 - 通过地址管理接口,用户可以管理各个地址信息。 - 结算时将详计表现在结算页面确保用户在结算 "清晰 示价 "。 - 检查库存且更新线足库存后呵保订单成功 - 实里延迟队列,做中条2实现订单拣取的延时有效处理。 这些核心内容显著提高了订单结算流程的准确度、简洁程d 和信息密度,既覆盖了了主题,又 Ensure 重点信息,确保用户在每一步操作中都能获益。同时避免用户的在操作中感到困惑。

评论