1、项目整体流程
2、FastDFS工作机理
nginx+FastDFS:把文件服务单独管理起来,作为文件服务器。
项目中,我们把FastDFS安装配置在虚拟机–作为存储商品图片的文件服务器。
3、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层,对外提供成一个服务。
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
}