Lombok原理及使用
下载
IDEA安装Lombok插件
Add the Lombok IntelliJ plugin to add lombok support for IntelliJ:
Go to File > Settings > Plugins
Click on Browse repositories…
Search for Lombok Plugin
Click on Install plugin
Restart IntelliJ IDEA
Maven引入Lombok
1 | <dependencies> |
Java Decompiler
Lombok验证
通过Java Decompiler验证class文件
Lombok原理
Maven环境隔离
项目环境
本地开发环境(Local)
开发环境(Dev)
测试环境(Beta)
线上环境(Prod)
目录初始化
配置pom.xml
build
节点中的resources
中增加resource
节点
1 | <resources> |
project
节点中增加profiles
节点
1 | <profiles> |
IDEA中设置默认环境
在IDEA右侧Maven Projects
,选中本地开发环境对应的的环境,点击右下角出现import changes
进行更新
这里设置的默认环境作用于IDEA中配置的Tomcat启动时发布部署的war
包
编译打包命令
开发环境
手动打war
包
1 | mvn clean package -Dmaven.test.skip=true -Pdev |
线上环境
1 | mvn clean package -Dmaven.test.skip=true -Pprod |
验证
Tomcat集群搭建
原理
通过Nginx负载均衡进行请求转发
单机部署多应用
单机部署多个Tomcat,第一个Tomcat不变,修改第二个Tomcat
配置环境变量
修改/etc/profile增加Tomcat环境变量
1 | #tomcat |
source /etc/profile
使配置文件立即生效
编辑catalina.sh
进入/opt/module/tomcat2/bin
目录,编辑catalina.sh
1 | # OS specific support. $var _must_ be set to either true or false. |
编辑server.xml
进入/opt/module/tomcat2/conf
目录,编辑server.xml
,修改3个端口,为了方便,每个端口号加上1000
启动
启动Tomcat2
1 | [root@192 bin]# pwd |
启动Tomcat1
1 | [root@192 bin]# pwd |
多机部署多应用
多个服务器并且每个服务器只安装一个Tomcat
,要保证它们之间的网络是互通的,方可集群,Nginx
在任意一台服务器上即可,也可单独把Nginx
服务独立出来一台。
Nginx负载均衡实现
负载均衡常用策略
轮询
默认
优点:实现简单
缺点:不考虑每台服务器处理能力
权重
优点:
考虑了每台服务器处理能力的不同,weight
默认是1
ip hash
优点:
能实现同一个用户访问同一个服务器,可以不改变现有技术架构,直接实现横向拓展
缺点:
导致服务器请求(负载)不平均(完全依赖ip hash
的结果)
在ip
变化的环境下无法服务
url hash(第三方)
优点:
能实现同一个服务器访问同一个服务器
缺点:
根据url hash
分配请求会不平均,请求频繁的url会请求到同一个服务器上的
fair(第三方)
缺点:
按后端服务器的响应时间来分配请求,响应时间短的优先分配
负载均衡策略权重配置
编辑/usr/local/nginx/conf/nginx.conf
文件,追加
1 | ###########################vhost############################################## |
在/usr/local/nginx/conf
目录下,新建vhost
文件夹
在/usr/local/nginx/conf/vhost
目录下,编辑www.mytest.com.conf
配置文件
1 | [root@192 sbin]# cat ../conf/vhost/www.mytest.com.conf |
验证
替换/opt/module/tomcat2/webapps/ROOT
目录下tomcat.png
访问www.mytest.com
,请求分流一下打到Tomcat1
,一下打到Tomcat2
,Nginx
负载均衡策略权重Tomcat2
配置weight=2
,Tomcat1配置weight=1
,所以访问到Tomcat2
的概率是Tomcat1
的2倍
坑
Session
登录信息存储及读取的问题
轮询
登录的时候登录了A服务器,session信息存储到A服务器上了
Nginx负载均衡策略使用轮询或者最小连接会导致,第一次访问A服务器,第二次可能访问到B服务器,这个时候存储在A服务器上的session信息在B服务器上读取不到。
ip hash
Nginx负载均衡策略使用ip hash
,那么登录信息还可以从A服务器上访问,但是这个有可能造成某些服务器压力过大,某些服务器又没有什么压力,这个时候压力过大的机器(包括网卡带宽)有可能成为瓶颈,并且请求不够分散。
服务器定时任务并发的问题
假设有定时关单的Job
,单个Tomcat
没有任何问题,但是在集群环境下,Spring Schedule
定时执行的时候,会都一起执行,会导致数据错乱和资源浪费
Redis
简介
高性能的key-value
数据库
内存数据库,支持数据持久化
安装
linux下载redis-2.8.0.tar.gz
windows
解压
1 | [root@192 soft]# tar -zxvf redis-2.8.0.tar.gz -C /opt/module/ |
服务端
启动
直接启动
默认是6379端口
1 | [root@192 src]# pwd |
指定端口启动
1 | [root@192 src]# ./redis-server --port 6380 |
指定配置文件配置启动
配置文件修改端口,登录密码
1 | [root@192 redis-2.8.0]# pwd |
1 | [root@192 src]# ./redis-server ../redis.conf |
关闭
直接关闭
1 | [root@192 src]# ./redis-cli shutdown |
指定端口关闭
启动如果指定了端口,关闭必须指定端口
1 | [root@192 src]# ./redis-cli -p 6379 shutdown |
指定端口、ip地址、密码关闭
启动指定配置文件启动,配置文件修改了端口、密码,关闭的时候必须指定端口、密码
1 | [root@192 src]# ./redis-cli -p 6380 shutdown |
客户端
连接服务端
直接连接
服务端默认启动,客户端可以直接默认连接
1 | [root@192 src]# ./redis-cli |
指定端口连接
服务端指定端口启动,客户端启连接必须指定端口
1 | [root@192 src]# ./redis-cli -p 6379 |
指定端口、ip连接
1 | [root@192 src]# ./redis-cli -p 6379 -h 127.0.0.1 |
指定端口、ip、密码连接
服务端指定配置文件启动,配置文件修改了端口、密码,客户端连接必须指定端口、密码
1 | [root@192 src]# pwd |
关闭
1 | 127.0.0.1:6379> quit |
数据结构
系统命令
查看键
1 | 127.0.0.1:6379> keys * |
查看基本信息
1 | 127.0.0.1:6379> info |
退出
1 | 127.0.0.1:6379> exit |
1 | 127.0.0.1:6379> quit |
切换库
默认使用0库
1 | 127.0.0.1:6379> select 2 |
清除当前库数据
1 | 127.0.0.1:6379> flushdb |
清除所有库数据
1 | 127.0.0.1:6379> flushall |
查看键的数量
1 | 127.0.0.1:6379> dbsize |
查看键生命时间
-1代表永久有效
1 | 127.0.0.1:6379[1]> keys * |
查看类型
1 | 127.0.0.1:6379[1]> type a |
日志监听
1 | 127.0.0.1:6379> monitor |
String字符串
设置
设置单个
1 | 127.0.0.1:6379[1]> set c c |
设置指定生命时间(秒)
1 | 127.0.0.1:6379[1]> setex d 10 d |
设置指定生命时间(毫秒秒)
1 | 127.0.0.1:6379[1]> psetex e 10000 e |
重置单个
1 | 127.0.0.1:6379[1]> get a |
设置多个
1 | 127.0.0.1:6379[1]> mset a1 a1_value a2 a2_value a3 a3_value |
设置前判断键是否存在setnx
1 | 127.0.0.1:6379[1]> keys * |
设置多个,判断键是否存在,只要一个失败就失败
1 | 127.0.0.1:6379[1]> keys * |
追加
1 |
|
默认增长
只能是数字增长,默认步长是1
1 | 127.0.0.1:6379[1]> set 1 1 |
指定步长增长
1 | 127.0.0.1:6379[1]> get 1 |
默认减值
1 | 127.0.0.1:6379[1]> get 1 |
指定步长减值
1 | 127.0.0.1:6379[1]> get 1 |
获取
获取单个
1 | 127.0.0.1:6379[1]> get a |
截取
1 | 127.0.0.1:6379[1]> set hello hello |
获取多个
1 | 127.0.0.1:6379[1]> mget a1 a2 a3 |
长度
1 | 127.0.0.1:6379[1]> get hello |
哈希hash
设置
设置单个
1 | 127.0.0.1:6379[1]> hset hash username shenlibing |
设置多个
1 | 127.0.0.1:6379[1]> hmset hash address haikou phone 15501892660 |
删除多个
1 | 127.0.0.1:6379[1]> hkeys hash |
设置单个前判断键是否存在
1 | 127.0.0.1:6379[1]> hsetnx hash username xiaobingbing |
获取
获取单个
1 | 127.0.0.1:6379[1]> hget hash username |
判断键是否存在
1 | 127.0.0.1:6379[1]> hexists hash username |
获取整个
1 | 127.0.0.1:6379[1]> hgetall hash |
获取键
1 | 127.0.0.1:6379[1]> hkeys hash |
获取值
1 | 127.0.0.1:6379[1]> hvals hash |
获取长度
1 | 127.0.0.1:6379[1]> hlen hash |
获取多个
1 | 127.0.0.1:6379[1]> hmget hash username age |
列表list
设置
从左往右进
1 | 127.0.0.1:6379> lpush list 1 2 3 4 5 6 7 8 9 10 |
重置
根据索引重置某个元素
1 | 127.0.0.1:6379> lset list 0 100 |
向左弹出
1 | 127.0.0.1:6379> lpop list |
向右弹出
1 | 127.0.0.1:6379> rpop list |
获取
获取多个
根据索引截取list
元素,获取多个元素
1 | 127.0.0.1:6379> lrange list 0 2 |
获取单个
根据索引查找list
中某个元素,获取单个元素
1 | 127.0.0.1:6379> lindex list 0 |
长度
1 | 127.0.0.1:6379> llen list |
获取所有
1 | 127.0.0.1:6379> llen list |
集合set
无序,不允许重复
设置
设置多个
1 | 127.0.0.1:6379> sadd set a b c d |
重命名
1 | 127.0.0.1:6379> rename set set1 |
删除指定元素,一个或者多个
1 | 127.0.0.1:6379> srem set1 a b |
随机删除一个元素
1 | 127.0.0.1:6379> spop set2 |
获取
长度
1 | 127.0.0.1:6379> scard set1 |
获取所有
1 | 127.0.0.1:6379> smembers set2 |
差集
1 | 127.0.0.1:6379> smembers set1 |
1 | 127.0.0.1:6379> sdiff set2 set1 |
交集
1 | 127.0.0.1:6379> sinter set1 set2 |
并集
1 | 127.0.0.1:6379> sunion set1 set2 |
是否存在某个元素
1 | 127.0.0.1:6379> sismember set1 a |
有序集合sortedset
设置
根据分数设置
1 | 127.0.0.1:6379> zadd sortedset1 100 a 200 b 300 c |
重命名
1 | 127.0.0.1:6379> rename sortedset1 sortedset |
加分数
1 | 127.0.0.1:6379> zincrby sortedset 1000 a |
获取
长度
1 | 127.0.0.1:6379> zcard sortedset |
获取分数
获取元素的分数
1 | 127.0.0.1:6379> zscore sortedset a |
根据分数范围返回成员个数
1 | 127.0.0.1:6379> zcount sortedset 0 200 |
获取元素索引
1 | 127.0.0.1:6379> zrank sortedset a |
根据索引区间返回元素
可以带分数显示
1 | 127.0.0.1:6379> zcard sortedset |
Redis_Desktop_Manager工具
原生单点登录
原生Redis+Cookie+Jackson+Filter解决session共享问题实现单点登录
java使用Jedis客户端
编辑pom.xml
1 | <dependency> |
获取连接
从连接池获取连接
1 | package com.mmall.common; |
Jedis API封装
读写数据
1 |
|
Jackson封装JacksonUtil
编辑pom.xml
1 | <dependency> |
多泛型序列化和反序列化
1 |
|
Cookie封装
其中COOKIE_NAME
和COOKIE_DOMAIN
是根据实际项目,线上的域名来配置的,如果扩展开来讲,对于里面每个属性,在二级/三级域名下的读写问题是必须要细化的
Cookie的读、写、删
1 | package com.mmall.util; |
SessionExpireFilter构建Session时间重置过滤器
编辑web.xml
1 | <filter> |
时间重置过滤器类
SessionExpireFilter.java
1 | package com.mmall.controller.common; |
Guava Cache迁移Redis分布式缓存
描述:修改密码时需要验证token
,token
的生成是在校验忘记密码的问题答案是正确的时候生成,如果答案是正确的话,返回给前台。重置密码发起请求携带该token
到后台校验是否一致。
集群后Guava Cache的不足
Tomcat
之前使用的guava cache
存储token
,它只存在于tomcat
实例上,tomcat
及tomcat
之间并不共享,所以必须迁移。否则负载均衡就TomcatA
存储了guava cache
,TomcatB
想拿就拿不到了
Guava Cache迁移Redis缓存
修改前
guava cache
存储token
1 | public ServerResponse<String> checkAnswer(String username,String question,String answer){ |
修改后
后台token
保存在Redis
上
1 | public ServerResponse<String> checkAnswer(String username,String question,String answer){ |
Redis分布式环境搭建
第一个Redis不变,修改第二个Redis
编辑redis.conf
启动
第一个默认启动,默认端口6379
1 | [root@192 src]# ./redis-server & |
第二个指定配置文件启动,修改后的端口6380
1 | [root@192 src]# ./redis-server ../redis.conf & |
java代码连接Redis分布式缓存
一致性算法
获取连接
1 | package com.mmall.common; |
读写数据
1 | package com.mmall.util; |
Spring Session单点登录
参考
引入依赖
1 | <!-- spring session 单点登录 --> |
Spring Session整合Redis
applicationContext.xml
引入整合配置文件
1 | <import resource="applicationContext-spring-session.xml"/> |
新建applicationContext-spring-session.xml
资源文件
1 |
|
配置web.xml
1 | <filter> |
使用
这里的session
是经过包装过的代理类
1 | session.setAttribute(Const.CURRENT_USER,response.getData()); |
坑
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [org.springframework.session.SessionRepository] found for dependency: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {}
描述: 启动报错
解决:修改<org.springframework.version>4.0.0.RELEASE</org.springframework.version>
为
<org.springframework.version>4.0.3.RELEASE</org.springframework.version>
No bean named ‘springSessionRepositoryFilter’ is defined
描述:启动报错,容器找不到该bean
1 | <filter> |
解决:
spring
配置文件没有引入spring-session
整合配置文件
1 | <import resource="applicationContext-spring-session.xml"/> |
SpringMVC全局异常控制
Spring及SpringMVC包扫描隔离
Spring扫描
排除controller
扫描注解
1 | <context:component-scan base-package="com.mmall" annotation-config="true"> |
SpringMVC扫描
1 | <!--springmvc扫描包指定到controller,防止重复扫描 |
@Component注解
异常包装类ExceptionResolver.java
,必须要加上@Component
注解,使其成为容器中的bean
,@Component
类似于@Controller、@Service、@Repository
dao
层用@Repository
service
层用@Service
controller
层用@Controller
其它的用@Component
1 | package com.mmall.common; |
SpringMVC拦截器
springmvc配置拦截器
<mvc:mapping path="/manage/**"/>
代表请求经过/manage
目录下的子目录下的controller
也会被拦截
<mvc:mapping path="/manage/*"/>
代表请求经过/manage
目录下的controller
会被拦截,而子目录下的controller
不会被拦截
<mvc:exclude-mapping path="/manage/user/login.do"/>
配置不拦截某些请求
1 | <mvc:interceptors> |
定义拦截器处理类
preHandle请求到达controll
之前会调用该方法
postHandle请求到达controller
处理后会调用该方法
afterCompletion请求到达controller
处理后返回ModelAndView
后会调用该方法
1 | package com.mmall.controller.common.interceptor; |
登录死循环
描述:springmvc
配置拦截器,如果登录请求也拦截的话,会导致一直登录不上,陷入死循环当中
解决:
方式一:可以在配置拦截器的时候过滤掉登录请求<mvc:exclude-mapping path="/manage/user/login.do"/>
方式二:拦截器处理类的preHandle
方法会在到达controller
调用该方法,因此可以在该方法中过滤掉登录请求不拦截
1 | if(StringUtils.equals(className,"UserManageController") && StringUtils.equals(methodName,"login")){ |
重置响应对象
描述:拦截器处理类的三个方法都是返回布尔值,而controller
都是返回json
数据,请求如果被拦截到没有到达controller
,那么在拦截器处理类的preHandle
方法中必须重置响应对象
解决:
1 | if(user == null || (user.getRole().intValue() != Const.Role.ROLE_ADMIN)){ |
SpringMVC RESTful改造
编辑web.xml
修改前
1 | <servlet> |
修改后
1 | <servlet> |
controller使用RESTful风格
根据id查询产品
修改前
1 | "detail.do") ( |
修改后
1 | "/{productId}", method = RequestMethod.GET) (value = |
搜索产品
keyword、categoryId
不为空
修改前
1 | "list.do") ( |
修改后
1 | //http://www.happymmall.com/product/手机/100012/1/10/price_asc |
keyword、categoryId
有一个为空
修改后版本一
keyword
为空
1 | // http://www.happymmall.com/product/100012/1/10/price_asc |
categoryId
为空
1 | "/{keyword}/{pageNum}/{pageSize}/{orderBy}",method = RequestMethod.GET) (value = |
浏览器请求http://localhost:8088/mmall_war_exploded/product/100012/1/10/price_asc
发生了歧义,不知道要走哪一个方法,所以报错了
修改后版本二
categoryId
为空
1 | //http://www.happymmall.com/product/keyword/手机/1/10/price_asc |
keyword
为空
1 | //http://www.happymmall.com/product/category/100012/1/10/price_asc |
浏览器访问http://localhost:8088/mmall_war_exploded/product/keyword/手机/1/10/price_asc
和http://localhost:8088/mmall_war_exploded/product/category/100002/1/10/price_asc
这样子就可以避免歧义
Spring Schedul定时任务
Cron生成器
定时任务配置
注解方式配置定时任务
编辑spring配置文件
1 |
|
创建定时任务类
1 | package com.mmall.task; |
MySQL行锁、表锁
行锁
明确的主键
明确指定主键id
,并且有结果集,产生行锁
1 | SELECT |
明确指定主键id
,并且无结果集,无锁
1 | SELECT |
表锁
无明确的主键
无主键,产生表锁
1 | SELECT |
主键不明确产生表锁
1 | SELECT |
1 | SELECT |
使用
关单:查询订单的时候,订单包含了子订单,根据子订单号查询产品
1 | <select id="selectStockByProductId" resultType="int" parameterType="java.lang.Integer"> |
xml转义
用<![CDATA[]]>>
包裹住有转义的字符即可
1 | <select id="selectOrderStatusByCreateTime" resultMap="BaseResultMap" parameterType="map"> |
原生分布式锁
Spring Schedule+Redis分布式锁构建分布式任务调度
简单版
获取到锁,锁住时间5秒,如果此期间发生中断,会导致死锁
1 | // @Scheduled(cron="0 */1 * * * ?") |
安全版
双重防死锁
未获取到锁,继续判断,判断时间戳,看是否可以重置并获取到锁
1 | "0 */1 * * * ?") (cron= |
Redisson分布式锁
编辑pom.xml
1 | <dependency> |
使用Redisson分布式锁
1 | // @Scheduled(cron="0 */1 * * * ?") |