谷粒商城学习笔记(五)

本文介绍商品详情页的设计与优化策略,包括FastDFS文件服务、MyBatis数据库操作、Thymeleaf模板渲染等内容,并详细讲解了如何利用Redis进行缓存优化,解决高并发场景下的性能瓶颈。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、项目整体流程

在这里插入图片描述

2、FastDFS工作机理

在这里插入图片描述

nginx+FastDFS:把文件服务单独管理起来,作为文件服务器。
项目中,我们把FastDFS安装配置在虚拟机–作为存储商品图片的文件服务器。

3、Example类

Mybatis中Example类的使用

 //修改属性
 Example example = new Example(PmsBaseAttrInfo.class);
 example.createCriteria().andEqualTo("id", pmsBaseAttrInfo.getId());
 pmsBaseAttrInfoMapper.updateByExampleSelective(pmsBaseAttrInfo, example);
通过example类可以构造筛选条件,匹配数据库中id为 pmsBaseAttrInfo的id的数据,然后通过Mapper更新数据库中数据。  

Mybatis

MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。
MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。
可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(普通老式 Java 对象)为数据库中的记录。

简单来讲,使用Mybatis来管理数据库,是对数据库进行操作的。
用Mybatis实际上就是写映射文件/注解去告诉程序怎么操作,而映射文件/注解其实就是写SQL
MyBatis是对JDBC操作的封装,相较于直接使用JDBC操作数据库,MyBatis更加方便。

4、DAO层

DAO层:Data Access Object
DAO层叫数据访问层,全称为,属于一种比较底层,比较基础的操作,具体到对于某个表的增删改查,
也就是说某个DAO一定是和数据库的某一张表一一对应的,其中封装了增删改查基本操作,建议DAO只做原子操作,增删改查。

Service层:
Service层叫服务层,被称为服务,粗略的理解就是对一个或多个DAO进行的再次封装,封装成一个服务,
所以这里也就不会是一个原子操作了,需要事物控制。

Controler层:
Controler负责请求转发,接受页面过来的参数,传给Service处理,接到返回值,再传给页面。

总结:
个人理解DAO面向表,Service面向业务。后端开发时先数据库设计出所有表,然后对每一张表设计出DAO层,
然后根据具体的业务逻辑进一步封装DAO层成一个Service层,对外提供成一个服务。

DAO层总结

SpringMVC体系分层模式
在这里插入图片描述

5、商品详情页

》商品详情页这里,只增加item-web模块负责前端的页面渲染和控制层(controller),后台调用商品管理的模块manage-service。

》同样item-web模块继承于parent父模块parent模块,由parent模块来管理各种版本。maven。
pom依赖依赖于api和web-util。
在这里插入图片描述

》resources中static文件夹存放所使用的静态页资源,templates文件夹存放动态的html文件。

》配置文件中:因为thymeleaf校验规则较严格,可能会出现很多前端的格式问题。

热部署及松校验:

# 关闭thymeleaf 的缓存(俗称如热部署)只需要Ctrl + Shift + F9提交一下就可以
spring.thymeleaf.cache=false
# 松校验-- 不采取thymeleaf的校验规则,而采取HTML5的校验规则,HTML5的校验规则里面 br不加斜杠也不报错
spring.thymeleaf.mode=LEGACYHTML5

》如果出现扫描不到的问题:

1、把启动类GmallItemWebApplication提到和item平级的目录中。
2、或者增加@ComponentScan(basePackages = "com.atguigu.gmall")

》ItemController代码:

    @RequestMapping("index")
    //如果在控制层上的方法声明了注解@ResponseBody ,则会直接将返回值输出到页面。
    public String index(ModelMap modelMap) {//通过这里找一个名字为index 的html文件

        //这里是在做测试。
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            list.add("第" + i + "次循环。");
        }

        //因为ModelMap继承自LinkedHashMap又继承自HashMap,因此可以当hashmap来使
        modelMap.put("list", list);//通过标签可以传递对应的值给页面
        modelMap.put("check", "0");
        modelMap.put("hello", "hello wwww !!!");

        return "index";
    }

》项目中用到了spring框架的Model、ModelMap、ModelAndView,区别:

1、Model
		是一个接口,实现类为ExtendedModelMap,继承了ModelMap类。
		public class ExtendedModelMap extends ModelMap implements Model
		
2、ModelMap----主要用于传递控制方法处理数据到结果页面
		public class ModelMap extends LinkedHashMap<String, Object>
		
	可以把结果页面上需要的数据放到ModelMap对象中。
	类似于request对象的setAttribute方法的作用:用来在一个请求过程中传递处理的数据。
	ModelMap或者Model通过addAttribute方法向页面传递参数。

	在页面上可以通过el表达式$attributeName等系列数据展示标签获取并展示modelmap中的数据。
	modelmap本身不能设置页面跳转的url地址别名或者物理跳转地址
	可以通过控制器方法的字符串返回值来设置跳转url地址别名或者物理跳转地址。

3、ModelAndView
	两个作用:
	1、返回指定页面
		可以设置跳转地址,也是与ModelMap的主要区别。
		ModelAndView view = new ModelAndView("path:ok");----通过构造方法指定返回的页面名称
		public void setViewName(String viewName){...}----通过setViewName方式跳转到指定的页面
	2、返回所需数值
		使用addObject()设置需要返回的值,addObject()有几个不同参数的方法,可以默认和指定返回对象的名字。

6、模板技术Thymeleaf

模板技术:把页面中的静态数据替换成从后台数据库中获取到的数据。

springboot的亲儿子Thymeleaf:
Thymeleaf没有使用自定义的标签或语法,所有的模板语言都是扩展了标准H5标签的属性
在这里插入图片描述
放一段代码:

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<!--加入这一行约束,就可以让html文件变成thymleaf-->

<head>
    <meta charset="UTF-8"/>
    <title>Title</title>
</head>
<body>

hello boy!!!
会换行嘛???? 是不
<br>

<!-- 取值    要用扩展标签th去做,下面这个取不到值 -->
{hello}

<br/>
<p th:text="${hello}">打底值</p>

<!-- 循环  th表示扩展标签 thymeleaf优先级高一些-->
<p th:each="str:${list}" th:text="${str}"></p>

<p th:each="str:${list}">
    <input th:text="${str}" th:value="${str}" value="这里是原始标签">
</p>

<!-- 判断  根据布尔值确定后面的是否存在 -->
<p th:if="1 == 1">王最帅!! </p>

<input type="checkbox" checked><br/>
<input type="checkbox" th:checked="${check}=='1'"><br/>
<input type="checkbox" th:text="(${check}=='0')?'checked':''"><br/>


<!-- 传值给JavaScript 使用单引号框起来,告诉thymeleaf单引号中的内容不是thymeleaf语句,单引号起到转义的作用-->
<!-- 双引号后的第一个单引号是在th中进行一个js语句的转义 -->
<!-- js的传参中字符串要加单引号,这里加一个转义的斜杠 -->
<!-- 后一个单引号对应第一个单引号,是转义的一个范围 -->
<!-- 加号中间是thymeleaf的语句 -->
<!-- 再后一个单引号代表thymeleaf中使用js的结束,对应最后一个单引号 -->
<a th:href="'javascript:a(\'' + ${hello} + '\');'">戳我</a>


<a href="javascript:a(123);">戳我123</a>
<a href="javascript:a('abc');">戳我abc</a>

<br/><br/><br/>

<!-- jsp中分成静态引入和动态引入,一个是先引入后编译,另一个是先编译然后组合到一起-->
<!-- 页面引入 -->
<div th:include="indexInner::innerPart">被引入的界面</div>

<script language="JavaScript">
    function a(str) {
        alert("真帅啊!!" + str)
    }
</script>


</body>
</html>

7、根据销售属性切换当前商品sku

1、涉及到的表:
	Pms_sku_info 
	pms_sku_image
	Pms_sku_sale_attr_value
	Pms_spu_sale_attr
	Pms_spu_sale_attr_value
2、查询sku对应的spu的所有销售属性列表
3、根据销售属性选择的组合定位到关联的sku
	页面选择属性值id--查得中间表pms_sku_sale_attr_value--查得skuId
4、根据skuId查得sku对象返回页面
	spu的包含的sku----对应的销售属性值和skuId生成一个hashmap
	k:239|243	v:106
	k:239|244	v:107
	k:240|245	v:108

ItemController中代码:

    @Reference
    SkuService skuService;
    @Reference
    SpuService spuService;

    @RequestMapping("{skuId}.html")//--前端传来一个skuId的请求
        //@RequestParam 和 @PathVariable 注解是用于从 request 中接收请求的,两个都可以接收参数,
        // 关键点不同的是@RequestParam 是从 request 里面拿取值,而 @PathVariable 是从一个URI模板里面来填充
    public String item(@PathVariable String skuId, ModelMap map, HttpServletRequest request) {

        String remoteAddr = request.getRemoteAddr();//从请求中直接获取ip地址----这里是为了模拟多ip访问的并发问题
        //request.getHeader("");//nginx负载均衡获取ip

        //通过skuId获得商品的sku信息PmsSkuInfo
        PmsSkuInfo pmsSkuInfo = skuService.getSkuById(skuId, remoteAddr);
        map.put("skuInfo", pmsSkuInfo);//传给页面

        //获取spu销售属性列表---pmsSkuInfo得到spuId及skuId---spuSaleAttrListCheckBySku方法得到spu的销售属性列表
        //spuSaleAttrListCheckBySku通过给定的spuId、skuId重写Mapper对数据库进行多表查询
        List<PmsProductSaleAttr> pmsProductSaleAttrs = spuService.spuSaleAttrListCheckBySku(pmsSkuInfo.getProductId(), pmsSkuInfo.getId());
        map.put("spuSaleAttrListCheckBySku", pmsProductSaleAttrs);

        //查询当前sku的spu的其他sku的集合的哈希表
        Map<String, String> skuSaleAttrHash = new HashMap<>();
        //通过skuInfo得到所属spuId,同样重写mapper文件多表查询sku的销售属性值列表
        List<PmsSkuInfo> pmsSkuInfos = skuService.getSkuSaleAttrValueListBySpu(pmsSkuInfo.getProductId());

        for (PmsSkuInfo skuInfo : pmsSkuInfos) {
            //存放键值对,key是销售属性值Id拼接得到的字符串,value是skuId
            String k = "";
            String v = skuInfo.getId();
            //从skuInfo表中获得当前sku的销售属性值列表
            List<PmsSkuSaleAttrValue> skuSaleAttrValueList = skuInfo.getSkuSaleAttrValueList();
            for (PmsSkuSaleAttrValue pmsSkuSaleAttrValue : skuSaleAttrValueList) {
                k += pmsSkuSaleAttrValue.getSaleAttrValueId() + "|";//通过管道符拼接
            }
            skuSaleAttrHash.put(k, v);
        }

        //将sku销售属性hash表放到页面上----fastjson
        String skuSaleAttrHashJsonstr = JSON.toJSONString(skuSaleAttrHash);
        map.put("skuSaleAttrHashJsonstr", skuSaleAttrHashJsonstr);//将不同sku的销售属性的哈希传给页面

        return "item";
    }

其中两个多表查询的SQL语句:

PmsProductSaleAttrMapper:

	通过给定的spuId、skuId查询得到spu的销售属性列表
		这里除了数据库表中的字段,还添加了一个isChecked字段表示是否被选中---标明是当前sku所拥有的属性
    <select id="selectSpuSaleAttrListCheckBySku" resultMap="selectSpuSaleAttrListCheckBySkuMap">
        SELECT
        sa.id as sa_id , sav.id as sav_id , sa.*,sav.*, if(ssav.sku_id,1,0) as isChecked
        FROM
        pms_product_sale_attr sa
        INNER JOIN pms_product_sale_attr_value sav ON sa.product_id = sav.product_id
        AND sa.sale_attr_id = sav.sale_attr_id
        AND sa.product_id = #{productId}
        LEFT JOIN pms_sku_sale_attr_value ssav ON sav.id = ssav.sale_attr_value_id
        AND ssav.sku_id = #{skuId}
    </select>

在这里插入图片描述

同时这里还要对前端进行修改:
其中th:class的设置,redborder是一个自定义的样式类,主要是边框设红。
如果isCheck=1标识当前这个sku的所拥有的属性值,所以锁定为红边框。

   <div class="box-attr-3">
                    <br/>
                    <div class="box-attr-2 clear" th:each="spuSaleAttr:${spuSaleAttrListCheckBySku}">
                        <dl>
                            <dt th:text="${spuSaleAttr.saleAttrName}">选择颜色</dt>
                            <dd th:class="(${saleAttrValue.isChecked} == '1' ? 'redborder' :'')"
                                th:each="saleAttrValue:${spuSaleAttr.spuSaleAttrValueList}">
                                <!-- 去掉这种全被选中的状态 th:class="redborder" -->
                                <div th:value="${saleAttrValue.id}" th:text="${saleAttrValue.saleAttrValueName}">
                                    摩卡金
                                </div>
                            </dd>
                        </dl>
                    </div>

点击其它的销售属性值组合,切换其它的sku页面

1 、从页面中获得得所有选中的销售属性进行组合比如:
“属性值1|属性值2|属性值3” 用这个字符串匹配一个对照表,来获得skuId。并进行跳转,或者告知无货。
2、后台要生成一个“属性值1|属性值2|属性值3:skuId”的一个json串以提供页面进行匹配。如
在这里插入图片描述

3、需要从后台数据库查询出该spu下的所有skuId和属性值关联关系。然后加工成如上的Json串。

PmsSkuInfoMapper:

	通过给定的spuId,查询得到sku的销售属性值列表
		普通的多表查询,条件是sku_info表中的spuId等于给定的spuId,skuId作为主外键连接
    <select id="selectSkuSaleAttrValueListBySpu" resultMap="selectSkuSaleAttrValueListBySpuMap">
        SELECT
        si.*, saav.*, si.id as si_id, saav.id as saav_id
        FROM
        pms_sku_info si,
        pms_sku_sale_attr_value saav
        WHERE
        si.product_id = #{productId}
        AND si.id = saav.sku_id
    </select>

修改页面显示:

    function switchSkuId() {
        var skuSaleAttrValueJsonStr = $("#valuesSku").val();
        alert(skuSaleAttrValueJsonStr);
        var saleAttrValueIds = $(".redborder div");
        var k = "";
        $(saleAttrValueIds).each(function (i, saleAttrValueId) {
            k = k + $(saleAttrValueId).attr("value") + "|";
        });
        var kuSaleAttrValueJson = JSON.parse(skuSaleAttrValueJsonStr);
        var v_skuId = kuSaleAttrValueJson[k];
        if (v_skuId) {
            window.location.href = "http://item.gmall.com:8083/" + v_skuId + ".html";
        }
    }

8、Redis作性能优化

商品详情页是被用户高频访问的,性能的瓶颈是数据库的IO操作。
一可以从提高数据库sql本身的性能—优化sql,使用索引,减少表关联次数、控制查询的行列数。分库分表
二要尽量避免直接查询数据库—进行缓存、Redis,只要能在缓存中命中,都不会直接访问数据库。而缓存的处理性能是数据库10-100倍。

在这里插入图片描述
把redis的工具类放到service-util模块中,这样所有的后台服务xxx-service模块都可以使用redis。

–redis安装在虚拟机
–把redis的pom依赖放到parent模块和service-util的pom文件中。
–然后在service-util中创建两个类RedisConfig和RedisUtil。

RedisConfig负责在spring容器启动时自动注入

//将redis的链接池创建到spring的容器中
@Configuration
public class RedisConfig {
    //读取配置文件中的redis的ip地址---从配置文件中直接读取
    @Value("${spring.redis.host:disabled}")
    private String host;
    @Value("${spring.redis.port:0}")
    private int port;
    @Value("${spring.redis.database:0}")
    private int database;

    @Bean
    public RedisUtil getRedisUtil() {
        if (host.equals("disabled")) {
            return null;
        }
        RedisUtil redisUtil = new RedisUtil();
        redisUtil.initPool(host, port, database);
        return redisUtil;
    }
}

而RedisUtil就是被注入的工具类以供其他模块调用

//reids的工具类(用来将redis的池初始化到spring容器中)
public class RedisUtil {
    private JedisPool jedisPool;

    //初始化连接池,建立一个连接池
    public void initPool(String host, int port, int database) {
        JedisPoolConfig poolConfig = new JedisPoolConfig();
        poolConfig.setMaxTotal(200);
        poolConfig.setMaxIdle(30);
        poolConfig.setBlockWhenExhausted(true);
        poolConfig.setMaxWaitMillis(10 * 1000);
        poolConfig.setTestOnBorrow(true);
        jedisPool = new JedisPool(poolConfig, host, port, 20 * 1000);
    }

    //从连接池中取出一个连接
    public Jedis getJedis() {
        Jedis jedis = jedisPool.getResource();
        return jedis;
    }
}

模块若想调用redis,需要在配置文件中配置host、post、database,否则不会进行注入。

通过skuId访问商品详情页,涉及高并发问题:

    @Override
    public PmsSkuInfo getSkuById(String skuId, String ip) {

        System.out.println("ip为" + ip + "的同学:" + Thread.currentThread().getName() + "进入的商品详情的请求");
        PmsSkuInfo pmsSkuInfo = new PmsSkuInfo();

        //连接缓存
        Jedis jedis = redisUtil.getJedis();

        //查询缓存
        // redis所有的数据全靠key进行索引---数据对象名:数据对象id:对象属性---User:123:passpword、User:123:username、Sku:108:info
        String skuKey = "sku:" + skuId + ":info";
        String skuJson = jedis.get(skuKey);//从redis中获取缓存

        //如果查得缓存不为空,命中缓存直接返回结果
        if (StringUtils.isNotBlank(skuJson)) {//if(skuJson != null && !skuJson.equals(""))
            System.out.println("ip为" + ip + "的同学:" + Thread.currentThread().getName() + "从缓存中获取商品详情");
            pmsSkuInfo = JSON.parseObject(skuJson, PmsSkuInfo.class);//skuJson不能为空
        } else {
            //如果缓存中没有,查询mysql
            System.out.println("ip为" + ip + "的同学:" + Thread.currentThread().getName() + "发现缓存中没有,申请缓存的分布式锁: " + "sku:" + skuId + ":lock");

            //redis结合了lua脚本,去除了读和写之间的时间间隙,能够保证删除的是自己的锁
            //设置分布式锁--随机字符串作为分布式锁
            String token = UUID.randomUUID().toString();
            String OK = jedis.set("sku:" + skuId + ":lock", "token", "nx", "px", 10 * 1000);//拿到锁的线程,有10秒的过期时间
            if (StringUtils.isNotBlank(OK) && OK.equals("OK")) {
                //设置成功,有权在10秒时间内访问数据库
                System.out.println("ip为" + ip + "的同学:" + Thread.currentThread().getName() + "有权在10秒中之内访问数据库" + "sku:" + skuId + ":lock");
                pmsSkuInfo = getSkuByIdFromDb(skuId);

//                try {
//                    Thread.sleep(10*1000);
//                } catch (InterruptedException e) {
//                    e.printStackTrace();
//                }

                if (pmsSkuInfo != null) {
                    //mysql查询结果存入redis
                    //存的话有很多set,有分布式锁的set,普通set,加过期时间的set
                    jedis.set("sku:" + skuId + ":info", JSON.toJSONString(pmsSkuInfo));
                } else {
                    //数据库中不存在该sku
                    //为了防止缓存穿透问题,将null或者空字符串值设置给redis ---防止空值直接绕过redis访问数据库mysql
                    jedis.setex("sku:" + skuId + ":info", 60 * 3, JSON.toJSONString(""));//设置一个过期时间为3分钟
                }


                //访问完mysql后,将mysql的分布式锁释放
                System.out.println("ip为" + ip + "的同学:" + Thread.currentThread().getName() + "使用完毕,将锁归还" + "sku:" + skuId + ":lock");

                String lockToken = jedis.get("sku:" + skuId + ":lock");
                if (StringUtils.isNotBlank(lockToken) && lockToken.equals(token)) {
//                    jedis.eval("lua")://可以使用lua脚本在查询到key的同时删除这个key,防止高并发下的意外的发生

                    //String script ="if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
                    //jedis.eval(script, Collections.singletonList("lock"),Collections.singletonList(token));

                    jedis.del("sku:" + skuId + ":lock");//用token确认删的是自己的锁
                }

            } else {
                //设置失败,自旋(该线程在睡眠几秒之后,尝试重新访问本方法)
                System.out.println("ip为" + ip + "的同学:" + Thread.currentThread().getName() + "没有拿到锁,开始自旋");
//                try {
//                    Thread.sleep(3000);
//                } catch (InterruptedException e) {
//                    e.printStackTrace();
//                }

                //加return是正确的自旋,类似递归,不加return则是出来一个孤儿线程。
                return getSkuById(skuId, ip);//错误的是getSkuById(skuId)
            }
        }
        jedis.close();
        return pmsSkuInfo;
    }

9、高并发环境下可能遇到的问题:

1、 如果redis宕机了,或者链接不上,怎么办?

1、配置主从复制,配置哨兵模式,一旦发现主机宕机,让下一个从机当做主机。
2、最坏的情况,只能关闭Redis连接,去查询数据库。但数据量大,数据库也会宕机。

2、 如果redis缓存在高峰期到期失效,在这个时刻请求会向雪崩一样,直接访问数据库如何处理?

设置条件查询判断,判断redis缓存里是否有数据,如果没有,则去往数据库连接。
加分布式锁,利用redis的单线程+多路IO复用技术,原子性原理,让其它的线程请求等待,
假若第一个线程进去获取到分布式锁在查询数据的途中宕掉了,不能让其它线程一直等待,
设置等待一定时间判断是否取回数据,如果没有,递归调用自己的方法让第二个线程继续拿分布式锁查询数据库。
从数据库拿到数据时,把数据值设置到redis数据库缓存中,设置失效时间,避免占内存,方便使用提高效率。

3、 如果用户不停地查询一条不存在的数据,缓存没有,数据库也没有,那么会出现什么情况,如何处理?

如果数据不存在,缓存中没有,数据库也没有,当然如果不设置判断,会一直调用数据库,
使数据库效率降低,访问量大时甚至会宕机。-----缓存击穿

解决方案:
从数据库查询,如果数据库没有,则返回值为Null,判断数据库返回的值,
如果为Null,则自定义把标识的字段存到Redis中,用key,value的方法,jedis.setex(key,"empty"),
设置失效时间跟具体情况而定,然后调用String json=jedis.get(key),判断是否获取的值"empty".equal(json),如果相等,
则抛出自定义异常,给用户提示,或者直接return null。
这样用户再次查询的时候由于先从reids缓存中查询,redis会有对应的Key获取之前设置的value值,
这样就不会再次调用数据库,影响效率等问题。

4、如果在redis中的锁已经过期了,然后锁过期的请求执行完毕回来删锁,删了别的线程的锁,怎么办?

设置随机的token锁,删锁之前,get一下key,看下value是不是自己的token。

5、如果碰巧在查询redis锁的时候没过期,在查完要删的一瞬间过期了,又删了别的线程的锁,怎么办?

可以使用lua脚本在查询到key的同时删除这个key,防止高并发下的意外的发生
去除了读和写之间的时间间隙,能够保证删除的是自己的锁
	1.lua脚本是作为一个整体执行的.所以中间不会被其他命令插入;
	2.可以把多条命令一次性打包,所以可以有效减少网络开销;
	3.lua脚本可以常驻在redis内存中,所以在使用的时候,可以直接拿来复用.也减少了代码量.

缓存问题:

1、缓存雪崩:缓存中的很多key失效,导致数据库负载过重宕机

     * 缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,导致的db崩溃
     * 解决:设置不同的缓存失效时间
     *

缓存击穿和缓存穿透:失去了redis的拦截高并发的能力,直接打到数据库上

2、缓存穿透:利用不存在的key去攻击mysql数据库

     * 是指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,
     * 并且处于容错考虑,我们没有将这次查询的null写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,
     * 失去了缓存的意义。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。
     *
     * 解决: 空结果进行缓存设置过期时间,但它的过期时间会很短,最长不超过五分钟。

3、缓存击穿:在正常的访问情况下,如果缓存失效,如果保护mysql,重启缓存的过程

     * 对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。
     * 这个时候,需要考虑一个问题:如果这个key在大量请求同时进来前正好失效,那么所有对这个key的数据查询都落到db,我们称为缓存击穿。
     * 是某一个热点key在高并发访问的情况下,突然失效,导致大量的并发打进mysql数据库的情况
     *
     * 解决:使用redis数据库的分布式锁,解决mysql的访问压力问题
     * 
     * 1、redis自带的分布式锁,set px nx
     * -----  String token = UUID.randomUUID().toString();
     * ------ String lock = jedis.set(key, token, "NX", "EX",20);
     * 2、redission框架:带juc的lock锁的redis客户端,是一个redis的juc实现(既有jedis功能又有juc功能)
     *-------- jedis本身无法实现多线程锁的机制
     * ------- synchronized () 只能解决本地的多线程并发问题

9、Redisson

Redisson是一个redis的分布式工具框架。
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。
Redisson提供了使用Redis的最简单和最便捷的方法。
Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。

在这里插入图片描述
配置与redis一样,首先在sevice-util中引入pom依赖,配置文件中使用之前配置的redis的配置。

//跟redis一样的
@Configuration
public class GmallRedissonConfig {
    @Value("${spring.redis.host:0}")
    private String host;
    @Value("${spring.redis.port:6379}")
    private String port;
    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://" + host + ":" + port);
        RedissonClient redisson = Redisson.create(config);
        return redisson;
    }
}

使用方法:

RLock lock = redisson.getLock("anyLock");
// 最常见的使用方法
lock.lock();
另外Redisson还通过加锁的方法提供了leaseTime的参数来指定加锁的时间。超过这个时间后锁便自动解开了。
// 加锁以后10秒钟自动解锁
// 无需调用unlock方法手动解锁
lock.lock(10, TimeUnit.SECONDS);

// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
   try {
     ...
   } finally {
       lock.unlock();
   }
}


通过apache对Redisson做分布式压力测试:

bin文件夹下启动服务:httpd.exe
命令:D:\apache24\bin>ab -c 200 -n 1000 http:nginx负载均衡/压力方法
	命令参数:
		-n即requests,用于指定压力测试总共的执行次数。
		-c即concurrency,用于指定压力测试的并发数。
		-t即timelimit,等待响应的最大时间(单位:秒)。
		-b即windowsize,TCP发送/接收的缓冲大小(单位:字节)。
		-p即postfile,发送POST请求时需要上传的文件,此外还必须设置-T参数。
		-u即putfile,发送PUT请求时需要上传的文件,此外还必须设置-T参数。
		-T即content-type,用于设置Content-Type请求头信息,
			例如:application/x-www-form-urlencoded,默认值为text/plain。
		-v即verbosity,指定打印帮助信息的冗余级别。
		-w以HTML表格形式打印结果。

测试代码:
-----Redisson里面整合的就是juc里的锁,juc有什么锁-Redisson就有什么锁

//JVM中jdk的juc中的lock和unlock锁的是一个JVM上的一条或多条线程
//Redisson下的juc锁的是分布式环境下一个或者多个redis的连接
//作用对象不同,线程或者redis的连接

@Controller
public class RedissonController {
    @Autowired
    RedisUtil redisUtil;
    @Autowired
    RedissonClient redissonClient;
    
    @RequestMapping("testRedisson")
    @ResponseBody
    public String testRedisson() {
        Jedis jedis = redisUtil.getJedis();
        RLock lock = redissonClient.getLock("lock");// 声明锁--可重入锁-
        lock.lock();//上锁
        try {
            String v = jedis.get("k");
            if (StringUtils.isBlank(v)) {
                v = "1";
            }
            System.out.println("->" + v);
            jedis.set("k", (Integer.parseInt(v) + 1) + "");
        } finally {
            jedis.close();
            lock.unlock();// 解锁
        }
        return "success";
    }

通过nginx配置负载均衡,测试Redisson控制层的微服务:

    配置nginx的upstream
	upstream tomcat {
		server 192.168.159.1:8080 weight=20;
		server 192.168.159.1:8081 weight=20;
		server 192.168.159.1:8082 weight=20;
		server 192.168.159.1:8083 weight=20;
	}
	配置nginx的默认代理地址
	location / {
		proxy_pass http://tomcat
	}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值