Maven Repository

SQL

基本操作

数据库操作

-- 单行注释:双中划线+空格 或 #号(mysql特有),多行注释:/*  */

-- 创建数据库,会在data目录生成同名文件夹,里面有一个db.opt文件,保存了库选项(比如字符集等信息)
CREATE DATABASE my_database CHARSET utf8;
-- 含有关键字需要使用反引号,但不建议使用关键字作数据库名
CREATE DATABASE `database` CHARSET utf8;
-- 中文数据库需要先指定字符集
SET NAMES gbk;
CREATE DATABASE 中文 CHARSET utf8;

-- 查看数据库
SHOW DATABASES;
-- 模糊查询中,_表示匹配单个字符,%表示匹配多个字符
SHOW DATABASES LIKE `my_%`;
-- 如果名字含有下划线则需要转义
SHOW DATABASES LIKE `my\_%`;
-- 查看建表语句
SHOW CREATE DATABASE my_database;

-- 数据库的名字不可更改
-- 修改数据库的字符集
ALTER DATABASE my_database CHARSET utf8;
ALTER DATABASE my_database CHARACTER SET utf8;
-- 可以使用空格,也可以使用等号
ALTER DATABASE my_database CHARSET=utf8;
-- COLLATE:校对集
ALTER DATABASE my_database COLLATE utf8;

-- 删除数据库
DROP DATABASE my_database;

-- 选择数据库
USE my_database;

表操作

-- 建表,会在数据库对应的文件夹下创建frm格式的结构文件(和引擎有关)
-- 可以指定数据库名
CREATE TABLE IF NOT EXISTS my_database.mytable(
id int,
name varchar(10)
)CHARSET=utf8 COLLATE=utf8 ENGINE=innodb;

-- 创建临时表,临时表只在当前连接可见,当断开连接就会自动删除
CREATE TEMPORARY TABLE temptable(
id int,
name varchar(10)
);

SHOW TABLES;
SHOW TABLES LIKE `my%`;
SHOW CREATE TABLE mytable;
-- \g 等价于分号
SHOW CREATE TABLE mytable\g
-- \G 将查询的结果的显示格式改成纵向
SHOW CREATE TABLE mytable\G

-- 复制已有表的表结构(不会复制数据),可以指定数据库名
CREATE TABLE mytable2 LIKE my_database.mytable;
-- 复制数据
INSERT INTO mytable2 SELECT * FROM mytable;
-- 压力测试时可以复制自身数据,实现指数级增长数据
INSERT INTO mytable2 SELECT * FROM mytable2;

-- 查看表结构,一下三种方式是等价的
DESC COLUMNS FROM mytable;
DESCRIBE COLUMNS FROM mytable;
SHOW COLUMNS FROM mytable;
-- 查看表结构
DESCRIBE mytable;

-- 重命名表
RENAME TABLE mytable TO my_table;
-- 同样,可以使用空格或等号
ALTER TABLE mytable CHARSET=utf8;

-- 新增字段
ALTER TABLE mytable ADD COLUMN addr VARCHAR(20);
-- COLUMN可以不写
ALTER TABLE mytable ADD addr VARCHAR(20);
-- 新增字段并指定位置
ALTER TABLE mytable ADD COLUMN addr VARCHAR(20) FIRST;
ALTER TABLE mytable ADD COLUMN addr VARCHAR(20) AFTER id;
-- 修改字段,可以更改属性或位置
ALTER TABLE mytable MODIFY addr VARCHAR(25) AFTER name;
-- 重命名字段并修改属性和位置
ALTER TABLE mytable CHANGE addr address VARCHAR(20) AFTER id;
-- 删除字段
ALTER TABLE mytable DROP address;

-- 删表
DROP TABLE mytable1, mytable2;

字段操作

-- 不指定插入的字段时需数据的顺序需和字段顺序一致,且非数值数据都要使用引号(建议是单引号)
INSERT INTO mytable VALUE(1,'小明');
-- VALUE只能插入单条,VALUES可以插入单条或多条
INSERT INTO mytable(id,name) VALUES(1,'小明'), (2,'小红');

-- 主键没有冲突时直接插入数据;主键冲突时,更新对应记录
INSERT INTO mytable VALUES(1,'小红') ON DUPLICATE KEY UPDATE name='小红';
-- 主键没有冲突时直接插入数据;主键冲突时,先删除原记录,再插入新记录
REPLACE INTO mytable VALUES(1,'小红');
-- 主键没有冲突时直接插入数据;主键冲突时,保持原纪录,忽略新插入的记录
INSERT IGNORE INTO mytable VALUES(1,'小红');

-- 查看数据
SELECT ALL * FROM mytable; -- 查询所有记录
SELECT * FROM mytable; -- ALL可以省略
SELECT * FROM mytable,mytable2; -- 多个数据源
SELECT DISTINCT * FROM mytable; -- 去重
SELECT id, DISTINCT name FROM mytable; -- 部分去重
SELECT id AS my_id,name AS my_name FROM mytable AS t; -- 别名
SELECT id my_id,name my_name FROM mytable t; -- AS可以省略

SELECT * FROM mytable GROUP BY name; -- 分组
SELECT sex, count(*), max(age), min(age), avg(height), sum(age) FROM mytable1 GROUP BY sex; -- GROUP BY一般配合统计函数使用(sex、age、height都是我们的字段名,count、max、min、avg、sum是统计函数),GROUP BY会自动按照分组字段排序(默认ASC,升序)
SELECT sex, count(*), max(age), min(age) FROM mytable1 GROUP BY sex DESC; -- GROUP BY会对分组后合并的结果进行排序
SELECT city, sex, count(*), group_concat(name) FROM mytable1 GROUP BY city, sex; -- 多字段分组,group_concat是聚合函数,它可以对分组中的指定字段进行字符串连接,把多行数据变成单行数据
SELECT sex,count(*) FROM mytable1 GROUP BY sex WITH ROLLUP; -- 回溯统计
SELECT city,sex,count(*),group_concat(name) FROM mytable1 GROUP BY city,sex WITH ROLLUP; -- 多字段分组+回溯统计

SELECT * FROM mytable ORDER BY name DESC; -- 排序(ASC、DESC)
SELECT * FROM mytable ORDER BY city,name DESC; -- 多字段排序,这里是city升序、name降序

-- WHERE是从磁盘读一条记录,判断是否满足,不满足直接丢弃,从而保证加载到内存中的都是有效数据
SELECT * FROM mytable WHERE id<=5; -- 条件,可以使用>、<、=、>=、<=、、!=(不等于,旧标准)、<>(不等于,新标准)
SELECT * FROM mytable WHERE id BETWEEN 10 AND 20; -- BETWEEN TO 是闭区间,且左边的数要小于右边的
SELECT * FROM mytable WHERE name LIKE '小%';
SELECT * FROM mytable WHERE name IS NULL;
SELECT * FROM mytable WHERE id IN (1,2,3,4,5);
SELECT * FROM mytable WHERE id<=5 AND name NOT LIKE '小%'; -- 使用NOT、AND、OR组合多种条件,优先级:NOT > AND > OR
SELECT * FROM mytable WHERE id IN (SELECT mid FROM mytable2 WHERE name LIKE '%小%'); -- 使用子查询
SELECT * FROM mytable WHERE id=SELECT mid FROM mytable2 WHERE name LIKE '%小%' LIMIT 1;
SELECT id FROM mytable WHERE id > ALL(SELECT mid FROM mytable2); -- 使用IN(返回TRUE或FALSE,但如果含NULL,会返回UNKNOWN)、EXISTS(返回TRUE或FALSE,可以处理NULL)、ANY(满足任意一个,要和=或!=或<>一起使用,比如 xxxx=ANY(xxx))、SOME(some是any的别名,用的比较少,也要和=或!=或<>一起使用)、ALL(ALL必须与比较操作符一起使用,<>ALL等同于NOT IN)
SELECT * FROM mytable WHERE (age,height) = (SELECT max(age),max(height) FROM mytable2); -- 返回单行时可以这样用
SELECT * FROM mytable WHERE (age,height) IN (SELECT age,height FROM mytable2); -- 返回多行时可以这样用
SELECT * FROM mytable WHERE (1,2) = (SELECT mid,mname FROM mytable2 WHERE mid=2); -- (1,2)即第1、2列
SELECT * FROM (SELECT * FROM mytable2) AS derived_table WHERE derived_table.id > 5; -- 派生表,从返回的结果中再次执行查询

SELECT id AS my_id,name FROM mytable HAVING my_id>3; -- HAVING可以使实现WHERE的所有功能
SELECT city,count(*) FROM mytable1 GROUP BY city HAVING count(*)>=2; -- 但WHERE不能实现HAVING的部分功能,比如:HAVING可以使用别名、统计函数,如果和GROUP BY一起使用,HAVING可以过滤分组的结果
SELECT city,count(*) AS total FROM mytable1 GROUP BY city HAVING total>=2; -- 优化:不需要重复查询两次count(*),可以使用别名

SELECT * FROM mytable LIMIT 1,5; -- 分页,第1页,每页5条,即第6~10条记录
SELECT * FROM mytable LIMIT 5; -- 相当于LIMIT 0,5
SELECT * FROM mytable LIMIT 96,-1; -- 检索96~last

SELECT id,name FROM mytable UNION SELECT id,name FROM mytable2; -- 联合查询,把多条查询的得到的多个结果集合并成一个结果集,要求两个查询必须拥有相同数量的列,但列的数据类型不需要一致,且顺序必须相同
SELECT id,name FROM mytable UNION ALL SELECT id,name FROM mytable2; -- UNION会自动去重,要想不去重,使用UNION ALL
(SELECT id,name,sex FROM mytable ORDER BY sex DESC LIMIT 999) UNION (SELECT id,name,sex FROM mytable2 ORDER BY sex ASC LIMIT 999); -- 联合查询使用ORDER BY需要用括号括起来,但这样ORDER BY并不会生效,要使ORDER BY生效,还有搭配LIMIT使用(可以LIMIT一个足够大的数来实现不限制记录数)

SELECT t1.id,t1.name,t2.id id2,t2.name name2 FROM mytable t1 JOIN mytable2 t2 ON t1.id=t2.id WHERE name IS NOT NULL; -- 连接查询,JOIN分为CROSS JOIN(交叉连接)、JOIN(内连接)、LEFT JOIN(左外连接)、RIGHT JOIN(右外连接)
SELECT t1.*,t2.* FROM mytable t1 JOIN mytable2 t2 WHERE t1.id=t2.id; -- ON可以用WHERE替代,但WHERE效率会低一点
SELECT t1.*,t2.* FROM mytable t1 JOIN mytable2 t2 USING(id); -- USING(id)相当于ON t1.id=t2.id
SELECT * FROM mytable CROSS JOIN mytable2; -- CROSS JOIN
SELECT * FROM mytable,mytable2; -- CROSS JOIN也可以写成这样,其实就是前面提到的多数据源
SELECT * FROM mytable NATURAL JOIN mytable2; -- NATURAL JOIN 会自动根据相同的列名JOIN,并合并同名列,如果有多个同名列,会同时JOIN多个列

-- 更新数据
UPDATE mytable SET name='小明';
UPDATE mytable SET name='小明' WHERE name LIKE '小%' LIMIT 3;

-- 删除数据
DELETE FROM mytable WHERE id=2;
-- 删除表中所有数据(自增长不会重置)
DELETE FROM mytable;
-- 删除表中所有数据并重置自增长
TRUNCATE mytable;

视图操作

视图就是虚拟表,它可以通过封装SQL语句,定义对外访问的字段,而隐藏关键的字段,也可以简化复杂的查询,视图不会真正存储数据,它只是定义了表结构,真正的数据仍然是存在于数据表中

-- 创建视图
CREATE VIEW myview AS SELECT * FROM mytable; -- 单表视图,其中AS不能省略
CREATE VIEW myview2 AS SELECT t1.*,t2.name,t2.sex FROM mytable t1 LEFT JOIN mytable2 t2 ON t1.other_id = t2.id; -- 多表视图。字段名不能重复,如果有重复,需使用别名

-- 视图就是一个虚拟表(但是它不存储数据,只是定义了表结构),表的所有查看方式都能用于视图
SHOW TABLES myview;
SHOW TABLES LIKE 'my%';
SHOW CREATE TABLE myview;
-- 视图也有自己特有的查看方式
SHOW CREATE VIEW myview;

-- 使用视图主要是为了查询方便,可以自动执行定义时指定的SQL
SELECT * FROM myview;

-- 多表视图不能增和删里面的数据,只有单表视图才可以(要求视图包含的所有字段都允许为NULL或者有默认值),这时会往基表增或删数据而不是往视图中增或删,因为视图不存储数据
INSERT INTO myview VALUES(null,180,150,1);
DELETE FROM myview WHERE id=1;

-- 无论单表还是多表视图,都可以修改里面的数据
UPDATE myview2 SET other_id=3 WHERE id=1;
-- 如果在创建视图时指定了WITH CHECK OPTION,修改视图数据时就要保证修改后数据仍然能被视图查出来(即修改不能影响总记录数)
CREATE VIEW myview3 AS SELECT * FROM mytable2 WHERE age>20 WITH CHECK OPTION; -- 禁止视图将age设置为小于20
-- 如果修改数据使其变为视图的查询范围,不会提示ERROR,但是修改无效(即只能更新视图中可以查询到的数据)
-- 找不到id=6的数据,WHERE没有任何匹配,所以没有UPDATE任何记录,但不会报错
UPDATE myview3 SET age=23 WHERE id=6; -- 假设原来age<=20,无法通过视图查询到

-- 修改视图
ALTER VIEW myview AS SELECT t1.height,t1.age,t2.name,t2.sex FROM mytable t1 LEFT JOIN mytable2 t2 ON t1.other_id = t2.id;

-- 删除视图,删除视图不会影响数据
DROP VIEW myview;

-- 视图的算法
/*
视图的算法分为
UNDEFINED: 未定义(默认),系统自动选择下面的一种
TEMPTABLE: 临时表算法,系统先执行视图的SELECT,后执行外部的查询语句,当外部查询的子句比内部查询优先级低,需要使用该算法,比如内部使用了ORDER BY,而外部使用了GROUP BY,而GROUP BY会对分组后合并的结果进行排序,GROUP BY优先级比ORDER BY低,就要使用TEMPTABLE算法(其他情况默认即可)
MERGE: 合并算法,系统先把视图的SELECT和外部的查询语句进行合并,然后执行(效率高,因为只执行了一次)
*/
-- 先ORDER BY然后GROUP BY
CREATE ALGORITHM=TEMPTABLE VIEW myview AS SELECT * FROM mytable ORDER BY height DESC;
SELECT * FROM myview GROUP BY age; -- GROUP BY会选取不同age的第一条记录,这里就是选区不同年龄的最高身高

事务

事务的原理是把操作保存到事务日志里,只有当COMMIT的时候再写入数据库,当ROLLBACK就会清空事务日志,使操作失效。在事务中返回的结果都是经过事务日志加工的,所以在当前session中可以看到对数据的修改,但其他session不会读到未提交的记录

InnoDB引擎默认是行锁,但如果更改数据时没有用到索引(普通索引、主键索引、唯一索引、组合索引、全文索引)的字段,就会全文检索,升级为表锁,即另一session会阻塞至当前session提交或回滚后才能完成提交

事务隔离级别可以在my.ini通过transaction-isolation设置,有以下隔离级别

也可以通过命令行设置SET [GLOBAL | SESSION] TRANSACTION ISOLATION LEVEL <isolation-level>,isolation-level可以是READ UNCOMMITTEDREAD COMMITTEDREPEATABLE READSERIALIZABLE

-- 设置隔离级别
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- 手动事务
-- 开启事务(两种方式都是等价的)
BEGIN;
START TRANSACTION;

-- 创建事务保存点,一个事务中可以有多个SAVEPOINT
SAVEPOINT identifier;
-- 删除一个事务的保存点
RELEASE SAVEPOINT identifier;
-- 回滚到保存点
ROLLBACK TO identifier;

-- 提交事务(两种方式都是等价的)
COMMIT;
COMMIT WORK;

-- 回滚(两种方式都是等价的)
ROLLBACK;
ROLLBACK WORK;

-- 自动事务
-- 默认开启自动事务
SHOW VARIABLES LIKE 'autocommit';
-- 关闭自动事务(本次session有效)
SET autocommit = 0;

这时每次修改数据都需要手动提交
UPDATE mytable SET money=1000 WHERE id=1;
COMMIT; -- COMMIT或者ROLLBACK

字符集、校对集操作

-- 查看所有支持的字符集
SHOW CHARACTER SET;
-- 查看默认字符集
SHOW VARIABLES LIKE 'character_set%';

-- SET的更改只是当前会话有效,重新登陆命令行失效
-- 服务器端的编码是gbk,而数据库的编码是utf8,于是设置client的编码为gbk,接收到数据后会自动转成utf8
-- GBK两个字节一个汉字,UTF8三个字节一个汉字(英文和其他字符可能会优化成1~3个字节)
SET character_set_client=gbk;
-- 设置返回的数据的编码为gbk
SET character_set_results=gbk;
-- 设置字符集的快捷方式,相当于设置了character_set_client、character_set_results、character_set_connection
-- character_set_connection是中间转码的字符集,配置能提高效率,但不配置也不影响使用
SET NAMES gbk;

/*
校对集分为:
_bin: binary,二进制比较
_cs: case sensitive,大小写敏感
_ci: case insensitive,大小写不敏感
*/
-- 查看所有的校对集
SHOW COLLATION;
-- 在建表的时候指定校对集
CREATE TABLE mytable(
name char(2)
)CHARSET utf8 COLLATE utf8_general_ci
-- 校对集在比较的时候生效,比如ORDER BY
INSERT INTO mytable VALUES('a'),('A'),('B'),('b');
SELECT * FROM mytable ORDER BY name;
-- 校对集只能在没有数据之前声明好,如果有了数据,那么修改对之前的数据无效
ALTER TABLE mytable COLLATE=utf8_bin;

-- 查看警告
SHOW warnings;

备份还原

方式一:使用OUTFILE进行单表备份,这种备份只能备份数据,不能备份表结构,导入时需有相同的表结构才能导入

-- 备份表
SELECT * INTO OUTFILE '~/backup/mytable' FROM mytable; -- "*"也可以改成想备份的字段
-- 可以指定导出的数据格式
SELECT * INTO OUTFILE '~/backup/mytable'
CHARACTER SET utf8
FIELDS TERMINATED BY '|' OPTIONALLY ENCLOSED BY '"' ESCAPED BY '\\' -- 字段间用竖线分隔(默认为制表符'\t'),使用双引号包裹数据(默认不包裹),特殊符号用'\\'处理(默认是'\\')
LINES STARTING BY 'START:' TERMINATED BY '\n' -- 每行以START:开始,以换行符结束
FROM mytable;

-- 还原
LOAD DATA INFILE "~/backup/mytable" INTO TABLE mytable;
-- 如果备份时指定了格式,还原时也要指定相同的格式
LOAD DATA INFILE "~/backup/mytable"
INTO TABLE mytable
CHARACTER SET utf8
FIELDS TERMINATED BY '|' OPTIONALLY ENCLOSED BY '"' ESCAPED BY '\\'
LINES STARTING BY 'START:' TERMINATED BY '\n';

方法二:使用mysqldump工具,这种备份可以备份数据和表结构,该工具在mysql的bin目录下,用法如下

mysqldump -uroot -pPassword mydatabase1 > backup_file

# 只备份表结构
mysqldump -uroot -pPassword --no-data --databases mydatabase1 mydatabase2 mydatabase3 > backup_file

# 备份指定数据库中的所有表
mysqldump -uroot -pPassword --all-databases > backup_file

# 跨主机备份,把host1的指定数据库备份到host2中
mysqldump -uroot -pPassword --host=host1 --opt sourceDb | mysql --host=host2 -C targetDb

# 恢复
mysql -uroot -pPassword mydatabase1 < backup_file

# 通过mysql命令行的source命令恢复
mysql -uroot -pPassword
mysql> SOURCE backup_file;

数据类型

字符串

字符串分为:

CREATE TABLE mytable(
s1 CHAR(20),
s2 VARCHAR(20),
s3 TEXT,
s4 BLOB,
s5 ENUM('男','女','保密'),
s6 SET('北京','上海','广州')
)CHARSET utf8;

INSERT INTO mytable VALUES('ABCD','ABCD','ABCD','ABCD','男','上海,北京');
-- enum: 1对应'男',set: 3对应二进制为011,倒过来是110,所以对应是插入'北京,上海'
INSERT INTO mytable VALUES('ABCD','ABCD','ABCD','ABCD',1,3);

-- VARCHAR的实际最大长度受MYSQL中规定的最大记录长度为65535限制
CREATE TABLE mytable(
i1 TINYINT NOT NULL, -- 1
s2 VARCHAR(21844) NOT NULL, -- 21844*3+2=65534,UTF8一个字符最多占3个字节,分配空间是按最多的算的
)CHARSET utf8;

-- ENUM实际存储的是数值而不是字符串本身
-- 原理:
SELECT 1 + 'hello'; -- 得到1
SELECT 1 + '1hello'; -- 得到2,和PHP的自动转换类似,不以数字开头的字符串会自动转成0
-- 验证:如果s5是字符串,那么字符串+0结果还是0,如果是数值,那么结果会是其他数字;为了对比,我们也把原来的s5查出来
SELECT s5 + 0, s5 FROM mytable;
-- SET同理
SELECT s6 + 0, s6 FROM mytable;

整型

整型分为:

以上类型默认都是有符号的,要使用无符号类型,使用UNSIGNED声明

CREATE TABLE mytable(
int_1 TINYINT,
int_2 SMALLINT,
int_3 MEDIUMINT
int_4 INT,
-- 使用无符号类型
int_5 INT UNSIGNED,
-- 括号中的数字为显示宽度,比如TINYINT默认是4位,因为对于无符号的TINYINT,可以是-127,即包含了符号位
-- 显示宽度和存储的大小无关,更改显示宽度不会影响存储大小
int_6 INT(11),
-- 显示宽度一般配合ZEORFILL使用,ZEORFILL表示没有达到显示宽度的时候补零(达到或超过显示宽度正常显示),一旦使用了ZEORFILL,会自动把数据改为UNSIGNED
int_7 INT(8) ZEORFILL,
int_8 BIGINT
)CHARSET utf8;

小数

小数类型分为:

CREATE TABLE mytable(
-- 不使用小数位
f1 FLOAT,
-- 需要指定总长度(包括整数部分和小数部分,不包括小数点)和小数精度
f2 FLOAT(10,2),
d1 DECIMAL(10,2)
)CHARSET utf8;

日期时间

日期时间类型分为:

类型 显示格式 取值 存储空间 零值
DATETIME yyyy-MM-dd HH:mm:ss 1000-01-01 00:00:00到9999-12-31 23:59:59 八个字节 0000-00-00 00:00:00
TIMESTAMP yyyy-MM-dd HH:mm:ss 1970-01-01 00:00:00到2038-01-19 03:14:07 四个字节 0000-00-00 00:00:00
DATE yyyy-MM-dd 1000-01-01到9999-12-31 三个字节 0000-00-00
TIME HH:mm:ss -838:59:59到838:59:59 三个字节 00:00:00
YEAR yyyyy 有两种形式:YEAR(2)表示从1970到2069年;YEAR(4)表示从1901到2155年,YEAR(4)为默认值 一个字节 0000

比如

CREATE TABLE mytable(
d1 DATETIME,
d2 TIMESTAMP,
d3 DATE,
d4 TIME,
d5 YEAR
)CHARSET utf8;

INSERT INTO mytable VALUES(
'1900-01-02 01:02:03','1970-01-02 01:02:03','1900-01-02','01:02:03',1901);

INSERT INTO mytable VALUES(
'1900-01-02 01:02:03',
'1970-01-02 01:02:03',
'1900-01-02',
-- -2表示过去两天,所以实际上是 -(48小时+01:02:03)
'-2 01:02:03',
-- 使用2位的YEAR,插入后变成2069
69);

INSERT INTO mytable VALUES(
'1900-01-02 01:02:03',
'1970-01-02 01:02:03',
'1900-01-02',
'01:02:03',
-- 使用2位的YEAR,插入后变成1970
70);

-- TIMESTAMP只要所在记录被更新,就会自动更新成当前的时间
UPDATE mytable SET d1='1900-00-00 00:00:00' WHERE d5=1970;
-- 查看当前时间
SELECT unix_timestamp();

列属性

列属性有:

用法

CREATE TABLE mytable(
-- 参与业务的主键(如姓名等)称为业务主键/自然主键,不参与业务只用于唯一标识数据的主键称为逻辑主键
-- AUTO_INCREMENT只能有一个,一般配合PRIMARY KEY使用,类型一定是整型
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(20) UNIQUE KEY COMMENT '姓名',
gender CHAR(6) NOT NULL DEFAULT 'male'
)CHARSET utf8;

-- COMMENT可以这样查看
SHOW CREATE TABLE mytable;

-- 设置主键
CREATE TABLE mytable(
id INT,
name VARCHAR(20),
PRIMARY KEY(id)
)CHARSET utf8;
-- 或者追加主键,这时要求字段对应的数据不能重复
ALTER TABLE mytable MODIFY id INT PRIMARY KEY;

-- 联合主键
CREATE TABLE mytable(
id INT,
name VARCHAR(20),
PRIMARY KEY(id,name)
)CHARSET utf8;
-- 或者
ALTER TABLE mytable ADD PRIMARY KEY(id,name);

-- 主键无法更改,只能先删除再创建
ALTER TABLE mytable DROP PRIMARY KEY;

-- 自增长的主键传null或DEFAULT或不传都会触发自动增长
-- 传入DEFAULT或不传该字段的值会使用默认值
INSERT INTO mytable VALUES(null,'小明',DEFAULT);
INSERT INTO mytable VALUES(DEFAULT,'小明',DEFAULT);
INSERT INTO mytable(name) VALUES('小明');
-- 自增长如果传入值则不会触发,下次会从最大值继续+1
INSERT INTO mytable VALUES(10,'小明',DEFAULT);

-- 修改自增长的值,该值只能比当前自增长的值大,不能小(小了不会报错,但修改无效)
ALTER TABLE mytable AUTO_INCREMENT=10;
-- 查看自增长,可以修改起始值和步长,该修改是对整个数据库修改,通过SET修改是会话级的
SHOW VARIABLES LIKE 'auto_increment%';
SET auto_increment_increment=2;

-- 删除自增长就是MODIFY列的时候不写AUTO_INCREMENT
-- 有主键的时候不需要再声明主键,因为主键的声明不是在列后面的,而是通过PRIMARY KEY(id)在最后设置的(可以通过SHOW CREATE TABLE mytable查看)
ALTER TABLE mytable MODIFY id INT;


-- UNIQUE KEY和PRIMARY KEY类似
-- UNIQUE KEY允许为NULL,NULL不参与唯一性比较
CREATE TABLE mytable(
id INT,
name VARCHAR(20) UNIQUE KEY
)CHARSET utf8;
-- 或者
CREATE TABLE mytable(
id INT,
name VARCHAR(20),
UNIQUE KEY(id,name)
)CHARSET utf8;
-- 或者追加唯一键
ALTER TABLE mytable ADD UNIQUE KEY(id,name);

-- 删除UNIQUE KEY约束
-- name实际上是索引名,唯一键的索引名默认是列名
ALTER TABLE mytable DROP INDEX name; -- 这里不是删除列,只是删除唯一性约束

-- 查看索引
SHOW INDEX FROM mytable;


-- FOREIGN KEY和PRIMARY KEY类似,只能使用其他表的主键来作为外键
CREATE TABLE mytable(
id INT,
name VARCHAR(20) UNIQUE KEY,
other_id INT,
FOREIGN KEY(other_id) REFERENCES mytable2(id);
)CHARSET utf8;
-- 或者通过追加方式
ALTER TABLE mytable ADD FOREIGN KEY(other_id) REFERENCES mytable2(id);
-- 追加外键的同时指定外键的名字为id_1,也可以使用IF NOT EXISTS,括号可以写在一起也可以分开
ALTER TABLE `mytable` ADD CONSTRAINT `id_1` FOREIGN KEY IF NOT EXISTS (`other_id`) REFERENCES `mytable2` (`id`);

-- 外键的约束模式
-- 严格模式(RESTRICT),默认;RESTRICT和NO ACTION作用是一样的,父表在删除和更新记录并涉及到主键时,检查字表是否有关联的外键记录,如果有则不允许删除或更新(子表就是含外键的表)
CREATE TABLE mytable(
id INT,
name VARCHAR(20) UNIQUE KEY,
other_id INT,
FOREIGN KEY(other_id) REFERENCES mytable2(id) ON DELETE NO ACTION ON UPDATE NO ACTION;
)CHARSET utf8;
-- 置空(SET NULL),当父表更新、删除并涉及到主键时,子表会把外键字段置为NULL,此时要求字段可NULL
CREATE TABLE mytable(
id INT,
name VARCHAR(20) UNIQUE KEY,
other_id INT,
FOREIGN KEY(other_id) REFERENCES mytable2(id) ON DELETE SET NULL ON UPDATE SET NULL;
)CHARSET utf8;
-- 级联(CASCADE),当父表更新、删除并涉及到主键时,子表会同步更新和删除
CREATE TABLE mytable(
id INT,
name VARCHAR(20) UNIQUE KEY,
other_id INT,
FOREIGN KEY(other_id) REFERENCES mytable2(id) ON DELETE CASCADE ON UPDATE CASCADE;
)CHARSET utf8;

-- 删除外键,需使用外键的名字,即通过ADD CONSTRAINT指定的名字
ALTER TABLE mytable DROP FOREIGN KEY id_1;

索引

索引分类

  1. 普通索引index: 加速查找
  2. 唯一索引
  3. 联合索引
  4. 全文索引(fulltext): 用于搜索很长一篇文章的时候,效果最好,只能在CHAR、VARCHAR、TEXT类型列上创建(MYSQL只有MYISAM存储引擎支持全文索引)
  5. 空间索引(spatial): 空间索引是对空间数据类型(GEOMETRY、POINT、LINESTRING、POLYGON)的字段建立的索引,要求字段为NOT NULL(MYSQL只有MYISAM存储引擎支持空间索引)
/*
创建方法:
#方法一:创建表时
CREATE TABLE 表名 (
字段名1  数据类型 [完整性约束条件…],
字段名2  数据类型 [完整性约束条件…],
[UNIQUE | FULLTEXT | SPATIAL ] INDEX/KEY [索引名] (字段名[(长度)] [ASC |DESC]) 
);
#方法二:CREATE在已存在的表上创建索引
CREATE  [UNIQUE | FULLTEXT | SPATIAL ] INDEX 索引名 ON 表名 (字段名[(长度)] [ASC |DESC]);
#方法三:ALTER TABLE在已存在的表上创建索引
ALTER TABLE 表名 ADD [UNIQUE | FULLTEXT | SPATIAL] INDEX 索引名 (字段名[(长度)]  [ASC |DESC]) ;
*/

CREATE TABLE mytable(
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(20),
INDEX nameindex (name) -- 索引的名字也可以不指定
)CHARSET utf8;
-- 或者
CREATE INDEX name ON mytable(name);
CREATE UNIQUE INDEX id_name ON mytable(id,name); -- 联合索引
-- 或者
ALTER TABLE mytable ADD INDEX name (name(4)); -- 前面的name是索引名字,括号的name是列名,可以指定索引的长度(如果是BLOB或TEXT则必须指定,CHAR或VARCHAR可以不指定)

-- 插入初始数据
INSERT INTO mytable VALUES(1,'小明'), (2,'小红');

-- 查看索引
SHOW INDEX FROM mytable;

-- 分析SQL用到的索引
EXPLAIN SELECT * FROM mytable WHERE name LIKE '%小%'\G
/*输出
           id: 1
  select_type: SIMPLE   -- 表示查询中每个select子句的类型(简单 或 复杂)
        table: mytable
   partitions: NULL
         type: index    -- 表示MySQL在表中找到所需行的方式,又称“访问类型”
possible_keys: NULL     -- 可能使用到的索引(不一定真的会使用)
          key: nameindex    -- 实际使用的索引,若没有使用索引,显示为NULL
      key_len: 63       -- 索引中使用的字节数
          ref: NULL     -- 表示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值
         rows: 2        -- 表示MySQL根据表统计信息及索引选用情况,估算的找到所需的记录所需要读取的行数
     filtered: 50.00
        Extra: Using where; Using index     -- 包含不适合在其他列中显示但十分重要的额外信息 如using where,using index
*/

-- 删除索引
DROP INDEX name ON mytable;

-- 除此以外,还可以指定索引的查询方式,常用的有BTREE(二叉树结构,最常用)和HASH(只支持=、IN、<>,不支持范围查询;对于对于组合索引,只有使用了所有索引键时才会生效,因为是对所有组合键做HASH,少了无法匹配到;对于大量重复HASH的情况,效率不一定比BTREE高),不常用的有FULLTEXT(中文支持不是很好,MyISAM才支持)、RTREE(仅支持geometry数据类型)
CREATE TABLE mytable(
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(20),
INDEX nameindex (name) USING BTREE -- 索引的名字也可以不指定
)CHARSET utf8;
-- 或者
CREATE INDEX name ON mytable(name) USING BTREE;
-- 或者
ALTER TABLE mytable ADD INDEX name USING BTREE; 

生效原则

单列索引: 多个单列索引同时被用到时,会自动选择其中一个(自动选择低重复度的字段)

联合索引:

假设a、b、c、d是联合索引

ALTER TABLE mytable ADD INDEX abc (a,b,c,d);

SELECT * FROM mytable WHERE b=45 AND a=3 AND c=5 AND d=4; -- 全部生效
SELECT * FROM mytable WHERE a=3 and c=5; -- a生效,c无效
SELECT * FROM mytable WHERE b=45 AND c=5; -- 全部无效

SELECT * FROM mytable WHERE b=45 AND a=3 AND c=5 AND d>4; -- 全部生效
SELECT * FROM mytable WHERE a=3 AND b=45 AND d=4 ORDER BY c; -- a、b、c生效(正常order by使用EXPLAIN会在Extra中显示Using filesort,而这里没有,说明使用了c索引)
SELECT * FROM mytable WHERE a=3 AND d=4 ORDER BY c,b; -- 注意ORDER BY c,b和ORDER BY b,c不同,这里只有a生效(同样可以通过EXPLAIN查看是否使用了Using temporary和Using filesort,使用了索引不需要)

程序

变量

变量分为系统变量和自定义变量

-- 查看所有系统变量
SHOW VARIABLES;
-- 查看某个变量
SELECT @@version,@@autocommit;

-- 会话级别修改
SET autocommit = 0;
SET @@autocommit=0;

-- 全局级别修改(其他新登陆的session也可以看到修改,但如果已登陆,无法看到修改)
SET GLOBAL autocommit = 0;

-- 自定义变量,自定义的变量都是当前session有效(不区分数据库),退出后清空
-- 系统为了区分系统变量和自定义变量,自定义变量只能使用一个@
SET @name = '张三';
SELECT @name;
-- 等号在MYSQL中大多表示比较,而非赋值,MYSQL为了区分比较和赋值,定义了一个新的赋值符号:=
SET @age := 18;
-- 变量的定义或更改需要使用SET,使用SET系统就知道是赋值,这时既可以用=,又可以用:=
SET @age = 1;

SELECT @name := name FROM mytable; -- 从字段中取值赋给变量,此时只能用:=,不能用=;如果记录有多条,取最后一条(MYSQL不支持数组)
SELECT name,age FROM mytable WHERE id = 2 INTO @name,@age; -- 这种方式要求查询的记录只有一条

程序结构

只有IFWHILE

/*
分支结构
IF xxx THEN
    xxx
ELSE
    xxx
END IF;
*/

/*
WHILE xxx DO
    xxx
END WHILE;

-- 可以用ITERATE实现跳过此次循环,用LEAVE实现跳出循环,但是要用到循环名字
循环名字:WHILE xxx DO
    xxx
    -- ITERATE 循环名字;
    -- 或者
    -- LEAVE 循环名字;
END WHILE;
*/

触发器

/*
一般定义如下:
CREATE TRIGGER 触发器名字 触发时间(BEFORE、AFTER) 事件类型(INSERT、DELETE、UPDATE) ON 表名 FOR EACH ROW
DELIMITER 自定义分隔符 -- 临时修改结束符,因为系统以结束符分隔SQL语句,而触发器内部需要用到多条语句,要是输入分号直接结束了SQL语句,就无法完整地定义一个触发器
BEGIN
    -- 触发器内容,以系统原来的结束符――分号(";")――结束
    -- 触发器内部的语句不要再次触发当前触发器监听的事件,否则会导致死循环
    -- 可以通过 old.字段名 或 new.字段名 获取当前的数据以及即将更新的数据
END
自定义的那个分隔符 -- 表示定义完触发器了
DELIMITER 系统原来的分隔符 -- 把结束符修正过来

一张表中每种 触发时间+触发类型 只能有一个触发器,即一张表最多只能有6个触发器
*/
CREATE TRIGGER mytrigger AFTER INSERT ON mytable FOR EACH ROW
DELIMITER $$ 
BEGIN
    SELECT money FROM mytable2 WHERE id = new.wallet_id INTO @money;
    IF @money > new.price THEN
        UPADTE mytable2 SET money = money - new.price WHERE id = new.wallet_id;
    ELSE
        -- 触发器不能禁止插入/修改/删除,只能通过报错的方式使得操作失败
        INSERT INTO xxx VALUES(XXX); -- 不存在该表,所以保存,使得事务回滚,插入失败
    END IF;
END
$$
DELIMITER ;

-- 查看触发器
SHOW TRIGGERS;
SHOW TRIGGERS LIKE '%trigger%';

SHOW CREATE TRIGGER mytrigger;

-- 所有的触发器都会保存在information_schema.triggers中
SELECT * FROM information_schema.triggers\G

-- 触发器无法修改,只能先删除再创建
DROP TRIGGER mytrigger;

函数

/*
一般定义如下
DELIMITER $$
CREATE FUNCTION 函数名(形参列表) RETURNS 返回值数据类型
BEGIN
    xxx;
    RETURN xxx;
END
$$
DELIMITER ;
*/

-- 只有一行的函数可以不写BEGINEND
CREATE FUNCTION display100() RETURNS INT
RETURN 100;

-- 从1加到指定数
DELIMITER $$
CREATE FUNCTION sumTo(num INT) RETURNS INT
BEGIN
    SET @i = 1;
    SET @ret = 0;
    WHILE @i <= num DO
        SET @ret = @ret + @i;
    SET @i = @i + 1;
    END WHILE;
    RETURN @ret;
END
$$
DELIMITER ;

-- 调用
SELECT display100();
SELECT sumTo(100);

-- 查看函数
SHOW FUNCTION STATUS;
SHOW FUNCTION STATUS LIKE '%display%';
SHOW CREATE FUNCTION display100;

-- 函数无法修改,只能先删除再创建
DROP FUNCTION display100;


-- 带@的都是全局变量,要定义局部变量,使用DECLARE关键字声明,且没有@符号,局部变量只能在函数体开头声明
-- 从1加到指定数,5的倍数不加
DELIMITER $$
CREATE FUNCTION sumTo(num INT) RETURNS INT
BEGIN
    DECLARE i INT DEFAULT 1;
    DECLARE ret INT DEFAULT 0;
    mywhile:WHILE i <= num DO
        IF i % 5 = 0 THEN -- 这里=是比较符号
        SET i = i + 1;
        ITERATE mywhile;
    END IF;

        SET ret = ret + i;
    SET i = i + 1;
    END WHILE;
    RETURN ret;
END
$$
DELIMITER ;

存储过程

/*
存储过程可以理解为没有返回值的函数,一般定义如下
CREATE PROCEDURE 过程名字(参数列表)
DELIMITER $$
BEGIN
    xxx;
END
$$
DELIMITER ;
*/

-- 只有一行可以不写BEGINEND
CREATE PROCEDURE procedure1()
SELECT * FROM mytable; -- 存储过程中使用SELECT可以显示数据

-- 查看存储过程
SHOW PROCEDURE STATUS;
SHOW PROCEDURE STATUS LIKE '%procedure%';
SHOW CREATE PROCEDURE procedure1;

-- 调用存储过程(SELECT只能获取返回值,而存储过程没有返回值,所以不能通过SELECT调用)
CALL procedure1();

-- 存储过程无法修改,只能先删除再创建
DROP PROCEDURE procedure1;

-- 存储过程有自己的类型限定
/*
三种类型:
IN: 数据只能从外部传入,供内部使用(值传递,可以传值或变量)
OUT: 引用传递,外部的变量会被先清空,才会进入到内部(只能传变量)
INOUT: 引用传递,既可以在内部修改然后给外部使用,也可以在内部使用外部的数据(只能传变量)
*/
CREATE PROCEDURE procedure2(IN num1 INT, OUT num2 INT, INOUT num3 INT)
DELIMITER $$
BEGIN
    SELECT num1,num2,num3; -- num2的值一定为NULL
END
$$
DELIMITER ;

-- 使用
SET @num1 = 1;
SET @num2 = 2;
SET @num3 = 3;
SELECT @num1,@num2,@num3; -- 显示1,2,3
CALL procedure2(@num1,@num2,@num3); -- 显示1,NULL,3
SELECT @num1,@num2,@num3; -- 显示1,NULL,3


-- 存储过程结束后修改才会同步到全部变量中
CREATE PROCEDURE procedure3(IN num1 INT, OUT num2 INT, INOUT num3 INT)
DELIMITER $$
BEGIN
    SELECT num1,num2,num3; -- 显示1,NULL,3
    SET num1 = 10;
    SET num2 = 100;
    SET num3 = 1000;
    SELECT num1,num2,num3; -- 查看局部变量,显示10,100,1000
    SELECT @num1,@num2,@num3; -- 查看全局变量,显示1,2,3

    SET @num1 = 11; -- 修改全局变量
    SET @num2 = 111;
    SET @num3 = 1111;
    SELECT @num1,@num2,@num3; -- 查看全局变量,显示11,111,1111
    -- 存储过程结束,把OUT和INOUT类型赋值给全局变量
END
$$
DELIMITER ;

-- 使用
SET @num1 = 1;
SET @num2 = 2;
SET @num3 = 3;
CALL procedure2(@num1,@num2,@num3);
SELECT @num1,@num2,@num3; -- 显示11,100,1000

JAVA WEB

Servlet

Servlet基本使用

Servlet规范要求应用在web应用程序的根目录(不是工程的根目录,可以是一层或多层文件夹)下有WEB-INF文件夹,该文件夹下必须要有一个web.xml文件,同时还可以有classes文件夹用于存放编译生成的class文件,以及lib文件夹用于存放web应用程序用到的jar包

/myproject
├── src
│   └──jdbc.properties
│   └──webapp
│       └──servlets
│           └──HelloServlet.java
├── build
└── WebContent
    └── META-INF
    └── WEB-INF
    │   └── classes
    │   │   └──jdbc.properties
    │   │   └──webapp
    │   │       └──servlets
    │   │           └──HelloServlet.class
    │   └── lib
    │   └── web.xml
    └── hello.jsp

上面的WebContent就是web应用程序的根目录,web应用程序要访问的资源必须在该目录内

像hello.jsp可以通过http://localhost:8080/myproject/hello.jsp访问,但是WEB-INF目录下的任何文件都不能直接通过URL请求访问(但仍可以通过转发的方式访问)

web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0" >

    <!-- 配置当前web应用的初始化参数,可以通过ServletContext获取 -->
    <context-param>
        <param-name>configLocation1</param-name>
        <param-value>WEB-INF/myconfig1.properties</param-value>
    </context-param>
    <context-param>
        <param-name>configLocation2</param-name>
        <param-value>WEB-INF/myconfig2.properties</param-value>
    </context-param>

    <servlet>
        <servlet-name>HelloServlet</servlet-name>
        <servlet-class>webapp.servlets.HelloServlet</servlet-class>

        <!-- 配置Servlet的初始化参数,可以在init方法中通过ServletConfig获取 -->
        <!-- 和context-param不同的是,init-param只是当前servlet有效,而context-param是整个web应用有效 -->
        <init-param>
            <param-name>user</param-name>
            <param-value>root</param-value>
        </init-param>
        <init-param>
            <param-name>password</param-name>
            <param-value>123456</param-value>
        </init-param>

        <!-- 默认是在第一次请求该Servlet对应路径时创建Servlet实例,使用load-on-startup可以在Servlet容器初始化时就创建,该值为非负数,数值越小越先创建 -->
        <load-on-startup>1</load-on-startup>
    </servlet>

    <!-- 一个servlet可以有多个servlet-mapping -->
    <servlet-mapping>
        <servlet-name>HelloServlet</servlet-name>
        <!-- 要么指定后缀(/jsps/*.jsp)要么使用通配符(/jsps/*),通配符和后缀不能同时使用 -->
        <!-- /*表示拦截所有请求,比如http://localhost:8080/工程名字/xxx,就会调用该Servlet的service方法 -->
        <url-pattern>/*</url-pattern>
    </servlet-mapping>


    <welcome-file-list>
        <welcome-file>index.html</welcome-file>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>

    <error-page>
        <error-code>404</error=code>
        <location>/index.jsp</location>
    </error-page>
    <error-page>
        <exception-type>java.lang.ArithmeticException</exception-type>
        <location>/WEB-INF/error.jsp</location>
    </error-page>

</web-app>

HelloServlet.java

public class HelloServlet implements Servlet {

    //Servlet是单例的,默认在第一次请求的时候创建,如果在web.xml中使用了load-on-startup,则在容器初始化时就创建
    public HelloServlet(){
        System.out.println("HelloServlet's constructor");
    }

    //在创建好实例后调用,只会被调用一次
    public void init(ServletConfig servletConfig) throws ServletException {
        System.out.println("init");

        //获取servlet的init-param
        String user = servletConfig.getInitParameter("user");
        String password = servletConfig.getInitParameter("password");
        //或者
        Enumeration<String> names1 = servletConfig.getInitParameterNames();
        while(names.hasMoreElements()){
            String name = names1.nextElement();
            String value = servletConfig.getInitParameter(name);
            System.out.println(name + "--" + value);
        }

        //获取context-param
        ServletContext servletContext = servletConfig.getServletContext();
        String configLocation1 = servletContext.getInitParameter("configLocation1");
        String configLocation2 = servletContext.getInitParameter("configLocation2");
        //或者
        Enumeration<String> names2 = servletContext.getInitParameterNames();
        while(names2.hasMoreElements()){
            String name = names2.nextElement();
            String value = servletContext.getInitParameter(name);
            System.out.println(name + "--" + value);
        }

        //获取web应用程序根目录下文件的绝对路径
        String realPath = servletContext.getRealPath("/hello.jsp");//斜杠表示当前web应用的根路径
        System.out.println(realPath);

        //获取web应用的根路径,即http://localhost:8080/web应用根路径/servlet-mapping的路径/xxx,通常web应用根路径为工程名字前面加斜杠(/)
        String contextPath = servletContext.getContextPath();
        System.out.println(contextPath);

        //读取文件
        try{
            ClassLoader classLoader = getClass().getClassLoader();
            InputStream is1 = classLoader.getResourceAsStream("jdbc.properties");//读取src目录下的jdbc.properties文件

            //这里的路径是web应用目录的路径,而不是src的路径
            InputStream is2 = servletConfig.getServletContext().getResourceAsStream("/WEB-INF/classes/jdbc.properties");//读取web应用目录下的/WEB-INF/classes/jdbc.properties文件(编译后会被拷贝到/WEB-INF/classes/下)
        }catch(Exception e){  }
    }

    public ServletConfig getServletConfig() {
        System.out.println("getServletConfig");
        return null;
    }

    public String getServletInfo() {
        System.out.println("getServletInfo");
        return null;
    }

    //每次请求该Servlet都会被调用
    public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException {
        System.out.println("service");

        //-------------ServletRequest-------------
        //http://localhost:8080/myproject/xxx?user=root&password=123456
        String user = request.getParameter("user");
        String password = request.getParameter("password");
        System.out.println(user + "--" + password);

        /* 用于表单中的多选框
         <input type="checkbox" name="habit" value="read">看书
         <input type="checkbox" name="habit" value="movie">电影
         <input type="checkbox" name="habit" value="game">游戏
        */
        String[] habbit = request.getParameterValues("habbit");
        System.out.println(Arrays.asList(habbit));

        //还有其他获取参数的方法
        //Enumeration<String> names = request.getParameterNames();
        //Map<String, String[]> map = request.getParameterMap();


        //有些参数要其子类HttpServletRequest才能获取
        HttpServletRequest httpRequest = (HttpServletRequest)request;

        httpRequest.setCharcterEncoding("utf-8");

        //获取 http://127.0.0.1:8080/web应用根路径/,web应用根路径一般就是工程名字前面加斜杠(/)
        String basePath = httpRequest.getScheme()+"://"+httpRequest.getServerName()+":"+httpRequest.getServerPort()+httpRequest.getContextPath()+"/";
        //获取 /servlet-mapping的路径/xxx
        String servletPath = httpRequest.getServletPath();
        //获取GET请求的参数(不包含?问号),如果是POST,获得的是null
        String queryString = httpRequest.getQueryString();

        //URI就是 /web应用根路径/servlet-mapping的路径/xxx ,即http://localhost:8080后面那一串(如果是GET请求,是不包含参数的,即不包含?及后面的内容)
        String requestURI = httpRequest.getRequestURI();
        //完整的访问路径叫URL(不包含GET的参数)
        String requestURL = httpRequest.getRequestURL();

        //获取请求方式
        String method = httpRequest.getMethod();

        //接收二进制数据,文件上传(form表单中method="post" enctype="multipart/form-data")
        //这时得到的数据包含请求头、分隔符、其他请求参数等与文件数据无关的信息,需要手动解析获取的数据
        //所以一般使用Apache提供的工具包commons-fileupload.jar和commons-io.jar来实现文件上传
        //httpRequest.getInputStream();
        if(ServletFileUpload.isMultipartContent(httpRequest)){
            DiskFileItemFactory factory = new DiskFileItemFactory();
            factory.setSizeThreshold(1024 * 1024 * 3);
            factory.setRepository(new File(System.getProperty("java.io.tmpdir")));

            ServletFileUpload upload = new ServletFileUpload(factory);
            upload.setFileSizeMax(1024 * 1024 * 40);
            upload.setSizeMax(1024 * 1024 * 50);
            upload.setHeaderEncoding("UTF-8");

            String uploadPath = getServletContext().getRealPath("/") + File.separator + "upload";
            File uploadDir = new File(uploadPath);
            if (!uploadDir.exists()) {
                uploadDir.mkdir();
            }
            List<FileItem> formItems = upload.parseRequest(request);
            if (formItems != null && formItems.size() > 0) {
                // 迭代表单数据
                for (FileItem item : formItems) {
                    if (!item.isFormField()) {
                        String fileName = new File(item.getName()).getName();
                        String filePath = uploadPath + File.separator + fileName;
                        File storeFile = new File(filePath);
                        item.write(storeFile);
                        httpRequest.setAttribute("message", "文件上传成功!");
                    }
                }
            }
        }

        //转发请求
        String servletPath = "servlet2"
        RequestDispatcher requestDispatcher = httpRequest.getRequestDispatcher("/" + servletPath);//斜杠代表 域名+web应用的根路径
        requestDispatcher.forward(request, response);

        //-------------ServletResponse-------------
        response.setContentType("text/html");
        response.setCharcterEncoding("utf-8");

        //发送文本数据
        PrintWriter writer = response.getWriter();
        //writer.println("<!DOCTYPE html>");
        writer.println("<html><head><title>helloworld</title></head><body>Hello World!</body></html>");
        writer.flush();

        //发送二进制数据,文件下载
        /*
        //文件下载需要添加两个请求头
        response.setContentType("application/x-msdownload");
        //filename是服务器建议客户端浏览器保存的文件名,客户端可以更改实际保存的文件名
        response.setHeader("Content-Disposition", "attachment; filename=" + java.net.URLEncoder.encode("fileName", "UTF-8"));
        response.getOutputStream();
        */

        //重定向
        //response.sendRedirect("/" + servletPath);//斜杠代表 当前域名
    }

    //在当前Servlet所在WEB应用被卸载时调用,只会被调用一次
    public void destroy() {
        System.out.println("destroy");
    }
}

我们一般不会直接使用Servlet,而是使用其实现类GenericServlet或HttpServlet,HttpServlet根据不同的请求方式(get、post等)会调用不同的方法(doGet、doPost等)

public class HelloHttpServlet extends HttpServlet {
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.setContentType("text/html");
        PrintWriter writer = response.getWriter();
        writer.println("hello world!");
        writer.flush();
        writer.close();
    }

    public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.setContentType("text/html");
        PrintWriter writer = response.getWriter();
        writer.println("hello world!");
        writer.flush();
        writer.close();
    }
}

一般一个URL只能对应Servlet,要多个请求对应一个Servlet,可以配置servlet-mapping,然后在请求时带上请求哪个方法的参数,比如http://localhost:8080/myproject/helloservlet?method=doGet

另一个思路是拦截所有请求,然后根据请求的路径去匹配指定的类及其方法,通过反射的方式调用,这就是其他框架在做的事情

Cookie存储在客户端浏览器上,默认情况下,一个域名下的所有Cookie对应一个文件存储,所以Cookie不能跨域访问

不同浏览器对Cookie大小和数量的限制不一样,一般Cookie大小不能超过4K(大部分浏览器都限制为4095~4097个字节),数量应尽量保证不超过20个(IE6限制为20个,但部分浏览器可以存储更多,比如IE7+是50个,Chrome是53个)

//获取所有Cookie(Cookie无法获取单个,只能获取全部,然后遍历出想要的Cookie)
Cookie[] cookies = request.getCookies();
if(cookies != null && cookies.length > 0){
    for(Cookie cookie : cookies){
        System.out.println(cookie.getName + "---" + cookie.getValue() + "---" + cookie.getMaxAge());
    }
}

//创建
Cookie cookie = new Cookie("name", "小明");
//默认cookie是会话级别的,它存储在浏览器内存中,当浏览器退出后会被删除;要让cookie存储在磁盘上,需设置maxAge(过期时间,单位为秒)
cookie.setMaxAge(60 * 60);
//会将cookie放在Set-Cookie的HTTP响应报头中,但这个操作不会修改之前的cookie,而是添加新的报头(一个HTTP报头可以有多个Set-Cookie),所以不叫setCookie,而是叫addCookie
response.addCookie(cookie);

//负数表示不在磁盘中存储该cookie,而是保存在浏览器内存中
cookie.setMaxAge(-1);
//删除,将过期时间设置为0就是删除
cookie.setMaxAge(0);

//是否仅通过加密连接(比如SSL)传输
cookie.setSecure(false);

//当前jsp设置的cookie只能在本路径及子路径下的jsp获取,如果父路径的jsp需要获取子路径jsp设置的cookie,需要setPath为同一个
//cookie.setPath("/");//这里的"/"是指当前web应用的根路径
//cookie.setPath(request.getContextPath());//一个站点的cookie需要共享可以使用站点的根路径,也就是项目的名字
cookie.setPath("/webapp_1");//手动指定path

//解决跨域共享cookie的问题;setPath只能解决同一站点的web应用之间的cookie共享问题,但如果网站域名不同(包括子域不同),则需要setDomain为同一个
cookie.setDomain("mywebsite.com");

Session

Servlet的Session存储在服务器的内存中(其它比如PHP是存储在文件或数据库中的),每次传输通过Cookie中的JSESSIONID来记录当前客户端对应存储在服务器端的session

只有第一次调用request.getSession()才会在Cookie中加入JSESSIONID

//获取session,如果有JSESSIONID,则获取之前创建的session,否则新建一个HttpSession对象
HttpSession session = request.getSession(); //相当于request.getSession(true);
//如果有JSESSIONID,则获取之前创建的session;如果没有,则不创建,返回null
//HttpSession session = request.getSession(false);

Date createTime = new Date(session.getCreationTime());
//获取上次访问这个站点的时间
Date lastAccessTime = new Date(session.getLastAccessedTime());

if (session.isNew()) {
    title = "Welcome to my website";
    session.setAttribute("name", "游客");
    //设置过期时间,单位为秒(如果在指定的时间内没有访问则删除session,每次访问都重新计时)
    session.setMaxInactiveInterval(15 * 60);
}

String sessionId = session.getId();
String name = session.getAttribute("name");
String maxInactiveInterval = session.getMaxInactiveInterval();

//删除一个指定的属性
session.removeAttribute("name");
//删除整个session
session.invalidate();

由于sessionid通过Cookie存储,所以用户关闭浏览器时,Cookie被清空,下次打开浏览器时也无法访问原来的Session(但这时服务器端还没有删除Session),我们可以通过设置JSESSIONID的maxAge来持久化sessionid

Session只会在过期或者手动调用invalidate时才会被销毁,WEB应用关闭然后再重新启动时,关闭前会先把Session持久化到硬盘中(所以如果存入Session的是JavaBean,需要实现序列号接口Serializable),重启后再从硬盘中重新装载,然后再把持久化的文件删掉,所以WEB应用重启不会导致Session清空

Session的过期时间也可以在web.xml中统一指定

<session-config>
    <!-- 单位为分钟 -->
    <session-timeout>15</session-timeout>
</session-config>

对于JSP页面,如果设置了<%@ page session="false" %>,只是禁用了session的隐含对象,但我们仍可以显式地获取session

<%@ page language="java contextType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ page session="false" %>
<html>
    <head><title>JSP页面</title></head>
    <body>
        <%
            //获取当前站点的其他页面创建过的session,如果之前没有创建过(比如第一次访问该站点),则返回null
            //HttpSession session = request.getSession(false);

            //如果没有创建过session,则新建一个HttpSession
            HttpSession session = request.getSession(true);
            out.println(session.getId());
        %>
    </body>
</html>

如果客户端浏览器禁用了Cookie,那么自然也就无法通过上面的方式使用Session了,在这种情况下,我们可以把sessionid放在URL后面,作为GET参数传输到服务器,这种方式叫做URL重写

<a href="<%=response.encodeURL("index.jsp") %>" >index</a>
<a href="<%=response.encodeRedirectURL("index.jsp") %>" >redirect to index</a>

路径问题

关于根路径的说明:

在代码中的请求转发(比如request.getRequestDispatcher("/b.jsp"),包括java代码和jsp代码)或web.xml或JSTL的重定向<c:redirect url="/b.jsp">中,斜杠/表示当前域名+WEB应用的根路径,即http://localhost:8080/myproject/

在代码中的请求重定向(比如response.sendRedirect("/b.jsp");)或html页面中(比如<a href="/b.jsp">),斜杠/表示当前域名,即http://localhost:8080/

简而言之,如果斜杠是交由Servlet处理的,则代表域名+WEB应用的根路径,如果斜杠是交由客户端浏览器处理的,斜杠代表当前域名

关于绝对路径:

实际开发中,一般要使用绝对路径(即前面带斜杠/),这样的好处是,如果jsp文件位置变化,代码也能正常运行

如果斜杠/仅代表当前域名,则在路径前面加上request.getContextPath()即可

比如response.sendRedirect(request.getContextPath() + "/b.jsp")

转发和重定向

//转发
request.getRequestDispatcher("/b.jsp").forward(request, response);//斜杠代表 域名+web应用的根路径
//重定向
response.sendRedirect("/b.jsp");//斜杠代表 当前域名

转发和重定向的区别:

转发只能转发到本站的地址,而且浏览器地址栏不会变化,显示的还是当前servlet的地址(如果这时用户点击刷新,会有重复提交的问题)

重定向可以重定向到站外,浏览器的地址栏会变成重定向的地址(如果用户点击返回,依然有重复提交的问题)

转发只发出了一次请求,而重定向发出了两次请求(浏览器接收到服务器返回的重定向状态码后,再次发起请求,跳转到新页面),所以转发两个servlet的request是同一个,而重定向两个servlet的request不是同一个

解决重复提交的问题:

方案一:使用AJAX提交(如果不配合token,仍然无法避免恶意重复提交)

方案二:在HTML表单的隐藏域中生成一个token(随机数,比如MD5(时间).toHex()),并把该token作为key存入session中,value表示是否受理过,如果token已经被使用过了,那么服务器就不受理(或者第一次受理后把token从session中删除)

一次性验证码的实现思路和方案二一样

Filter

Filter,拦截器

public class MyFilter implements Filter  {

    //在加载WEB应用时调用,只会被调用一次
    public void init(FilterConfig config) throws ServletException {
        //获取init-param
        System.out.println(config.getInitParameter("param"));
    }

    //每次拦截都会调用该方法
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        //拦截请求

        //放行(执行后续的Filter,如果后面没有Filter,则执行Servlet对应方法)
        chain.doFilter(request,response);

        //拦截响应
    }

    //在销毁前调用,只会被调用一次
    public void destroy(){  }
}

在web.xml中配置

<filter>
    <filter-name>MyFilter</filter-name>
    <filter-class>webapp.filters.MyFilter</filter-class>
    <init-param>
        <param-name>param</param-name>
        <param-value>paramValue</param-value>
    </init-param>
</filter>

<!-- 如果有多个过滤器,按照filter-mapping定义的先后顺序进行拦截 -->
<filter-mapping>
    <filter-name>MyFilter</filter-name>
    <url-pattern>/*</url-pattern>
    <!-- 可以指定通过何种方式访问时执行拦截
         可选值有REQUEST、FORWARD(包括Java或JSP代码的forward、jsp标签的forward、通过JSP的page指令配置的errorPage)、INCLUDE、ERROR(ERROR只有在web.xml中配置的error-page才会触发,而通过JSP的page指令配置的errorPage不会触发)
         默认只有REQUEST才拦截,可以配置多个dispatcher来拦截不同请求方式 -->
    <dispatcher>REQUEST</dispatcher>
    <dispatcher>FORWARD</dispatcher>
</filter-mapping>

Listener

Listener监听器,它可以监听ServletContext、HttpSession、ServletRequest等域对象的创建和销毁,以及这些域对象的属性更改事件

一共有ServletContextListener、ServletContextAttributeListener、HttpSessionListener、HttpSessionAttributeListener、ServletRequestListener、ServletRequestAttributeListener六种监听器

除此以外还有两个特殊的监听器,HttpSessionBindingListener(监听新增、替换、移除事件)、和HttpSessionActivationListener(监听被持久化到硬盘及从硬盘中重新载入的事件,又叫钝化和活化),JavaBean实现这些接口可以感知自己在Session域中的状态的改变,这两个Listener不需要在web.xml中注册

public class MyServletContextListener implements ServletContextListener{

    public void contextInitialized(ServletContextEvent sce){
        //获取context-param
        System.out.println(sce.getServletContext().getInitParameter("contextParam"));
    }

    public void contextDestroyed(ServletContextEvent sce){  }
}

web.xml配置

<listener>
    <listener-class>webapp.listeners.MyServletContextListener</listener-class>
</listener>

<context-param>
    <param-name>contextParam</param-name>
    <param-value>contextValue</param-value>
</context-param>

JSP

用到的jar包可以从下面地址下载:

http://tomcat.apache.org/download-taglibs.cgi

JSTL相关的两个jar包

http://repo2.maven.org/maven2/javax/servlet/jstl/

http://repo2.maven.org/maven2/taglibs/standard/

JSP基本使用

JSP是在html页面中嵌入java代码来动态生成页面的一种方式

JSP本质上是一个Servlet,它在编译后会生成xxx.jsp.java文件,该java类是一个Servlet,里面把与JSP无关的html等代码用writer.println("xxx")的方式输出,把JSP标签内的java代码放在Servlet对应的方法中执行

JSP有9个隐含对象:

JSP四大域对象:application、session、request、pageContext

作用范围:context > session > request > page

<%-- JSP指令有
    page: 定义页面依赖属性,例如脚本语言,错误页面和缓冲的要求
    include: 静态引入,将其他文件的内容合并到当前文件,这种合并是源码级别的合并,被引入的页面可以使用引入的页面定义的变量,引入的无论是何种后缀,最后都会按照JSP解析(相当于先把源码复制过来,再解析)
    taglib: 引入标签库 --%>
<%@ page language="java contextType="text/html; charset=UTF-8" pageEncoding="UTF-8" import="java.util.*" %>

<%-- 指定错误页面,出错会转发到指定页面 --%>
<%@ page errorPage="/WEB-INF/error.jsp" %>

<%-- JSP声明,可以用于声明变量或方法 --%>
<%-- 前面提到,JSP是一个Servlet,JSP解析器会把JSP脚本片段或JSP表达式放到service方法中,而如果想定义函数或全局变量,就要使用JSP声明 --%>
<%! int day = 3; %>
<html>
    <head><title>JSP页面</title></head>
    <body>
        <%-- 使用include指令导入网页头部的模板,静态引入header.jsp不会生成header.jsp.java --%>
        <%@ include file = "header.jsp" %>
        <%-- 使用jsp标签进行动态引入,这时header.jsp会生成header.jsp.java,且header.jsp无法使用当前jsp定义的变量(相当于先解析,再放到这里输出) --%>>
        <jsp:include page="header.jsp"></jsp:include>

        <!-- html注释 -->
        <%-- JSP注释,和html注释的不同之处在于JSP注释不会被输出到网页源码中,而且JSP脚本只能用JSP注释来注释 --%>

        <%-- JSP脚本 --%>
        <%
            Date date = new Date();
            out.println(date);
        %>
        <br />

        <%-- JSP表达式(不用写分号) --%>
        <p>今天是:<%=(new java.util.Date()).toLocaleString()%></p>
        <br />

        <%-- JSP混合HTML代码,使用if-else(同理,可以写switch-case、while、for) --%>
        <% if (day == 1 | day == 7) { %>
              <p> Today is weekend</p>
        <% } else { %>
              <p> Today is not weekend</p>
        <% } %>
        <br />

        <%-- 定义和使用方法(方法的定义要使用JSP声明,以"<%!"开头) --%>
        <%!
        public int mul(int a, int b){
            return a * b;
        }
        %>
        <%= mul(2, 2) %>
        <br />

        <%-- 使用Session和Cookie --%>
        <%
        String name = request.getParameter("name");
        //以下四个方法四大域对象都有
        session.setAttribute("username",name);
        session.getAttribute("username");
        session.removeAttribute("username");
        Enumeration<String> names = session.getAttributeNames();
        %>
        <%
        Cookie[] cookies = request.getCookies();
        for (int i=0; i<cookies.length; i++){
            out.println(cookies[i].getValue());
        }

        String name=request.getParameter("name");
        Cookie cookie = new Cookie("name",name);
        response.addCookie(cookie);
        cookie.setMaxAge(50 * 50);//单位为秒(删除Cookie:cookie.setMaxAge(0))
        %>

        <%-- 转发请求与重定向
        <%
        //request.getRequestDispatcher("/b.jsp").forward(request, response);
        response.sendRedirect("b.jsp");
        %>
        --%>

    </body>
</html>

乱码问题

  1. 保证contextType="text/html; charset=UTF-8"pageEncoding="UTF-8"一致,可以解决显示页面的乱码问题
  2. 在接收请求参数前使用request.setCharacterEncoding("UTF-8"),指定接收参数的编码,可以解决POST乱码问题
  3. 对于GET请求,默认在传输时会转成iso-8859-1编码,要修改只能修改tomcat目录/config/server.xml,在Connector标签中添加useBodyEncodingForURI="true",来让GET参数使用页面的编码,此时在接收参数前仍然要使用request.setCharacterEncoding("UTF-8")指定编码,或者直接指定URIEncoding来修改默认的传输编码。如果不想修改tomcat配置文件,也可以在接收参数后使用new String(param.getBytes("iso-8859-1"), "UTF-8")进行转码

JSP标签

JSP标签又叫JSP动作元素,与JSP脚本不同在于,JSP动作元素在请求处理阶段起作用

常用标签

<%-- 动态引入(区别于`<%@ include file="xxx"%>`) --%>
<jsp:include page="/index.jsp"></jsp:include>

<%-- 转发请求,可以使用jsp:param传递参数,另一页面可以通过request.getParameter获取 --%>
<jsp:forward page="/index.jsp">
    <jsp:param name="username" value="user" />
</jsp:forward>

<%-- 使用JavaBean,并设置、获取属性,name是指JavaBean的id --%>
<jsp:useBean id="user" class="webapp.pojo.User" scope="request" />
<jsp:setProperty name="user" property="username" value="小明" />
<jsp:getProperty name="user" property="username" />

<%-- 使用*可以自动为所有属性赋值为请求参数的值 --%>
<%-- http://localhost:8080/myproject/a.jsp?id=1&name=xiaoming&age=14 --%>
<jsp:setProperty name="user" property="*" />

EL表达式

EL表达式(Expression Language)可以简化JSP表达式的写法,它的语法是${表达式}

EL表达式可以直接访问JavaBean的属性、数组、List、Map元素,进行数学运算、逻辑判断(数学、逻辑运算符支持java的>、>=、==、||、&&等,也可以使用gt、ge、eq、and、or替代),直接访问四大域(也可以通过隐含对象访问其他域)

EL表达式支持如下隐含对象:

<%-- JSP的写法,如果username为null,会输出"null"字符串,为了避免这种情况,需要手动处理 --%>
<%=request.getParameter("username") == null ? "":request.getParameter("username") %>
<%-- EL的写法,如果username为null,则输出空字符串"" --%>
${param.username}

<%-- 直接访问对象的属性 --%>
${sessionScope.user.name}
<%-- 或者 --%>
${sessionScope.user["name"]}

<%-- 特殊情况需要用中括号 --%>
<%
    User user = new User(1,"小明",14);
    session.setAttribute("webapp.pojo.user", user);
%>
${sessionScope["webapp.pojo.user"].name}

<%-- 如果不指定scope,会自动按照作用范围从小到大寻找page、request、session、application中的内容 --%>
${user.name}

<%-- EL表达式可以自动类型转换 --%>
${param.age + 10}
<!-- 复杂数据类型也可以自动转换 -->
<% application.setAttribute("time", new Date()); %>
${applicationScope.time.time}

<%-- empty运算符 --%>
<%
    List<String> names = new ArrayList<String>();
    request.setAttribute("names", names);
%>
<!-- 输出true,属性不存在或内容为空都会输出true -->
${empty requestScope.names}

<%-- EL表达式使用函数需要导入标签库 --%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>
${fn:length(param.name) }

JSTL标签

JSTL全称为JavaServer Pages Standard Tag Library,JSP标准标签库

<%@ page language="java contextType="text/html; charset=UTF-8" pageEncoding="UTF-8" import="java.util.*" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/sql" prefix="sql" %>

<html>
    <head><title>JSP页面</title></head>
    <body>

        <%-- scope可以指定为application、session、request、page --%>
        <c:set var="sesssionValue" value="<<java>>" scope="session" />
        <%-- 给JavaBean的属性赋值 --%>
        <% User user = new User(); %>
        <c:set target=${requestScope.user} property="id" value="1" />

        <%-- 如果sesssionValue含有特殊字符,可以自动转义(比如尖括号) --%>
        <c:out value="${sesssionValue}" default="null" escapeXml="true" />

        <c:remove var="sesssionValue" scope="session" />

        <%-- 条件判断 --%>
        <c:if test="${param.age > 18}" var="isAdult" scope="request"></c:if>
        <c:out value="${requestScope.isAdult}"></c:out>

        <c:choose>
            <c:when test="${param.age > 18}">成年</c:when>
            <c:when test="${param.age >= 0}">未成年</c:when>
            <c:otherwise>参数不合法</c:otherwise>
        </c:choose>

        <%-- 循环 --%>
        <c:forEach begin="0" end="10" step="2" var="i">
            ${i}
        </c:forEach>

        <%-- 遍历Collection或数组 --%>
        <%
            List<User> users1 = new ArrayList<User>();
            list.add(new User(0, 小明", 14));
            list.add(new User(1, 小红", 13));
            list.add(new User(2, 小王", 12));
            request.setAttribute("users1", users1);
        %>
        <c:forEach var="user" items="${requestScope.users1}" varStatus="status">
            ${status.index} -- ${user.id} -- ${user.name} -- ${user.age}
        </c:forEach>

        <%-- 遍历Map --%>
        <%
            Map<String, User> users2 = new HashMap<String, User>();
            users2.put("a", new User(0, 小明", 14));
            users2.put("b", new User(1, 小红", 13));
            users2.put("c", new User(2, 小王", 12));
            request.setAttribute("users2", users2);
        %>
        <c:forEach var="user" items="${requestScope.users2}">
            ${user.key} -- ${user.value.id} -- ${user.value.name} -- ${user.value.age}
        </c:forEach>

        <%-- JSTL或JSP标签中,无论转发还是重定向,斜杠都表示 当前域名+WEB应用的根路径 --%>
        <%-- 重定向 <c:redirect url="/b.jsp" /> --%>
        <%-- 转发可以使用 <jsp:forward page="/b.jsp"> --%>

        <%-- c:url生成的URL会自动加上WEB应用的根路径,且会在URL后面加上JSESSIONID,自动进行URL重写,并对GET参数进行转码 --%>
        <%-- c:url可以存储在指定的域中 --%>
        <c:url value="/b.jsp" var="testurl" scope="page">
            <c:param name="username" value="小明" />
        </c:url>
        ${testurl}

        <%-- 使用mysql的jstl --%>
        <sql:setDataSource var="connection" driver="com.mysql.jdbc.Driver"
            url="jdbc:mysql://localhost/test?useSSL=false&characterEncoding=utf8" user="user" password="root" />

        <sql:update dataSource="${connection}" var="count">
            INSERT INTO user(`id`,`name`,`age`) VALUES (null,'小明', 14);
        </sql:update>

        <c:set var = "userId" value = "1"/>
        <sql:update dataSource="${connection}" var="count">
            UPDATE user SET name='小红' WHERE id=?
            <sql:param value = "${userId}" />
        </sql:update>

        <sql:update dataSource="${connection}" var="count">
            DELETE FROM user WHERE id=1;
        </sql:update>

        <sql:query dataSource="${connection}" var="result">
            SELECT * FROM user;
        </sql:query>
        <table border="1" width="100%">
            <tr>
                <th>id</th>
                <th>name</th>
                <th>age</th>
            </tr>
            <c:forEach var="row" items="${result.rows}">
                <tr>
                    <td><c:out value="${row.id}" /></td>
                    <td><c:out value="${row.name}" /></td>
                    <td><c:out value="${row.age}" /></td>
                </tr>
            </c:forEach>
        </table>

    </body>
</html>

自定义标签

自定义标签需要创建一个继承自SimpleTagSupport的类,还要创建一个tld文件用于描述自定义的标签

各方法的调用顺序:setJspContext->setParent->setXXX->setJspBody->doTag

//简单标签
public class MyIPTag extends SimpleTagSupport {
    PageContext pageContext;

    @Override
    public void doTag() throws JspException, IOException {
        HttpServletRequest request = (HttpServletRequest)pageContext.getRequest();
        JspWriter out = getJspContext().getOut();

        String ip = request.getRemoteAddr();
        out.write(ip);
    }

    @Override
    public void setJspContext(JspContext jspContext) {        
        this.pageContext = (PageContext)jspContext;  
    }
}

//有标签体和标签属性的复杂标签
public class MyComplexTag extends SimpleTagSupport {
    public String attr1;

    public void doTag() throws JspException, IOException {
        //获取标签体(如果有EL表达式或JSP动作元素(即JSP命名空间下的标签)会自动执行),并输出
        JspFragment jspFragment = this.getJspBody();

        //invoke(null)会直接输出到页面上
        //jspFragment.invoke(null);

        //要更改输出的内容可以invoke到指定的writer中
        StringWriter sw = new StringWriter();
        jspFragment.invoke(sw);
        getJspContext().getOut().println(sw.toString().toUpperCase());

        //输出属性
        if(attr1 != null){
            getJspContext().getOut().println(attr1);
        }

        //标签嵌套时获取父标签属性并输出
        //父标签无法获取子标签,但子标签能获取父标签,因为如果没有掉invoke,子标签就不会被初始化,解析器在解析时无法得知是否需要加载子标签
        JspTag parent = getParent();
        if(parent instanceof MyComplexTag){
            MyComplexTag parentTag = (MyComplexTag)parent;
            getJspContext().getOut().println(parentTag.attr1);
        }
    }

    //标签的属性会自动通过set方法设置
    public void setAttr1(String attr1) {
        this.attr1 = attr1;
    }
}

WEB-INF/mytag.tld

<?xml version="1.0" encoding="UTF-8" ?>  
<taglib xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd"
    version="2.0">

    <display-name>My Tag</display-name>
    <description>This is my Tag</description>
    <tlib-version>1.0</tlib-version>
    <jsp-version>2.0</jsp-version>

    <!-- 建议在jsp中使用的前缀(prefix) -->
    <short-name>my</short-name>
    <!-- 用于唯一标识当前的TLD文件,该地址不需要真实存在,多个TLD文件的uri不能重复 -->
    <uri>http://mywebsite.com/mytag/my</uri>

    <tag>
        <!-- 标签名字 -->
        <name>ip</name>
        <!-- 标签对应的class -->
        <tag-class>webapp.jstl.MyIPTag</tag-class>
        <!-- 标签类型,empty就是没有标签体的简单标签 -->
        <body-content>empty</body-content>
    </tag>

    <!-- 有标签体及标签属性的标签 -->
    <tag>
        <name>complex</name>
        <tag-class>webapp.jstl.MyComplexTag</tag-class>
        <!-- scriptless表示标签体可以包含EL表达式或JSP动作元素,但不能包含JSP脚本(包括JSP表达式)
             除了empty、scriptless外,还可以指定为tagdependent,表示标签体交由我们自己解析,标签体中的所有内容会原封不动地交给我们的标签处理器 -->
        <body-content>scriptless</body-content>
        <attribute>
            <name>attr1</name>
            <!-- 以下参数是可选的 -->
            <required>false</required>
            <type>java.lang.String</type>
            <!-- 能否使用JSP脚本或EL表达式 -->
            <rtexprvalue>true</rtexprvalue>
            <!-- 是否将该属性视为JspFragment -->
            <fragment>false</fragment>
        </attribute>
    </tag>
</taglib>

使用

<%@ page language="java contextType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="my" uri="http://mywebsite.com/mytag/my"%>
<html>
    <head><title>mytag</title></head>
    <body>
        我的IP是:<my:ip/>
        <br />
        <my:complex attr1="属性" >标签体</my:complex>

        <my:complex attr1="parent" >
            <my:complex attr1="child" >
                child content
            </my:complex>
        </my:complex>
    </body>
</html>

自定义EL函数

每个EL函数对应Java类中的一个静态方法,要求该Java类是public的,静态方法也是public的,然后还要通过编写tld文件用于描述自定义的EL函数

public class MyELFunction{

    public static String concat(String str1, String str2){
        return str1 + str2;
    }
}

WEB-INF/myfunction.tld

<?xml version="1.0" encoding="UTF-8" ?>  
<taglib xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd"
    version="2.0">

    <display-name>My EL Function</display-name>
    <description>This is my EL Function</description>
    <tlib-version>1.0</tlib-version>
    <jsp-version>2.0</jsp-version>
    
    <short-name>myfunc</short-name>
    <uri>http://mywebsite.com/mytag/myfunc</uri>

    <function>
        <name>concat</name>
        <function-class>webapp.jstl.MyELFunction</function-class>
        <function-signature>java.lang.String concat(java.lang.String, java.lang.String)</function-signature>
    </function>
</taglib>

使用

<%@ page language="java contextType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="myfunc" uri="http://mywebsite.com/mytag/myfunc"%>

${myfunc:concat(param.name1, param.name2)}

三层架构和mvc

三层架构将整个项目划分为:

实际开发中一般会定义一套service接口和controller接口,然后新建一个impl的子包来完成对接口的实现,这时其他层就可以通过接口和IOC框架获取对象,从而实现各层之间的解耦

一般目录结构如下:

├── controller
│   └── UserController.java
├── dao
│   ├── pojo
│   │   └── User.java
│   └── repository
│       ├── impl
│       │   └── UserDaoImpl.java
│       └── IUserDao.java
└── service
    ├── impl
    │   └── UserServiceImpl.java
    └── IUserSerivce.java

MVC为:

MVC是三层架构中的表现层的一种架构

Spring

Spring Framework

Spring Docs

Spring中文文档

项目结构

src
├── applicationContext.xml
├── jdbc.properties
├── config
│   └── SpringConfig.java
├── controller
│   └── UserController.java
├── dao
│   ├── pojo
│   │   └── User.java
│   └── repository
│       ├── impl
│       │   └── UserDaoImpl.java
│       └── IUserDao.java
├── service
│   ├── impl
│   │   └── UserServiceImpl.java
│   └── IUserService.java
├── test
│   └── Test.java
└── Utils
    ├── LogUtils.java
    └── UserFactory.java

IOC

xml

原理:通过Class.forName的方式,获取接口实现类的实例,这样的话只要接口写好了,就能通过接口调用,各模块就可以同时开发。为了方便维护,一般会选择xml或者注解等非硬编码的方式把实现类的类名抽取出来,根据id获取对应实例(Bean默认是单例的)

直接注入

使用xml的方式把bean注入到容器中:

在src下创建applicationContext.xml,内容如下

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="userDao" class="dao.repository.impl.UserDaoImpl"/>
</beans>

Bean的属性

<bean id="userDao" class="dao.repository.impl.UserDaoImpl" init-method="init" destroy-method="destory" lazy-init="true" scope="prototype"/>
工厂模式注入

如果是通过工厂模式创建的,可以:

public class UserFactory {
    public UserDaoImpl createUser(){
        return new UserDaoImpl();
    }
}

对应xml

<!-- 先创建工厂的bean,然后再使用其工厂方法 -->
<bean id="userFactory" class="Utils.UserFactory" />
<bean id="userDao" factory-bean="userFactory" factory-method="createUser" />
public class UserFactory {
    static public UserDaoImpl createUser(){
        return new UserDaoImpl();
    }
}

对应xml

<!-- 静态的工厂方法无需创建工厂的bean,直接使用工厂方法即可 -->
<bean id="userDao" class="Utils.UserFactory" factory-method="createUser" />
含参数的注入

如果Bean的创建需要参数

public UserDaoImpl(String arg1,List arg2,Map arg3) {
}

对应xml

<bean id="userDao" class="dao.repository.impl.UserDaoImpl">
    <constructor-arg index="0" value="hello"/>
    <constructor-arg index="1">
        <!-- list和array可以不区分,只要数据结构相同,标签可以互换 -->
        <list>
            <value>1</value>
            <value>2</value>
        </list>
    </constructor-arg>
    <constructor-arg index="2">
        <!-- map和prop也可以互换 -->
        <map>
            <entry key="1" value="hello"/>
            <entry key="2" value="world"/>
        </map>
    </constructor-arg>
</bean>
User arg1;
public void setArg1(User arg1) {
    this.arg1 = arg1;
}

对应xml

<bean id="user" class="dao.pojo.User"/>
<bean id="userDao" class="dao.repository.impl.UserDaoImpl">
    <!-- 复杂数据类型使用ref -->
    <property name="arg1" ref="user"/>
</bean>
通过代码获取

通过代码获取bean:

先在UserDaoImpl的构造函数中增加System.out.println("UserDaoImpl init"),以观察加载顺序

使用ApplicationContext

//classpath:就是指src目录下,可以省略
ApplicationContext ap = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
System.out.println("applicationContext loaded");
IUserDao userDao1 = (IUserDao) ap.getBean("userDao");
System.out.println(userDao1.findUserById(1));

输出

UserDaoImpl init
applicationContext loaded
User{id=1, name='root', pwd='123456'}

使用BeanFactory

BeanFactory bf = new XmlBeanFactory(new FileSystemResource(new File("src/applicationContext.xml")));
System.out.println("beanFactory loaded");
IUserDao userDao2 = (IUserDao) bf.getBean("userDao");
System.out.println(userDao2.findUserById(1));

输出

beanFactory loaded
UserDaoImpl init
User{id=1, name='root', pwd='123456'}

ApplicationContextBeanFactory都可以获取bean,但ApplicationContext是立即加载,BeanFactory是懒加载

注解

使用注解需要开启扫描

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context
                           http://www.springframework.org/schema/context/spring-context.xsd">

<context:component-scan base-package="dao"/>
<context:component-scan base-package="service"/>
</beans>
@ComponentScan({"dao","service"})//扫描IOC
@Configuration//配置类需要使用该注解
public class SpringConfig {

}

此时需要使用该类创建ApplicationContext

public static void main(String[] args){
    ApplicationContext ap = new AnnotationConfigApplicationContext(SpringConfig.class);
    IUserDao userDao1 = (IUserDao) ap.getBean("userDao");
    System.out.println(userDao1.findUserById(1));
}
通过注解注入

也可以通过注解把bean注入到容器中:

@Component("userDao")//指定id为userDao,也可以不指定,默认id为类名(首字母小写)
public class UserDaoImpl implements IUserDao {

    @Override
    public User findUserById(int id) throws Exception{
        //模拟查询数据库操作
        return new User(1,"root","123456");
    }
    
    @Override
    public void insertUser(User user) throws Exception {
        System.out.println("插入了用户" + user);
    }
}

@Component@Controller@Service@Repository的作用是一样的,一般为了代码的可读性会在不同的层使用不同的注解

通过注解获取

通过注解获取

@Autowired
//@Qualifier("userDao")
//@Resource(name="userDao")//name就是对应的id
IUserDao userDao;

如果通过注解把bean注入到容器中时,使用了@Primary,则表示在匹配时如果有多个,则使用该bean作为默认值,@Autowired@Qualifier都支持使用默认值,但@Resource不支持

对于基本数据类型,可以使用@Value注入,一般配合@PropertySource使用

循环依赖注入问题

A依赖于B,B依赖于C,C又依赖于A,这样就形成了一个依赖环

解决循环依赖也很简单,因为对象的field或则属性是可以延后设置的,只要在构造函数中没有用到依赖的类,那么我们可以先初始化A、B、C三个类,然后在调用各自的set方法,把依赖传入(而非在创建Bean的时候就调用set传入)

Spring在创建Bean的时候使用三级缓存来解决这个问题

/** Cache of singleton objects: bean name --> bean instance */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(256);

/** Cache of singleton factories: bean name --> ObjectFactory */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<String, ObjectFactory<?>>(16);

/** Cache of early singleton objects: bean name --> bean instance */
private final Map<String, Object> earlySingletonObjects = new HashMap<String, Object>(16);

创建Bean的代码:

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        synchronized (this.singletonObjects) {
            singletonObject = this.earlySingletonObjects.get(beanName);
            if (singletonObject == null && allowEarlyReference) {
                ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                if (singletonFactory != null) {
                    singletonObject = singletonFactory.getObject();
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    return (singletonObject != NULL_OBJECT ? singletonObject : null);
}

Spring首先从一级缓存singletonObjects中获取。如果获取不到,并且对象正在创建中,就再从二级缓存earlySingletonObjects中获取。如果还是获取不到且允许singletonFactories通过getObject()获取,就从三级缓存singletonFactory获取,如果获取到了则从singletonFactories中移除,并放入earlySingletonObjects中,其实也就是从三级缓存移动到了二级缓存

从上述分析可知,Spring无法解决两种情况下的循环依赖

  1. 构造器参数循环依赖
  2. prototype类型的Bean的依赖,因为prototype类型的Bean的生命周期不是由Spring进行管理的,所以Spring不会对prototype类的Bean进行缓存

只有在Bean是singleton类型且构造器参数不需要传入依赖的Bean的时候,才能解决循环依赖的问题

AOP

持久层和业务层通过IOC把数据库的操作解耦了,但数据库的事务、日志等仍然需要写在业务层,而且会有大量的重复代码,这时就需要使用AOP

AOP术语:

1.通知(Advice)

织入到目标类连接点上的一段程序代码,就是要实现的功能,比如记录日志、过滤等,一般是静态工具类来实现这些功能

2.连接点(Joinpoint)

使用通知的地方,Spring支持的有方法开头、方法中try结束位置、出现异常时catch处、函数返回前,我们也可以通过获取代理对象(ProceedingJoinPoint)指定在何处使用通知(joinpoint.proceed)

3.切入点(Pointcut)

要使用通知的方法,连接点确定了方法中使用通知的位置,而哪些方法使用则由切入点表达式确定

4.切面(Aspect)

切面定义通知和切入点,Spring AOP就是负责实施切面的框架,它将切面所定义的横切逻辑织入到切面所指定的连接点中

5.引入(Introduction)

允许我们向被切入点选中的类中添加新方法属性,可以通过在通知类中定义属性来实现

6.目标对象(Target)

被切入点表达式选中的类,即要使用通知的类

7.代理(Proxy)

Spring AOP实现原理就是动态代理

8.织入(Weaving)

把切面应用到目标对象来创建新的代理对象的过程,织入有三种方式:编译期织入、类装载期织入、动态代理织入。Spring使用的是动态代理织入,而AspectJ采用编译期织入和类装载期织入

xml

通过xml配置(需要用到aop命名空间)

<bean id="userDao" class="dao.repository.impl.UserDaoImpl"/>
<bean id="userService" class="service.impl.UserServiceImpl">
    <property name="userDao" ref="userDao"/>
</bean>

<bean id="logUtils" class="Utils.LogUtils"/>
<aop:config>
    <!-- pointcut: 切入点表达式,表示切入service包及其子包下 方法名以get开头的所有方法(权限修饰符可以省略)
         全通配: *  *..*.*(..)-->
    <aop:pointcut id="pt1" expression="execution(* service..*.get*(..))"/>
    <!-- logUtils: 通知类 -->
    <aop:aspect ref="logUtils">
        <!--
         通知类型分为:
         before:开始执行方法前调用 
         after-returning: 执行完方法且没有异常时调用
         after-throwing: 出现异常时调用
         after: 方法执行完后调用
        -->
        <aop:before method="logBefore" pointcut-ref="pt1"/>
        <aop:after-returning method="logAfterReturning" pointcut-ref="pt1"/>
        <aop:after-throwing method="logAfterThrowing" pointcut-ref="pt1"/>
        <aop:after method="logAfter" pointcut-ref="pt1"/>
    </aop:aspect>
</aop:config>

LogUtils.java

public class LogUtils {
    public void logBefore() {
        System.out.println("before");
    }

    public void logAfterReturning() {
        System.out.println("after returning");
    }

    public void logAfterThrowing() {
        System.out.println("after throwing");
    }

    public void logAfter() {
        System.out.println("after");
    }
}

UserServiceImpl.java

public class UserServiceImpl implements IUserService {
    IUserDao userDao;
    
    public void setUserDao(IUserDao userDao) {
        this.userDao = userDao;
    }

    @Override
    public User getUserById(int id) throws Exception{
        return userDao.findUserById(id);
    }

    @Override
    public void insertUser(User user) throws Exception {
        userDao.insertUser(user);
    }
}

使用

ApplicationContext ap = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
IUserService userService = (IUserService) ap.getBean("userService");
System.out.println(userService.getUserById(1));

输出

before
after returning
after
User{id=1, name='root', pwd='123456'}

环绕通知

环绕通知可以替代上面的四个通知(before、after-returning、after-throwing、after)

<bean id="userDao" class="dao.repository.impl.UserDaoImpl"/>
<bean id="userService" class="service.impl.UserServiceImpl">
    <property name="userDao" ref="userDao"/>
</bean>

<bean id="logUtils" class="Utils.LogUtils"/>

<aop:config>
    <aop:pointcut id="pt1" expression="execution(* service..*.get*(..))"/>
    <aop:aspect ref="logUtils">
        <aop:around method="logAround" pointcut-ref="pt1"/>
    </aop:aspect>
</aop:config>

logUtils.java

public class LogUtils {
    //环绕通知需要加ProceedingJoinPoint的参数
    public Object logAround(ProceedingJoinPoint joinpoint) {
        Object result = null;
        try {
            System.out.println("around - before");
            result = joinpoint.proceed(joinpoint.getArgs());
            System.out.println("around - after returning");
        } catch (Throwable throwable) {
            throwable.printStackTrace();
            System.out.println("around - after throwing");
        }
        System.out.println("around - after");
        return result;
    }
}

注解

使用注解AOP也需要开启扫描

xml方式

<aop:aspectj-autoproxy />

注解方式(此时也需要使用AnnotationConfigApplicationContext来加载)

@EnableAspectJAutoProxy
@Configuration
public class SpringConfig {

}

通知类LogUtils.java

@Component//通知类也需要加入容器
@Aspect
public class LogUtils {

    @Pointcut("execution(* service..*.get*(..))")
    void pt1() {  }

    @Before("pt1()")
    public void logBefore() {
        System.out.println("before");
    }

    @AfterReturning("pt1()")
    public void logAfterReturning() {
        System.out.println("after returning");
    }

    @AfterThrowing("pt1()")
    public void logAfterThrowing() {
        System.out.println("after throwing");
    }

    @After("pt1()")
    public void logAfter() {
        System.out.println("after");
    }

    @Around("pt1()")
    public Object logAround(ProceedingJoinPoint joinpoint) {
        Object result = null;
        try {
            System.out.println("around - before");
            result = joinpoint.proceed(joinpoint.getArgs());
            System.out.println("around - after returning");
        } catch (Throwable throwable) {
            throwable.printStackTrace();
            System.out.println("around - after throwing");
        }
        System.out.println("around - after");
        return result;
    }
}

txManager

JdbcTemplate

DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql:///user?useUnicode=true&characterEncoding=UTF-8");
dataSource.setUsername("root");
dataSource.setPassword("root");
JdbcTemplate template = new JdbcTemplate(dataSource);

template.execute("CREATE TABLE user(id INT PRIMARY KEY, name VARCHAR(32), pwd VARCHAR(32))default charset = utf8");

template.update("INSERT INTO user VALUES(?,?,?)", 1, "小明", "123456");

List<User> list = template.query("select * from user", new BeanPropertyRowMapper<User>(User.class));

也可以手动实现rowMapper

List<User> list = template.query("select * from user", new RowMapper<User>() {
    @Override
    public User mapRow(ResultSet rs, int rowNum) throws SQLException {
        User user = new User();
        user.setId(rs.getInt("id"));
        user.setName(rs.getString("name"));
        user.setPwd(rs.getString("pwd"));
        return user;
    }
});

也可以通过xml获取Template

<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <property name="driverClassName"><value>com.mysql.jdbc.Driver</value></property>
    <property name="url"><value>jdbc:mysql:///user?useUnicode=true&characterEncoding=UTF-8</value></property>
    <property name="username"><value>root</value></property>
    <property name="password"><value>root</value></property>
</bean>

<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"/>

transactionManager

数据的并发问题:

MySQL的事务隔离级别:

隔离级别 脏读 不可重复读 幻读
读未提交(read-uncommitted)
不可重复读(read-committed)
可重复读(repeatable-read)
串行化(serializable)

Spring的事务传播机制:

事务传播行为类型 说明
PROPAGATION_REQUIRED 如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。
PROPAGATION_SUPPORTS 支持当前事务,如果当前没有事务,就以非事务方式执行。
PROPAGATION_MANDATORY 使用当前的事务,如果当前没有事务,就抛出异常。
PROPAGATION_REQUIRES_NEW 新建事务,如果当前存在事务,把当前事务挂起。
PROPAGATION_NOT_SUPPORTED 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
PROPAGATION_NEVER 以非事务方式执行,如果当前存在事务,则抛出异常。
PROPAGATION_NESTED 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。
代码使用

要求TransactionManagerJdbcTemplate(或者其他ORM框架)使用的是同一个DataSource

ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
DataSource dataSource = (DataSource) ac.getBean("dataSource");
IUserService userService = (IUserService) ac.getBean("userService");

DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(dataSource);
//通过TransactionDefinition配置事务隔离级别、传播机制等
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
definition.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
definition.setTimeout(-1);//永不超时

//开启事务
TransactionStatus status = transactionManager.getTransaction(definition);
try {
    userService.insertUser(new User(2, "小红", "root"));
    //提交
    transactionManager.commit(status);
} catch (Exception e) {
    //回滚
    transactionManager.rollback(status);
    e.printStackTrace();
}

UserDaoImpl(这里的DataSource是通过容器管理的单例的DataSource,每次getBean获得的都是同一个实例)

public class UserDaoImpl implements IUserDao, ApplicationContextAware {
    //方法1:通过实现ApplicationContextAware接口获取ApplicationContext,在通过ApplicationContext获取DataSource或者JdbcTemplate
    ApplicationContext applicationContext;
    
    //方法2:使用注解获取
    @Autowired
    JdbcTemplate template;
    
    //方法3:继承JdbcDaoSupport,里面其实就是自动设置了JdbcTemplate属性(略)

    @Override
    public User findUserById(int id) throws Exception {
        User user = template.queryForObject("select * from user where id=?",new Object[]{id}, new BeanPropertyRowMapper<User>(User.class));
        return user;
    }
    
    @Override
    public void insertUser(User user) throws Exception {
        DataSource dataSource = (DataSource) applicationContext.getBean("dataSource");
        JdbcTemplate template = new JdbcTemplate(dataSource);
        template.update("insert into user value(?,?,?)", user.getId(), user.getName(), user.getPwd());
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
xml使用

AOP式事务(包括xml和注解)是通过检测函数是否抛出unchecked exception(即Error、RunTimeException及其子类)来决定是否进行回滚的,如果在代码中使用try、catch截获了异常并且没有再抛出,则是不会回滚的;而且如果抛出的异常是checked exception(即普通Exception,比如IOException、NullPointerException),也是不会回滚的,这时需要我们指定rollback-for参数对指定异常进行回滚

<bean id="userDao" class="dao.repository.impl.UserDaoImpl"/>
<bean id="userService" class="service.impl.UserServiceImpl">
    <property name="userDao" ref="userDao"/>
</bean>

<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql:///user?useUnicode=true&amp;characterEncoding=UTF-8"/>
    <property name="username" value="root"/>
    <property name="password" value="root"/>
</bean>

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>

<!-- 为insertUser方法添加事务,这里也可以使用通配符 -->
<tx:advice id="txadvice1" transaction-manager="transactionManager">
    <tx:attributes>
        <tx:method name="insertUser" isolation="READ_COMMITTED" propagation="REQUIRED" read-only="false" timeout="-1" rollback-for="java.io.IOException"/>
    </tx:attributes>
</tx:advice>

<aop:config>
    <aop:pointcut id="pt1" expression="execution(public * service..*.*(..))" />
    <aop:advisor advice-ref="txadvice1" pointcut-ref="pt1" />
</aop:config>

测试是否回滚(这里不会回滚,因为这里的异常是java.lang.ArithmeticException,不是Error或RuntimeException):

@Override
public void insertUser(User user) throws Exception {
    userDao.insertUser(user);
    int i = 1/0;
}
注解使用

使用注解事务也需要开启扫描(需要用到tx命名空间,同时通过context:component-scan指定要扫描的包)

xml方式

<tx:annotation-driven transaction-manager="transactionManager" />

注解方式

@EnableTransactionManagement
@Configuration
public class SpringConfig {

}

UserDaoImpl.java

@Transactional(readOnly = true, rollbackFor = { Exception.class })
public class UserDaoImpl implements IUserDao {

    @Override
    public User findUserById(int id) throws Exception {
        return new User(1, "root", "123456");
    }

    @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED, timeout = -1, readOnly = true)
    @Override
    public void insertUser(User user) throws Exception {
        System.out.println("插入了用户" + user);
    }
}

其他注解

@Import

@Import: 使用纯注解开发Spring时,用于注入第三方的bean

@Import({OtherJavaClass1.class,OtherJavaClass2.class})
@Configuration
public class SpringConfig {

}

@Bean

@Bean: 将方法返回值注入容器

@Configuration
public class SpringConfig {

    @Bean
    IUserService createUserService(){
        return new UserServiceImpl();
    }
}

@Configuration

@Configuration: 配置类用的注解

@PropertySource

@PropertySource: 导入properties文件,此时可以通过在@Value使用${propertyKey}的方式获取properties文件内容

@PropertySource({"classpath:jdbc.properties"})
@Configuration
public class SpringConfig {

    @Value("${jdbc,driverClass}")
    String jdbcDriverClassName;
}

或者

@PropertySource({"classpath:jdbc.properties"})
@Configuration
public class SpringConfig {

    //当方法需要参数时,如果是复杂类型,spring会在容器里面找匹配的类型(也可以使用@Qualifier指定id),但对于基本数据类型,需要手动注入
    @Bean
    DataSource createDataSource(@Value("${jdbc,driverClass}") String driverClassName, @Value("${jdbc.url}") String url, @Value("${jdbc.username}") String username, @Value("${jdbc.password}") String password) {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName(driverClassName);
        dataSource.setUrl(url);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        return dataSource;
    }
}

@PropertySource也可以在applicationContext.xml中配置

<context:property-placeholder location="classpath:jdbc.properties"/>

@Conditional

@Conditional: 指定当什么时候把bean注入到容器

@Conditional(MyCondition.class)
public class LogUtils {
    //...   
}

MyCondition.java

public class MyCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        if(context.getEnvironment().getActiveProfiles().equals("test"))
            return true;
        else
            return false;
    }
}

@Profile

@Profile: 指定当什么时候把bean注入到容器,和@Conditional不同的是,@Profile的条件是通过运行参数指定的

注解方式

@Profile("test")
public class LogUtils {
    //...
}

xml方式:

<beans profile="test">
    <bean id="logUtils" class="Utils.LogUtils"/>
</beans>

配置:

通过ApplicationContext配置

AnnotationConfigApplicationContext ap = new AnnotationConfigApplicationContext();
//在加载配置之前设置
ap.getEnvironment().setActiveProfiles("test");
ap.register(SpringConfig.class);

通过servlet参数配置

<context-param>  
    <param-name>spring.profiles.default</param-name>  
    <param-value>test</param-value>  
</context-param>  

单元测试时配置

@RunWith(SpringJUnit4ClassRunner.class)  
@ContextConfiguration(locations = "applicationContext.xml")  
@ActiveProfiles("dev")  
public class Test {  

}

通过环境变量配置

set JAVA_OPTS="-Dspring.profiles.active=test"

Processor

Processor负责控制所有Bean的生命周期

@Component//需要加入容器,Spring会自动识别Processor
public class ProcessorTest implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        System.out.println(beanName + "--" + bean);
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println(beanName + "--" + bean);
        return bean;
    }
}

如果要控制单个Bean的生命周期,可以实现Lifecycle接口

public class LifecycleTest implements Lifecycle {
    boolean isRunning;

    @Override
    public void start() {
        System.out.println("start");
        isRunning = true;
    }

    @Override
    public void stop() {
        System.out.println("stop");
        isRunning = false;
    }

    @Override
    public boolean isRunning() {
        return isRunning;
    }
}

Aware

Aware通过回调的方式给我们传一些系统的资源,比如

public class ResolverTest implements EmbeddedValueResolverAware {
    
    @Override
    public void setEmbeddedValueResolver(StringValueResolver resolver) {
        //El表达式的处理器
        resolver.resolveStringValue("${jdbc.driverClass}");
    }
}

ApplicationListener

事件监听器

@Component
public class ApplicationListenerTest implements ApplicationListener {
    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        System.out.println(event);
    }
}

发送Event

public static void main(String[] args) {
    ClassPathXmlApplicationContext ap = new ClassPathXmlApplicationContext("applicationContext.xml");
    ap.publishEvent(new User(1,"小明","123456"));
}

Spring MVC

和Struts2不同Spring MVC中Controller是单例的,所以不能定义全局变量,否则会有线程安全问题,如果一定要用全局变量,就要使用ThreadLocal

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>webapp</groupId>
  <artifactId>springmvc</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>war</packaging>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
    <spring-version>5.1.0.RELEASE</spring-version>
  </properties>

    <dependencies>
      <!-- log -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.6.1</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.6.1</version>
        </dependency>

      <!-- Spring -->
      <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
        <version>${spring-version}</version>
      </dependency>

      <!-- CGLib for @Configuration -->
      <dependency>
        <groupId>cglib</groupId>
        <artifactId>cglib-nodep</artifactId>
        <version>3.2.9</version>
        <scope>runtime</scope>
      </dependency>

      <!-- ORM -->
      <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-orm</artifactId>
        <version>${spring-version}</version>
      </dependency>

      <!-- Spring MVC -->
      <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>${spring-version}</version>
      </dependency>

      <!-- json 解析 -->
      <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.9.5</version>
      </dependency>

      <!-- 文件上传 -->
      <dependency>
        <groupId>commons-fileupload</groupId>
        <artifactId>commons-fileupload</artifactId>
        <version>1.3.3</version>
      </dependency>

      <!-- Servlet -->
      <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>servlet-api</artifactId>
        <version>2.5</version>
      </dependency>

      <!-- jsp -->
      <dependency>
        <groupId>javax.servlet.jsp</groupId>
        <artifactId>jsp-api</artifactId>
        <version>2.1</version>
        <scope>provided</scope>
      </dependency>

      <!-- junit -->
      <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>${spring-version}</version>
        <scope>test</scope>
      </dependency>
      <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.11</version>
        <scope>test</scope>
      </dependency>
    </dependencies>

  <build>
    <finalName>springmvctest</finalName>
    <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
      <plugins>
        <plugin>
          <artifactId>maven-clean-plugin</artifactId>
          <version>3.0.0</version>
        </plugin>
        <!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging -->
        <plugin>
          <artifactId>maven-resources-plugin</artifactId>
          <version>3.0.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>3.7.0</version>
        </plugin>
        <plugin>
          <artifactId>maven-surefire-plugin</artifactId>
          <version>2.20.1</version>
        </plugin>
        <plugin>
          <artifactId>maven-war-plugin</artifactId>
          <version>3.2.0</version>
        </plugin>
        <plugin>
          <artifactId>maven-install-plugin</artifactId>
          <version>2.5.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-deploy-plugin</artifactId>
          <version>2.8.2</version>
        </plugin>
      </plugins>
    </pluginManagement>
  </build>
</project>

log4j.properties

log4j.rootLogger=INFO, stdout

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m %n

# General Apache libraries
log4j.logger.org.apache=WARN

# Spring
log4j.logger.org.springframework=WARN

web.xml配置

web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0" metadata-complete="false"><!-- metadata-complete: 是否使用注解配置servlet -->

    <servlet>
        <display-name>DispatcherServlet</display-name>
        <servlet-name>DispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!-- 指定spring mvc配置文件路径,默认为servlet名称-servlet.xml(比如这里的就是DispatcherServlet-servlet.xml) -->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>WEB-INF/springmvc-servlet.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <!-- url-pattern: 配置成'/'即除了jsp文件外其他都拦截,'/*'即拦截所有,也可以设置成*.action、*.do等,表示指定后缀的页面不拦截;此时需要在Spring MVC的配置文件中设置静态资源不拦截 -->
    <servlet-mapping>
        <servlet-name>DispatcherServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

<!-- 解决post乱码(get乱码需要修改tomcat配置文件server.xml的Connector,添加属性useBodyEncodingForURI="true"或URIEncoding="UTF-8"(如果要使用AJAX异步请求,需使用URIEncoding))
配置编码方式过滤器,注意一点:要配置在所有过滤器的前面(filter-mapping在其他的前面即可)
  <filter>
    <filter-name>CharacterEncodingFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <init-param>
        针对request
        <param-name>encoding</param-name>
        <param-value>utf-8</param-value>
    </init-param>
    <init-param>
        针对response
        <param-name>forceEncoding</param-name>
        <param-value>true</param-value>
    </init-param>
  </filter>
  <filter-mapping>
    <filter-name>CharacterEncodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping
-->
    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>
    
    <!-- 加载Spring
Spring和Spring MVC的配置文件可以是同一个,也可以分开。如果是分开的话,要注意service和controller的包要分开扫描,因为Spring是父容器,Spring MVC是子容器,子容器可以访问父容器的内容,而父容器不能访问子容器的内容,如果service和controller统一在service层的Spring扫描,会导致controller无法获取三大组件(HandlerMapping、HandlerAdapter、ViewResolver),页面就会404;用同一个配置文件就相当于只使用Spring MVC的容器,没有使用Spring的容器,也不影响使用
可以使用通配符,Spring的配置文件可以有多个(比如把DAO层的配置和Service层分开,可以在Service层使用include包含另一applicationContext,也可以在这里指定加载多个配置文件),用的还是同一个Spring容器 -->
    <!--
    <listener>
        <listener-class>
            org.springframework.web.context.ContextLoaderListener
        </listener-class>
    </listener>
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>WEB-INF/applicationContext-*.xml</param-value>
    </context-param>
    -->

<!-- session配置(timeout单位为分钟) -->
<session-config>
    <session-timeout>180</session-timeout>
</session-config>

</web-app>

springmvc-servlet.xml配置

springmvc-servlet.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
                           http://www.springframework.org/schema/context
                           http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/mvc
                           http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <context:component-scan base-package="controller"/>
    <context:component-scan base-package="service.impl"/>
    <context:component-scan base-package="dao.repository.impl"/>

    <mvc:annotation-driven />

<!-- spring 3.1.x 以上可以这样解决中文乱码问题(指定返回页面的字符串流的编码)
<mvc:annotation-driven>
    <mvc:message-converters register-defaults="true">
        <bean class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter"/>
        <bean class="org.springframework.http.converter.StringHttpMessageConverter">
            <!-- <property name="supportedMediaTypes" value="text/html;charset=UTF-8"/> -->
            <property name="supportedMediaTypes">
                <list>
                    <value>text/plain;charset=utf-8</value>
                    <value>text/html;charset=utf-8</value>
                </list>
            </property>
        </bean> 
    </mvc:message-converters> 
</mvc:annotation-driven>
-->

    <!-- 视图解析器 -->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <!-- 前缀 -->
        <property name="prefix" value="/WEB-INF/jsp/" />
        <!-- 后缀 -->
        <property name="suffix" value=".jsp" />
    </bean>

<!-- 配置静态资源不拦截(两种方式) -->
<!-- 方法一:
会在Spring MVC上下文中定义一个org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler,对进入DispatcherServlet的URL进行筛查,如果发现是静态资源的请求,就将该请求转由Web应用服务器默认的Servlet处理,如果不是静态资源的请求,才由DispatcherServlet继续处理
    <mvc:default-servlet-handler />
-->

<!--  方法二:
配置DispatcherServlet不拦截静态资源,和mvc:default-servlet-handler不一样的是,mvc:resources允许静态资源放置在任何地方
         location是指webapp目录下的文件夹,也可以使用classpath:指定类路径下的目录
         mapping这里是指/css开头的请求都映射到指定的location中
         ?匹配一个字符
         *匹配零个或多个字符
         **匹配多重路径
-->
    <mvc:resources location="/css/" mapping="/css/**"/>
    <mvc:resources location="/js/" mapping="/js/**"/>
<!-- 可以指定多个location
    <mvc:resources location="/,classpath:/META-INF/publicResources/" mapping="/resources/**"/>
-->
</beans>

request

基本数据类型

直接写即可(参数自动匹配)

@RequestMapping("findbyid")
public String findUserById(int id) {
    System.out.println(id);
    return "success";
}

如果url中参数名字和controller的参数名字不一样,可以使用@RequestParam,默认url中的参数要和controller中参数完全对应,这里也可以通过required=false指定该参数可以省略

@RequestMapping(value = "findbyid", produces = "text/html;charset=UTF-8")//可以指定返回字符串流的编码,比如返回json时可以指定为application/json;charset=utf-8
//findbyid?userid=1
//RequestParam指定url中userid对应这里的id
public String findUserById(@RequestParam("userid", required = false) int id) {
    System.out.println(id);
    return "success";
}

也可以获取header中的参数

@RequestMapping("/headerInfo")  
public void headerInfo(@RequestHeader("Accept-Encoding") String encoding, @RequestHeader("Keep-Alive") long keepAlive)  {  
    System.out.println(encoding);
    System.out.println(keepAlive);
} 

array、List、Map

数组可以直接写(参数自动匹配)

@RequestMapping("findbyids")
public String findUserByIds(int ids[]) {
    for (int id : ids)
        System.out.println(id);
    return "success";
}

list要求VO中有list成员变量

//list: findbyids?ids[0]=1&ids[1]=2&ids[2]=3
//map: findbyids?map[key1]=value&map[key2]=value2
@RequestMapping(value="findbyids", method = RequestMethod.POST)
public String findUserByIds(UserVo userVo) {
    for (int id : userVo.getIds())
        System.out.println(id);
    return "success";
}

UserVo.java

public class UserVo {
    Integer id;
    String name;
    String pwd;
    List<Integer> ids;

    public UserVo() {  }

    public UserVo(Integer id, String name, String pwd) {
        this.id = id;
        this.name = name;
        this.pwd = pwd;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPwd() {
        return pwd;
    }

    public void setPwd(String pwd) {
        this.pwd = pwd;
    }

    public List<Integer> getIds() {
        return ids;
    }

    public void setIds(List<Integer> ids) {
        this.ids = ids;
    }
}

VO

对于url中参数,如果每个标签的name对应了VO类的每个属性时可以自动装箱

//adduser?id=1&username=xiaoming&pwd=123456
@RequestMapping("adduser")
public String addUser(UserVo userVo){
    System.out.println(userVo);
    return "success";
}

此时相当于(当VO中有无参数的构造函数或者是setter方法时,@ModelAttribute可以省略不写)

@RequestMapping("adduser")
public String addUser(@ModelAttribute UserVo userVo){
    System.out.println(userVo);
    return "success";
}

jsp页面

<form action ="<%=request.getContextPath()%>/demo/addUser5" method="post">
     用户名:<input type="text" name="username"/><br/>
     密码:<input type="password" name="password"/><br/>
     <input type="submit" value="提交"/>
</form>

Json

把json转成VO参数以及把VO转成json返回需要导包

<mvc:annotation-driven />以及帮我们配置好,所以不需要配置json解析器,只需导包即可

//此时前端传过来的是json串,会被解析成VO,返回时会把VO转成json返回
@RequestMapping(value="checkuser",method = RequestMethod.POST, produces ="application/json;charset=utf-8")
public @ResponseBody UserVo checkUser(@RequestBody UserVo userVo){
    return userVo;
}

使用自定义的返回值转换器时

<!--配置返回值转换器-->  
<bean id="contentNegotiationManagerFactoryBean"  
      class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean">  
    <property name="favorPathExtension" value="false"/>  
    <property name="favorParameter" value="false"/>  
    <property name="ignoreAcceptHeader" value="false"/>  
    <property name="mediaTypes">  
        <map>  
            <entry key="json" value="application/json"/>  
        </map>  
    </property>  
</bean>

 <mvc:annotation-driven content-negotiation-manager="contentNegotiationManagerFactoryBean"/>  

MutipartFile

文件上传需要导包

在springmvc-servlet.xml中配置文件解析器

<!-- 这里multipartResolver的名字是固定的,不能改 -->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
    <!-- 这里的编码要和jsp页面编码保持一致 -->
    <property name="defaultEncoding" value="UTF-8"/>
    <property name="maxUploadSize" value="5400000"/>
    <property name="maxInMemorySize" value="4096" />
    <property name="resolveLazily" value="true" />
    <property name="uploadTempDir" value="/WEB-INF/upload"/>
</bean>

使用MultipartFile(多文件上传只需改成@RequestParam MultipartFile[] multipartFile即可)

@RequestMapping(value = "upload", method = RequestMethod.POST)
public String upload(MultipartFile multipartFile) {
    if (!multipartFile.isEmpty() && multipartFile.getSize()>0) {
        String fileName = UUID.randomUUID() + String.valueOf(Calendar.getInstance().getTimeInMillis());
        fileName += multipartFile.getOriginalFilename();
        try {
            multipartFile.transferTo(new File(fileName));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    return "success";
}

或者使用原生Servlet API

@RequestMapping(value = "/upload", method = RequestMethod.POST)
public String upload(HttpServletRequest req) throws Exception{
    MultipartHttpServletRequest mreq = (MultipartHttpServletRequest)req;
    MultipartFile file = mreq.getFile("file");
    String fileName = file.getOriginalFilename();
    SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");        
    FileOutputStream fos = new FileOutputStream(req.getSession().getServletContext().getRealPath("/")+
                                                "upload/"+sdf.format(new Date()) + fileName.substring(fileName.lastIndexOf('.')));
    fos.write(file.getBytes());
    fos.flush();
    fos.close();

    return "success";
}

文件下载

@RequestMapping(value="/download")
public ResponseEntity<byte[]> download(HttpServletRequest request, @RequestParam("filename") String filename, Model model)throws Exception {
    //下载文件路径
    String path = request.getServletContext().getRealPath("/images/");
    File file = new File(path + File.separator + filename);
    HttpHeaders headers = new HttpHeaders();  
    //下载显示的文件名,解决中文名称乱码问题  
    String downloadFielName = new String(filename.getBytes("UTF-8"),"iso-8859-1");
    //通知浏览器以attachment(下载方式)打开图片
    headers.setContentDispositionFormData("attachment", downloadFielName); 
    //application/octet-stream : 二进制流数据(最常见的文件下载)。
    headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
    return new ResponseEntity<byte[]>(FileUtils.readFileToByteArray(file), headers, HttpStatus.CREATED);  
}

其他类型(Convertor)

除了以上自动类型转换,我们还可以自定义类型转换器

@Component
public class UserConverter implements Converter<String, UserVo> {

    //将user-xxx-xxx-xxx转换成UserVo
    public UserVo convert(String source) {
        if (source != null && source.startsWith("user")) {
            String[] values = source.split("-");
            if (values != null && values.length == 4) {
                UserVo userVo = new UserVo();
                userVo.setId(Integer.valueOf(values[1]));
                userVo.setName(values[2]);
                userVo.setPwd(values[3]);
                return userVo;
            }
        }
        return null;
    }
}

在springmvc-servlet.xml中配置

<!--配置自定义的解析器,这里的id是固定的,不能改  -->
<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
    <property name="converters">
        <set>
            <ref bean="userConverter"/>
        </set>
    </property>
</bean>

<!-- 注册解析器 -->
<mvc:annotation-driven conversion-service="conversionService"/>

@InitBinder

@InitBinder属性编辑器,可以指定允许/不允许哪些字段作为url参数,或者像Convertor那样对参数进行预处理,它的作用范围是当前Controller

@Controller
@RequestMapping("/user")
class UserController{

    //@InitBinder方法不能有返回值,它必须声明为void
    //@InitBinder方法的参数通常是是 WebDataBinder
    @InitBinder
    public void initUserData(WebDataBinder binder){
        //不允许出现name字段的参数,如果出现了,会把该字段置为空
        binder.setDisallowedFields("name");

        //StringTrimmerEditor是Spring MVC内置的PropertyEditor(如果要自定义,继承PropertyEditorSupport),这里配合@InitBinder使用,把参数中所有的String都先执行trim再传入Controller中对应的方法
        binder.registerCustomEditor(String.class, new StringTrimmerEditor(true));

        //CustomDateEditor也是Spring MVC提供的,把日期先格式化再传入Controller中对应的方法
        binder.registerCustomEditor(Date.class, new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd"), false));
    }
}

如果form中要传入两个不同POJO中的字段,这时如果直接使用两个POJO作为参数是接收不到的(无法自动装箱),这时需要使用@InitBinder

//绑定变量名字和属性,参数封装进类  
@InitBinder("User")  
public void initBinderUser(WebDataBinder binder) {
    binder.setFieldDefaultPrefix("user.");  
}  
//绑定变量名字和属性,参数封装进类  
@InitBinder("Student")  
public void initBinderAddr(WebDataBinder binder) {
    binder.setFieldDefaultPrefix("student.");
}
//对应的Controller方法
@RequestMapping("updateUserAndStudent")
public String updateUserAndStudent(User user, Student student){
    System.out.println(user + "--" + student);
    return "success";  
}

@ControllerAdvice

@InitBinder作用范围是当前Controller,如果想让它作用于全部Controller(使用了@RequestMapping的控制器内的方法),可以在Controller(或者其他任意类)上加@ControllerAdvice

@ControllerAdvice//通常会把对于控制器的全局配置放置在同一个位置
public class GlobalControllerAdvice {
    @InitBinder
    public void initData(WebDataBinder binder) {
        //...
    }
}

如果不使用注解,使用RequestMappingHandlerAdapter也可以达到相同的效果

@Bean
public RequestMappingHandlerAdapter webBindingInitializer() {
    RequestMappingHandlerAdapter adapter = new RequestMappingHandlerAdapter();
    adapter.setWebBindingInitializer(new WebBindingInitializer(){

        @Override
        public void initBinder(WebDataBinder binder, WebRequest request) {
            binder.registerCustomEditor(Date.class, new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd"), false));
        }
    });
    return adapter;
}

除了扩展@InitBinder的作用范围,@ControllerAdvice也可以扩展@ExceptionHandler@ModelAttribute的范围

REST风格URL

//GET: 获取
//可以使用正则,比如这里id只允许数字类型
@RequestMapping(value = "/user/{id:\\d+}/{name}/{pwd}", method = RequestMethod.GET)
public String addUser(@PathVariable int id, @PathVariable String name, @PathVariable String pwd) {
    //userService.addUser(new User(id, name, pwd));
    return "success";
}

//POST: 创建
@RequestMapping(value = "/user/{id}/{name}/{pwd}", method = RequestMethod.POST)
public String insertUser(@PathVariable int id, @PathVariable String name, @PathVariable String pwd) {
    //userService.insertUser(new User(id, name, pwd));
    return "success";
}

//PUT: 更新
@RequestMapping(value = "/user/{id}/{name}/{pwd}", method = RequestMethod.PUT)
public String updateUser(@PathVariable int id, @PathVariable String name, @PathVariable String pwd) {
    //userService.updateUser(new User(id, name, pwd));
    return "success";
}

//DELETE: 删除
@RequestMapping(value = "/user/{id}", method = RequestMethod.DELETE)
public String deleteUser(@PathVariable int id, @PathVariable String name, @PathVariable String pwd) {
    //userService.deleteUserById(id);
    return "success";
}

浏览器form表单只支持GET与POST请求(如果使用AJAX,可以不用配置),而DELETE、PUT等method并不支持,spring3添加了一个过滤器,可以将这些请求转换为标准的http方法,使得支持GET、POST、PUT与DELETE请求

在web.xml中开启PUT和DELETE请求支持(要在DispatcherServlet之前设置才能生效):

<!-- configure the HiddenHttpMethodFilter,convert the post method to put or delete -->
<filter>
    <filter-name>HiddenHttpMethodFilter</filter-name>
    <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>HiddenHttpMethodFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

此时对应的form表单(该Filter只对method为post的表单进行过滤,所以要使用POST)

<form action="..." method="post">
    <input type="hidden" name="_method" value="put" />
        ......
</form>

校验(@Validated

Spring MVC本身没有数据校验的功能,数据校验需要以下第三方jar包

在spring配置文件中配置校验器

<!-- 配置校验器 -->
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
    <!-- 校验器,使用hibernate校验器 -->
    <property name="providerClass" value="org.hibernate.validator.HibernateValidator"/>
    <!-- 指定校验使用的资源文件,在文件中配置校验错误信息,用于国际化,如果不指定则默认使用classpath下面的ValidationMessages.properties文件 -->
    <property name="validationMessageSource" ref="messageSource"/>
</bean>
<!-- 校验错误信息配置文件 -->
<bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
    <!-- 资源文件名 -->
    <property name="basenames">
        <list>
            <value>classpath:CustomValidationMessage</value>
        </list>
    </property>
    <!-- 资源文件编码格式 -->
    <property name="fileEncodings" value="utf-8"/>
    <!-- 对资源文件内容缓存时间,单位秒 -->
    <property name="cacheSeconds" value="120"/>
</bean>

<mvc:annotation-driven conversion-service="conversionService" validator="validator" />

CustomValidationMessage.properties

user.name.notnull=名字不能为空
user.pwd.size=密码长度应为6-20位

在VO中定义规则,同时支持给规则分组(可以使用JSR303中定义的所有验证规则)

public class UserVo {
    Integer id;

    //这里的message可以通过读取properties文件的方式获取,也可以直接写
    @NotBlank(message = "{user.name.notnull}",groups= {ValidateGroup1.class})
    String name;

    @Size(min = 6, max = 20, message = "{user.pwd.size}", groups= {ValidateGroup1.class})
    String pwd;

    //以下省略get、set方法
}

常用的规则有

JSR提供的校验注解:         
@Null   被注释的元素必须为 null    
@NotNull    被注释的元素必须不为 null    
@AssertTrue     被注释的元素必须为 true    
@AssertFalse    被注释的元素必须为 false    
@Min(value)     被注释的元素必须是一个数字,其值必须大于等于指定的最小值    
@Max(value)     被注释的元素必须是一个数字,其值必须小于等于指定的最大值    
@DecimalMin(value)  被注释的元素必须是一个数字,其值必须大于等于指定的最小值    
@DecimalMax(value)  被注释的元素必须是一个数字,其值必须小于等于指定的最大值    
@Size(max=, min=)   被注释的元素的大小必须在指定的范围内    
@Digits (integer, fraction)     被注释的元素必须是一个数字,其值必须在可接受的范围内    
@Past   被注释的元素必须是一个过去的日期    
@Future     被注释的元素必须是一个将来的日期    
@Pattern(regex=,flag=)  被注释的元素必须符合指定的正则表达式    


Hibernate Validator提供的校验注解:  
@NotBlank(message =)   验证字符串非null,且长度必须大于0    
@Email  被注释的元素必须是电子邮箱地址    
@Length(min=,max=)  被注释的字符串的大小必须在指定的范围内    
@NotEmpty   被注释的字符串的必须非空    
@Range(min=,max=,message=)  被注释的元素必须在合适的范围内

在Controller中通过@Validated注解使用规则,且@Validated的参数后面加一个bindingResult,用于获取验证的错误信息(@Validated和bindingResult是成对出现的)

@RequestMapping("/adduser")
public String addUser(@Validated(ValidateGroup1.class) UserVo userVo, BindingResult bindingResult,Model model) {
    System.out.println(userVo);
    if(bindingResult.hasErrors()){
        for (ObjectError error : bindingResult.getAllErrors()){
            System.out.println(error.getDefaultMessage());
        }
        model.addAttribute("errors", bindingResult.getAllErrors());
        return "error";
    }
    return "success";
}

ValidateGroup1是一个空的接口/类,只是用于标识不同分组

public interface ValidateGroup1 {
}

response

redirect和forward

Spring mvc返回的字符串默认是forward,也可以指定为forward或redirect

@RequestMapping("/findbyid")
public String findUserById(@RequestParam("id") int id) {
    System.out.println(id);
    //return "forward:success";
    return "redirect:success";
}

数据回显

void

如果返回void则转发到方法名.jsp

//转发到findUserById.jsp
@RequestMapping("/findbyid")
public void findUserById(int id) {
}
VO
//jsp中使用${id}、${name}、${pwd}获取
@RequestMapping("/findbyid")
public UserVo findUserById(int id) {
    return new UserVo(id, "小明", "123456");
}
Map

使用Map参数

//jsp中使用${requestScope.names }可获取我们设置的names
@RequestMapping("/findbyid")
public String findUserById(Map<String,Object> map) {
    map.put("names", Arrays.asList("aaa","bbb","ccc"));
    return "success";
}
Model

使用Model参数(每个请求都有一个隐含的Model参数)

@RequestMapping("/findbyid")
public String findUserById(int id,Model model) {
    UserVo userVo = new UserVo(0,"小明","123456");
    model.addAttribute(userVo);//jsp中直接使用"${id}"即可获取vo中的属性
    model.addAttribute("user",userVo);//jsp中使用"${user.id}"获取vo中的属性
    return "success";
}
ModelMap

使用ModelMap(也是隐含参数)

@RequestMapping("/findbyid")
public String findUserById(ModelMap modelMap) {
    modelMap.put("id", "1");
    modelMap.addAttribute("name", "aaa");
    return "success";
}
ModelAndView

使用ModelAndView(需要我们new)

@RequestMapping("/findbyid")
public ModelAndView findUserById(int id) {
    UserVo userVo = new UserVo(0,"小明","123456");
    ModelAndView modelAndView = new ModelAndView();
    modelAndView.setViewName("forward:success");//success.jsp
    modelAndView.addObject(userVo);
    return modelAndView;
}
servlet API

也可以使用原生的servlet API(也是隐含参数)

@RequestMapping("/findbyid")
public String findUserById(HttpServletRequest request, HttpServletResponse response, HttpSession session) {
    System.out.println(request.getParameter("id"));
    //request.getRequestDispatcher("index.html").forward(request, response);
    //response.sendRedirect("http://www.baidu.com");
    return "success";
}
PrintWriter

使用PrintWriter时,Spring MVC的跳转不会生效,使用一般用于在JS中使用AJAX异步请求返回JSON数据给页面

@RequestMapping("/TestJSONObject")  
public void  testReturrnJSONWithoutBean(HttpServletRequest request, HttpServletResponse response) throws IOException{
    response.setContentType("application/json");
    response.setCharacterEncoding("UTF-8");//防止出现乱码
    PrintWriter writer = response.getWriter();
    JsonObject jsonObject = new JsonObject();
    jsonObject.addProperty("name","小明");
    writer.println(jsonObject.toString());
    writer.flush();
    writer.close();//用完关不关都可以,最好还是close
}

PrintWriter也可以直接作为参数

public void queryScoreForStudent(PrintWriter printWriter, HttpSession sesion){
    //...
}
@ModelAttribute

@ModelAttribute的作用范围是当前的Controller,也可以使用@InitBinder来让它作用于全部使用了@RequestMapping的Controller内的方法

@ModelAttribute
public void populateModel(@RequestParam(required=false) String abc, Model model) {
    model.addAttribute("attributeName", abc); 
}

//先执行所有带@ModelAttribute注解的方法,再执行@RequestMapping的方法
@RequestMapping(value = "/findbyid")
public String findUserById(int id) {
    return "success";
}

/*
//以上两个方法相当于
@RequestMapping(value = "/findbyid")
public String findUserById(int id, @RequestParam(required=false) String abc, Model model) {
    model.addAttribute("attributeName", abc);
    return "success";
}
*/

/*
@ModelAttribute("m_abc")
public String populateModel(@RequestParam(required=false) String abc, Model model) {
   return abc; 
}

@RequestMapping(value = "/findbyid")
public String findUserById(int id, @ModelAttribute("m_abc") String abc) {
    return "success";
}
*/
//相当于return new Model.addAttribute("id", id)并返回到success页面
@RequestMapping(value = "/findbyid")
public String findUserById(@ModelAttribute int id) {
    return "success";
}

Session、Cookie

使用@SessionAttribute指定从session、cookie中取值

@RequestMapping("findbyid")
public String findUserById(@SessionAttribute int id, @CookieValue(value="name", required=false) String username) {
    System.out.println(id + "--" + username);
    return "success";
}

当类中所有方法都需要同一个session时,可以把@SessionAttribute放在Controller上

@Controller
@RequestMapping("/user")
//@SessionAttributes(names="id")
@SessionAttributes(value={"id"}, types={Integer.class})
class UserController{

    @RequestMapping("findbyid")
    public String findUserById(Model model) {
        //因为前面已经声明过id是session中的值,所以这里是往session中设置id,而非放到request域中
        model.setAttritute("id",1);
        System.out.println(model.getAttritute("id"));
        return "success";
    }
}

session操作也可以使用原生的servlet API

@RequestMapping("/find1")
public String find1(HttpSession session) {
    session.setAttritute("id", 1);
    System.out.println(session.getAttribute("id"));
    return "success";
}

cookie也可以使用原生的servlet API

@RequestMapping("/findbyid")
public String findUserById(HttpServletRequest request, HttpServletResponse response){
    Cookie[] cookies = request.getCookies();
    if (null == cookies) {//如果没有cookie数组
        System.out.println("没有cookie");
    } else {
        for(Cookie cookie : cookies){
            System.out.println("cookieName:"+cookie.getName()+",cookieValue:"+ cookie.getValue());
        }
    }
    
    Cookie cookie = new Cookie("name_test","value_test");//创建新cookie
    cookie.setMaxAge(5 * 60);// 设置存在时间为5分钟
    cookie.setPath("/");//设置作用域
    response.addCookie(cookie);//将cookie添加到response的cookie数组中返回给客户端
}

ExceptionResolver

ExceptionResolver只能有一个,它用于处理全局的异常

@Component//加入容器即可,Spring可以自动识别
class MyExceptionHandler implements HandlerExceptionResolver{

    public ModelAndView resolveException(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) {
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName("error");
        return modelAndView;
    }
}

如果希望只处理当前Controller的异常,可以使用@ExceptionHandler(也可以配合@ControllerAdvice使用,变为处理全局异常)

//可以指定该@ExceptionHandler处理的异常类型
@ExceptionHandler({RuntimeException.class})
public String myExceptionHandle1(Exception exception){
    System.out.println("myExceptionHandle1");
    System.out.println("异常: " + exception);
    return "error";
}

//如果我们自定义异常类继承自RuntimeException,而且同时有两个@ExceptionHandler分别处理RuntimeException和MyRunTimeException,则会调用我们的MyRunTimeException
@ExceptionHandler({MyRuntimeException.class})
public String myExceptionHandle2(Exception exception){
    System.out.println("myExceptionHandle2");
    System.out.println("异常: " + exception);
    return "error";
}

还可以使用SimpleMappingExceptionResolver处理全局异常

<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
    <!-- 定义默认的异常处理页面 -->
    <property name="defaultErrorView" value="error"/>
    <!-- 定义异常处理页面用来获取异常信息的变量名,如果不添加exceptionAttribute属性,则默认为exception -->
    <property name="exceptionAttribute" value="exception"/>
    <!-- 定义需要特殊处理的异常,用类名或完全路径名作为key,异常页面名作为值 -->
    <property name="exceptionMappings">
        <props>
            <prop key="java.lang.RuntimeException">error</prop>
        </props>
    </property>
</bean>

Interceptor

class MyInterceptor implements HandlerInterceptor {

    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
        System.out.println("preHandle");
        return false;
    }

    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
        System.out.println("postHandle");
    }

    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
        System.out.println("afterCompletion");
    }
}

配置

<mvc:interceptors>
    <mvc:interceptor>
        <mvc:mapping path="/**" />
        <mvc:exclude-mapping path="/css/**" />
        <mvc:exclude-mapping path="/js/**" />
        <bean class="controller.interceptors.MyInterceptor" />
    </mvc:interceptor>
</mvc:interceptors>

如果有多个拦截器,则按加入的顺序执行,而且只要前面的拦截器拦截了,后面的拦截器都不会执行

跨域问题

跨域请求

浏览器默认禁止js跨域请求,跨域是指通过js在不同的域之间进行数据传输或通信

这里的跨域是狭义的,是由浏览器同源策略限制的一类请求场景

不同域包括:

同源策略/SOP(Same origin policy)限制了不能访问不同域的Cookie、LocalStorage、IndexDB、DOM、json对象,AJAX请求不能发送

而广义的跨域包括:

这里不讨论广义的跨域

解决方法

解决跨域问题有如下方法

JSONP

JSONP的跨域原理:json不能跨域请求数据(json对象),但可以跨域请求json脚本,得到脚本中会立即执行

我们可以把数据封装成js语句,调用我们定义好的方法,比如

<script>
function dosomething(jsondata){
    //处理返回的数据
}
</script>
<script src="https://mywebsite.com/callback/dosomething">

对应返回的脚本

@RequestMapping("/callback/{callback}")
public String callback(@PathVariable String callback){
    //获取数据
    String data = "'user':{'id':1,'name':'小明'}";
    return callback + "(" + data + ")";
}

//或者使用Spring MVC的MappingJacksonValue
@RequestMapping("/id/{id}/callback/{callback}")
@ResponseBody
public Object findById(@PathVariable id, String callback){
    User user = UserDao.findById(id);
    MappingJacksonValue mappingJacksonValue = new MappingJacksonValue(user);
    mappingJacksonValue.setJsonpFunction(callback);
    retrun mappingJacksonValue;
}

JSONP帮我们把上面的逻辑封装好了:

$.getJSONP('https://mywebsite.com/callback/?',function(jsondata){
    //处理返回的数据
});

//或者
$.ajax({
 url: 'https://mywebsite.com/callback/?', //不指定回调名,可省略callback参数,会由jQuery自动生成
 dataType: 'jsonp',
 jsonpCallback: 'demo', //可省略
 success: function(jsondata) {
   //处理返回的数据
 }
});

JSONP的局限在于它只支持GET请求,而且也没有错误处理机制

window.domain

对于两个iframe分别使用a.mywebsite.comb.mywebsite.com作为src的情况下,两个iframe无法获取对方的属性或对象

<script>
function onLoad(){
    var iframe2 = document.getElementById('iframe2');
    var win2 = iframe2.contentWindow; //可以获取到window对象,但无法访问里面的属性和方法
    var doc = win2.document; //获取不到
    var name = win2.name; //获取不到
}
</script>

对于这种基础域名相同的情况(同时端口、协议也必须相同),可以通过设置document.domain来进行跨域请求

<script>
document.domain = 'mywebsite.com';
</script>

这样即使是两个iframe,只要document.domain相同,都可以通过js获取另一iframe中的各种属性和对象

postMessage

postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一

a.mywebsite.com/a.html

<iframe id="iframe2" src="http://b.mywebsite.com/b.html" style="display:none;"></iframe>
<!-- a页面 -->
<script>       
    var iframe2 = document.getElementById('iframe2');
    iframe2.onload = function() {
        var data = {
            name: 'myname'
        };
        //向b页面传送跨域数据
        iframe2.contentWindow.postMessage(JSON.stringify(data), 'http://b.mywebsite.com');
    };

    //接收b.mywebsite.com返回数据
    window.addEventListener('message', function(msg) {
        alert('data from b ---> ' + msg.data);
    }, false);
</script>

b.mywebsite.com/b.html

<script>
    //接收a.mywebsite.com的数据
    window.addEventListener('message', function(msg) {
        alert('data from a ---> ' + msg.data);

        var data = JSON.parse(msg.data);
        if (data) {
            data.number = 16;

            //处理后返回给a
            window.parent.postMessage(JSON.stringify(data), 'http://a.mywebsite.com');
        }
    }, false);
</script>
CORS

CORS(Cross-origin resource sharing,跨域资源共享)是一个用于解决跨域问题的标准,它通过添加几个HTTP头来允许部分的跨域通信

对于非简单请求(比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json),会在正式通信之前,增加一次HTTP查询请求,称为”预检”请求(preflight),预检的请求类型为OPTIONS

浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段(自动在header增加Origin、Access-Control-Request-Method、Access-Control-Request-Headers等信息用于匹配)。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错

方法一:使用Servlet的Filter自动添加用于支持跨域的header

@Component
public class CORSFilter implements Filter{
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //Access-Control-Allow-Origin只能配置一个url;也可以用*,表示允许所有请求来源;要限定指定url列表可以跨域访问当前站点,可以提供代码动态判断,如果在url列表中,则把对应url加到Access-Control-Allow-Origin中
        response.addHeader("Access-Control-Allow-Origin", "http://mywebsite.com"); //指定授权访问的域(即允许来自http://mywebsite.com的请求跨域访问当前站点)
        response.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE"); //授权请求的方法,多个用逗号分隔
        response.addHeader("Access-Control-Allow-Headers", "Content-Type, X-PINGOTHER"); //允许请求中携带Content-Type和X-PINGOTHER字段,多个用逗号分隔
    response.addHeader("Access-Control-Allow-Credentials", "true"); //是否支持跨域Cookie,为true时Access-Control-Allow-Origin不能为*
        response.addHeader("Access-Control-Max-Age", "1800");//该响应的有效时间为1800秒
        filterChain.doFilter(request, response);
    }
}

在web.xml中配置Filter

<filter>
    <filter-name>cors</filter-name>
    <filter-class>com.myapp.filter.CORSFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>cors</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

方法二:使用Spring MVC的Adapter配置

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
 
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
            .allowedOrigins("http://mywebsite.com")
            .allowedMethods("GET", "POST", "PUT", "DELETE")
            .allowedHeaders("Content-Type", "X-PINGOTHER")
            .exposedHeaders("header1", "header2") //可选,CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma,如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定
            .allowCredentials(false)
        .maxAge(1800);
    }
}

方法三:使用Spring MVC封装的CORS

<mvc:cors>
    <mvc:mapping path="/**" />
</mvc:cors>

<!-- 或者 -->
<mvc:cors>
    <mvc:mapping path="/api/**"
        <!-- 这里支持多个origin -->
        allowed-origins="http://a.mywebsite1.com, http://b.mywebsite2.com"
        allowed-methods="GET, POST, PUT, DELETE"
        allowed-headers="Content-Type, X-PINGOTHER"
        exposed-headers="header1, header2"
        allow-credentials="false"
        max-age="1800" />
 
    <mvc:mapping path="/resources/**"
        allowed-origins="http://mywebsite.com" />
</mvc:cors>

方法四:@CrossOrigin

上面的配置都是全局的,使用@CrossOrigin可以针对单个Controller或里面的方法配置

@CrossOrigin(origins = "http://mywebsite.com", maxAge = 1800)
@Controller
@RequestMapping("/user")
public class UserController {
 
    @RequestMapping(method = RequestMethod.GET, path = "/id/{id}")
    @ResponseBody
    public User findById(@PathVariable Long id) {
        // ...
    }
}

如果使用了Nginx,还需在Nginx中配置跨域支持

server {
    listen 80;
    server_name mywebsite.com;
    root /var/www/;

    location / {
        # 预检请求类型是OPTIONS
        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Allow-Origin' '*';
            add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
            add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
            add_header 'Access-Control-Max-Age' 1800;
            add_header 'Content-Type' 'text/plain charset=UTF-8';
            add_header 'Content-Length' 0;
            return 204;
        }
        if ($request_method = 'POST') {
            add_header 'Access-Control-Allow-Origin' '*';
            add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
            add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
        }
    }
}

js

<script>
//原生AJAX
var url = 'http://mywebsite.com/user/id/1';
var xhr = new XMLHttpRequest();
xhr.withCredentials = false; //不带cookie
xhr.open('GET', url, true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send();
xhr.onreadystatechange = function() {
    if (xhr.readyState == 4 && xhr.status == 200) {
        alert(xhr.responseText);
    }
};

//jQuery
$(document).ready(function() {
    $.ajax({
    url: "http://mywebsite.com/user/id/1",
    method: "GET",
    contentType: "application/json; charset=utf-8",
    xhrFields: {
            withCredentials: false //不带cookie
        },
        crossDomain: true //让请求头中包含跨域的额外信息,但不会含cookie
    }).then(function(data, status, jqxhr) {
    alert(data)
    });
});
</script>
服务器跨域

让服务器来完成跨域资源的请求

通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录

server {
    listen 81;
    server_name  www.domain1.com;

    location / {
        proxy_pass http://loginservers; #反向代理
        proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie domain
        index index.html index.htm;

        # 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
        add_header Access-Control-Allow-Origin http://www.domain1.com; #当前端只跨域不带cookie时,可为*
        add_header Access-Control-Allow-Credentials true;
    }
}

# 使用ip_hash方式的负载均衡
upstream loginservers {
    server www.domain2.com:8080;
    server www.domain3.com:8080;
    ip_hash;
}
附:Nginx常用配置

nginx使用

# 启动
nginx -c /etc/nginx/nginx.conf
# 停止
nginx -s stop -c /etc/nginx/nginx.conf
# 重启
nginx -s reload -c /etc/nginx/nginx.conf

匹配规则

字符 匹配规则
= 精确匹配,不可以使用正则
~ 区分大小写的正则匹配
~* 不区分大小写的正则匹配
!~ !开头表示不匹配,比如!~区分大小写不匹配,!~*不区分大小写不匹配
^~ uri以某个常规字符串开头,不可以使用正则
/ 通用匹配, 如果没有其它匹配,任何请求都会匹配到
空匹配符 不写任何匹配规则则匹配以后面的路径开头的地址

匹配规则是:最大前缀匹配(与顺序无关)

对于正则location的匹配规则是:按编辑顺序逐个匹配(与顺序有关)

文件目录匹配

字符 匹配内容
-f!-f 文件是否存在
-d!-d 目录是否存在
-e!-e 文件或目录是否存在
-x!-x 文件是否存在且可执行

常用在if中,比如

if (!-f $request_filename) {
    return 404
}

常用正则

字符 匹配内容
. 匹配除换行符以外的任意字符
? 重复0次或1次
+ 重复1次或更多次
* 重复0次或更多次
\d 匹配数字
^ 匹配字符串的开始
$ 匹配字符串的结尾
{n} 重复n次
{n,} 重复n次或更多次
[c] 匹配单个字符c
[a-z] 匹配a-z小写字母的任意一个
(xxx) 分组,后面可以通过$1$2…引用分组的内容

内置变量

$args                    #请求中的参数值
$query_string            #同 $args
$arg_NAME                #GET请求中NAME的值
$is_args                 #如果请求中有参数,值为"?",否则为空字符串
$uri                     #请求中的当前URI(不带请求参数,参数位于$args),可以不同于浏览器传递的$request_uri的值,它可以通过内部重定向,或者使用index指令进行修改,$uri不包含主机名,如"/foo/bar.html"。
$document_uri            #同 $uri
$document_root           #当前请求的文档根目录或别名
$host                    #优先级:HTTP请求行的主机名>"HOST"请求头字段>符合请求的服务器名
$hostname                #主机名
$https                   #如果开启了SSL安全模式,值为"on",否则为空字符串。
$binary_remote_addr      #客户端地址的二进制形式,固定长度为4个字节
$body_bytes_sent         #传输给客户端的字节数,响应头不计算在内;这个变量和Apache的mod_log_config模块中的"%B"参数保持兼容
$bytes_sent              #传输给客户端的字节数
$connection              #TCP连接的序列号
$connection_requests     #TCP连接当前的请求数量
$content_length          #"Content-Length" 请求头字段
$content_type            #"Content-Type" 请求头字段
$cookie_name             #cookie名称
$limit_rate              #用于设置响应的速度限制
$msec                    #当前的Unix时间戳
$nginx_version           #nginx版本
$pid                     #工作进程的PID
$pipe                    #如果请求来自管道通信,值为"p",否则为"."
$proxy_protocol_addr     #获取代理访问服务器的客户端地址,如果是直接访问,该值为空字符串
$realpath_root           #当前请求的文档根目录或别名的真实路径,会将所有符号连接转换为真实路径
$remote_addr             #客户端地址
$remote_port             #客户端端口
$remote_user             #用于HTTP基础认证服务的用户名
$request                 #代表客户端的请求地址
$request_body            #客户端的请求主体:此变量可在location中使用,将请求主体通过proxy_pass,fastcgi_pass,uwsgi_pass和scgi_pass传递给下一级的代理服务器
$request_body_file       #将客户端请求主体保存在临时文件中。文件处理结束后,此文件需删除。如果需要之一开启此功能,需要设置client_body_in_file_only。如果将次文件传递给后端的代理服务器,需要禁用request body,即设置proxy_pass_request_body off,fastcgi_pass_request_body off,uwsgi_pass_request_body off,or scgi_pass_request_body off
$request_completion      #如果请求成功,值为"OK",如果请求未完成或者请求不是一个范围请求的最后一部分,则为空
$request_filename        #当前连接请求的文件路径,由root或alias指令与URI请求生成
$request_length          #请求的长度 (包括请求的地址,http请求头和请求主体)
$request_method          #HTTP请求方法,通常为"GET"或"POST"
$request_time            #处理客户端请求使用的时间; 从读取客户端的第一个字节开始计时
$request_uri             #这个变量等于包含一些客户端请求参数的原始URI,它无法修改,请查看$uri更改或重写URI,不包含主机名,例如:"/cnphp/test.php?arg=freemouse"
$scheme                  #请求使用的Web协议,"http" 或 "https"
$server_addr             #服务器端地址,需要注意的是:为了避免访问linux系统内核,应将ip地址提前设置在配置文件中
$server_name             #服务器名
$server_port             #服务器端口
$server_protocol         #服务器的HTTP版本,通常为 "HTTP/1.0" 或 "HTTP/1.1"
$status                  #HTTP响应代码
$time_iso8601            #服务器时间的ISO 8610格式
$time_local              #服务器时间(LOG Format 格式)
$cookie_NAME             #客户端请求Header头中的cookie变量,前缀"$cookie_"加上cookie名称的变量,该变量的值即为cookie名称的值
$http_NAME               #匹配任意请求头字段;变量名中的后半部分NAME可以替换成任意请求头字段,如在配置文件中需要获取http请求头:"Accept-Language",$http_accept_language即可
$http_cookie
$http_post
$http_referer
$http_user_agent
$http_x_forwarded_for
$sent_http_NAME          #可以设置任意http响应头字段;变量名中的后半部分NAME可以替换成任意响应头字段,如需要设置响应头Content-length,$sent_http_content_length即可
$sent_http_cache_control
$sent_http_connection
$sent_http_content_type
$sent_http_keep_alive
$sent_http_last_modified
$sent_http_location
$sent_http_transfer_encoding

配置示例:

server {
    listen 80;
    server_name domain1.com domain2.com # 多个用空格隔开

    sendfile on;
    #charset koi8-r;
    access_log /var/log/nginx/log/static_access.log main;   # 访客记录
    error_log  /var/log/nginx/log/error.log;   # 错误记录

    error_page  500 502 503 504  /50x.html;

    # 图片服务器
    location ~ .*\.(jpg|gif|png)$ {

        # 缓存(添加Expires、Cache-Control、ETag等头)
        # expires 24h;

        # 使用gzip压缩
        gzip on;
        gzip_http_version 1.1;
        gzip_comp_level 2;
        gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;

        # 防盗链
        valid_referers none blocked server_names
                        *.domain1.com
                        *.domain2.com;
        if ($invalid_referer) {
            return 403;
        }

        # 路由重写
        if ($http_user_agent ~ MSIE) {
            # last(执行路由重写时不通过nginx解析,如果需要使用if语句的路由会无法访问)、break(执行路由重写时通过nginx解析)、redirect(302临时重定向)、permanent(301永久重定向,清浏览器缓存可以恢复)
            # header中有MSIE则重写到msie目录
            rewrite ^(.*)$ /msie/$1 break;
        }

        root /var/www/images;
    }

    # 提高文件传输效率(需开启sendfile模块,且只在长连接时有效)
    location ~ ^/download {
        tcp_nopush on;      # 在Linux上使用TCP_CORK套接字选项(数据包不会马上传送出去,等到数据包最大时,一次性的传输出去,这样有助于解决网络堵塞)
        #tcp_nodelay on;    # 功能和tcp_nopush on相反,收到数据包后马上传送出去,不等待;两个选项是互斥的
    }

    # CORS跨域
    location ~ .*\.(htm|html)$ {
        add_header Access-Control-Allow-Origin http://domain1.com;
        add_header Access-Control-Allow-Methods GET,POST,PUT,DELETE,OPTIONS;
        root /var/www/html;
    }

    # 正向代理
    resolver 8.8.8.8;   # 配置DNS解析IP地址(必需)
    resolver_timeout 5s;
    location ^~ /site1/ {
        proxy_pass $scheme://$host$request_uri;
        proxy_set_header Host $http_host;

        proxy_buffers 256 4k;       # 缓存大小
        proxy_max_temp_file_size 0; # 关闭磁盘缓存读写减少I/O
        proxy_connect_timeout 30;   # 连接超时时间
 
        # 配置代理服务器Http状态缓存时间
        proxy_cache_valid 200 302 10m;
        proxy_cache_valid 301 1h;
        proxy_cache_valid any 1m;
    }

    # 反向代理
    upstream backend {
        #ip_hash;    # 负载均衡算法有轮询、加权轮询、ip_hash、least_conn、hash
        hash $request_uri;
        hash_method  crc32;
        server 172.18.103.1:8001 weight=5;  # 带权重
        server 172.18.103.2:8001 max_fails=3 fail_timeout=5s;  # 失败重试3次
        server 172.18.103.3:8001 backup;    # 备用服务器
        server 172.18.103.4:8001 down;      # 模拟服务器下线
    }
    location ^~ /site2/ {
        proxy_pass http://backend;
    }

    # 分片存储
    location ^~ /upload/ {
        slice             1m;
        proxy_cache       cache;
        proxy_cache_key   $uri$is_args$args$slice_range;
        proxy_set_header  Range $slice_range;
        proxy_cache_valid 200 206 1h;
        proxy_pass        http://localhost:8000;
    }
}

使用SSL

server {
    listen 443;
    server_name domain3.com;

    # HTTPS优化:延长keepalive超时时间
    keepalive_timeout 100;

    ssl on;

    # HTTPS优化:使用SSL session缓存
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;

    ssl_certificate xxx.crt;
    ssl_certificate_key xxx.key;

    index index.html index.htm;
    location / {
        root /var/www/html;
    }
}

Mybatis

项目结构

src/main
├── java
│   ├── dao
│   │   ├── pojo
│   │   │   ├── Student.java
│   │   │   └── Teacher.java
│   │   └── repository
│   │       └── StudentMapper.java
│   └── test
│       └── Test.java
└── resources
    ├── jdbc.properties
    ├── mapper
    │   └── StudentMapper.xml
    └── mybatis-config.xml

注意,一般xxxMapper.xml在编译后不会生成到target文件夹,一种办法是把xxxMapper.xml放到resources文件夹(即和xxxMapper.java分开放,配置扫描的时候扫描xml,会根据namespace和id找到对应的java文件),另一种做法是在maven的pom.xml中配置xml路径,然后把xxxMapper.xml和xxxMapper.java放到同一个包下,扫描的时候可以使用包扫描

<build>
    <resources>
        <!-- 默认就是不过滤resources文件夹 -->
        <resource>
            <directory>src/main/resources</directory>
            <includes>
                <include>**/*.properties</include>
                <include>**/*.xml</include>
            </includes>
            <filtering>false</filtering>
        </resource>
        <!-- 配置不过滤xml,**表示匹配多重路径 -->
        <resource>
            <directory>src/main/java</directory>
            <includes>
                <include>**/*.xml</include>
            </includes>
            <filtering>false</filtering>
        </resource>
    </resources>
</build>

如果是用gradle,则是配置

sourceSets.main.resources.srcDirs = ["src/main/java","src/main/resources"]

建表

CREATE TABLE student(
id INT PRIMARY KEY,
name VARCHAR(32),
teacher_id INT,
class_name VARCHAR(32)
)default charset = utf8;

CREATE TABLE teacher(
id INT PRIMARY KEY,
name VARCHAR(32),
class_name VARCHAR(32)
)default charset = utf8;

插入测试数据

INSERT INTO student(id,name,teacher_id,class_name) VALUE(1,'小明',1,'语文');
INSERT INTO student(id,name,teacher_id,class_name) VALUE(2,'小王',1,'语文');
INSERT INTO student(id,name,teacher_id,class_name) VALUE(3,'小红',2,'数学');

INSERT INTO teacher(id,name,class_name) VALUE(1,'张老师','语文');
INSERT INTO teacher(id,name,class_name) VALUE(2,'李老师','数学');
INSERT INTO teacher(id,name,class_name) VALUE(3,'王老师','英语');

全局配置文件

mybatis-config.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!-- 加载jdbc配置文件 -->
    <properties resource="jdbc.properties"/>

    <!-- 打印执行的SQL,调试时开启 -->
    <settings>
        <setting name="logImpl" value="STDOUT_LOGGING"/>
    </settings>

    <!-- 配置别名 -->
    <typeAliases>
        <!-- 一个个配置
        <typeAlias type="dao.pojo.Student" alias="student" />
        -->
        <!-- 整个包扫描 -->
        <package name="dao.pojo"/>
    </typeAliases>

    <environments default="development">
        <environment id="development">
            <!-- 事务管理:
                   JDBC: jdbc事务管理器,对应JdbcTransactionFactory.class
                   MANAGED: 使用第三方的事务管理器,对应ManagedTransactionFactory.class,如果没有配置第三方事务管理器,则增删改查都不会生效
            -->
            <transactionManager type="JDBC"/>
            <!-- 数据源:
                   UNPOOLED: 不适用连接池 即 每次请求都会为其创建一个DB连接,适用完毕后,会马上将连接关闭
                   POOLED: 数据库连接池来维护连接
                   JNDI: 数据源可以定义到应用的外部,通过JDNI容器来获取数据库连接
            -->
            <dataSource type="POOLED">
                <!-- 配置数据库连接信息(通过${}或#{}读取配置文件) -->
                <property name="driver" value="${jdbc.driver}"/>
                <property name="url" value="${jdbc.url}"/>
                <property name="username" value="${jdbc.name}"/>
                <property name="password" value="${jdbc.password}"/>
            </dataSource>
        </environment>
    </environments>

    <!-- 导入mapper -->
    <mappers>
        <!-- 扫描包下所有的xml
        命名规范:要求xml文件名和java文件名一致,一般以Mapper结尾,namespace为对应XXXMapper.java的全类名,CURD的id和XXXMapper.java的方法完全一致
        <package name="dao.repository"/>
        -->
        <!-- 把与类对应的xml加载进来,也需要满足命名规范
        <mapper class="dao.repository.UserMapper"/>
        -->
        <!-- 加载单个,无需满足命名规范
         -->
        <mapper resource="mapper/StudentMapper.xml"/>
    </mappers>
</configuration>

jdbc.properties

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=UTF-8
jdbc.name=root
jdbc.password=root

Mapper.java

StudentMapper.java

public interface StudentMapper {
    //插入
    void insertStudent(Student student);

    //删除
    void deleteStudentById(int id);

    //更新
    void updateStudent(Student student);

    //一对一:根据id查学生
    Student findStudentById(int id);

    //一对多:查询所有学生
    List<Student> findAll();

    //模糊查询
    List<Student> findStudentLikeName(String studentName);

    //一对多:查找所有老师和学生的对应关系
    List<Teacher> findAllTeacherAndStudent();

    //多对一:根据指定的学生id查找老师并放在student的teacher属性中
    List<Student> findTeacherByStudentIds(int[] studentId);
}

Student.java

public class Student {
    private Integer id;
    private String name;

    //每门课对应一个老师
    private Integer teacherId;
    private String className;
    private Teacher teacher;

    //以下省略get、set和无参构造器
}

Teacher.java

public class Teacher {
    private Integer id;
    private String name;

    //一个老师教一门课
    private String className;
    //一个老师可以同时教多个学生
    private List<Student> students;

    //以下省略get、set和无参构造器
}

mapper.xml

StudentMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="dao.repository.StudentMapper">

    <!-- 这里使用了全类名,由于设置了typeAliases,也可以使用student代替dao.pojo.Student -->
    <!-- 这里使用OGNL表达式获取POJO的属性,比如成员变量为基本数据类型:#{teacherId},成员变量为POJO类型:#{teacher.id} -->
    <insert id="insertStudent" parameterType="dao.pojo.Student">
        INSERT INTO student(id,name,teacher_id,class_name) VALUE(#{id}, #{name}, #{teacherId}, #{className})
    </insert>

    <!-- 对于基本数据类型,#{}中可以使用任何变量名来指代参数,一般用value;而${}中只能用value -->
    <delete id="deleteStudentById" parameterType="int">
        DELETE FROM student WHERE id=#{value}
    </delete>

    <!-- 可以使用if、where、foreach等标签动态生成sql -->
    <update id="updateStudent" parameterType="student">
        <if test="id!=null and id!=''">
            UPDATE student set name=#{name},teacher_id=#{teacherId},class_name=#{className} WHERE id=#{id}
        </if>
    </update>

    <!-- 返回值会自动封装到POJO中对应的属性,如果列不对应,需要给列起别名 -->
    <select id="findStudentById" parameterType="int" resultType="student">
        SELECT id,name,teacher_id AS teacherId,class_name AS className FROM student WHERE id=#{value}
    </select>

    <!-- 别名可以封装成sql片段,然后通过include引入 -->
    <sql id="student_info">
        id,name,teacher_id AS teacherId,class_name AS className
    </sql>

    <!-- 返回list和返回POJO的resultType是一样的,只是接口处的定义不同 -->
    <select id="findAll" resultType="student">
        SELECT
        <include refid="student_info"/>
        FROM student
    </select>

    <!-- ${}有注入问题,但#{}两边又不能加任何非空字符,这时就要用bind,_parameter是内置的变量,指传过来的的参数(类似的内置变量还有_databaseId,指通过全局配置文件中配置的databaseIdProvider) -->
    <select id="findStudentLikeName" parameterType="String" resultType="student">
        <bind name="m_name" value="'%' + _parameter + '%'"/>
        SELECT
        <include refid="student_info"/>
        FROM student WHERE name LIKE #{m_name}
    </select>


    <!-- collection表示映射到一个集合属性的成员变量中,这里的ofType是指集合的泛型,而非集合的类型 -->
    <resultMap id="teachermap" type="teacher">
        <id property="id" column="id"/>
        <result property="name" column="name"/>
        <result property="className" column="class_name"/>
        <collection property="students" ofType="student">
            <id property="id" column="s_id"/>
            <result property="name" column="s_name"/>
            <result property="teacherId" column="s_tid"/>
            <result property="className" column="s_classname"/>
        </collection>
    </resultMap>

    <select id="findAllTeacherAndStudent" resultMap="teachermap">
        SELECT t.id,t.name,t.class_name,s.id as s_id,s.name as s_name,s.teacher_id as s_tid,s.class_name as s_classname FROM teacher t LEFT JOIN student s ON t.id=s.teacher_id
    </select>


    <!-- 根据teacherid查找teacher -->
    <select id="findTeacherById" parameterType="int" resultType="teacher">
        SELECT id,name,class_name AS className FROM teacher WHERE id=#{value}
    </select>

    <!-- 这里的association指映射到属性为POJO类型的成员变量中,这里的javaType就是POJO的类型 -->
    <!-- 通过association的select根据指定column执行子查询(如果不在当前命名空间中,需通过命名可见.id调用),然后把子查询的结果放到POJO的teacher属性中 -->
    <resultMap id="studentMap" type="student">
        <id property="id" column="id"/>
        <result property="name" column="name"/>
        <result property="teacherId" column="teacher_id"/>
        <result property="className" column="class_name"/>
        <association property="teacher" javaType="teacher" select="findTeacherById" column="teacher_id"/>
    </resultMap>

    <!-- SELECT id,name,teacher_id,class_name from student where id in (1,2,4,6) -->
    <select id="findTeacherByStudentIds" parameterType="int" resultMap="studentMap">
        SELECT * FROM student WHERE id IN
        <!-- collection的类型可以是array、list、map,,如果入参是一个集合,那么collection可以              等于任意名字,如果User有属性List ids,入参是User对象,那么这个collection = "ids"。如果User有属性Ids ids;其中Ids是个对象,Ids有个属性List id;入参是User对象,那么collection = "ids.id"
可以通过index属性获取下标(如果是map,则获取的是key),用法和item属性类似 -->
        <foreach collection="array" open="(" separator="," close=")" item="id">
            #{id}
        </foreach>
    </select>
</mapper>

使用

InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = factory.openSession();

//编写DAO时,通过session的方法调用xml中写好的sql,第一个参数为xml的命名空间+sql的id
//Student student = session.selectOne("dao.repository.StudentMapper.findStudentById", 1);

//也可以直接通过接口生成代理对象
StudentMapper studentMapper = session.getMapper(StudentMapper.class);

studentMapper.insertStudent(new Student(4, "小花", 1, "语文"));
session.commit();//Mybatis默认关闭了自动提交,需要手动提交

studentMapper.deleteStudentById(4);
session.commit();

studentMapper.updateStudent(new Student(3, "小红", 3, "英语"));
session.commit();

System.out.println(studentMapper.findStudentById(1));
System.out.println("------------------");

for (Student student : studentMapper.findAll()) {
    System.out.println(student);
}
System.out.println("------------------");

for (Student student : studentMapper.findStudentLikeName("小")) {
    System.out.println(student);
}
System.out.println("------------------");

for (Teacher teacher :studentMapper.findAllTeacherAndStudent()){
    System.out.println(teacher);
    for(Student student : teacher.getStudents()){
        System.out.println("\t" + student);
    }
}
System.out.println("------------------");

for (Student student : studentMapper.findTeacherByStudentIds(new int[]{1,2,4,6})){
    System.out.println(student);
    System.out.println("\t" + student.getTeacher());
}

session.close();

其他

where

where和if配合使用可以自动删去不需要的AND

<delete id="deleteStudent" parameterType="student">
    DELETE FROM student
    <where>
        <if test="_parameter.id!=null and _parameter.id!=''">
            AND id=#{id}
        </if>

        <if test="_parameter.name!=null and _parameter.name!=''">
            AND name=#{name}
        </if>

        <if test="_parameter.teacherId!=null and _parameter.teacherId!=''">
            AND teacher_id=#{teacherId}
        </if>

        <if test="_parameter.className!=null and _parameter.className!=''">
            AND class_name=#{className}
        </if>
    </where>
</delete>

使用

Student student = new Student();
student.setName("小明");
student.setClassName("英语");
session.delete("dao.repository.StudentMapper.deleteStudent",student);

生成的SQL

==>  Preparing: DELETE FROM student WHERE name=? AND class_name=? 
==> Parameters: 小明(String), 英语(String)
<==    Updates: 0

selectKey

有时在插入的时候希望获取插入数据的主键,又或者使用SQL的函数生成主键,这时可以使用selectKey

主键返回

<insert id="insertStudent" parameterType="dao.pojo.Student">
    <!-- 使用selectKey把主键返回,此时要求主键是 auto increment的 -->
    <selectKey keyProperty="id" order="AFTER" resultType="int">
        SELECT LAST_INSERT_ID()
    </selectKey>
    INSERT INTO student(name,teacher_id,class_name) VALUE(#{name}, #{teacherId}, #{className});
</insert>

UUID

<insert id="insertStudent" parameterType="dao.pojo.Student">
    <!-- 使用selectKey通过SQL中的uuid函数生成主键,并放到student.id中(此时student.id应为String类型) -->
    <selectKey keyProperty="id" order="BEFORE" resultType="String">
        SELECT uuid()
    </selectKey>
    INSERT INTO student(id,name,teacher_id,class_name) VALUE(#{id}, #{name}, #{teacherId}, #{className});
</insert>

懒加载

子查询可以使用懒加载

在全局配置文件中开启

<!-- 开启懒加载配置 -->
<settings>
    <!-- 全局性设置懒加载。如果设为‘false',则所有相关联的都会被初始化加载。 -->
    <setting name="lazyLoadingEnabled" value="true"/>
    <!-- 当设置为‘true'的时候,懒加载的对象可能被任何懒属性全部加载。否则,每个属性都按需加载。 -->
    <setting name="aggressiveLazyLoading" value="false"/>
</settings>

resultMapassociationcollection中使用select执行子查询(就是上面多对一的例子,代码没有变)时就可以实现懒加载

<!-- 根据teacherid查找teacher -->
<select id="findTeacherById" parameterType="int" resultType="teacher">
    SELECT id,name,class_name AS className FROM teacher WHERE id=#{value}
</select>

<!-- 通过association的select根据指定column执行子查询,然后把子查询的结果放到POJO的teacher属性中 -->
<resultMap id="studentMap" type="student">
    <id property="id" column="id"/>
    <result property="name" column="name"/>
    <result property="teacherId" column="teacher_id"/>
    <result property="className" column="class_name"/>
    <association property="teacher" javaType="teacher" select="findTeacherById" column="teacher_id"/>
</resultMap>

<!-- SELECT id,name,teacher_id,class_name from student where id in (1,2,4,6) -->
<select id="findTeacherByStudentIds" parameterType="int" resultMap="studentMap">
    SELECT * FROM student WHERE id IN
    <foreach collection="array" open="(" separator="," close=")" item="id">
        #{id}
    </foreach>
</select>

缓存

一级缓存默认是开启的,一级缓存就是每个sqlSession的缓存,使用同一个sqlSession多次查询,第一次会查询数据库,以后如果没有执行过任何修改数据库的操作(DML操作: insert、update、delete),就会从缓存中取,不会再查询数据库

二级缓存是sqlSession之间共享的缓存,一个namespace共用一个缓存,二级缓存默认没有开启,需要手动开启

全局配置文件:

<settings>
    <!-- 默认是false的,表示关闭二级缓存 -->
    <setting name="cacheEnabled" value="true"/>
<settings>

Mapper.xml

<!-- 为当前Mapper开启二级缓存:
使用LRU缓存,并每隔60秒刷新,最大存储512个对象,而却返回的对象是只读的 -->
<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>

如果单个语句需要禁用缓存,使用useCache="false"

<insert id="insertStudent" parameterType="dao.pojo.Student" useCache="false">
    INSERT INTO student(id,name,teacher_id,class_name) VALUE(#{id}, #{name}, #{teacherId}, #{className});
</insert>

一级和二级缓存同时开启时,先从一级缓存中找,如果没有再从二级缓存中找

禁用预编译

使用statementType="STATEMENT"禁用预编译,此时只能使用${},不能再使用#{}statementType默认为PREPARED,即使用预编译)

<insert id="insertStudent" parameterType="dao.pojo.Student" statementType="STATEMENT">
    INSERT INTO student(id,name,teacher_id,class_name) VALUE(${id}, ${name}, ${teacherId}, ${className});
</insert>

注解开发

不需要在为mapper.java写mapper.xml了,这时mapper.xml可以去掉;如果和spring整合,全局配置文件也可以去掉

public interface StudentMapper {

    //插入
    @Insert("INSERT INTO student(id,name,teacher_id,class_name) VALUE(#{id}, #{name}, #{teacherId}, #{className})")
    void insertStudent(Student student);

    //删除
    @Delete("DELETE FROM student WHERE id=#{value}")
    void deleteStudentById(int id);

    //更新
    @Update("UPDATE student set name=#{name},teacher_id=#{teacherId},class_name=#{className} WHERE id=#{id}")
    void updateStudent(Student student);

    //一对一:根据id查学生
    @Select(" SELECT * FROM student WHERE id=#{value}")
    @Results(id = "studentmap", value = {
            @Result(property = "id", column = "id"),
            @Result(property = "name", column = "name"),
            @Result(property = "teacherId", column = "teacher_id"),
            @Result(property = "className", column = "class_name")
    })
    Student findStudentById(int id);

    //一对多:查询所有学生
    @Select("SELECT id,name,teacher_id AS teacherId,class_name AS className FROM student")
    List<Student> findAll();

    //模糊查询,这里要使用SQL的拼接函数进行拼接
    @Select("SELECT * FROM student WHERE name LIKE CONCAT(CONCAT('%', #{_parameter} ),'%')")
    @ResultMap("studentmap")
    List<Student> findStudentLikeName(String studentName);


    //一对多:查找所有老师和学生的对应关系
    //many相当于xml中的collection,fetchType指定是否懒加载
    @Select("SELECT id,name,class_name FROM teacher")
    @Results({
            @Result(property = "id", column = "id", id = true),
            @Result(property = "name", column = "name"),
            @Result(property = "className", column = "class_name"),
            @Result(property = "students", column = "id", many = @Many(select = "dao.repository.StudentMapper.findStudentByTeacherId", fetchType = FetchType.LAZY))
    })
    List<Teacher> findAllTeacherAndStudent();

    //根据老师id找学生
    @Select("SELECT id,name,teacher_id AS teacherId,class_name AS className FROM student WHERE teacher_id=#{value}")
    Student findStudentByTeacherId(int teacherId);


    //多对一:根据指定的学生id查找老师并放在student的teacher属性中
    //one相当于xml中的association
    @Select("<script>"
            + "SELECT * FROM student WHERE id IN" +
            "<foreach collection='array' open='(' separator=',' close=')' item='id'>" +
            "#{id}" +
            "</foreach>"
            + "</script>")
    @Results(value = {
            @Result(property = "id", column = "id"),
            @Result(property = "name", column = "name"),
            @Result(property = "teacherId", column = "teacher_id"),
            @Result(property = "className", column = "class_name"),
            @Result(property = "teacher", column = "teacher_id", one = @One(select = "dao.repository.StudentMapper.findTeacherById", fetchType = FetchType.EAGER))
    })
    List<Student> findTeacherByStudentIds(int[] studentId);

    @Select("SELECT id,name,class_name AS className FROM teacher WHERE id=#{value}")
    Teacher findTeacherById(int teacherId);
}

这时全局配置文件通过class属性加载mapper

<mappers>
    <mapper class="dao.repository.StudentMapper"/>
</mappers>

用法和以前是一样的

InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = factory.openSession();

StudentMapper studentMapper = session.getMapper(StudentMapper.class);

studentMapper.insertStudent(new Student(4, "小花", 1, "语文"));
session.commit();
session.close();

Spring整合

maven依赖

<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.4.6</version>
</dependency>
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>1.3.2</version>
</dependency>

配置applicationContext.xml:

使用Mybatis全局配置文件

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <!-- 数据库连接池 -->
    <property name="dataSource" ref="dataSource" />
    <!-- 加载mybatis的全局配置文件 -->
    <property name="configLocation" value="classpath:mybatis/mybatis-config.xml" />
</bean>

去掉Mybatis全局配置文件

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <!-- dataSource -->
    <property name="dataSource" ref="datasource" />
    <!-- 别名,单个包用value属性,多个注入array数组 -->
    <property name="typeAliasesPackage" value="dao.pojo" />
    <!-- XXXMapper.xml -->
    <!-- 如果xxxMapper.java和xxxMapper.xml在同一包下可以不配置,但此时需要配置maven不过滤xml文件
    <property name="mapperLocations" value="classpath*:dao/mappers/*Mapper.xml" /> -->
</bean>

<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <!--指定会话工厂,如果当前上下文中只定义了一个则该属性可省去 -->
    <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
    <!-- XXXMapper.java -->
    <property name="basePackage" value="dao.mappers" />
    <!-- <property name="mapperInterface" value="dao.mappers.UserMapper" /> -->
</bean>

<!-- 不使用扫描的方式
<bean id="userMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">
    <property name="mapperInterface" value="dao.mappers.UserMapper" />
    <property name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>
-->

Spring Data JPA

文档

项目结构

├── main
│   ├── java
│   │   └── com
│   │       └── myapp
│   │           ├── config
│   │           │   └── MySQL8InnoDBUTF8Dialect.java
│   │           ├── controller
│   │           ├── dao
│   │           │   ├── model
│   │           │   │   └── User.java
│   │           │   └── UserDao.java
│   │           └── service
│   │               └── UserService.java
│   └── resources
│       └── applicationContext.xml
└── test
    └── java
        └── UserTest.java

初始化数据库

建表

CREATE TABLE User(
id INT PRIMARY KEY NOT NULL AUTO_INCREMENT,
name VARCHAR(32) NOT NULL,
role VARCHAR(6)
)DEFAULT CHARSET=utf8;

插入测试数据

INSERT INTO User VALUES
(1, '小明', 'normal'),
(2, '小红', 'normal'),
(3, '小王', 'admin');

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>myapp</groupId>
    <artifactId>springdatajpatest</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <!-- spring-data-jpa -->
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-jpa</artifactId>
            <version>2.1.1.RELEASE</version>
        </dependency>

        <!-- 配置hibernate的实体管理依赖-->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
            <version>5.3.7.Final</version>
        </dependency>
        <!-- 在JAVA9以上版本使用hibernate需要导入jaxb依赖 -->
        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
            <version>2.3.1</version>
        </dependency>
        <!-- jdbc连接驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.13</version>
        </dependency>
        <!-- junit -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

快速入门

实体类

@Entity
@Table(name = "User")//可以指定表名,默认表名就是实体类的类名
public class User {
    //需要使用Integer而不是int
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;

    //除了id以外,所有的column都会按照字母顺序创建,而不是按定义的顺序
    @Column(length = 32, nullable = false)
    private String name;

    @Column(name = "role", length = 6)//指定列名,默认为字段名
    private String role;
    //省略空构造器及get、set、toString(注意toString不要输出集合类型,否则在双向包含的情况下或出现递归)
}

DAO接口

接口编写有两种方式,要么用@RepositoryDefinition注解,要么用继承自Repository,使用这两种方式编写的接口Spring data JPA才会动态代理生成实现类的字节码

//@RepositoryDefinition(domainClass = User.class ,idClass = Integer.class)
//两个泛型分别是要操作的实体Bean和主键类型
public interface UserDao extends Repository<User, Integer> {

    User findByName(String name);

    List<User> findByRoleLike(String role);
}

Service层

Spring Data JPA会自动生成接口的实现类,我们只需直接调用即可

@Service
public class UserService {
    @Autowired
    UserDao userDao;

    public User getUserByName(String name) {
        return userDao.findByName(name);
    }

    public List<User> getUserByRoleLike(String role){
        return userDao.findByRoleLike(role);
    }
}

applicationContext.xml

<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:jpa="http://www.springframework.org/schema/data/jpa"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/data/jpa
           http://www.springframework.org/schema/data/jpa/spring-jpa.xsd
           http://www.springframework.org/schema/tx
           http://www.springframework.org/schema/tx/spring-tx.xsd
           http://www.springframework.org/schema/context
           http://www.springframework.org/schema/context/spring-context.xsd">

    <!--配置数据源-->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="url" value="jdbc:mysql:///webapp?useUnicode=true&amp;characterEncoding=UTF-8"/>
        <property name="username" value="root"/>
        <property name="password" value="123456"/>
        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
    </bean>
    <!--配置entityManagerFactory 用于管理实体的一些配置-->
    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"/>
        </property>
        <!-- dao包,如果写实体类所在的包,会报错 -->
        <property name="packagesToScan" value="com.myapp.dao"/>
        <property name="jpaProperties">
            <props>
                <prop key="hibernate.ejb.naming_strategy">org.hibernate.cfg.ImprovedNamingStrategy</prop>
                <!-- 指定方言为MySQL
                <prop key="hibernate.dialect">org.hibernate.dialect.MySQL8Dialect</prop> -->
                <!-- 由于我们要使用UTF8编码,所以要使用自定义的Dialect -->
                <prop key="hibernate.dialect">com.myapp.config.MySQL8InnoDBUTF8Dialect</prop>
                <!-- 显示生成的SQL -->
                <prop key="hibernate.show_sql">true</prop>
                <prop key="hibernate.format_sql">true</prop>
                <!--配置是否实体自动生成数据表-->
                <prop key="hibernate.hbm2ddl.auto">update</prop>
            </props>
        </property>
    </bean>
    <!--配置事务管理器-->
    <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="entityManagerFactory"/>
    </bean>
    <!--配置支持事务注解-->
    <tx:annotation-driven transaction-manager="transactionManager"/>
    <!--配置spring data-->
    <jpa:repositories base-package="com.myapp.dao" entity-manager-factory-ref="entityManagerFactory"/>
    <!--配置spring的扫描包-->
    <context:component-scan base-package="com.myapp"/>
</beans>

自定义方言类MySQL8InnoDBUTF8Dialect.java

public class MySQL8InnoDBUTF8Dialect extends MySQL8Dialect {
    //使用InnoDB引擎以及UTF8编码
    @Override
    public String getTableTypeString() {
        return " ENGINE=InnoDB DEFAULT CHARSET=utf8";
    }
}

单元测试

public class UserTest {
    private ApplicationContext context = null;
    private UserService userService;

    @Before
    public void getContext() {
        context = new ClassPathXmlApplicationContext("applicationContext.xml");
        userService = context.getBean(UserService.class);
    }

    @Test
    public void testUser() {
        System.out.println(userService.getUserByName("小明"));
        //这里仍然要写百分号,否则无法模糊查询
        System.out.println(userService.getUserByRoleLike("%nor%"));
    }
}

DAO接口函数命名规范

DAO接口的函数是有命名规范的,Spring Data JPA按照这些命名规则生成对应的SQL,执行我们想要执行的操作

函数命名关键字 示例 对应的 where条件
And findByNameAndPwd where name= ? and pwd =?
Or findByNameOrSex where name= ? or sex=?
Is,Equals findById,findByIdEquals where id= ?
Between findByIdBetween where id between ? and ?
LessThan findByIdLessThan where id < ?
LessThanEquals findByIdLessThanEquals where id <= ?
GreaterThan findByIdGreaterThan where id > ?
GreaterThanEquals findByIdGreaterThanEquals where id > = ?
After findByIdAfter where id > ?
Before findByIdBefore where id < ?
IsNull findByNameIsNull where name is null
isNotNull,NotNull findByNameNotNull where name is not null
Like findByNameLike where name like ?
NotLike findByNameNotLike where name not like ?
StartingWith findByNameStartingWith where name like ‘?%’
EndingWith findByNameEndingWith where name like ‘%?’
Containing findByNameContaining where name like ‘%?%’
OrderBy findByIdOrderByXDesc where id=? order by x desc
Not findByNameNot where name <> ?
In findByIdIn(Collection<?> c) where id in (?)
NotIn findByIdNotIn(Collection<?> c) where id not in (?)
True findByAaaTue where aaa = true
False findByAaaFalse where aaa = false
IgnoreCase findByNameIgnoreCase where UPPER(name)=UPPER(?)

自定义SQL

上面那么多的命名规范很难记住,所以Spring Data JPA允许我们不按照规范命名,而是我们自己指定要执行的SQL,注意默认@Query写的是JPQL(使用实体类的类名代替表名,实体类的属性名代替列名,和HQL差不多)而不是SQL,不过可以通过nativeQuery=true指定执行SQL

//"?1"表示函数的第一个参数,"?2"表示函数的第二个参数,以此类推
@Query("select u from User u where u.name like %?1%")
List<User> findUserLike1(String name);

//支持hibernate中给参数起名字的语法
@Query("select u from User u where u.name like %:name%")
List<User> findUserLike2(@Param("name") String name);

//使用原生的SQL语句进行操作(注意select这时用的是表的列名而不是实体的属性名,from这时用的是数据库的表名而不是实体类名)
@Query(nativeQuery = true, value = "select count(*) from User")
Long userCount();

增删改

对数据库进行修改的操作要使用@Modifying注解,对于update或者delete操作(insert最好也添加事务),还必须在使用该方法的地方(一般是service层)使用事务

DAO层

@Modifying
@Query("update User set name = ?1 where id = ?2")
void updateUserById(String name, Integer id);

service层

@Transactional
public void updateUserById(String name, Integer id) {
    userDao.updateUserById(name, id);
}

其他Repository接口

CrudRepository

CrudRepository接口继承自Repository,并提前为我们写好了增删改查等方法,动态代理会自动生成这些方法的实现

public interface UserDao extends CrudRepository<User, Integer> {  }

CrudRepository的方法

@NoRepositoryBean
public interface CrudRepository<T, ID> extends Repository<T, ID> {
    <S extends T> S save(S var1);
    <S extends T> Iterable<S> saveAll(Iterable<S> var1);
    Optional<T> findById(ID var1);
    boolean existsById(ID var1);
    Iterable<T> findAll();
    Iterable<T> findAllById(Iterable<ID> var1);
    long count();
    void deleteById(ID var1);
    void delete(T var1);
    void deleteAll(Iterable<? extends T> var1);
    void deleteAll();
}

PagingAndSortingRepository

PagingAndSortingRepository继承自CrudRepository,并且实现了分页相关的功能

@NoRepositoryBean
public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {
    Iterable<T> findAll(Sort var1);
    Page<T> findAll(Pageable var1);
}

DAO

public interface UserDao extends PagingAndSortingRepository<User, Integer> {  }

用法

//相当于MySQL中"limit 0, 5"
//PageRequest pageRequest = PageRequest.of(0, 5);

//使用排序
Sort.Order sortOrder = new Sort.Order(Sort.Direction.DESC, "id");
Sort sort = Sort.by(sortOrder);
PageRequest pageRequest = PageRequest.of(0, 5, sort);

Page page = userDao.findAll(pageRequest);

System.out.println("查询的总页数:" + page.getTotalPages());
System.out.println("查询的总数据条数:" + page.getTotalElements());
System.out.println("查询的当前页数:" + (page.getNumber() + 1));
System.out.println("查询的数据的内容:" + page.getContent());
System.out.println("查询的当前页的数据条数:" + page.getNumberOfElements());

JpaRepository

JpaRepository继承自PagingAndSortingRepository,并拓展了一些功能

@NoRepositoryBean
public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
    List<T> findAll();
    List<T> findAll(Sort var1);
    List<T> findAllById(Iterable<ID> var1);
    <S extends T> List<S> saveAll(Iterable<S> var1);
    void flush();
    <S extends T> S saveAndFlush(S var1);
    void deleteInBatch(Iterable<T> var1);
    void deleteAllInBatch();
    T getOne(ID var1);
    <S extends T> List<S> findAll(Example<S> var1);
    <S extends T> List<S> findAll(Example<S> var1, Sort var2);
}

JpaSpecificationExecutor

通过创建方法名来做查询,只能做简单的查询,如果我们要做复杂一些的查询,就要使用JpaSpecificationExecutor(它不属于Repository)

public interface JpaSpecificationExecutor<T> {
    Optional<T> findOne(@Nullable Specification<T> var1);
    List<T> findAll(@Nullable Specification<T> var1);
    Page<T> findAll(@Nullable Specification<T> var1, Pageable var2);
    List<T> findAll(@Nullable Specification<T> var1, Sort var2);
    long count(@Nullable Specification<T> var1);
}

DAO

public interface UserDao extends CrudRepository<User, Integer>, JpaSpecificationExecutor<User> {  }

用法

//分页的过滤条件
Specification specification = new Specification<User>() {
    public Predicate toPredicate(Root<User> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
        Path<Integer> idPath = root.get("id");
        Path<String> namePath = root.get("name");
        Path<String> rolePath = root.get("role");
        //设置过滤条件为id小于等于10
        Predicate idPredicate = criteriaBuilder.le(idPath, 10);
        //名字包含"小"
        Predicate namePredicate = criteriaBuilder.like(namePath, "%小%");
        //权限为admin
        Predicate rolePredicate = criteriaBuilder.equal(rolePath, "admin");

        //多个条件组合
        Predicate predicate1 = criteriaBuilder.and(idPredicate, namePredicate);
        Predicate predicate2 = criteriaBuilder.or(predicate1, rolePredicate);

        /*join查询
        //User表和Admin表进行join查询
        Join<User, Admin> join = root.join("role", JoinType.LEFT);
        //Admin表中的属性
        Path adminNamePath = join.get("name");
        Predicate adminNamePredicate = criteriaBuilder.like(adminNamePath, "%管理员%");
        Predicate predicate3 = criteriaBuilder.or(predicate2,adminNamePredicate);
        */
        return predicate2;
    }
};
//使用排序
Sort.Order sortOrder = new Sort.Order(Sort.Direction.ASC, "id");
Sort sort = Sort.by(sortOrder);
PageRequest pageRequest = PageRequest.of(0, 5, sort);

//使用JpaSpecificationExecutor的Page<T> findAll(Specification<T>, Pageable)执行复杂查询
Page<User> page = userDao.findAll(specification, pageRequest);
System.out.println("查询的当前页数:" + (page.getNumber() + 1));
System.out.println("查询的数据的内容:" + page.getContent());

最后生成的SQL是:

select
    user0_.id as id1_0_,
    user0_.name as name2_0_,
    user0_.role as role3_0_ 
from
    User user0_ 
where
    user0_.id<=10 
    and (
    user0_.name like ?
    ) 
    or user0_.role=? 
order by
    user0_.id asc limit ?

使用join

User中有Role的外键时(一个User对应一个Role的情况,一对一)

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;
    private String name;

    @OneToOne
    @JoinColumn(name = "role_id",referencedColumnName = "id")
    private Role role;
}

@Entity
public class Role {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;
    private String roleName;

    @OneToOne(mappedBy = "role")
    private User user;
}

此时的join查询

//根据指定条件的Role找对应的User
//SELECT * FROM User u LEFT OUTER JOIN Role r ON u.role_id=r.id where r.id=1 or r.roleName like %normal%
List userList = userDao.findAll(new Specification<User>() {
    public Predicate toPredicate(Root<User> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
        //root是User,根据User实体的role属性left join
        // LEFT OUTER JOIN Role r ON u.role_id=r.id
        Join<User, Role> join = root.join("role", JoinType.LEFT);
        //join为Role实体
        // r.id=1
        Predicate predicate3 = criteriaBuilder.equal(join.get("id"), 1);
        // r.roleName like %normal%
        Predicate predicate4 = criteriaBuilder.like(join.<String>get("roleName"), "%normal%");
        // or
        return criteriaBuilder.or(predicate3, predicate4);
    }
});

Role中有User的外键时(一个User对应多个Role的情况,一对多、多对一)

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;
    private String name;

    @OneToMany(mappedBy = "user")
    private List<Role> roles = new ArrayList<>(0);
}

@Entity
public class Role {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;
    private String roleName;

    @ManyToOne
    @JoinColumn(name = "user_id", referencedColumnName = "id")
    private User user;
}

此时的join查询

//根据指定的Role找对应的User
//SELECT * FROM User u LEFT OUTER JOIN Role r ON u.id=r.user_id where u.id<10
List userList = userDao.findAll(new Specification<User>() {
    public Predicate toPredicate(Root<User> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
        //list的情况
        //ListJoin<User, Role> join = root.join(root.getModel().getList("roles", Role.class), JoinType.LEFT);
        //等价于
        ListJoin<User, Role> join = root.joinList("roles", JoinType.LEFT);
        
        //return criteriaBuilder.lt(root.<Integer>get("id"), 10);
        //也可以直接执行,然后返回null,两者是等价的
        query.where(criteriaBuilder.lt(root.<Integer>get("id"), 10));
        return null;
    }
});

如果不需要where,那直接返回null就可以了

//SELECT * FROM User u LEFT OUTER JOIN Role r ON u.id=r.user_id
List userList = userDao.findAll(new Specification<User>() {
    public Predicate toPredicate(Root<User> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
        root.joinList("roles", JoinType.LEFT);
        //多表join:可以链式调用,第一次joinList后,返回的join是Role,第二次joinList就是根据Role的other_column属性(外键字段)再join其他的表
        //root.joinList("roles", JoinType.LEFT).joinList("other_column", JoinType.INNER);
        return null;
    }
});

扩展Repository方法

JpaSpecificationExecutor一般写在DAO接口的实现类中,前面的例子中都是只写接口,没有实现类,如果要在Spring Data JPA提供的Repository的基础上扩展方法,实现类需要实现我们的自定义接口,并且按照指定的命名方式(指定后缀)放在和接口同一个包下

在applicationContext.xml中通过repository-impl-postfix指定实现类的后缀

<jpa:repositories base-package="com.myapp.dao" repository-impl-postfix="Impl" entity-manager-factory-ref="entityManagerFactory" transaction-manager-ref="transactionManager" />

DAO接口同时继承Repository和我们自定义的接口,实现类只需继承自定义的接口,实现类一般使用@PersistenceContext注解获取原生的EntityManager对数据库进行操作

//DAO接口
public interface PersonDao extends JpaRepository<Person,Integer>,MyInterface {  }

//这是我们自定义的接口
public interface MyInterface{
    void myMethod();
}

//实现类需要放在和接口同一个包(或子包)下
public class PersonDaoImpl implements MyInterface{
    @PersistenceContext
    private EntityManager em;

    public void myMethod() {
        //使用原生的EntityManager实现QBC(Query By Criteria)查询
        CriteriaBuilder builder = em.getCriteriaBuilder();
        CriteriaQuery<Person> query = builder.createQuery(Person.class);

        Root<Person> root = query.from(Person.class);
        query.where(builder.like(root.<String>get("name"), "%小%"));

        List<Person> list = em.createQuery(query.select(root)).getResultList();
        System.out.println(list);

        /*
        //使用原生的EntityManager实现OID查询,其他操作都是基于OID的
        EntityTransaction transaction = em.getTransaction();
        transaction.begin();
        try {
            Person p1 = new Person();
            p1.setName("小明");
            //插入(增)
            em.persist(p1);
            //根据OID查询(查)
            Person p2 = em.find(Person.class, 1L);//查出来的对象是持久态
            //Person p2 = em.getReference(Person.class,1L);//find是立即加载,要使用延迟加载,用getReference
            //更新(改)
            p2.setName("小红");//set持久态对象的属性后,在commit的时候会和快照对比,如果不一致则自动更新
            em.merge(p2);//也可以调用merge,手动更新;merge的实际作用是,在Hibernate中,当我们查询出一个对象后,session关闭了,对象从持久态转为游离态,这时如果我们更改了游离态对象的属性,然后我们再另开一个session,再根据该对象OID查询出对象(持久态),这时就会导致内存中有两个相同OID对象的缓存,但快照中只有一份,这时如果要保存游离态对象,会报错,这时merge的作用就是把游离态对象的属性同步到刚刚查询出来的持久态对象中,把两份缓存合并成一份,这时保存持久态对象才能保存成功;这里由于Spring Data JPA中没有session的概念(一个线程只有一个session,session由容器自动创建,不需要手动管理),所以merge的作用就是单单保存
            /*
            //要在JPA中使用session也是可以的
            Session session = em.upwrap(Session.class);
            session.doWork(new Work(){
                public void execute(Connection conn) throws SQLException{
                    System.out.println(conn.getClass().getName());
                }
            });
            */
            //删除(删)
            em.remove(p1);
            transaction.commit();
        } catch (Exception e) {
            transaction.rollback();
        }
        */

        /*
        //使用原生的EntityManager实现JPQL查询,JPQL和HQL的不同在于JPQL在查询的时候要写"select xxx",而且xxx不能用星号(*),而应该使用实体名或它的别名
        Query query = em.createQuery("select p from Perosn p where p.id > :p_id", Person.class);
        //支持聚合函数
        //Query query = em.createQuery("select count(*) from Perosn");
        //支持order by(默认asc(升序),可以指定desc(倒序))
        //Query query = em.createQuery("select p from Perosn p order by p.id desc", Person.class);
        //支持HQL的投影查询语法(用于查询部分数据,此时Person类中要有对应的构造器;也可以直接指定要查询的字段来替代这种写法)
        //Query query = em.createQuery("select new Person(p.id, p.name) from Perosn p", Person.class);

        //如果没有使用别名而是直接使用?占位符,那么setParameter的下标从1开始(setParameter(1, xxx))
        query.setParameter("p_id", 10);
        //分页 limit 5,5(因为不是所有数据库都支持直接分页,所以要使用对应方法设置分页),JPQL和QBC的分页设置方式一样
        query.setFirstResult(5);
        query.setMaxResults(5);
        for (Person p : query.getResultList())
            System.out.println(p);
        */
        
        /*
        //支持原生SQL
        Query query = entityManager.createNativeQuery("select * from Person p where p.id=?");
        //index从1开始,指定SQL中的id为10
        query.setParameter(1, 10);
        List list = query.getResultList();
        */
    }
}

数据关系

一对一

直接包含

假设一个person对应一个address

person.java

@Entity
public class Person {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Integer id;

    @Column(name = "name")
    private String name;

    @OneToOne(cascade = CascadeType.ALL)//使用级联,比如当删除person时,会同时删除address中对应的记录
    @JoinColumn(name = "address_id", referencedColumnName = "id")//people中的address_id字段参考address表中的id字段。即使用Address表的id作为外键字段,字段名为address_id
    private Address address;
    
    //省略空构造函数和get、set、toString,下面同理
    //...
}

Address.java

@Entity
public class Address {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Integer id;

    @Column(name = "country")
    private String country;

    @Column(name = "city")
    private String city;

    @Column(name = "street")
    private String street;
    
    //如果不需要根据Address级联查询People,可以注释掉
    //optional=false,表示person不能为空。删除address,不影响person
    //@OneToOne(mappedBy = "address", cascade = {CascadeType.MERGE, CascadeType.REFRESH}, optional = false)
    //private Person person;

    //...
}

生成的sql(无论Address中是否有person属性都是一样的)

Hibernate: 
    
    create table Address (
        id integer not null auto_increment,
        city varchar(255),
        country varchar(255),
        street varchar(255),
        primary key (id)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8
Hibernate: 
    
    create table Person (
        id integer not null auto_increment,
        name varchar(255),
        address_id integer,
        primary key (id)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8
Hibernate: 
    
    alter table Person 
       add constraint FK6i7nduc8blbwp1dbfwavvnvvx 
       foreign key (address_id) 
       references Address (id)
使用中间表

如果使用中间表保存两者的关系,Person改为如下,Address不变

@Entity
public class Person {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Integer id;

    @Column(name = "name")
    private String name;

    @OneToOne(cascade = CascadeType.ALL)//使用级联,比如当删除person时,会同时删除address中对应的记录
    //中间表的表名为person_address
    @JoinTable(name = "person_address",
            //当前实体在中间表的外键字段名是people_id,它的reference是当前实体对应表的id列,如果不指定referencedColumnName,默认为主键
            joinColumns = @JoinColumn(name="people_id", referencedColumnName="id"),
            inverseJoinColumns = @JoinColumn(name = "address_id"))
    private Address address;
    
    //...
}

此时生成的SQL

Hibernate: 
    
    create table Address (
       id integer not null auto_increment,
        city varchar(255),
        country varchar(255),
        street varchar(255),
        primary key (id)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8
Hibernate: 
    
    create table Person (
       id integer not null auto_increment,
        name varchar(255),
        primary key (id)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8
Hibernate: 
    
    create table person_address (
       address_id integer,
        people_id integer not null,
        primary key (people_id)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8
Hibernate: 
    
    alter table person_address 
       add constraint FK5gaighxlfytjmaiy4xrma2sev 
       foreign key (address_id) 
       references Address (id)
Hibernate: 
    
    alter table person_address 
       add constraint FKpbmn6d7ivwvid8r4leifxd5cn 
       foreign key (people_id) 
       references Person (id)
参数说明

mappedBy

mappedBy: 指定关系维护端,值为另一方对应的属性名,如果关系是单向的就不需要。由于JoinTableJoinColumn一般定义在拥有关系(在one-to-many、many-to-one中为many端)的这一端,而Hibernate又不让mappedByJoinTableJoinColumn定义在一起,所以mappedBy一定是定义在关系的被拥有方(one)。关系维护端拥有建立、解除和更新与另一方关系的能力,而另一方没有,只能被动管理,而且关系维护端可以直接删除,而另一方要先在关系维护端删除关联(把外键置为NULL),才能被删除;在双向一对多和双向多对多中同理

比如没有配置mappedBy

Person.java中:

@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "address_id", referencedColumnName = "id")
private Address address;

Address.java中:

@OneToOne//不配置mappedBy(mappedBy = "address")
private Person person;

此时生成的建表语句会在两个表中都有外键,即两个外键

Hibernate: 
    
    create table Address (
       id integer not null auto_increment,
        city varchar(255),
        country varchar(255),
        street varchar(255),
        person_id integer,
        primary key (id)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8
Hibernate: 
    
    create table Person (
       id integer not null auto_increment,
        name varchar(255),
        address_id integer,
        primary key (id)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8
Hibernate: 
    
    alter table Address 
       add constraint FKdu13rl17o4h24m9gt7b2bdobo 
       foreign key (person_id) 
       references Person (id)
Hibernate: 
    
    alter table Person 
       add constraint FK6i7nduc8blbwp1dbfwavvnvvx 
       foreign key (address_id) 
       references Address (id)

虽然两边都有外键也能用,但是一次修改要更新两次,会影响性能,而且实际上只需一个外键就能描述清楚两者的关系,所以我们更希望删掉其中一方的外键,只留下一个外键

配置mappedBy

Address.java中:

@OneToOne(mappedBy = "address")
private Person person;

此时生成的建表语句只在关系维护方的表中有外键

Hibernate: 
    
    create table Address (
       id integer not null auto_increment,
        city varchar(255),
        country varchar(255),
        street varchar(255),
        primary key (id)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8
Hibernate: 
    
    create table Person (
       id integer not null auto_increment,
        name varchar(255),
        address_id integer,
        primary key (id)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8
Hibernate: 
    
    alter table Person 
       add constraint FK6i7nduc8blbwp1dbfwavvnvvx 
       foreign key (address_id) 
       references Address (id)

cascade

cascade: 级联,CascadeType.ALL只能写在关系的维护端(one)(否则比如拆迁时,address没了,结果把person也删了),如果实体中含有另一实体的引用,当当前实体更改时是否要同步更改另一实体,可以指定具体哪些更改操作需要同步更改,可选值有

关于实体状态转换:新建的对象为瞬态(或临时态),保存后或从数据库中查询出来为持久态。区分这两个状态的依据是实体是否有OID,OID就是表的主键值,只有当拥有OID时才是持久态(手动设置无效,因为主键是由框架维护的,我们不应该手动设置;即使我们手动设置了OID,也不是持久态)

在Hibernate中,除了上面两个状态,还有游离态,即对象拥有OID,但是由于session关闭,实体的缓存和快照(在Hibernate中,所有查询出来的对象都会被缓存到实体缓存和快照两个区域,当实体缓存做过更改,在session关闭时会判断实体缓存和快照是否一致,如果不一致,则执行更新操作,这就是为什么Hibernate可以在我们set持久态的实体属性时能够更新数据库的原因)都被清空了,即使拥有OID,也无法知道是否为持久态

比如没有配置级联

Person.java中:

@OneToOne//不配置级联(cascade = CascadeType.ALL)
@JoinColumn(name = "address_id", referencedColumnName = "id")
private Address address;

Address.java中:

@OneToOne(mappedBy = "address")
private Person person;

那么如果在插入时没有保存address

Person person = new Person();//新建的对象为瞬态
person.setName("小明");

Address address = new Address();//瞬态
address.setCountry("China");

person.setAddress(address);

//没有保存address
//addressDao.save(address);
personDao.save(person);//保存后变为持久态

会报错:save the transient instance before flushing,因为person的外键指向了一个瞬态对象

除非我们手动把address保存一下

Person person = new Person();
person.setName("小明");

Address address = new Address();
address.setCountry("China");

person.setAddress(address);

//要注意保存的顺序,当前外键存在于Person表中,所以要先存address,后存person,如果顺序颠倒,会报错:save the transient instance before flushing
addressDao.save(address);
personDao.save(person);

当我们配置了级联

Person.java中:

@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "address_id", referencedColumnName = "id")
private Address address;

此时即使不保存address,也能保存成功

Person person = new Person();
person.setName("小明");

Address address = new Address();
address.setCountry("China");

person.setAddress(address);

//配置了级联,不需要手动保存address
//addressDao.save(address);
personDao.save(person);

optional

optional: 为false时表明该参数不是可选的,即必须填入,不能为空

一对多和多对一

一对多多对一 一般是同时使用的,即双向包含

假设我们的person突然发财了,买了两套房子,这时一个person对应多个address(一对多),多个address可能对应同一个person(多对一)。当然,如果不需要双向包含的话,可以把任意一方的对应属性删掉,变为单向关系

简单地说,就是如果你所需的是一个POJO,那么就是ManyToOne,如果所需的是一个List或Set,那么就是OneToMany

首先要明确的是,外键应该设置在多(many)的一方,而不是一(one)的一方,那么既然确定了@JoinColumn,那么另一方就要使用mappedBy

Person.java

@Entity
public class Person {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Integer id;

    @Column(name = "name")
    private String name;

    //一般person决定买房和卖房,所以mappedBy应该是person
    //可能address有很多,一次性加载会占很多内存,使用使用懒加载
    @LazyCollection(LazyCollectionOption.EXTRA)
    @OneToMany(mappedBy = "person", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    //因为ArrayList有默认初始容量10,如果直接new,会导致浪费内存,需要设置初始容量为0
    private List<Address> address  = new ArrayList<>(0);

    //set或者list一般不在toString中输出
    @Override
    public String toString() {
        return "Person{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }

    //省略空构造器及get、set
    //...
}

Address.java

@Entity
public class Address {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Integer id;

    @Column(name = "country")
    private String country;

    @Column(name = "city")
    private String city;

    @Column(name = "street")
    private String street;

    @ManyToOne(cascade = {CascadeType.MERGE, CascadeType.REFRESH}, optional = false)
    //设置在address表中的关联字段(外键)
    @JoinColumn(name = "person_id")
    private Person person;

    @Override
    public String toString() {
        return "Address{" +
                "id=" + id +
                ", country='" + country + '\'' +
                ", city='" + city + '\'' +
                ", street='" + street + '\'' +
                ", person=" + person +
                '}';
    }

    //...
}

生成的SQL

Hibernate: 
    
    create table Address (
       id integer not null auto_increment,
        city varchar(255),
        country varchar(255),
        street varchar(255),
        person_id integer not null,
        primary key (id)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8
Hibernate: 
    
    create table Person (
       id integer not null auto_increment,
        name varchar(255),
        primary key (id)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8
Hibernate: 
    
    alter table Address 
       add constraint FKdu13rl17o4h24m9gt7b2bdobo 
       foreign key (person_id) 
       references Person (id)

多对多

多对多就一定要使用中间表实现了

假设我们的person是租的房子,和别一起合租,这时多个person可以对应一个address,而且一个address可以对应多个person,这就构成了多对多

多对多关系中一般不设置级联保存、级联删除、级联更新等操作(因为中间可能涉及到很多表,配置会很复杂)

Person.java

@Entity
public class Person {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Integer id;

    @Column(name = "name")
    private String name;

    @ManyToMany
    @JoinTable(name = "person_address",
            joinColumns = @JoinColumn(name = "person_id"),
            inverseJoinColumns = @JoinColumn(name = "address_id"))
    private List<Address> addressList = new ArrayList<>(0);

    //...
}

Address.java

@Entity
public class Address {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Integer id;

    @Column(name = "country")
    private String country;

    @Column(name = "city")
    private String city;

    @Column(name = "street")
    private String street;

    @ManyToMany(mappedBy = "addressList")
    private List<Person> personList = new ArrayList<>(0);

    //...
}

生成的SQL

Hibernate: 
    
    create table Address (
       id integer not null auto_increment,
        city varchar(255),
        country varchar(255),
        street varchar(255),
        primary key (id)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8
Hibernate: 
    
    create table Person (
       id integer not null auto_increment,
        name varchar(255),
        primary key (id)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8
Hibernate: 
    
    create table person_address (
       person_id integer not null,
        address_id integer not null
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8
Hibernate: 
    
    alter table person_address 
       add constraint FK5gaighxlfytjmaiy4xrma2sev 
       foreign key (address_id) 
       references Address (id)
Hibernate: 
    
    alter table person_address 
       add constraint FK83s2b7rjpq74x0sebtccprse1 
       foreign key (person_id) 
       references Person (id)

Redis

中文官网下载地址

命令参考

Redis是一个内存数据结构存储系统,可用作数据库、缓存和消息中间件

Redis是单线程的,但它因为避免了频繁的加锁解锁以及处理复杂的并发问题,所以它的效率也很高

应用场景:

  1. 数据库缓存(配合Spring Cache注解使用)
  2. 分布式session;任务队列(list数据类型实现FIFO队列,代码中定时获取任务队列信息)
  3. 秒杀倒计时(过期时间=当前时间+抢购时间,通过TTL命令获得剩余时间)
  4. 秒杀(decr命令,每次-1,如果得到的不是负数,则抢到商品,创建订单)
  5. 分布式自增长id(incr命令,不过一般使用MyCat全局序列号或者snowflake等技术生成)
  6. 解决用户连续快速点击导致的重复提交问题(创建一个过期时间比较短的key,该key格式为默认前缀:用户的token:方法名:参数,通过AOP对有没有该key进行检测,有就拦截)

安装

进入解压目录,执行

make
cd src
make install PREFIX=/opt/redis

缺少什么就根据提示下载即可

配置

配置文件详解

#redis.conf
# Redis configuration file example.
# ./redis-server /path/to/redis.conf

################################## INCLUDES ###################################
#这在你有标准配置模板但是每个redis服务器又需要个性设置的时候很有用。
# include /path/to/local.conf
# include /path/to/other.conf

################################ GENERAL #####################################

#是否在后台执行,yes:后台运行;no:不是后台运行(老版本默认)
daemonize yes

  #3.2里的参数,是否开启保护模式,默认开启。要是配置里没有指定bind和密码。开启该参数后,redis只会本地进行访问,拒绝外部访问。要是开启了密码   和bind,可以开启。否   则最好关闭,设置为no。
protected-mode yes
#redis的进程文件
pidfile /var/run/redis/redis-server.pid

#redis监听的端口号。
port 6379

#此参数确定了TCP连接中已完成队列(完成三次握手之后)的长度, 当然此值必须不大于Linux系统定义的/proc/sys/net/core/somaxconn值,默认是511,而Linux的默认参数值是128。当系统并发量大并且客户端速度缓慢的时候,可以将这二个参数一起参考设定。该内核参数默认值一般是128,对于负载很大的服务程序来说大大的不够。一般会将它修改为2048或者更大。在/etc/sysctl.conf中添加:net.core.somaxconn = 2048,然后在终端中执行sysctl -p。
tcp-backlog 511

#指定 redis 只接收来自于该 IP 地址的请求,如果不进行设置,那么将处理所有请求
bind 127.0.0.1

#配置unix socket来让redis支持监听本地连接。
# unixsocket /var/run/redis/redis.sock
#配置unix socket使用文件的权限
# unixsocketperm 700

# 此参数为设置客户端空闲超过timeout,服务端会断开连接,为0则服务端不会主动断开连接,不能小于0。
timeout 0

#tcp keepalive参数。如果设置不为0,就使用配置tcp的SO_KEEPALIVE值,使用keepalive有两个好处:检测挂掉的对端。降低中间设备出问题而导致网络看似连接却已经与对端端口的问题。在Linux内核中,设置了keepalive,redis会定时给对端发送ack。检测到对端关闭需要两倍的设置值。
tcp-keepalive 0

#指定了服务端日志的级别。级别包括:debug(很多信息,方便开发、测试),verbose(许多有用的信息,但是没有debug级别信息多),notice(适当的日志级别,适合生产环境),warn(只有非常重要的信息)
loglevel notice

#指定了记录日志的文件。空字符串的话,日志会打印到标准输出设备。后台运行的redis标准输出是/dev/null。
logfile /var/log/redis/redis-server.log

#是否打开记录syslog功能
# syslog-enabled no

#syslog的标识符。
# syslog-ident redis

#日志的来源、设备
# syslog-facility local0

#数据库的数量,默认使用的数据库是DB 0。可以通过”SELECT “命令选择一个db
databases 16

################################ SNAPSHOTTING ################################
# 快照配置
# 注释掉“save”这一行配置项就可以让保存数据库功能失效
# 设置sedis进行数据库镜像的频率。
# 900秒(15分钟)内至少1个key值改变(则进行数据库保存--持久化) 
# 300秒(5分钟)内至少10个key值改变(则进行数据库保存--持久化) 
# 60秒(1分钟)内至少10000个key值改变(则进行数据库保存--持久化)
save 900 1
save 300 10
save 60 10000

#当RDB持久化出现错误后,是否依然进行继续进行工作,yes:不能进行工作,no:可以继续进行工作,可以通过info中的rdb_last_bgsave_status了解RDB持久化是否有错误
stop-writes-on-bgsave-error yes

#使用压缩rdb文件,rdb文件压缩使用LZF压缩算法,yes:压缩,但是需要一些cpu的消耗。no:不压缩,需要更多的磁盘空间
rdbcompression yes

#是否校验rdb文件。从rdb格式的第五个版本开始,在rdb文件的末尾会带上CRC64的校验和。这跟有利于文件的容错性,但是在保存rdb文件的时候,会有大概10%的性能损耗,所以如果你追求高性能,可以关闭该配置。
rdbchecksum yes

#rdb文件的名称
dbfilename dump.rdb

#数据目录,数据库的写入会在这个目录。rdb、aof文件也会写在这个目录
dir /var/lib/redis

################################# REPLICATION #################################
#复制选项,slave复制对应的master。
# slaveof <masterip> <masterport>

#如果master设置了requirepass,那么slave要连上master,需要有master的密码才行。masterauth就是用来配置master的密码,这样可以在连上master后进行认证。
# masterauth <master-password>

#当从库同主机失去连接或者复制正在进行,从机库有两种运行方式:1) 如果slave-serve-stale-data设置为yes(默认设置),从库会继续响应客户端的请求。2) 如果slave-serve-stale-data设置为no,除去INFO和SLAVOF命令之外的任何请求都会返回一个错误”SYNC with master in progress”。
slave-serve-stale-data yes

#作为从服务器,默认情况下是只读的(yes),可以修改成NO,用于写(不建议)。
slave-read-only yes

#是否使用socket方式复制数据。目前redis复制提供两种方式,disk和socket。如果新的slave连上来或者重连的slave无法部分同步,就会执行全量同步,master会生成rdb文件。有2种方式:disk方式是master创建一个新的进程把rdb文件保存到磁盘,再把磁盘上的rdb文件传递给slave。socket是master创建一个新的进程,直接把rdb文件以socket的方式发给slave。disk方式的时候,当一个rdb保存的过程中,多个slave都能共享这个rdb文件。socket的方式就的一个个slave顺序复制。在磁盘速度缓慢,网速快的情况下推荐用socket方式。
repl-diskless-sync no

#diskless复制的延迟时间,防止设置为0。一旦复制开始,节点不会再接收新slave的复制请求直到下一个rdb传输。所以最好等待一段时间,等更多的slave连上来。
repl-diskless-sync-delay 5

#slave根据指定的时间间隔向服务器发送ping请求。时间间隔可以通过 repl_ping_slave_period 来设置,默认10秒。
# repl-ping-slave-period 10

#复制连接超时时间。master和slave都有超时时间的设置。master检测到slave上次发送的时间超过repl-timeout,即认为slave离线,清除该slave信息。slave检测到上次和master交互的时间超过repl-timeout,则认为master离线。需要注意的是repl-timeout需要设置一个比repl-ping-slave-period更大的值,不然会经常检测到超时。
# repl-timeout 60

#是否禁止复制tcp链接的tcp nodelay参数,可传递yes或者no。默认是no,即使用tcp nodelay。如果master设置了yes来禁止tcp nodelay设置,在把数据复制给slave的时候,会减少包的数量和更小的网络带宽。但是这也可能带来数据的延迟。默认我们推荐更小的延迟,但是在数据量传输很大的场景下,建议选择yes。
repl-disable-tcp-nodelay no

#复制缓冲区大小,这是一个环形复制缓冲区,用来保存最新复制的命令。这样在slave离线的时候,不需要完全复制master的数据,如果可以执行部分同步,只需要把缓冲区的部分数据复制给slave,就能恢复正常复制状态。缓冲区的大小越大,slave离线的时间可以更长,复制缓冲区只有在有slave连接的时候才分配内存。没有slave的一段时间,内存会被释放出来,默认1m。
# repl-backlog-size 5mb

#master没有slave一段时间会释放复制缓冲区的内存,repl-backlog-ttl用来设置该时间长度。单位为秒。
# repl-backlog-ttl 3600

#当master不可用,Sentinel会根据slave的优先级选举一个master。最低的优先级的slave,当选master。而配置成0,永远不会被选举。
slave-priority 100

#redis提供了可以让master停止写入的方式,如果配置了min-slaves-to-write,健康的slave的个数小于N,mater就禁止写入。master最少得有多少个健康的slave存活才能执行写命令。这个配置虽然不能保证N个slave都一定能接收到master的写操作,但是能避免没有足够健康的slave的时候,master不能写入来避免数据丢失。设置为0是关闭该功能。
# min-slaves-to-write 3

#延迟小于min-slaves-max-lag秒的slave才认为是健康的slave。
# min-slaves-max-lag 10

# 设置1或另一个设置为0禁用这个特性。
# Setting one or the other to 0 disables the feature.
# By default min-slaves-to-write is set to 0 (feature disabled) and
# min-slaves-max-lag is set to 10.

################################## SECURITY ###################################
#requirepass配置可以让用户使用AUTH命令来认证密码,才能使用其他命令。这让redis可以使用在不受信任的网络中。为了保持向后的兼容性,可以注释该命令,因为大部分用户也不需要认证。使用requirepass的时候需要注意,因为redis太快了,每秒可以认证15w次密码,简单的密码很容易被攻破,所以最好使用一个更复杂的密码。
# requirepass foobared

#把危险的命令给修改成其他名称。比如CONFIG命令可以重命名为一个很难被猜到的命令,这样用户不能使用,而内部工具还能接着使用。
# rename-command CONFIG b840fc02d524045429941cc15f59e41cb7be6c52

#设置成一个空的值,可以禁止一个命令
# rename-command CONFIG ""
################################### LIMITS ####################################

# 设置能连上redis的最大客户端连接数量。默认是10000个客户端连接。由于redis不区分连接是客户端连接还是内部打开文件或者和slave连接等,所以maxclients最小建议设置到32。如果超过了maxclients,redis会给新的连接发送’max number of clients reached’,并关闭连接。
# maxclients 10000

#redis配置的最大内存容量。当内存满了,需要配合maxmemory-policy策略进行处理。注意slave的输出缓冲区是不计算在maxmemory内的。所以为了防止主机内存使用完,建议设置的maxmemory需要更小一些。
# maxmemory <bytes>

#内存容量超过maxmemory后的过期策略
#volatile-lru:利用LRU算法移除设置过过期时间的key。
#volatile-random:随机移除设置过过期时间的key。
#volatile-ttl:移除即将过期的key,根据最近过期时间来删除(辅以TTL)
#内存容量超过maxmemory后的淘汰策略
#allkeys-lru:利用LRU算法移除任何key。
#allkeys-random:随机移除任何key。
#noeviction:不移除任何key,只是返回一个写错误。
#上面的这些驱逐策略,如果redis没有合适的key驱逐,对于写命令,还是会返回错误。redis将不再接收写请求,只接收get请求。写命令包括:set setnx setex append incr decr rpush lpush rpushx lpushx linsert lset rpoplpush sadd sinter sinterstore sunion sunionstore sdiff sdiffstore zadd zincrby zunionstore zinterstore hset hsetnx hmset hincrby incrby decrby getset mset msetnx exec sort。
# maxmemory-policy noeviction

#lru检测的样本数。使用lru或者ttl淘汰算法,从需要淘汰的列表中随机选择sample个key,选出闲置时间最长的key移除。
# maxmemory-samples 5

############################## APPEND ONLY MODE ###############################
#默认redis使用的是rdb方式持久化,这种方式在许多应用中已经足够用了。但是redis如果中途宕机,会导致可能有几分钟的数据丢失,根据save来策略进行持久化,Append Only File是另一种持久化方式,可以提供更好的持久化特性。Redis会把每次写入的数据在接收后都写入 appendonly.aof 文件,每次启动时Redis都会先把这个文件的数据读入内存里,先忽略RDB文件。
appendonly no

#aof文件名
appendfilename "appendonly.aof"

#aof持久化策略的配置
#no表示不执行fsync,由操作系统保证数据同步到磁盘,速度最快。
#always表示每次写入都执行fsync,以保证数据同步到磁盘。
#everysec表示每秒执行一次fsync,可能会导致丢失这1s数据。
appendfsync everysec

# 在aof重写或者写入rdb文件的时候,会执行大量IO,此时对于everysec和always的aof模式来说,执行fsync会造成阻塞过长时间,no-appendfsync-on-rewrite字段设置为默认设置为no。如果对延迟要求很高的应用,这个字段可以设置为yes,否则还是设置为no,这样对持久化特性来说这是更安全的选择。设置为yes表示rewrite期间对新写操作不fsync,暂时存在内存中,等rewrite完成后再写入,默认为no,建议yes。Linux的默认fsync策略是30秒。可能丢失30秒数据。
no-appendfsync-on-rewrite no

#aof自动重写配置。当目前aof文件大小超过上一次重写的aof文件大小的百分之多少进行重写,即当aof文件增长到一定大小的时候Redis能够调用bgrewriteaof对日志文件进行重写。当前AOF文件大小是上次日志重写得到AOF文件大小的二倍(设置为100)时,自动启动新的日志重写过程。
auto-aof-rewrite-percentage 100
#设置允许重写的最小aof文件大小,避免了达到约定百分比但尺寸仍然很小的情况还要重写
auto-aof-rewrite-min-size 64mb

#aof文件可能在尾部是不完整的,当redis启动的时候,aof文件的数据被载入内存。重启可能发生在redis所在的主机操作系统宕机后,尤其在ext4文件系统没有加上data=ordered选项(redis宕机或者异常终止不会造成尾部不完整现象。)出现这种现象,可以选择让redis退出,或者导入尽可能多的数据。如果选择的是yes,当截断的aof文件被导入的时候,会自动发布一个log给客户端然后load。如果是no,用户必须手动redis-check-aof修复AOF文件才可以。
aof-load-truncated yes

################################ LUA SCRIPTING ###############################
# 如果达到最大时间限制(毫秒),redis会记个log,然后返回error。当一个脚本超过了最大时限。只有SCRIPT KILL和SHUTDOWN NOSAVE可以用。第一个可以杀没有调write命令的东西。要是已经调用了write,只能用第二个命令杀。
lua-time-limit 5000

################################ REDIS CLUSTER ###############################
#集群开关,默认是不开启集群模式。
# cluster-enabled yes

#集群配置文件的名称,每个节点都有一个集群相关的配置文件,持久化保存集群的信息。这个文件并不需要手动配置,这个配置文件有Redis生成并更新,每个Redis集群节点需要一个单独的配置文件,请确保与实例运行的系统中配置文件名称不冲突
# cluster-config-file nodes-6379.conf

#节点互连超时的阀值。集群节点超时毫秒数
# cluster-node-timeout 15000

#在进行故障转移的时候,全部slave都会请求申请为master,但是有些slave可能与master断开连接一段时间了,导致数据过于陈旧,这样的slave不应该被提升为master。该参数就是用来判断slave节点与master断线的时间是否过长。判断方法是:
#比较slave断开连接的时间和(node-timeout * slave-validity-factor) + repl-ping-slave-period
#如果节点超时时间为三十秒, 并且slave-validity-factor为10,假设默认的repl-ping-slave-period是10秒,即如果超过310秒slave将不会尝试进行故障转移 
# cluster-slave-validity-factor 10

#master的slave数量大于该值,slave才能迁移到其他孤立master上,如这个参数若被设为2,那么只有当一个主节点拥有2 个可工作的从节点时,它的一个从节点会尝试迁移。
# cluster-migration-barrier 1

#默认情况下,集群全部的slot有节点负责,集群状态才为ok,才能提供服务。设置为no,可以在slot没有全部分配的时候提供服务。不建议打开该配置,这样会造成分区的时候,小分区的master一直在接受写请求,而造成很长时间数据不一致。
# cluster-require-full-coverage yes

################################## SLOW LOG ###################################
###slog log是用来记录redis运行中执行比较慢的命令耗时。当命令的执行超过了指定时间,就记录在slow log中,slog log保存在内存中,所以没有IO操作。
#执行时间比slowlog-log-slower-than大的请求记录到slowlog里面,单位是微秒,所以1000000就是1秒。注意,负数时间会禁用慢查询日志,而0则会强制记录所有命令。
slowlog-log-slower-than 10000

#慢查询日志长度。当一个新的命令被写进日志的时候,最老的那个记录会被删掉。这个长度没有限制。只要有足够的内存就行。你可以通过 SLOWLOG RESET 来释放内存。
slowlog-max-len 128

################################ LATENCY MONITOR ##############################
#延迟监控功能是用来监控redis中执行比较缓慢的一些操作,用LATENCY打印redis实例在跑命令时的耗时图表。只记录大于等于下边设置的值的操作。0的话,就是关闭监视。默认延迟监控功能是关闭的,如果你需要打开,也可以通过CONFIG SET命令动态设置。
latency-monitor-threshold 0

############################# EVENT NOTIFICATION ##############################
#键空间通知使得客户端可以通过订阅频道或模式,来接收那些以某种方式改动了 Redis 数据集的事件。因为开启键空间通知功能需要消耗一些 CPU ,所以在默认配置下,该功能处于关闭状态。
#notify-keyspace-events 的参数可以是以下字符的任意组合,它指定了服务器该发送哪些类型的通知:
##K 键空间通知,所有通知以 __keyspace@__ 为前缀
##E 键事件通知,所有通知以 __keyevent@__ 为前缀
##g DEL 、 EXPIRE 、 RENAME 等类型无关的通用命令的通知
##$ 字符串命令的通知
##l 列表命令的通知
##s 集合命令的通知
##h 哈希命令的通知
##z 有序集合命令的通知
##x 过期事件:每当有过期键被删除时发送
##e 驱逐(evict)事件:每当有键因为 maxmemory 政策而被删除时发送
##A 参数 g$lshzxe 的别名
#输入的参数中至少要有一个 K 或者 E,否则的话,不管其余的参数是什么,都不会有任何 通知被分发。详细使用可以参考http://redis.io/topics/notifications

notify-keyspace-events ""

############################### ADVANCED CONFIG ###############################
#数据量小于等于hash-max-ziplist-entries的用ziplist,大于hash-max-ziplist-entries用hash
hash-max-ziplist-entries 512
#value大小小于等于hash-max-ziplist-value的用ziplist,大于hash-max-ziplist-value用hash。
hash-max-ziplist-value 64

#数据量小于等于list-max-ziplist-entries用ziplist,大于list-max-ziplist-entries用list。
list-max-ziplist-entries 512
#value大小小于等于list-max-ziplist-value的用ziplist,大于list-max-ziplist-value用list。
list-max-ziplist-value 64

#数据量小于等于set-max-intset-entries用iniset,大于set-max-intset-entries用set。
set-max-intset-entries 512

#数据量小于等于zset-max-ziplist-entries用ziplist,大于zset-max-ziplist-entries用zset。
zset-max-ziplist-entries 128
#value大小小于等于zset-max-ziplist-value用ziplist,大于zset-max-ziplist-value用zset。
zset-max-ziplist-value 64

#value大小小于等于hll-sparse-max-bytes使用稀疏数据结构(sparse),大于hll-sparse-max-bytes使用稠密的数据结构(dense)。一个比16000大的value是几乎没用的,建议的value大概为3000。如果对CPU要求不高,对空间要求较高的,建议设置到10000左右。
hll-sparse-max-bytes 3000

#Redis将在每100毫秒时使用1毫秒的CPU时间来对redis的hash表进行重新hash,可以降低内存的使用。当你的使用场景中,有非常严格的实时性需要,不能够接受Redis时不时的对请求有2毫秒的延迟的话,把这项配置为no。如果没有这么严格的实时性要求,可以设置为yes,以便能够尽可能快的释放内存。
activerehashing yes

##对客户端输出缓冲进行限制可以强迫那些不从服务器读取数据的客户端断开连接,用来强制关闭传输缓慢的客户端。
#对于normal client,第一个0表示取消hard limit,第二个0和第三个0表示取消soft limit,normal client默认取消限制,因为如果没有寻问,他们是不会接收数据的。
client-output-buffer-limit normal 0 0 0
#对于slave client和MONITER client,如果client-output-buffer一旦超过256mb,又或者超过64mb持续60秒,那么服务器就会立即断开客户端连接。
client-output-buffer-limit slave 256mb 64mb 60
#对于pubsub client,如果client-output-buffer一旦超过32mb,又或者超过8mb持续60秒,那么服务器就会立即断开客户端连接。
client-output-buffer-limit pubsub 32mb 8mb 60

#redis执行任务的频率为1s除以hz。
hz 10

#在aof重写的时候,如果打开了aof-rewrite-incremental-fsync开关,系统会每32MB执行一次fsync。这对于把文件写入磁盘是有帮助的,可以避免过大的延迟峰值。
aof-rewrite-incremental-fsync yes

启动服务

修改配置文件把redis改成后台启动,把daemonize的值改成yes

daemonize yes

复制配置文件到安装目录

sudo cp redis.conf /opt/redis/bin/

启动服务

 /opt/redis/bin/redis-server /opt/redis/bin/redis.conf

停止服务

ps -ef | grep redis
kill -9 redis的pid

命令行客户端连接

/opt/redis/bin/redis-cli

如果给Redis设置了密码,连接后需要使用auth命令登录,可以通过CONFIG get requirepass查看是否需要密码(一些基本的命令不需要密码也能执行)

# 返回 "" 表示不需要密码
CONFIG get requirepass

# 设置密码
CONFIG set requirepass "mypassword"

在远程服务上执行命令

/opt/redis/bin/redis-cli -h hostIP -p port -a password

key

Redis默认有16个表(可以在配置文件中改),每个表的key是独立的

# 切换表(0-15号表,默认在0号表)
select 1

# 插入、获取数据(String类型有无双引号都一样)
# a是key,1是value
set a 1
set b abc
set c 你好
set d "你好"
get a

# 把key移动到其他表
move d 3

# 查看所有key
keys *

# 删除key
del c
# 同时删除多个key
del a b

# 重复插入相同key的数据会覆盖
set a 1
set a 你好
get a

# 检查key是否存在
exists a

# 设置过期时间,单位为秒
expire a 10

# 查看剩余时间(-1表示不会过期),单位为秒
ttl a

# 移除过期时间,把key变成永不过期
persist a

# 查看key的类型
type a

# 重命名key(如果存在相同名字的key,会覆盖)
rename a key1

# 仅当不存在相同名字的key时,重命名key
renamenx key1 key2

# 如果配置了持久化,删除所有key的操作也会同步到rdb和aof中
# 删除所有key(当前表)
flushdb
# 删除所有key(所有表)
flushall

五大数据类型

Redis存储的数据是二进制安全的,除了可以存储5大数据类型外,Redis还可以存储图片等二进制数据

String

127.0.0.1:6379> SET key1 value1
OK
127.0.0.1:6379> SET key2 value2
OK
127.0.0.1:6379> GET key1
"value1"

#截取字符串
127.0.0.1:6379> GETRANGE key1 0 2
"val"

# 设置新值并返回旧值
127.0.0.1:6379> GETSET key1 haha
"value1"

# 一次获取多个key的值
127.0.0.1:6379> MGET key1 key2
1) "haha"
2) "value2"

# 一次设置多个key
127.0.0.1:6379> MSET key1 value1 key2 value2
OK

# 设置key的同时设置过期时间
127.0.0.1:6379> SETEX key3 10 value3
OK

# 当key不存在时,设置key的值
127.0.0.1:6379> SETNX key1 aaa
(integer) 0

# 将key中储存的数字值加1
127.0.0.1:6379> set num1 1
OK
127.0.0.1:6379> INCR num1
(integer) 2
# 将key中储存的数字值加上指定的值
127.0.0.1:6379> INCRBY num1 5
(integer) 7

127.0.0.1:6379> DECR num1
(integer) 6
127.0.0.1:6379> DECRBY num1 2
(integer) 4

127.0.0.1:6379> APPEND key1 aaa
(integer) 9
127.0.0.1:6379> get key1
"value1aaa"

List

列表,可以当栈使用,由于它可以指定入栈、出栈的方向,一般用于实现任务队列(FIFO)

#将一个或多个值插入到列表头部
127.0.0.1:6379> LPUSH mykey 1 2 3 4 5
(integer) 5

# 获取列表指定范围内的元素(从顶部到尾部输出)
127.0.0.1:6379> LRANGE mykey 0 4
1) "5"
2) "4"
3) "3"
4) "2"
5) "1"

# 删除顶部元素
127.0.0.1:6379> LPOP mykey
"5"

# 删除尾部元素
127.0.0.1:6379> RPOP mykey
"1"

# 在尾部插入元素
127.0.0.1:6379> RPUSH mykey 6
(integer) 5
# 0 -1表示获取所有元素
127.0.0.1:6379> LRANGE mykey 0 -1
1) "4"
2) "3"
3) "2"
4) "6"

Set

无序集合,不允许重复成员

127.0.0.1:6379> SADD mykey value1 value3 value2
(integer) 3

# 返回集合中元素个数
127.0.0.1:6379> SCARD mykey
(integer) 3

# 获取所有值
127.0.0.1:6379> SMEMBERS mykey
1) "value3"
2) "value1"
3) "value2"

# 移除并返回集合中的一个随机元素
127.0.0.1:6379> SPOP mykey
"value2"

# 删除元素(当集合中没有元素时,对应的key也会被删除)
127.0.0.1:6379> SREM mykey value1 value3
(integer) 2
127.0.0.1:6379> keys *
(empty list or set)

127.0.0.1:6379> SADD set1 1 4 6 7 2
(integer) 0
127.0.0.1:6379> SADD set2 3 5 6 1
(integer) 4

# 差集
127.0.0.1:6379> SDIFF set1 set2
1) "2"
2) "4"
3) "7"

# 交集
127.0.0.1:6379> SINTER set1 set2
1) "1"
2) "6"

# 并集
127.0.0.1:6379> SUNION set1 set2
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
6) "6"
7) "7"

Hash

存储key-value对

127.0.0.1:6379> HSET mykey key1 value1 key2 value2 key3 value3
(integer) 3

# 获取所有key
127.0.0.1:6379> HKEYS mykey
1) "key1"
2) "key2"
3) "key3"

# 获取所有value
127.0.0.1:6379> HVALS mykey
1) "value1"
2) "value2"
3) "value3"

# 获取所有的key、value
127.0.0.1:6379> HGETALL mykey
1) "key1"
2) "value1"
3) "key2"
4) "value2"
5) "key3"
6) "value3"

# 一次获取多个
127.0.0.1:6379> HMGET mykey key1 key2
1) "value1"
2) "value2"

# 获取hash表的长度
127.0.0.1:6379> HLEN mykey
(integer) 3

# 获取value
127.0.0.1:6379> HGET mykey key1
"value1"

# 删除(可删除多个)
127.0.0.1:6379> HDEL mykey key3
(integer) 1

127.0.0.1:6379> HEXISTS mykey key3
(integer) 0

# 设置值
127.0.0.1:6379> HSET mykey key3 aaa
(integer) 1

# 当key不存在时设置值
127.0.0.1:6379> HSETNX mykey key3 bbb
(integer) 0

# 一次设置多个
127.0.0.1:6379> HMSET mykey key4 value4 key5 value5
OK

Zset

有序集合,不允许重复的成员,且每个成员都会关联一个double类型的分数,redis通过该分数来为集合中的成员进行从小到大的排序

127.0.0.1:6379> ZADD key1 5 value1 4 value2 5 value3
(integer) 3

# 返回指定索引范围内的成员,0 -1表示获取所有
127.0.0.1:6379> ZRANGE key1 0 -1
1) "value2"
2) "value1"
3) "value3"

# 逆序返回指定索引范围内的成员,
127.0.0.1:6379> ZREVRANGE key1 0 -1
1) "value3"
2) "value1"
3) "value2"

# 返回指定分数的区间内的成员
127.0.0.1:6379> ZRANGEBYSCORE key1 0 5
1) "value2"
2) "value1"
3) "value3"

# 按照字典顺序获取成员
127.0.0.1:6379> ZADD key2 0 a 0 b 0 c 0 d 0 e 0 f 0 g
(integer) 7
127.0.0.1:6379> ZRANGEBYLEX key2 - [c
1) "a"
2) "b"
3) "c"

# 获取指定成员的索引
127.0.0.1:6379> ZRANK key1 value3
(integer) 2

# 获取指定成员的score
127.0.0.1:6379> ZSCORE key1 value1
"5"

# 删除指定成员
127.0.0.1:6379> ZREM key1 value3 value2
(integer) 2
127.0.0.1:6379> ZRANGE key1 0 -1
1) "value1"

事务

MULTI

MULTI开启事务,EXEC提交事务,DISCARD回滚

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET key1 value1
QUEUED
127.0.0.1:6379> SET key2 value2
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) OK
127.0.0.1:6379> keys *
1) "key1"
2) "key2"

如果输错命令,整个事务都不会执行

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET key1 value1
QUEUED
127.0.0.1:6379> SET key2
(error) ERR wrong number of arguments for 'set' command
127.0.0.1:6379> EXEC
(error) EXECABORT Transaction discarded because of previous errors.

只要事务没有提交,外部(其他客户端)无法看到更改

# 客户端1(还没有EXEC)
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET key1 value1
QUEUED

# 客户端2
127.0.0.1:6379> keys *
(empty list or set)

事务不能嵌套

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> MULTI
(error) ERR MULTI calls can not be nested

如果某一命令执行失败,后面的仍然可以执行(不具备原子性)

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET key1 value1
QUEUED
127.0.0.1:6379> RENAME key2 key1
QUEUED
127.0.0.1:6379> SET key2 value2
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) (error) ERR no such key
3) OK

WATCH

watch用于监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断

客户端1(未提交事务)

127.0.0.1:6379> set key1 value1
OK
127.0.0.1:6379> WATCH key1
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set key1 value2
QUEUED

此时客户端2对key1做修改

127.0.0.1:6379> set key1 aaa
OK

客户端1再提交会返回nil,表示事务执行失败

127.0.0.1:6379> EXEC
(nil)
127.0.0.1:6379> get key1
"aaa"

如果事务中还有其他操作,也不会执行

127.0.0.1:6379> WATCH key1
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set key1 value2
QUEUED
127.0.0.1:6379> set key2 value2
QUEUED
127.0.0.1:6379> EXEC
(nil)
127.0.0.1:6379> keys *
1) "key1"
127.0.0.1:6379> get key1
"aaa"

watch在每次提交事务后失效,也可以使用unwatch取消监视

持久化

如果只配置了rdb,则启动时只加载rdb

如果只配置了aof,则启动时只加载aof

当rdb和aof同时存在时,只加载aof

RDB

rdb就是Redis Database,默认是开启的,他存储内存中所有的键值对,rdb默认的存储路径为安装目录下的dump.rdb,如果权限不够,会放到当前用户的home目录下(Linux环境下),可以通过配置文件修改

# 保存的时间间隔,save 900 1表示如果在900秒内有1条数据改动,则执行保存操作;可以设置多条保存规则,如果要禁用所有的保存规则,可以使用save "",此时所有保存规则都会失效,此时Redis仅用作内存缓存
save 900 1
save 300 10
save 60 10000

# 在后台保存时如果出错是否停止
stop-writes-on-bgsave-error yes

# 是否压缩后再保存
rdbcompression yes

# 导出的rdb文件名
dbfilename dump.rdb

# 导出的rdb目录
dir ./

手动执行备份

执行shutdownflushall等命令时会把内存数据保存到rdb中,也可以手动执行save保存

# 保存,运行在前台,会阻塞,此时前台无法使用任何命令
127.0.0.1:6379> SAVE
OK

# 在后台保存,前台可以执行其他命令,但如果更改了数据,会导致无法保存和内存中完全一样的数据
127.0.0.1:6379> BGSAVE
Background saving started

备份文件修复

Redis安装目录下有修复脚本redis-check-rdb

redis-check-rdb rdb文件

恢复

把rdb放到Redis安装目录(如果权限不够,目录有可能是当前用户的home目录)中再启动服务即可恢复rdb中的内容,可以通过CONFIG GET dir获取目录

CONFIG GET dir

AOF

aof就是append-only file(只进行追加操作的文件),aof保存的是执行过的写命令,恢复时是根据命令重建数据的,所以恢复时间比rdb要慢,而且aof保存的文件可读性较高,不够安全;但是每次保存不需要把整个内存同步到数据库,只需要增加新的写命令,保存的时间少,所以可靠性高,同时它支持rewrite,可以生成重建数据所需的最少命令。aof默认是关闭的,可以通过配置文件修改

# 是否开启aof,默认关闭
appendonly no

# 导出的aof文件名
appendfilename "appendonly.aof"

# 持久化策略
# always: 同步持久化,表示每次更新操作后手动调用fsync()将数据写到磁盘(慢,安全)
# everysec: 异步操作,表示每秒同步一次(折衷,默认值),如果一秒钟内宕机,会有数据丢失
# no: 表示将缓存回写的策略交给系统,linux 默认是30秒将缓冲区的数据回写硬盘的(快)
# appendfsync always
appendfsync everysec
# appendfsync no

# 在aof重写或写入rdb时是否进行append操作,设置为yes表示rewrite期间对新写操作不fsync,暂时存在内存中,等rewrite完成后再写入,默认为no
no-appendfsync-on-rewrite no

# 当目前aof文件大小超过上一次重写的aof文件大小的百分之多少进行重写,即当aof文件增长到一定大小的时候Redis能够调用bgrewriteaof对日志文件进行重写,100表示两倍于之前重写后的文件大小时进行重写
auto-aof-rewrite-percentage 100
# 最小重写大小,小于该大小不会进行重写
auto-aof-rewrite-min-size 64mb

# 当系统宕机时,保存的aof文件的尾部可能是不完整的,如果设置为yes,表示让Redis尽可能地加载数据,如果为no,必须使用redis-check-aof命令修复aof文件后才能使用
aof-load-truncated yes

# 在以前rdb和aof是独立的,它们互不影响,Redis4.0开始允许使用RDB-AOF混合持久化的方式,可以在这里打开混合开关
aof-use-rdb-preamble yes

AOF同步过程:

  1. Redis 执行 fork() ,现在同时拥有父进程和子进程
  2. 子进程开始将新 AOF 文件的内容写入到临时文件
  3. 对于所有新执行的写入命令,父进程一边将它们累积到一个内存缓存中,一边将这些改动追加到现有 AOF 文件的末尾: 这样即使在重写的中途发生停机,现有的 AOF 文件也还是安全的
  4. 当子进程完成重写工作时,它给父进程发送一个信号,父进程在接收到信号之后,将内存缓存中的所有数据追加到新 AOF 文件的末尾
  5. 原子地用新文件替换旧文件,之后所有命令都会直接追加到新 AOF 文件的末尾

手动执行备份

可以通过BGREWRITEAOF命令备份aof,BGSAVEBGREWRITEAOF只能同时执行一个,且同时只能备份一个aof

如果BGSAVE正在执行,而且用户显式调用了BGREWRITEAOF,那么服务器将向用户回复一个 OK 状态, 并告知用户,BGREWRITEAOF已经被预定执行: 一旦BGSAVE 执行完毕,BGREWRITEAOF就会正式开始

备份文件修复

Redis安装目录下有修复脚本redis-check-aof

redis-check-aof aof文件

恢复

aof的恢复和rdb一样,把aof放到Redis安装目录或者home目录(通过CONFIG GET dir获取目录)下即可

从rdb转aof

可以在不重启的情况下,将rdb转成aof(需要先备份当前的rdb文件)

CONFIG SET appendonly yes
CONFIG SET save ""

主从复制(slaveof)

master-slave模式

SLAVEOF master的ip地址 端口号

此时slave无法进行写操作,只能执行读操作,而且每次同步都是增量同步

如果master意外下线,slave还是只读,master重新上线后,slave继续和master保持同步

如果在master下线期间,任意一个slave执行了SLAVEOF no one,则slave之间会根据算法决定出一个新的master,如果原来的master重新上线,就会变成slave

哨兵模式

哨兵(sentinel)会一直检测当前所有主机的运行状态,如果master下线,就会自动执行SLAVEOF no one,自动根据算法选举出一个新的master,而且原来的master上线后,会变为slave

配置sentinel.conf

# sentinel端口
port 26379

# 工作路径,注意路径不要和主重复
dir /tmp

# 守护进程模式(后台运行)
daemonize yes

# 关闭保护模式
protected-mode no

# 指明日志文件名
logfile "./sentinel.log"

# 使sentinel监视一个名为mymaster的主服务器,这个主服务器的IP地址为127.0.0.1,端口号为6379,而将这个主服务器判断为失效至少需要2个sentinel同意(如果同意的sentinel的数量不达标,自动故障迁移就不会执行)
sentinel monitor mymaster 127.0.0.1 6379 2

# master或slave多长时间(默认30秒)不能使用后标记为SDOWN状态,SDOWN为主观下线,即单个sentinel对服务器做出的下线判断,当被一定数量的sentinel做出SDOWN判断后,会变成ODOWN,客观下线
sentinel down-after-milliseconds mymaster 6000

# 若sentinel在该配置值内未能完成failover操作(即故障时master/slave自动切换),则认为本次failover失败。
sentinel failover-timeout mymaster 18000

# 指定了在执行故障转移时, 最多可以有多少个从服务器同时对新的主服务器进行同步
sentinel parallel-syncs mymaster 1

# 设置master和slaves验证密码
sentinel auth-pass mymaster 123456 

sentinel参数在运行时可以使用SENTINEL SET命令更改

启动哨兵

# 方式一(推荐,这种方式启动和redis实例没有任何关系)
redis-sentinel /path/to/sentinel.conf
# 方式二
redis-server /path/to/sentinel.conf --sentinel

连接哨兵的客户端(这里的端口是哨兵配置文件中指定的端口)

redis-cli -p 26379

# 查看master的状态,这里的mymaster是上面配置文件中指定的master的名称
sentinel master mymaster
# 查看salves的状态
SENTINEL slaves mymaster
# 查看哨兵的状态
SENTINEL sentinels mymaster

# 获取当前master的地址,如果正在执行故障转移,会返回新的主服务器地址
SENTINEL get-master-addr-by-name mymaster
# 查看哨兵信息
info sentinel

# 当主服务器失效时,在不询问其他Sentinel意见的情况下,强制开始一次自动故障迁移(不过发起故障转移的Sentinel会向其他Sentinel发送一个新的配置,其他Sentinel会根据这个配置进行相应的更新)
SENTINEL failover mymaster

master或者slave可以通过INFO replication查看当前的角色以及slave的数量(slave也可以设置slave)

127.0.0.1:6379> INFO replication
# Replication
role:master
connected_slaves:0
master_replid:98615a1467c7828b9f774f7fc87ed30db45a65be
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0

设置master密码:

config set masterauth 123456

要永久地设置这个密码, 那么可以将它加入到配置文件中:

masterauth 123456

发布订阅

开启两个客户端

接收端

127.0.0.1:6379> SUBSCRIBE mytopic
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "mytopic"
3) (integer) 1

发送端

127.0.0.1:6379> PUBLISH mytopic "aaa"
(integer) 1

收到消息后接收端显示如下

127.0.0.1:6379> SUBSCRIBE mytopic
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "mytopic"
3) (integer) 1
1) "message"
2) "mytopic"
3) "aaa"

其他

订阅多个频道
PSUBSCRIBE mytopic1 mytopic2

# 可以使用通配符
PSUBSCRIBE mytopic*

# 查看所有频道(查看的所有频道的客户端要早于订阅的客户端启动才能看得到)
PUBSUB CHANNELS

Jedis

在JAVA中使用Redis所用的api叫Jedis,和命令行中各命令一一对应

Jedis-API

maven依赖

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>

使用

@Test
public void test1() {
    //连接本地的 Redis 服务
    Jedis jedis = new Jedis("127.0.0.1", 6379);
    System.out.println("连接成功");
    //设置 redis 字符串数据
    jedis.set("key1", "value1");
    // 获取存储的数据并输出
    System.out.println(jedis.get("key1"));
    jedis.close();
}


//使用连接池
@Test
public void test2() {
    JedisPool jedisPool = new JedisPool("127.0.0.1", 6379);
    Jedis jedis = pool.getResource();
    jedis.set("key1", "value1");
    System.out.println(jedis.get("key1"));
    jedis.close();
    jedisPool.close();
}

分布式锁

需要了解的命令:

SET key value [EX seconds][PX milliseconds] [NX|XX]

要注意加锁和释放锁的过程必须具有原子性,即只能通过一条命令(一次连接)实现

class JedisLock {
    Jedis jedis;

    public JedisLock(Jedis jedis) {
        this.jedis = jedis;
    }

    /**
     * 尝试获取分布式锁,记录锁的名称和客户端标识,这里用了setnx,所以是不可重入锁,要让锁可重入,可以使用LUA脚本,额外判断lockKey对应的值是不是requestId,如果是则返回true
     *
     * @param lockKey    锁的名称
     * @param requestId  请求标识,用于标识当前客户端,比如IP
     * @param expireTime 超期时间,单位为毫秒(当客户端由于网络原因无法释放锁,就要有自动释放的机制)
     * @return 是否获取成功
     */
    public boolean tryLock(String lockKey, String requestId, int expireTime) {
        //NX表示如果不存在则创建,PX表示超时时间的单位是毫秒
        String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
        if ("OK".equals(result)) {
            return true;
        }
        return false;
    }

    //阻塞式获取
    public void tryLockBlocking(String lockKey, String requestId, int expireTime) throws InterruptedException {
        while (true) {
            String result = this.jedis.set(lockKey, requestId, "NX", "PX", expireTime);
            if ("OK".equals(result)) {
                break;
            }
            //防止一直消耗CPU
            Thread.sleep(100L);
        }
    }

    /**
     * 释放分布式锁
     *
     * @param lockKey   锁的名称
     * @param requestId 请求标识,用于标识当前客户端,比如IP
     * @return 是否释放成功
     */
    public boolean release(String lockKey, String requestId) {
        //无法使用Jedis API通过一条命令实现,所以使用了LUA脚本
        //先判断锁是不是当前客户端加的,是就DEL
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        //eval(String script, List<String> keys, List<String> args)
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
        if (result == "1") {
            return true;
        }
        return false;
    }
}

上面的代码还有一个问题没有解决:如果处理时间超过了expireTime,就会让其他客户端获得锁,导致锁失效,解决方法是添加一个守护进程,定时给锁延时;又或者把expireTime设置得尽量长

关于开源的Redis的分布式锁实现有很多,比较出名的有redisson、百度的dlock

穿透、雪崩、热点Key

缓存穿透

缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,造成缓存穿透

解决方法:

  1. 缓存层缓存空值
    1. 缓存太多空值,占用更多空间(优化:给个空值过期时间)
    2. 存储层更新代码了,缓存层还是空值(优化:后台设置时主动删除空值,并缓存把值进去)
  2. 对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则丢弃

缓存雪崩

如果缓存集中在一段时间内失效,发生大量的缓存穿透,所有的查询都落在数据库上,造成了缓存雪崩

解决方法:

  1. 不同的key,设置不同的过期时间(随机),让缓存失效的时间点尽量均匀
  2. 可以通过缓存reload机制,预先去更新缓存,在即将发生大并发访问前手动触发加载缓存
  3. 做二级缓存,或者双缓存策略。A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期

缓存雪崩还有一种可能,就是Redis挂了

解决方法:

  1. 保持缓存层服务器的高可用(监控、集群、哨兵)
  2. 依赖隔离组件为后端限流并降级服务
  3. 使用熔断策略

热点key

热卖商品、热点新闻、热点评论、明星直播等业务场景会出现热点key,如果在热点key缓存失效的瞬间,有大量客户端同时请求缓存,导致一个key同时被请求创建多次,会造成后端负载加大,甚至可能会让系统崩溃

解决方法:

  1. 使用互斥锁(synchronized或分布式锁),只让一个线程构建缓存
  2. 不设置过期时间,让key永远不过期,同时定时向数据库发起更新缓存的异步请求
  3. 使用熔断策略

集群搭建

从Redis3开始,支持集群环境。登录任意一个Redis节点,使用get命令,会自动计算出key所在的Redis服务器,然后自动重定向,所以只要访问Redis集群中任一节点,就可以访问集群中所有Redis中的任意数据

Redis的集群使用了Hash槽(slot),最多可以有16384个Hash槽(slot),存储在Redis Cluster中的所有键都会使用公式CRC16(key)%slot映射到这些slot中,即按slot进行分片。我们可以在搭建集群时通过reshard参数选择slot的数量

若集群中超过半数的Redis主机宕机,那么整个集群就无法工作了

在每个服务器上安装好Redis(Redis集群至少需要三台服务器),修改配置文件,每台服务器的配置主要的不同处如下:

# 通用配置,如果使用伪集群模拟,可能需要修改
port 6379
daemonize yes
dir /var/lib/redis
pidfile /var/run/redis/redis-server.pid
appendonly yes
# ----------集群环境配置-----------
# 绑定当前服务器的IP
bind 192.168.1.2
# 开启集群模式
cluster-enabled yes
# 集群相关的配置文件名称
cluster-config-file nodes-6379.conf
cluster-node-timeout 15000

然后使用redis-cli --cluster SUBCOMMAND [ARGUMENTS] [OPTIONS]来配置集群

./redis-cli --cluster create 192.168.1.2:6379 192.168.1.3:6379 192.168.1.4:6379 192.168.1.5:6379 192.168.1.6:6379 192.168.1.7:6379 --cluster-replicas 1

其中 --replicas 1表示自动为每一个master节点分配一个slave节点(主从复制比例为 1:1),上面有6个节点,程序会按照一定规则生成 3个master(主)3个slave(从)

如果创建失败,检查防火墙中对应的端口有没有开放

创建成功后,可以使用-c参数(表示以集群方式登录)登录命令行客户端,redis-cli -c -h 192.168.1.2 -p 6379,然后使用cluster nodes命令查看集群节点信息,cluster info查看集群状态信息

使用redis-cli --cluster help可以查看所有可用命令:

Cluster Manager Commands:
  create         host1:port1 ... hostN:portN
                 --cluster-replicas <arg>
  check          host:port
  info           host:port
  fix            host:port
  reshard        host:port
                 --cluster-from <arg>
                 --cluster-to <arg>
                 --cluster-slots <arg>
                 --cluster-yes
                 --cluster-timeout <arg>
                 --cluster-pipeline <arg>
  rebalance      host:port
                 --cluster-weight <node1=w1...nodeN=wN>
                 --cluster-use-empty-masters
                 --cluster-timeout <arg>
                 --cluster-simulate
                 --cluster-pipeline <arg>
                 --cluster-threshold <arg>
  add-node       new_host:new_port existing_host:existing_port
                 --cluster-slave
                 --cluster-master-id <arg>
  del-node       host:port node_id
  call           host:port command arg arg .. arg
  set-timeout    host:port milliseconds
  import         host:port
                 --cluster-from <arg>
                 --cluster-copy
                 --cluster-replace
  help

比如新添加节点

./redis-cli --cluster add-node 192.168.1.8:6379 192.168.1.9:6379
# 添加完新节点后需要重新分片Hash槽(连接集群中任意一个可用结点都行)
./redis-cli --cluster reshard 192.168.1.2:6379

更改主从节点

./redis-cli --cluster add-node --slave --master-id 5d6c61ecff23bff3b0fb01a86c66d882f2d402a0 192.168.1.8:6379 192.168.1.9:6379

使用了集群后,Jedis需要使用JedisCluster

Set<HostAndPort> nodes = new HashSet<>();
nodes.add(new HostAndPort("192.168.1.2", 6379));
nodes.add(new HostAndPort("192.168.1.3", 6379));
nodes.add(new HostAndPort("192.168.1.4", 6379));
nodes.add(new HostAndPort("192.168.1.5", 6379));
nodes.add(new HostAndPort("192.168.1.6", 6379));
nodes.add(new HostAndPort("192.168.1.7", 6379));
nodes.add(new HostAndPort("192.168.1.8", 6379));
nodes.add(new HostAndPort("192.168.1.9", 6379));

// 创建JedisCluster对象
JedisCluster jedisCluster = new JedisCluster(nodes);
//...
jedisCluster.close();

整合Spring

maven

<!-- Jedis -->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>

<!-- spring-data-redis(1.7.2 开始支持Redis集群)-->
<dependency>                            
    <groupId>org.springframework.data</groupId> 
    <artifactId>spring-data-redis</artifactId>
    <version>1.7.2.RELEASE</version>
</dependency>

<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-lang3</artifactId> 
  <version>3.3.2</version>
</dependency>

非集群版本

appplicationContext-redis.xml

<bean id="propertyPlaceholderConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
    <property name="locations">
        <list>
            <value>classpath*:/redis.porperties</value>
        </list>
    </property>
</bean>

<!-- 配置JedisPoolConfig实例 -->
<bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
    <property name="maxIdle" value="${redis.maxIdle}" />
    <property name="maxTotal" value="${redis.maxActive}" />
    <property name="maxWaitMillis" value="${redis.maxWait}" />
    <property name="testOnBorrow" value="${redis.testOnBorrow}" />
    <property name="testOnReturn" value="${redis.testOnReturn}" />
    <property name="testWhileIdle" value="${redis.testWhileIdle}" />
</bean>

<!-- 配置JedisConnectionFactory -->
<bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
    <property name="hostName" value="${redis.host}" />
    <property name="port" value="${redis.port}" />
    <!-- <property name="password" value="${redis.pass}" /> -->
    <property name="database" value="${redis.dbIndex}" />
    <property name="timeout" value="${redis.timeout}" />
    <property name="poolConfig" ref="poolConfig" />
</bean>

<!-- 配置RedisTemplate -->
<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
    <property name="connectionFactory" ref="jedisConnectionFactory" />
    <!-- 如果不配置Serializer,那么存储的时候缺省使用String,如果用User类型存储,那么会提示错误User can't cast to String!! -->
    <property name="keySerializer" ref="stringRedisSerializer" />
    <property name="hashKeySerializer" ref="stringRedisSerializer" />
    <property name="valueSerializer" ref="stringRedisSerializer" />
    <property name="hashValueSerializer" ref="stringRedisSerializer" />
    <!--开启事务支持  -->  
    <property name="enableTransactionSupport" value="true"></property>  
</bean>

<bean id="stringRedisSerializer" class="org.springframework.data.redis.serializer.StringRedisSerializer" />

<!-- 使用注解让Spring使用Redis管理缓存 -->
<!-- 配置RedisCacheManager -->
<bean id="redisCacheManager" class="org.springframework.data.redis.cache.RedisCacheManager">
    <constructor-arg name="redisOperations" ref="redisTemplate" />
    <property name="defaultExpiration" value="${redis.expiration}" />
</bean>

<!-- 使用缓存注解 -->
<cache:annotation-driven cache-manager="redisCacheManager" />

redis.porperties

# Redis settings
redis.host=127.0.0.1
redis.port=6379
#redis.pass=password
redis.dbIndex=0
redis.timeout=3000
redis.expiration=3000
redis.maxIdle=300
redis.maxActive=600
redis.maxWait=1000
redis.testOnBorrow=true
redis.testOnReturn=true
redis.testWhileIdle=true

集群版本

appplicationContext-redis-cluster.xml

<!-- 配置JedisPoolConfig实例 -->
<bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
    <property name="maxIdle" value="${redis.maxIdle}" />
    <property name="maxTotal" value="${redis.maxActive}" />
    <property name="maxWaitMillis" value="${redis.maxWait}" />
    <property name="testOnBorrow" value="${redis.testOnBorrow}" />
    <property name="testOnReturn" value="${redis.testOnReturn}" />
    <property name="testWhileIdle" value="${redis.testWhileIdle}" />
</bean>

<!-- Redis集群配置 -->
<bean id="redisClusterConfig" class="org.springframework.data.redis.connection.RedisClusterConfiguration">
    <property name="maxRedirects" value="3"></property>
    <property name="clusterNodes">
        <set>
            <bean class="org.springframework.data.redis.connection.RedisNode">
                <constructor-arg name="host" value="192.168.1.2"></constructor-arg>
                <constructor-arg name="port" value="6379"></constructor-arg>
            </bean>

            <bean class="org.springframework.data.redis.connection.RedisNode">
                <constructor-arg name="host" value="192.168.1.3"></constructor-arg>
                <constructor-arg name="port" value="6379"></constructor-arg>
            </bean>
            <bean class="org.springframework.data.redis.connection.RedisNode">
                <constructor-arg name="host" value="192.168.1.4"></constructor-arg>
                <constructor-arg name="port" value="6379"></constructor-arg>
            </bean>
        </set>
    </property>
</bean>

<!-- Redis连接工厂 -->
<bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
    <constructor-arg name="clusterConfig" ref="redisClusterConfig" />
    <property name="timeout" value="${redis.timeout}" />
    <property name="poolConfig" ref="poolConfig" />
</bean>

<!-- 集群Redis使用模板 -->
<bean id="clusterRedisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
    <property name="connectionFactory" ref="jedisConnectionFactory" />
    <property name="keySerializer" ref="stringRedisSerializer" />
    <property name="hashKeySerializer" ref="stringRedisSerializer" />
    <property name="valueSerializer" ref="stringRedisSerializer" />
    <property name="hashValueSerializer" ref="stringRedisSerializer" />
</bean>
<!-- 存储序列化 -->
<bean name="stringRedisSerializer" class="org.springframework.data.redis.serializer.StringRedisSerializer" />

<!-- 使用注解让Spring使用Redis管理缓存 -->
<!-- 配置RedisCacheManager -->
<bean id="redisCacheManager" class="org.springframework.data.redis.cache.RedisCacheManager">
    <constructor-arg name="redisOperations" ref="redisTemplate" />
    <property name="defaultExpiration" value="${redis.expiration}" />
</bean>

<!-- 使用缓存注解 -->
<cache:annotation-driven cache-manager="redisCacheManager" />

使用RedisTemplate

clusterRedisTemplate.execute(new RedisCallback<Long>() {
    public Long doInRedis(RedisConnection connection) throws DataAccessException {
        if (connection.exists("key1".getBytes())) {
            connection.del("key1".getBytes());
        }
        connection.set("key1".getBytes(), "value1".getBytes());
        return 1L;
    }
});

缓存注解使用

使用注解让Spring使用Redis管理缓存:

@Service("userService")
@Transactional(propagation=Propagation.REQUIRED, rollbackFor=Exception.class)
//@CacheConfig: 用cacheNames属性指定本类中所有用到缓存的地方,都去找这个库。只要使用了这个注解,在方法上@Cacheable、@CachePut、@CacheEvict就可以不用写value去找具体库名了(一般不怎么用)
//@CacheConfig
public class UserServiceImpl implements IUserService {

    @Resource
    private UserMapper iUserDao;

    //@Cacheable: 标注该方法查询的结果进入缓存,再次访问时直接读取缓存中的数据
    //如果未在类上使用@CacheConfig注解规定数据要缓存到哪个库中,就必须给value一个值,规定数据最后缓存到哪个redis库中
    //如果不指定key属性,则会按我们自定义的规则生成,key可以使用EL表达式
    //可以指定condition,condition用EL表达式写,当condition为true时才存入缓存
    @Cacheable("getUserById")
    @Override
    public User getUserById(int userId) {
        return this.iUserDao.selectByPrimaryKey(userId);
    }
    
    //@CachePut: 每次不管缓存中有没有结果,都从数据库查找结果,并将结果更新到缓存
    @CachePut("getAllUser")
    @Override
    public List<User> getAllUser() {
        return this.iUserDao.selectAllUser();
    }

    //@CacheEvict: 清空指定的缓存,allEntries变量表示所有对象的缓存都清除
    //一般进行了插入、更新、删除等操作都要更新缓存,所谓更新缓存,就是把对应的key删掉,让下次从数据库获取数据
    @CacheEvict(value={"getUserById", "getAllUser"}, key="'user'+#id.toString()")
    public boolean deleteById(int id){
        return this.iUserDao.deleteUser();
    }
}

key生成策略

如果没有指定key生成策略,会默认使用SimpleKeyGenerator,它是按照参数生成缓存的key的,如果2个方法,参数是一样的,但执行逻辑不同,那么将会导致执行第二个方法时命中第一个方法的缓存

public class SimpleKeyGenerator implements KeyGenerator {
    @Override
    public Object generate(Object target, Method method, Object... params) {
        return generateKey(params);
    }

    public static Object generateKey(Object... params) {
        if (params.length == 0) {
            return SimpleKey.EMPTY;
        }
        if (params.length == 1) {
            Object param = params[0];
            if (param != null && !param.getClass().isArray()) {
                return param;
            }
        }
        return new SimpleKey(params);
    }
}

自定义key生成策略

@Configuration
public class RedisCacheConfig extends CachingConfigurerSupport {

    //自定义redis的key生成规则,如果不在注解参数中注明key=“”的话,就采用这个类中的key生成规则生成key
    //使用 本类名+方法名+参数名(中间没有逗号区分) 生成Redis的key
    @Bean
    @Override
    public KeyGenerator keyGenerator() {
        return new KeyGenerator() {
            @Override
            public Object generate(Object obj, Method method, Object... params) {
                StringBuilder sb = new StringBuilder();
                sb.append(obj.getClass().getName());
                sb.append(method.getName());
                for (Object param : params) {
                    sb.append(param.toString());
                }
                return sb.toString();
            }
        };
    }
}

ZooKeeper

项目地址: zookeeper.apache.org

配置

配置conf/zoo.cfg

# 时间单位(毫秒)
tickTime=2000
# zookeeper实例中的从节点同步到主节点的初始化连接时间限制,超出时间限制则连接失败;这里的10表示是tickTime的10倍,即20秒
initLimit=10
# 主从节点之间同步数据的时间限制,若超过这个时间限制,那么从节点将会被丢弃;同样,这里的5表示是tickTime的5倍
syncLimit=5
# 存放数据的目录
dataDir=/tmp/zookeeper
# 用于连接客户端的端口
clientPort=2181
# the maximum number of client connections
#maxClientCnxns=60

# Be sure to read the maintenance section of the 
# administrator guide before turning on autopurge.
#
# http://zookeeper.apache.org/doc/current/zookeeperAdmin.html#sc_maintenance
#
# The number of snapshots to retain in dataDir
#autopurge.snapRetainCount=3
# Purge task interval in hours
# Set to "0" to disable auto purge feature
#autopurge.purgeInterval=1

启动

bin/zkServer.sh start

命令行

客户端连接

bin/zkCli.sh -server 127.0.0.1:2181

常用命令

[zk: 127.0.0.1:2181(CONNECTED) 0] help
ZooKeeper -server host:port cmd args
    stat path [watch]
    set path data [version]
    ls path [watch]
    delquota [-n|-b] path
    ls2 path [watch]
    setAcl path acl
    setquota -n|-b val path
    history 
    redo cmdno
    printwatches on|off
    delete path [version]
    sync path
    listquota path
    rmr path
    get path [watch]
    create [-s] [-e] path data acl
    addauth scheme auth
    quit 
    getAcl path
    close 
    connect host:port

比如

 create /node1 data1    # 增
 create -e /node2 data2 # 创建临时节点,在客户端退出(CTRL + C)一定时间后就会被删除,或者使用quit退出时立即删除
 create -s /node data   # 创建顺序节点,此时实际创建的是/node0000000001
 delete /node1          # 删
 set /node1 newdata     # 改
 get /node1             # 查
 ls /       # 查看节点及其所有子节点
 ls2 /      # 查看节点及详细信息
 stat /node1            # 查看该节点详细信息
 history        # 查看输入的所有命令

Watch机制

添加watch有多种方式

stat /node1 watch
ls /node1 watch
ls2 /node1 watch
get /node1 watch

此时如果对/node1进行修改,就会触发watch,但在命令行中watch被触发一次就会失效

[zk: 127.0.0.1:2181(CONNECTED) 0] set /node1 newdata

WATCHER::

WatchedEvent state:SyncConnected type:NodeDataChanged path:/node1

也可以给不存在的节点添加watch,如果以后添加该节点,就会触发watch

[zk: 127.0.0.1:2181(CONNECTED) 0] stat /node2 watch
Node does not exist: /node2
[zk: 127.0.0.1:2181(CONNECTED) 1] create /node2 data2

WATCHER::

WatchedEvent state:SyncConnected type:NodeCreated path:/node2
Created /node2

ACL

ACL是Access Control List,它可以控制权限

查看

[zk: 127.0.0.1:2181(CONNECTED) 0] getAcl /node1
'world,'anyone
: cdrwa

world是权限模式的一种,anyone表示任何人都可以对该节点进行cdrwa操作,而cdrwa分别代表create、delete、read、write、admin(设置权限)

scheme

scheme是权限模式,常用的有如下

1、world: 最开放的权限控制模式

setAcl /node1 world:anyone:cdrwa

2、auth: 和digest一样,只是这里的密码使用明文

[zk: 127.0.0.1:2181(CONNECTED) 0] addauth digest username:password # 使用前需先添加用户
[zk: 127.0.0.1:2181(CONNECTED) 1] setAcl /node1 auth:username:password:cdrwa # 然后使用该用户给节点添加权限
[zk: 127.0.0.1:2181(CONNECTED) 2] getAcl /node1                             
'digest,'username:+Ir5sN1lGJEEs8xBZhZXKvjLJ7c=
: cdrwa

3、digest: 密码输入时使用密文

[zk: 127.0.0.1:2181(CONNECTED) 0] addauth digest username:password # 使用前需先添加用户
[zk: 127.0.0.1:2181(CONNECTED) 1] setAcl /node1 digest:username:+Ir5sN1lGJEEs8xBZhZXKvjLJ7c=:cdrwa
[zk: 127.0.0.1:2181(CONNECTED) 2] getAcl /node1
'digest,'username:+Ir5sN1lGJEEs8xBZhZXKvjLJ7c=
: cdrwa

4、ip: 根据ip授权

setAcl /node1 ip:127.0.0.1:cdrwa

5、super: 管理员权限,它和其他权限模式不一样,设置super需要更改bin/zkServer.sh

首先获取加密的字符串,这里需要用JAVA代码生成

String str = DigestAuthenticationProvider.generateDigest("super:admin");

得到super:xQJmxLMiHGwaqBvst5y6rkB6HQs=

打开bin/zkServer.sh,找到

nohup "$JAVA" "-Dzookeeper.log.dir=${ZOO_LOG_DIR}" "-Dzookeeper.root.logger=${ZOO_LOG4J_PROP}" \

改成

nohup "$JAVA" "-Dzookeeper.log.dir=${ZOO_LOG_DIR}" "-Dzookeeper.root.logger=${ZOO_LOG4J_PROP}" "-Dzookeeper.DigestAuthenticationProvider.superDigest=super:xQJmxLMiHGwaqBvst5y6rkB6HQs=" \

使用:

addauth digest super:admin

四字命令

文档

使用四字命令可以在不登录客户端的情况下获取运行的信息(需安装nc

root@root:~$ echo conf | nc 127.0.0.1 2181
clientPort=2181
dataDir=/tmp/zookeeper/version-2
dataLogDir=/tmp/zookeeper/version-2
tickTime=2000
maxClientCnxns=60
minSessionTimeout=4000
maxSessionTimeout=40000
serverId=0

集群搭建

zookeeper集群至少需要三台主机,一台leader(主节点),两台follower(从节点)

这里用伪集群来模拟集群环境。伪集群就是在一台机器上模拟集群的环境,此时各zookeeper之间的ip相同,端口号不同(集群就是ip不同,但端口号可以相同)

配置conf/zoo.cfg,给不同的zookeeper配置不同的端口(clientPort,客户端连接用的端口),然后添加server.1节点,其中192.168.1.2是zookeeper所在服务器的ip,2888是主从复制用的端口,3888是当主节点宕机后选举新的主节点用的端口(和Redis不同,这里的选举是自动的,不需要额外配置,而且如果以后主节点重新上线,就会变成从节点)

zookeeper01/conf/zoo.cfg

tickTime=2000
initLimit=10
syncLimit=5
dataDir=/tmp/zookeeper01
# 不同的zookeeper在伪集群环境下用的端口号不同,其余配置一样
clientPort=2181

# 三台zookeeper主机都要在这里配置,各主机这里的配置是一样的
server.1=192.168.1.2:2888:3888
server.2=192.168.1.2:2889:3889
server.3=192.168.1.2:2890:3890

然后在指定的dataDir下创建myid文件,server.1就创建内容为1的文件,server.2就创建内容为2,依次类推

echo 1 > /tmp/zookeeper01/myid

/tmp/zookeeper01/myid

1

然后依次启动zookeeper服务即可

bin/zkServer.sh start
# 查看状态,主节点会显示leader,从节点会显示follower,主从节点和启动顺序无关,和选举算法有关
bin/zkServer.sh status

原生JAVA客户端

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>myapp</groupId>
    <artifactId>zktest</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
            <version>3.4.13</version>
        </dependency>

        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.16</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.6.1</version>
        </dependency>
    </dependencies>
    <!-- 指定JDK版本为8 -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.3.2</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

增删改查

class MyWatcher implements Watcher {
    private CountDownLatch countDownLatch;
    private ZooKeeper zooKeeper;

    public MyWatcher(CountDownLatch countDownLatch) {
        this.countDownLatch = countDownLatch;
    }

    public void setZooKeeper(ZooKeeper zooKeeper) {
        this.zooKeeper = zooKeeper;
    }

    @Override
    public void process(WatchedEvent event) {
        System.out.println("Receive watched event: " + event);
        switch (event.getState()) {
            case SyncConnected:
                System.out.println("[SyncConnected]");
                countDownLatch.countDown();
                break;
            case Disconnected:
                System.out.println("[Disconnected]");
                break;
            case AuthFailed:
                System.out.println("[AuthFailed]");
                break;
            case SaslAuthenticated:
                System.out.println("[SaslAuthenticated]");
                break;
            case Expired:
                System.out.println("[Expired]");
            default:
                break;
        }

        switch (event.getType()) {
            case None:
                System.out.println("[None]");
                break;
            case NodeCreated:
                System.out.println("[NodeCreated]");
                break;
            case NodeDeleted:
                System.out.println("[NodeDeleted]");
                break;
            case NodeDataChanged:
                System.out.println("[NodeDataChanged]");
                break;
            case NodeChildrenChanged:
                System.out.println("[NodeChildrenChanged]");
                //获取子节点
                try {
                    for (String child : zooKeeper.getChildren(event.getPath(), false))
                        System.out.print(child + " ");
                    System.out.println();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                break;
            default:
                break;
        }
    }
}

class Main {
    public static void main(String[] args) throws Exception {

        CountDownLatch countDownLatch = new CountDownLatch(1);

        //连接服务端,Watcher不仅可以监听当前连接的事件,还可以监听其他连接(包括命令行客户端)的事件
        MyWatcher myWatcher = new MyWatcher(countDownLatch);
        ZooKeeper zookeeper = new ZooKeeper("127.0.0.1:2181", 5000, myWatcher);
        myWatcher.setZooKeeper(zookeeper);
        System.out.println("connection status: " + zookeeper.getState());

        //创建连接需要时间,这里不用countDownLatch也可以,执行命令时会等待连接完成再执行
        countDownLatch.await();
        System.out.println("connection status: " + zookeeper.getState());

        //如果断开连接,可以使用sessionId和sessionPasswd重连
        long sessionId = zookeeper.getSessionId();
        byte[] sessionPasswd = zookeeper.getSessionPasswd();
        zookeeper = new ZooKeeper("127.0.0.1:2181", 5000, myWatcher, sessionId, sessionPasswd);

        //同步方式创建节点,Ids.OPEN_ACL_UNSAFE为world权限,CreateMode.EPHEMERAL相当于-e参数,表示是临时节点;如果一个节点已经存在,那么创建同名节点时,会抛出NodeExistsException异常,且无法在父节点不存在的情况下创建一个子节点
        String path1 = zookeeper.create("/node1", "data1".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);

        //异步方式创建节点
        zookeeper.create("/node2", "data2".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL,
                new AsyncCallback.StringCallback() {
                    public void processResult(int rc, String path, Object ctx, String name) {
                        System.out.println("rc = " + rc + ", path = " + path + ", ctx = " + ctx + ", name = " + name);

                        //ctx是一个标志,如果多个方法使用同一个callback,可以根据ctx执行对应的操作
                        if (ctx.equals("create:succeeded")) {
                            //...
                        }
                    }
                }, "create:succeeded");

        //更改节点数据,第二个参数为版本号,-1表示匹配所有的版本
        Stat stat1 = zookeeper.setData(path1, "newdata".getBytes(), -1);
        System.out.println(path1 + " version: " + stat1.getVersion());
        //也可以异步更改
        //zookeeper.setData(String path,byte data[], int version, StatCallback cb, Object ctx)

        //获取子节点,第二个参数为是否触发watcher
        System.out.println(path1 + " children: ");
        for (String child : zookeeper.getChildren(path1, true))
            System.out.print(child + " ");
        System.out.println();
        //也可以使用其他Watcher
        //zookeeper.getChildren(String path, Watcher watcher)
        //也可以异步使用
        //zookeeper.getChildren(String path, Watcher watcher, ChildrenCallback cb, Object ctx)

        //获取节点数据
        Stat stat2 = new Stat();
        byte[] bytes = zookeeper.getData("/node1",true,stat2);
        System.out.println(new String(bytes));
        System.out.println("/node1 version: "+stat2.getVersion());
        //也可以异步获取
        //zookeeper.getData(final String path, Watcher watcher, DataCallback cb, Object ctx)

        //删除节点,只允许删除叶子节点,即一个节点如果有子节点,那么该节点将无法直接删除,必须先删掉其所有子节点;第二个参数为版本号,-1表示匹配所有的版本
        zookeeper.delete(path1, -1);
        //也可以异步删除
        //zookeeper.delete(String path, int version, VoidCallback cb, Object ctx)

        //由于没有使用异步的方式执行命令,需等待至命令执行完成再退出
        Thread.sleep(500L);
        zookeeper.close();
    }
}

ACL

ZooKeeper zookeeper = new ZooKeeper("127.0.0.1:2181", 5000, new Watcher() {
    @Override
    public void process(WatchedEvent event) {
        System.out.println("Receive watched event: " + event);
    }
});

//命令行设置权限的命令为 setAcl /node1 auth:username:password:cdrwa
List<ACL> acls = new ArrayList<>();
Id token1 = new Id("digest", DigestAuthenticationProvider.generateDigest("username1:password1"));
Id token2 = new Id("digest", DigestAuthenticationProvider.generateDigest("username2:password2"));

acls.add(new ACL(ZooDefs.Perms.ALL, token1));
//多个权限可以用"|"
acls.add(new ACL(ZooDefs.Perms.READ | ZooDefs.Perms.DELETE, token2));

zookeeper.create("/node1", "data1".getBytes(), acls, CreateMode.EPHEMERAL);

//ip方式
List<ACL> aclsIP = new ArrayList<>();
Id token3 = new Id("ip", "127.0.0.1");
aclsIP.add(new ACL(ZooDefs.Perms.ALL, token3));
zookeeper.create("/node2", "data2".getBytes(), aclsIP, CreateMode.EPHEMERAL);

//操作时需要先授权
zookeeper.addAuthInfo("digest", "username1:password1".getBytes());
zookeeper.setData("/node1", "test".getBytes(), -1);

Thread.sleep(500L);
zookeeper.close();

Curator客户端

Curator是在原生zookeeper的JAVA客户端的基础上增加了更多功能的zookeeper的JAVA客户端

pom.xml

<dependencies>
    <!-- curator依赖原生的zookeeper API -->
    <dependency>
        <groupId>org.apache.zookeeper</groupId>
        <artifactId>zookeeper</artifactId>
        <version>3.4.13</version>
    </dependency>
    <!-- curator -->
    <dependency>
        <groupId>org.apache.curator</groupId>
        <artifactId>curator-framework</artifactId>
        <version>4.0.1</version>
    </dependency>
    <dependency>
        <groupId>org.apache.curator</groupId>
        <artifactId>curator-recipes</artifactId>
        <version>4.0.1</version>
    </dependency>
    <!-- 日志 -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>1.6.1</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-nop</artifactId>
        <version>1.7.2</version>
    </dependency>
</dependencies>

增删改查

public static void main(String[] args) throws Exception {
    String connectionString = "127.0.0.1:2181";
    //多个zookeeper可以用逗号分隔
    //String connectionString = "192.168.1.2:2181,192.168.1.3:2181,192.168.1.4:2181";

    //执行过程中如果连接中断,执行的重试策略,1000为每次重试的时间间隔,3为重试次数
    ExponentialBackoffRetry retryPolicy = new ExponentialBackoffRetry(1000, 3);
    //创建CuratorFramework
    CuratorFramework curator = CuratorFrameworkFactory.newClient(connectionString, retryPolicy);
    /*
    //通过builder创建
    CuratorFramework curator = CuratorFrameworkFactory.builder()
            .connectString(connectionString)
            .retryPolicy(retryPolicy)
            .connectionTimeoutMs(10000)
            .sessionTimeoutMs(5000)
            .namespace("curator_namespace")
            .build();
    */
    //开启连接
    curator.start();

    //----------------------------------------------------------------------

    //增(可以使用同步或异步方式)
    curator.create()
        .withMode(CreateMode.EPHEMERAL)//默认创建的是PERSISTENT类型的节点,也可以指定为临时、顺序等模式
        .forPath("/node1", "data1".getBytes());
    curator.create()
        .creatingParentsIfNeeded()//如果父路径不存在,可以递归创建
        .withMode(CreateMode.EPHEMERAL)//此时node3是临时的,但node2是永久的
        .forPath("/node2/node3", "data23".getBytes());
    //如果在创建节点成功,但是服务器在返回结果前宕机,会出现虽然节点创建成功,但客户端不知道的情况;withProtection创建的节点以GUID为前缀,在创建节点时先在父路径中搜索其中包含GUID的节点,如果存在,则认为是之前尝试创建但丢失的节点
    curator.create()
        .withProtection()
        .withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
        .forPath("/node4", "data4".getBytes());

    //----------------------------------------------------------------------

    //改
    //同步方式
    curator.setData()
        .withVersion(-1)//可以指定version
        .forPath("/node1", "newData1".getBytes());
    //异步方式(可以使用listener或者callback)
    //listener
    CuratorListener setDataListener = new CuratorListener() {
        @Override
        public void eventReceived(CuratorFramework client, CuratorEvent event) throws Exception {
            System.out.print("eventReceived-- " + event + " ");
            //执行成功为0,执行失败不会报错,而是通过resultcode通知
            /*
               0:    成功
               -4:   ConnectionLoss,即客户端与服务端断开连接
               -110: NodeExists,即节点已经存在
               -112: SessionExpired,即会话过期
            */
            if (event.getResultCode() == 0)
                System.out.print("执行成功 ");
            if (event.getType() == CuratorEventType.SET_DATA)
                System.out.println("执行了SET_DATA操作,更改的节点为" + event.getPath());
        }
    };
    curator.getCuratorListenable()
        .addListener(setDataListener);
    curator.setData()
        .inBackground()//使用异步方式,会触发监听
        .forPath("/node1", "newData2".getBytes());
    //listener会触发多次,直至curator.getCuratorListenable().removeListener(setDataListener);
    curator.setData()
        .inBackground()
        .forPath("/node1", "newData3".getBytes());
    //callback只是当前操作有效
    curator.setData().inBackground(new BackgroundCallback() {
        @Override
        public void processResult(CuratorFramework client, CuratorEvent event) throws Exception {
            System.out.println("processResult-- " + event);
        }
    }).forPath("/node1", "newData4".getBytes());

    //----------------------------------------------------------------------

    //查
    //获取节点的数据(可以使用同步或异步方式)
    Stat stat1 = new Stat();
    byte[] data1 = curator.getData()
        .storingStatIn(stat1)//获取节点状态,如果获取失败,stat不会为null,但其内容为空(比如version为0)
        .forPath("/node1");
    System.out.println(new String(data1));
    System.out.println(stat1);
    //还可以使用Watcher
    byte[] data2 =curator.getData()
        .usingWatcher(new Watcher() {
            @Override
            public void process(WatchedEvent event) {

            }
        }).forPath("/node1");
    //检查节点是否存在
    Stat stat2 = curator.checkExists()
        .forPath("/node4");//如果节点不存在stat为null
    System.out.println(stat2);
    //获取子节点
    List<String> children1 = curator.getChildren()
        .forPath("/node2");
    System.out.println(children1);

    //----------------------------------------------------------------------

    //删(可以使用同步或异步方式)
    if (curator.checkExists().forPath("/node1") != null)
        curator.delete()
        .withVersion(-1)//可以指定version
        .forPath("/node1");
    if (curator.checkExists().forPath("/node1") != null)
        curator.delete()
        .guaranteed()//保障机制,若未删除成功,只要会话有效会在后台一直尝试删除
        .deletingChildrenIfNeeded()//若当前节点包含子节点
        .forPath("/node1");

    curator.close();
}

ACL

CuratorFramework curator = CuratorFrameworkFactory.builder()
    .connectString("127.0.0.1:2181")
    .retryPolicy(new ExponentialBackoffRetry(1000, 3))
    .connectionTimeoutMs(10000)
    .sessionTimeoutMs(5000)
    .authorization("digest", "username:password".getBytes())
    .build();

List<ACL> acls = new ArrayList<ACL>();
acls.add(new ACL(ZooDefs.Perms.ALL, new Id("digest", DigestAuthenticationProvider.generateDigest("username:password"))));

curator.create()
    .withMode(CreateMode.PERSISTENT)
    .withACL(acls)
    .forPath("/node1", "data1".getBytes());

事务

String connectionString = "127.0.0.1:2181";
ExponentialBackoffRetry retryPolicy = new ExponentialBackoffRetry(1000, 3);
CuratorFramework curator = CuratorFrameworkFactory.newClient(connectionString, retryPolicy);
curator.start();

//方式一 已过时(deprecated)
curator.inTransaction()
    .create().withMode(CreateMode.EPHEMERAL).forPath("/node5", "data5".getBytes())
    .and()
    .setData().withVersion(-1).forPath("/node5", "newData1".getBytes())
    .and()
    .commit();
//方式二
CuratorOp createOp = curator.transactionOp()
    .create()
    .withMode(CreateMode.EPHEMERAL)
    .forPath("/node6", "data6".getBytes());
CuratorOp setDataOp = curator.transactionOp()
    .setData()
    .forPath("/node5", "newData2".getBytes());
CuratorOp deleteOp = curator.transactionOp()
    .delete()
    .forPath("/node2/node3");

Collection<CuratorTransactionResult> results = curator.transaction()
    .forOperations(createOp, setDataOp, deleteOp);
for (CuratorTransactionResult result : results) {
    System.out.println(result.getForPath() + " - " + result.getType());
}
curator.close();

子节点监听

原生的zookeeper API中,watcher每次操作都要设置,而使用Curator的Cache,可以设置一次,多次监听

ring connectionString = "127.0.0.1:2181";
ExponentialBackoffRetry retryPolicy = new ExponentialBackoffRetry(1000, 3);
CuratorFramework curator = CuratorFrameworkFactory.newClient(connectionString, retryPolicy);
curator.start();

//监听当前节点事件
final NodeCache nodeCache = new NodeCache(curator, "/node1");
nodeCache.start();//开始监听
nodeCache.getListenable().addListener(new NodeCacheListener() {
    @Override
    public void nodeChanged() throws Exception {
        byte[] res = nodeCache.getCurrentData().getData();
        System.out.println("data: " + new String(res));
    }
});
//在这里执行的增删改查都会被监听到
nodeCache.close();//结束监听

//监听当前节点和子节点事件
//第三个参数表示是否在初始化时拉取缓存数据
final PathChildrenCache pathChildrenCache = new PathChildrenCache(curator, "/node1", true);
//POST_INITIALIZED_EVENT: 当Cache初始化数据后发送一个PathChildrenCacheEvent.Type#INITIALIZED事件;如果不传StartMode,则初始会把监听节点的所有存在的节点都作为节点改变,发出事件,导致初始化时有大量事件产生
pathChildrenCache.start(PathChildrenCache.StartMode.POST_INITIALIZED_EVENT);
pathChildrenCache.getListenable().addListener(new PathChildrenCacheListener() {
    @Override
    public void childEvent(CuratorFramework curator, PathChildrenCacheEvent event) throws Exception {
        switch (event.getType()) {
            case CHILD_ADDED:
                System.out.println("add:" + event.getData());
                break;
            case CHILD_UPDATED:
                System.out.println("update:" + event.getData());
                break;
            case CHILD_REMOVED:
                System.out.println("remove:" + event.getData());
                break;
            default:
                break;
        }
    }
});
pathChildrenCache.close();

//监听整个节点树的事件
TreeCache treeCache = new TreeCache(curator, "/node1");
treeCache.start();
treeCache.getListenable().addListener(new TreeCacheListener() {
    @Override
    public void childEvent(CuratorFramework client, TreeCacheEvent event) throws Exception {
        if (event.getData() != null) {
            System.out.println("type=" + event.getType() + " path=" + event.getData().getPath());
        } else {
            System.out.println("type=" + event.getType());
        }
    }
});
treeCache.close();

curator.close();

Leader选取

LeaderLatch

LeaderLatch选举模式的本质是连接ZooKeeper,然后在指定节点下为每个LeaderLatch创建临时有序节点,然后选取序列号最小的节点作为leader,其他节点则监听比当前节点序号小的节点的删除事件(这里用for循环模拟,实际的分布式环境中,应该是在每个应用节点中完成创建)

//创建CuratorFramework和LeaderLatch
List<CuratorFramework> clients = new ArrayList<>();
List<LeaderLatch> latchList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
    ExponentialBackoffRetry retryPolicy = new ExponentialBackoffRetry(1000, 3);
    CuratorFramework curator = CuratorFrameworkFactory.newClient("127.0.0.1:2181", retryPolicy);

    final LeaderLatch leaderLatch = new LeaderLatch(curator, "/latchPath", "client#" + i);
    leaderLatch.addListener(new LeaderLatchListener() {
        @Override
        public void isLeader() {
            System.out.println(leaderLatch.getId() + ":I am leader. I am doing jobs!");
        }

        @Override
        public void notLeader() {
            System.out.println(leaderLatch.getId() + ":I am not leader. I will do nothing!");
        }
    });
    clients.add(curator);
    latchList.add(leaderLatch);

    curator.start();
    //start后, LeaderLatch会和其它使用相同latch path的其它LeaderLatch交涉,然后随机的选择其中一个作为leader
    leaderLatch.start();
}

//等待选举完成
Thread.sleep(5000L);

while (latchList.size() > 0) {
    LeaderLatch currentLeader = null;
    //找出当前leader
    for (LeaderLatch leaderLatch : latchList) {
        if (leaderLatch.hasLeadership()) {
            currentLeader = leaderLatch;
            break;
        }
    }
    if (currentLeader != null) {
        System.out.println("当前leader是: " + currentLeader.getId());
        //模拟leader宕机,从剩下的节点中继续选举leader
        currentLeader.close();
        latchList.remove(currentLeader);
        //等待选举完成
        Thread.sleep(5000L);
    }
}

//回收资源
for (CuratorFramework client : clients) {
    CloseableUtils.closeQuietly(client);
}
LeaderElection

LeaderElection与LeaderLatch选举策略不同之处在于每个实例都能公平获取领导权,而且当获取领导权的实例在释放领导权之后,该实例还有机会再次获取领导权。另外,选举出来的leader不会一直占有领导权,当 takeLeadership(CuratorFramework client) 方法执行结束之后会自动释放领导权

//创建CuratorFramework和LeaderSelector
List<CuratorFramework> curators = new ArrayList<>();
List<LeaderSelector> selectors = new ArrayList<>();
for (int i = 0; i < 10; i++) {
    ExponentialBackoffRetry retryPolicy = new ExponentialBackoffRetry(1000, 3);
    CuratorFramework curator = CuratorFrameworkFactory.newClient("127.0.0.1:2181", retryPolicy);

    final int clientIndex = i;
    LeaderSelector leaderSelector = new LeaderSelector(curator, "/selectPath", new LeaderSelectorListenerAdapter() {
        final String name = "client#" + clientIndex;
        AtomicInteger leaderCount = new AtomicInteger();

        @Override
        public void takeLeadership(CuratorFramework client) throws Exception {
            System.out.println("当前leader是: " + name + ",之前被选举为leader的次数: " + leaderCount.getAndIncrement());
            //只要该方法不返回,当前节点就一直是leader,返回后会重新选举
            Thread.sleep(2000L);
        }
    });
    //自动重新排队,如果不设置,每个selector被选举为leader一次后就不再参加选举了
    leaderSelector.autoRequeue();

    curators.add(curator);
    selectors.add(leaderSelector);

    curator.start();
    //start后, leaderSelector会和其它使用相同select path的其它leaderSelector交涉,然后随机的选择其中一个作为leader
    leaderSelector.start();
}

//按任意键退出
System.in.read();

//释放资源
for (CuratorFramework client : curators) {
    CloseableUtils.closeQuietly(client);
}

分布式锁

在分布式的情况下,无法像JAVA中,使用对象标识一个锁,curator的做法是用节点来标识一个锁,其实现如下

  1. 客户端连接zookeeper,并在/lock下创建临时的且有序的子节点,第一个客户端对应的子节点为/lock/lock-0000000000,第二个为/lock/lock-0000000001,以此类推
  2. 客户端获取/lock下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,如果是则认为获得锁,否则监听刚好在自己之前一位的子节点的删除消息,获得子节点变更通知后重复此步骤直至获得锁
  3. 执行业务代码
  4. 完成业务流程后,删除对应的子节点释放锁

同时,为了避免连接中断时,客户端没有释放锁造成死锁,这些节点必须是临时节点,这样当连接中断时,一段时间过后会自动删除节点,使业务继续执行

curator提供了几种常用的锁

简单的使用

class Main {
    public static void main(String[] args) {
        //模拟10台主机并发访问共享资源,不同主机使用的锁对象是不一样的,所以只能通过zookeeper的节点标识一个锁
        for (int i = 0; i < 10; i++) {
            int workerIndex = i;
            new Thread(() -> {
                ExponentialBackoffRetry retryPolicy = new ExponentialBackoffRetry(1000, 3);
                CuratorFramework curator = CuratorFrameworkFactory.newClient("127.0.0.1:2181", retryPolicy);
                curator.start();

                Worker worker = new Worker(curator, new InterProcessMutex(curator, "/locks"), "client" + workerIndex);
                try {
                    worker.doWork();
                } catch (Exception e) {
                    e.printStackTrace();
                }

                curator.close();
            }).start();
        }
    }
}

class Worker {
    CuratorFramework curator;
    private final InterProcessMutex lock;
    private final String workerName;

    public Worker(CuratorFramework curator, InterProcessMutex lock, String workerName) {
        this.curator = curator;
        this.lock = lock;
        this.workerName = workerName;
    }

    public void doWork() throws Exception {
        //阻塞至获得锁为止
        lock.acquire();
        //使用超时机制就不会阻塞,获取失败直接返回false
        if (!lock.acquire(500, TimeUnit.MILLISECONDS)) {
            lock.release();//前面acquire了一次,这里要release一次,否则会出现死锁
            throw new IllegalStateException(workerName + "could not acquire the lock");
        }
        try {
            System.out.println(workerName + " has the lock");
            //操作没有加锁的共享资源
            //curator.create().forPath("/otherPath/node1");
            Thread.sleep(1000L);
        } finally {
            System.out.println(workerName + " releasing the lock");
            lock.release();//可重入锁,acquire几次就要release几次
            lock.release();
        }
    }
}

对于InterProcessReadWriteLock

class Worker {
    CuratorFramework curator;
    private final InterProcessReadWriteLock lock;
    private final InterProcessMutex readLock;
    private final InterProcessMutex writeLock;
    private final String workerName;

    public Worker(CuratorFramework curator, InterProcessReadWriteLock lock, String workerName) {
        this.curator = curator;
        this.lock = lock;
        this.readLock = lock.readLock();
        this.writeLock = lock.writeLock();
        this.workerName = workerName;
    }

    public void doWork(long time, TimeUnit unit) throws Exception {
        // 注意只能先得到写锁再得到读锁,不能反过来!!!
        if (!writeLock.acquire(time, unit)) {
            throw new IllegalStateException(workerName + " 不能得到写锁");
        }
        if (!readLock.acquire(time, unit)) {
            throw new IllegalStateException(workerName + " 不能得到读锁");
        }
        System.out.println(workerName + " 已得到读锁");
        try {
            //...
            Thread.sleep(1000);
        } finally {
            System.out.println(workerName + " 释放读写锁");
            readLock.release();
            writeLock.release();
        }
    }
}

对于InterProcessSemaphoreV2

ExponentialBackoffRetry retryPolicy = new ExponentialBackoffRetry(1000, 3);
CuratorFramework curator = CuratorFrameworkFactory.newClient("127.0.0.1:2181", retryPolicy);
curator.start();

InterProcessSemaphoreV2 semaphore = new InterProcessSemaphoreV2(curator, "/locks", 1000);

Collection<Lease> leases = semaphore.acquire(5);
Lease lease = semaphore.acquire();

//do something...

semaphore.returnLease(lease);
semaphore.returnAll(leases);

Dubbo

项目地址: apache/incubator-dubbo

Dubbo是一个WebService应用程序,它用于把service层和controller层分开。当访问量大的时候,可以使用分布式的service架构来处理请求

同类型的框架还有Apache CXF、Spring Cloud,它们都是遵循WebService规范(包括JAX-WS(Java API For XML-WebService)、JAX-RS(Java API for RESTful Web Services)、JAXM&SAAJ三种规范)

Dubbo分为注册中心(Registry)、服务提供者(Provider)、服务消费者(Consumer)、监视器(Monitor) 四个模块 注册中心有以下选择:

结合Spring使用

安装并启动注册中心后,新建Spring工程(两个,一个是服务提供者,一个是服务消费者),在Spring中配置Dubbo

服务提供者

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
       xsi:schemaLocation="http://www.springframework.org/schema/beans 
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://code.alibabatech.com/schema/context
       http://code.alibabatech.com/schema/context/context.xsd
       http://code.alibabatech.com/schema/dubbo 
       http://code.alibabatech.com/schema/dubbo/dubbo.xsd">

    <context:component-scan base-package="dao.repository" />

    <!-- Dubbo相关配置 -->
    <!-- dubbo注解扫描 -->
    <dubbo:annotation package="dao.repository" />
    <!-- 定义了提供方应用信息,用于计算依赖关系;在 dubbo-admin 或 dubbo-monitor 会显示这个名字,方便辨识 -->
    <dubbo:application name="demotest-provider" owner="programmer" organization="dubbox"/>
    <!-- 使用 zookeeper 注册中心暴露服务,注意要先开启 zookeeper -->
    <dubbo:registry address="zookeeper://127.0.0.1:2181"/>
    <!-- 如果有多个zookeeper主机,用逗号隔开
    <dubbo:registry protocol="zookeeper"  address="192.168.1.2:2181,192.168.1.3:2181,192.168.1.4:2181" /> -->

    <!-- 用dubbo协议在20880端口暴露服务 -->
    <dubbo:protocol name="dubbo" port="20880" />

    <!-- 由于使用了扫描注解,以下两个配置就不需要了
    <dubbo:service interface="dao.repository.IUserService" ref="userServiceImpl" protocol="dubbo" connections="100" timeout="2000" retries="0" cluster="failover" loadbalance="roundrobin" />
    具体实现该接口的 bean
    <bean id="userServiceImpl" class="dao.repository.impl.UserServiceImpl" /> -->
</beans>

service接口

package dao.repository;

public interface IUserService {
    public String getName(String str);
}

service实现类

package dao.repository.impl;

import org.springframework.stereotype.Component;
import com.alibaba.dubbo.config.annotation.Service;

@Component
//这里的@Service不是spring的@Service,而是Dubbo的@Service注解
@Service(interfaceClass=dao.repository.IUserService.class, protocol={"dubbo"}, timeout=10000, retries=0)
public class UserServiceImpl implements UserServiceImpl {
    @Override
    public String getName(String str) {
        return "张三" + str;
    }
}

服务消费者

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
       xsi:schemaLocation="http://www.springframework.org/schema/beans 
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://code.alibabatech.com/schema/context
       http://code.alibabatech.com/schema/context/context.xsd
       http://code.alibabatech.com/schema/dubbo 
       http://code.alibabatech.com/schema/dubbo/dubbo.xsd">

<!--
如果同时使用Dubbo和Spring的注解扫描,要注意让Dubbo先扫描,否则如果先扫controller,再扫dubbo的服务,会出现空指针,因为在controller实例化的时候是没有对应的dubbo的服务实例的,所以就会造成无法注入(就是说Dubbo并不会真正注入,而是把生成的服务Bean交给Spring,让Spring通过set的方式负责注入)

另一种解决方案是在Spring配置文件中配置:
 <dubbo:reference id="userService" interface="dao.repository.IUserService" check="false" />
然后在代码中直接使用
@Reference
private UserService userService;
-->
    <dubbo:annotation package="controller" />
    <context:component-scan base-package="controller" />

    <dubbo:application name="demotest-consumer"/>
    <!--向 zookeeper 订阅 provider 的地址,由 zookeeper 定时推送-->
    <dubbo:registry address="zookeeper://127.0.0.1:2181"/>

    <!-- 由于使用了注解扫描,下面的配置就不需要了
    <dubbo:reference id="userService" interface="dao.repository.IUserService" check="false" /> -->

    <!-- 为所有dubbo:reference配置通用属性时可以使用dubbo:consumer -->
    <dubbo:consumer timeout="3000"/>
</beans>

controller

package controller;

import com.alibaba.dubbo.config.annotation.Reference;

@Controller
@RequestMapping("/user")
public class UserController {
    
    //通过Dubbo的@Reference获取service实例,也可以使用ApplicationContext通过getBean获取
    @Reference(interfaceClass = IUserService.class)
    private UserService userService;
    
    @RequestMapping("getName")
    @ResponseBody
    public String getName(String str){
        return userService.getName("Hello");
    }
}

参数说明

cluster

cluster可以指定集群容错模式,可以在dubbo:referencedubbo:service中配置

loadbalance

loadbalance可以指定负载均衡策略,可以在dubbo:referencedubbo:service中配置,可选值为

check

check可以指定依赖检查,如果一个服务既是服务提供者,又是服务消费者,而且提供服务的实现类使用了其他服务(消费),则需要设置check="true"来确保依赖的服务先启动(check默认就是true)

对于立即加载的服务Bean,如果check="true",那么Dubbo会在启动时检查依赖的服务是否可用,不可用时会抛出异常,阻止Spring初始化完成,以便上线时,能及早发现问题;如果设置check="false",那么不会影响启动,后续服务初始化完成后依然可以正常连上

对于懒加载的服务Bean(比如使用了beanFactory.getBean("userService")),如果check="true",则在使用该Bean的时候(即Bean初始化时)才进行依赖检查,此时如果服务不可用,则会抛出异常,返回null,这时除非重启应用,否则使用时不会再次连上服务。所以对于懒加载的Bean,需要把设置check=false,这样当服务恢复时,依然可以正常连上

设计原则: 幂等性

上面提到使用failover策略时如果retries不为0时,可能造成重复服务,这就要求服务端的接口设计具有幂等性

幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次请求而产生了副作用,比如用户购买商品后支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额发现多扣钱了,流水记录也变成了两条,这样的系统就不具有幂等性

在以前的单应用系统中,我们只需要把数据操作放入事务中即可,发生错误立即回滚,但是在响应客户端的时候也有可能出现网络中断等异常

要保证单次操作和多次操作的结果一致,我们需要关心增加和修改操作(查询不会造成结果不一致,而删除多次对结果没有影响)

方法一:单次支付请求,不需要额外的数据库操作了,这个时候发起异步请求创建一个唯一的ticketId,就是门票,这张门票只能使用一次就作废,具体步骤如下

  1. 异步请求获取门票
  2. 调用支付,传入门票
  3. 根据门票ID查询此次操作是否存在,如果存在则表示该操作已经执行过,直接返回结果;如果不存在,支付扣款,保存结果
  4. 返回结果到客户端

如果步骤4通信失败,用户再次发起请求,那么最终结果还是一样的

方法二:给订单设置支付状态

  1. 查询订单支付状态
  2. 如果已经支付,直接返回结果
  3. 如果未支付,则支付扣款并且保存流水
  4. 返回支付结果

泛化调用

有时我们在服务消费方没有对应的服务接口,这时可以通过泛化调用来使用服务

服务提供方代码不变,服务消费方需添加generic="true"属性:

<dubbo:reference id="userService" interface="dao.repository.IUserService" generic="true"/>

通过GenericService使用服务:

ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
context.start();

//对于没有重裁的方法,参数类型可以不指定,传null即可;但方法重载的情况下,必须指定参数类型
GenericService service = (GenericService) context.getBean("userService");
Object result = service.$invoke("getName", new String[] { "java.lang.String" }, new Object[]{ "Hello" });
System.out.println(result);

如果服务消费方不使用Spring,可以:

//普通编码配置方式  
ApplicationConfig application = new ApplicationConfig();
application.setName("demotest-consumer");

//连接注册中心配置
RegistryConfig registry = new RegistryConfig();
registry.setAddress("zookeeper://127.0.0.1:2181");

ReferenceConfig<GenericService> reference = new ReferenceConfig<GenericService>();
reference.setApplication(application);
reference.setRegistry(registry);
reference.setTimeout(6000);
reference.setInterface("dao.repository.IUserService");
reference.setGeneric(true); //声明为泛化接口

//GenericService创建的开销很大,所以使用了缓存机制
ReferenceConfigCache cache = ReferenceConfigCache.getCache();
GenericService genericService = cache.get(reference);
//GenericService genericService = (GenericService) reference.get();

//基本类型以及Date,List,Map等不需要转换,直接调用
Object result = genericService.$invoke("getName", new String[] { "java.lang.String" }, new Object[] { "Hello" });
System.out.println(result);

后台管理页面

dubbo-admin可以展示服务提供者、消费者、路由信息、权重等信息,以及修改服务提供者,消息者的属性,以达到服务治理的目的

dubbo-2.6.0及之前的版本

下载源码: dubbo-2.6.0

找到dubbo-admin工程,进入dubbo-admin文件夹,使用maven命令mvn package -Dskiptest=true打包成war文件,并放到tomcat目录下

如果dubbo和注册中心不在同一台主机,先启动一次tomcat,让tomcat解压war包,然后修改webapps-admin-2.6.0-INF下的dubbo.properties文件,设置注册中心

dubbo.registry.address=zookeeper://127.0.0.1:2181
dubbo.admin.root.password=root
dubbo.admin.guest.password=guest

重启tomcat,浏览器打开对应页面登录即可

dubbo-2.6.1及之后的版本

# 安装tomcat
wget https://archive.apache.org/dist/tomcat/tomcat-6/v6.0.35/bin/apache-tomcat-6.0.35.tar.gz
tar zxvf apache-tomcat-6.0.35.tar.gz
cd apache-tomcat-6.0.35
rm -rf webapps/ROOT

# dubbo-admin在2.6.0后改名为dubbo-ops
git clone https://github.com/dubbo/dubbo-ops.git
/var/tmp/dubbo-ops
pushd /var/tmp/dubbo-ops
mvn clean package
popd

# 解压到tomcat目录
unzip /var/tmp/dubbo-ops/dubbo-admin/target/dubbo-admin-2.0.0.war -d webapps/ROOT

配置 dubbo.properties 把 dubbo 注解中心地址配置成真正项目里面的注册中心

vi webapps/ROOT/WEB-INF/dubbo.properties

dubbo.properties

dubbo.registry.address=zookeeper://127.0.0.1:2181
dubbo.admin.root.password=root
dubbo.admin.guest.password=guest

启动、停止

./bin/startup.sh

./bin/shutdown.sh

然后可以通过: http://127.0.0.1:8080/ 进行访问

Dubbox

项目地址: dangdangdotcom/dubbox

Dubbox是在Dubbo的基础上添加了REST、JSON、Kryo等支持

RESTful

接口

public class UserService {
    void registerUser(User user);

    User getUser(Long id);
}

实现(注解写在实现或接口均可,建议写在实现上;写在实现上的注解会覆盖接口上的)

@Path("users")
@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_XML})//该URL可以使用JSON或XML格式返回
@Consumes({ContentType.APPLICATION_JSON_UTF_8, ContentType.TEXT_XML_UTF_8})//该URL可以使用JSON或XML格式的参数;如果出现乱码,可以指定编码为UTF-8
public class UserServiceImpl implements UserService {

    @POST
    @Path("register")
    @Consumes({MediaType.APPLICATION_JSON})//使用JSON类型的参数,自动映射成POJO
    public void registerUser(User user) {
        // save the user...
    }

    @GET
    @Path("getuser")
    @Produces({"application/json", "text/xml"})
    public void User getUser(@PathParam("id") Long id){
        return new User();
    }
}

如果需要上下文信息

//方法一: 使用@Context获取
public User getUser(@PathParam("id") Long id, @Context HttpServletRequest request) {
    System.out.println("Client address is " + request.getRemoteAddr());
}

//方法一: 使用RpcContext获取
public User getUser(@PathParam("id") Long id) {
    System.out.println("Client address is " + RpcContext.getContext().getRemoteAddressString());
}

此时User类需实现序列化接口

@XmlRootElement//使用XML
@XmlAccessorType(XmlAccessType.FIELD)
public class User implements Serializable {

    @XmlElement(name="username")
    private String name;

    /*使用JSON
    @JsonProperty("username")
    private String name;
    */
}

配置REST协议,server可以使用tomcatjettynettyservlet

<!-- 使用外部的服务 -->
<dubbo:protocol name="rest" server="servlet"/>

<!-- 使用内置的服务配置端口即可 -->
<dubbo:protocol name="rest" port="8888"/>

如果使用servlet还需要配置web.xml

<web-app>
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/classes/META-INF/spring/dubbo-demo-provider.xml</param-value>
    </context-param>

    <!-- dubbo的listener的必须放在Spring的listener前面 -->
    <listener>
        <listener-class>com.alibaba.dubbo.remoting.http.servlet.BootstrapListener</listener-class>
    </listener>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>com.alibaba.dubbo.remoting.http.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>
</web-app>

ActiveMQ

下载ActiveMQ

ActiveMQ是Apache的基于JMS的消息中间件,同类型的有RabbitMQ(支持AMQP事务处理),Kafka(使用zero-copy机制,高吞吐量(tps))

ActiveMQ支持点对点(queue)和一对多(topic)两种消息机制

启动

apache-activemq-5.15.7/bin/activemq start

登录后台: http://127.0.0.1:8161/admin/ (默认账号: admin,密码: admin,可以在conf/jetty.xml中配置)

开发时可以使用ActiveMQ解压目录下的activemq.jar,也可以使用maven

pom.xml

<dependencies>
    <!-- https://mvnrepository.com/artifact/org.apache.activemq/activemq-core -->
    <dependency>
        <groupId>org.apache.activemq</groupId>
        <artifactId>activemq-core</artifactId>
        <version>5.7.0</version>
    </dependency>
</dependencies>

使用(注意要让消费者先启动):

class Test {
    @Test
    public void test1() {
        //ActiveMq 的默认用户名、密码、连接地址
        String username = ActiveMQConnection.DEFAULT_USER;
        String password = ActiveMQConnection.DEFAULT_PASSWORD;
        String brokenUrl = ActiveMQConnection.DEFAULT_BROKER_URL;//默认为failover://tcp://localhost:61616

        //连接工厂
        ConnectionFactory connectionFactory;
        //连接接对象
        Connection connection = null;
        //事务管理
        Session session = null;

        try {
            //用户名密码可以在conf/jetty-realm.properties中配置
            connectionFactory = new ActiveMQConnectionFactory(username, password, brokenUrl);
            connection = connectionFactory.createConnection();
            connection.start();

            //第一个参数指定是否使用事务,第二个参数为应答方式,使用事务时为Session.SESSION_TRANSACTED
            //生产者和消费者的session模式可以不同,但最好一致
            session = connection.createSession(true, Session.SESSION_TRANSACTED);

            /*
            //使用点对点
            Destination destination = session.createQueue("name");
            producer = session.createProducer(destination);
            //设置持久化传输(对该生产者有效),持久化可以通过conf/activemq.xml中的persistenceAdapter属性配置
            producer.setDeliveryMode(DeliveryMode.PERSISTENT);
            */
            //使用一对多
            //producter和consumer要使用同一个topic才能收到消息
            Topic topic = session.createTopic("topic1");
            TextMessage message = session.createTextMessage("消息1");
            /*
            //设置持久化传输(对该消息有效)
            message.setJMSDeliveryMode(DeliveryMode.PERSISTENT);
            */
            //生产者
            MessageProducer producter = session.createProducer(topic);
            producter.send(message);
            session.commit();

            System.out.println(Thread.currentThread().getName() + "--发送了消息");
        } catch (JMSException e) {
            e.printStackTrace();
        } finally {
            if (connection != null) {
                try {
                    connection.close();
                } catch (JMSException e) {
                    e.printStackTrace();
                }
            }
            if (session != null) {
                try {
                    session.close();
                } catch (JMSException e) {
                    e.printStackTrace();
                }
            }
        }

    }

    @Test
    public void test2() {
        //ActiveMq 的默认用户名、密码、连接地址
        String username = ActiveMQConnection.DEFAULT_USER;
        String password = ActiveMQConnection.DEFAULT_PASSWORD;
        String brokenUrl = ActiveMQConnection.DEFAULT_BROKER_URL;

        //连接工厂
        ConnectionFactory connectionFactory;
        //连接接对象
        Connection connection = null;
        //事务管理
        Session session = null;

        try {
            connectionFactory = new ActiveMQConnectionFactory(username, password, brokenUrl);
            connection = connectionFactory.createConnection();
            connection.start();

            //第一个参数指定是否使用事务,第二个参数为应答方式,这里使用Session.CLIENT_ACKNOWLEDGE,需要我们手动应答,这时失败可以重发(也可以设置成Session.AUTO_ACKNOWLEDGE,但可能会丢失信息)
            session = connection.createSession(false, Session.CLIENT_ACKNOWLEDGE);

            //producter和consumer要使用同一个topic才能收到消息
            Topic topic = session.createTopic("topic1");
            TextMessage message = session.createTextMessage("消息1");
            //消费者
            MessageConsumer consumer = session.createConsumer(topic);
            /*
            消费者可以指定接收何种类型的message,这里可以支持SQL的条件语法(AND、OR、IN...)
            MessageConsumer consumer = session.createConsumer(destination,"JMSXGroupID='A'");
            此时生产者需要:message.setStringProperty("JMSXGroupID","A");
            */
            final Session finalSession = session;
            consumer.setMessageListener(new MessageListener() {
                public void onMessage(Message message) {
                    try {
                        System.out.println(Thread.currentThread().getName() + "--收到: -" + ((TextMessage) message).getText());
                        //手动应答
                        message.acknowledge();
                    } catch (Exception e) {
                        e.printStackTrace();
                        try {
                            System.out.println(Thread.currentThread().getName() + "--接收失败,正在重试");
                            //失败时重发
                            finalSession.recover();
                        } catch (JMSException e1) {
                            e1.printStackTrace();
                        }
                    }
                }
            });
            //如果线程结束,则无法接收到发过来的消息
            while (true) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        } catch (JMSException e) {
            e.printStackTrace();
        } finally {
            if (connection != null) {
                try {
                    connection.close();
                } catch (JMSException e) {
                    e.printStackTrace();
                }
            }
            if (session != null) {
                try {
                    session.close();
                } catch (JMSException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}

RabbitMQ

RabbitMQ基于ErLang开发,安装前需安装ErLang环境

配置文件

/etc/rabbitmq/rabbitmq-env.conf关键参数:

参数 作用
RABBITMQ_NODENAME 节点名称
RABBITMQ_NODE_IP_ADDRESS 监听IP,一般设置为127.0.0.1
RABBITMQ_NODE_PORT 监听端口,一般设置为5672
RABBITMQ_LOG_BASE 日志目录
RABBITMQ_PLUGINS_DIR 插件目录
RABBITMQ_MNESIA_BASE 后端存储目录

/etc/rabbitmq/rabbitmq.config关键参数:

参数 作用
tcp_listerners 设置rabbimq的监听端口,默认为[5672]
disk_free_limit 磁盘低水位线,若磁盘容量低于指定值则停止接收数据,默认值为{mem_relative, 1.0},即与内存相关联1:1,也可定制为多少byte
vm_memory_high_watermark 设置内存低水位线,若低于该水位线,则开启流控机制,默认值是0.4,即内存总量的40%
hipe_compile 将部分rabbimq代码用High Performance Erlang compiler编译,可提升性能,该参数是实验性,若出现erlang vm segfaults,应关掉
force_fine_statistics 该参数属于rabbimq_management,若为true则进行精细化的统计,但会影响性能

基本使用

# 启动与停止(rabbitmq-server和rabbitmqctl均可)
rabbitmq-server start
rabbitmqctl stop_app

# 开启图形化管理插件(http://127.0.0.1:15672,默认用户名密码都是guest)
rabbitmq-plugins enable rabbitmq_management

也可以通过命令行管理

# 用户管理
rabbitmqctl add_user username password
rabbitmqctl list_users
rabbitmqctl change_password username newpassword
rabbitmqctl delete_user username

# 用户权限管理
rabbitmqctl set_permissions -p vhost username ".*" ".*" ".*"
rabbitmqctl list_user_permissions username
rabbitmqctl clear_permissions -p vhostpath username

# 虚拟主机
rabbitmqctl add_vhost vhostpath
rabbitmqctl list_vhosts
rabbitmqctl list_permissions -p vhostpath
rabbitmqctl delete_vhost vhostpath

# 消息队列
rabbitmqctl list_queues
rabbitmqctl -p vhostpath purge_queue blue # 清除队列里的消息

# 其他
# 清除所有数据,要在rabbitmqctl stop_app之后使用
rabbitmqctl reset

# 集群
# 加入集群(会重置当前节点),可以指定数据存在内存中还是磁盘中
rabbitmqctl join_cluster clusternode [--ram | --disc]
# 修改集群节点存储形式(必须先停止该节点)
rabbitmqctl change_cluster_node_type disc|ram
# 修改节点名称
rabbitmqctl rename_cluster_node oldnode1 newnode1 [oldnode2 newnode2 ...]
# 从集群中删除节点(下次使用被删除的节点需先reset再start_app)
# 情况一:slave节点宕机可以在任意在线的节点上直接使用forget命令剔除
# 情况二:master和slave节点同时宕机,且master无法恢复,导致slave也无法重新启动,则需添加--offline参数,在所有节点都离线的状态下剔除master节点,这样slave节点才能正常的重新启动(此时会在slave节点中选取一个当master,并把原来的主节点剔除)
# 情况三:master和slave同时宕机,且都无法恢复,但可以访问其中一个的磁盘文件,则需拷贝$RABBIT_HOME/var/lib中的数据库文件到新的节点上,并把新节点的hostname改成和原来的一致,如果可以访问的是master中的磁盘文件,则按情况一处理(直接forget),如果可以访问的是slave中的磁盘文件,则按情况二处理(添加--offline参数)
rabbitmqctl forget_cluster_node nodename [--offline]
rabbitmqctl cluster_status

消息发送与接收

//消费者
new Thread(new Runnable(){
    public void run(){
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("127.0.0.1");
        connectionFactory.setPort(5672);
        //同一个vhost中不能有同名的exchange或同名的queue
        connectionFactory.setVirtualHost("/");

        Connection connection = connectionFactory.newConnection();
        Channel channel = connection.createChannel();

        String queueName = "test001";
        channel.queueDeclare(queueName, true, false, false, null);
        QueueingConsumer queueingConsumer = new QueueingConsumer(channel);
        channel.basicConsume(queueName, true, queueingConsumer);

        while(true) {
            Delivery delivery = queueingConsumer.nextDelivery();
            System.out.println(new String(delivery.getBody()));
        }
    }
}).start();

Thread.sleep(500L);    //确保消费者先启动

//生产者
new Thread(new Runnable(){
    public void run(){
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("127.0.0.1");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");

        Connection connection = connectionFactory.newConnection();
        Channel channel = connection.createChannel();

        for(int i = 0; i < 5; i++) {
            String msg = "Hello world";
            channel.basicPublish("", "test001", null, msg.getBytes());
        }

        channel.close();
        connection.close();
    }
}).start();

消息投递方式

消息投递方式由exchangeType控制,不同的exchangeType匹配不同类型的routingKey,而每个queue对应一个routingKey(不同的queue可以使用同一个routingKey)

exchange有以下类型: - direct: 直接根据routingkey全匹配 - topic: routingkey可以使用通配符,#匹配一个或多个单词,*只匹配一个单词(比如user.#可以匹配到user.name.update,而user.*只能匹配到user.name) - fanout: 不管routingkey,把消息发送到所有绑定到当前exchange的queue

//消费者
new Thread(new Runnable(){
    public void run(){
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("127.0.0.1");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");

        connectionFactory.setAutomaticRecoveryEnabled(true);
        connectionFactory.setNetworkRecoveryInterval(3000);

        Connection connection = connectionFactory.newConnection();
        Channel channel = connection.createChannel();

        String exchangeName = "test_exchange";
        String exchangeType = "direct";     //direct、topic、fanout
        String queueName = "test001";
        String routingKey = "user";         //根据exchangeType不同可以换成user.#、user.*等形式


        //exchange和queue的声明放在生产端或消费端都可以(如果已有同名且属性相同不会重复创建;但如果属性不同,重复声明可能会有问题)
        //exchangeDeclare(exchange, type, durable /*持久化*/, autoDelete, arguments)
        channel.exchangeDeclare(exchangeName, exchangeType, true, false, false, null);

        //queueDeclare(queue, durable, exclusive /*独占,queue只对当前connection可见*/, autoDelete /*断开连接后是否自动删除,设置exclusive后一般也要设置这个*/, arguments  /*可以设置TTL(过期时间)、数量限制等,可以使用图形化的管控台查看所有选项*/)
        channel.queueDeclare(queueName, false, false, false, null);

        channel.queueBind(queueName, exchangeName, routingKey);


        QueueingConsumer queueingConsumer = new QueueingConsumer(channel);
        channel.basicConsume(queueName, true, queueingConsumer);

        while(true) {
            Delivery delivery = queueingConsumer.nextDelivery();
            Map<String, Object> map = delivery.getProperties().getHeaders();
            System.out.println(new String(delivery.getBody()) + "--" + map.get("mypro1"));
        }
    }
}).start();

//生产者
new Thread(new Runnable(){
    public void run(){
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("127.0.0.1");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");

        Connection connection = connectionFactory.newConnection();
        Channel channel = connection.createChannel();

        Map<String, Object> map = new HashMap<>();
        map.put("mypro1", "aaa");

        //message附加属性
        AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
                .deliveryMode(2)        //持久化消息
                .contentEncoding("UTF-8")
                .expiration("10000")    //10s后过期
                .headers(map)           //自定义属性
                .build();

        String exchangeName = "test_exchange";
        String routingKey1 = "user";
        String routingKey2 = "user.name";
        String routingKey3 = "user.name.save";
        String routingKey4 = "xxxxx";

        channel.basicPublish(exchangeName, routingKey1, properties, "Hello direct".getBytes());
        channel.basicPublish(exchangeName, routingKey2, properties, "Hello topic1".getBytes());
        channel.basicPublish(exchangeName, routingKey3, properties, "Hello topic2".getBytes());
        channel.basicPublish(exchangeName, routingKey4, properties, "Hello fanout".getBytes());

        channel.close();
        connection.close();
    }
}).start();

可靠性投递

可靠性投递

方案一:生产端数据入库,并对消息状态打标(两次入库),为避免消费端接收完消息就故障,还需要设置超时时间(分布式定时任务检测超时没有ack的消息并重新发送),有时因为routingkey不对或其他原因导致有些消息确实无法正确发送,这时还要设置最大重试次数

方案二:生产端数据入库然后立即发送第一条消息到消费端,然后生成第二条消息并做延迟投递(RabbitMQ实现可以用rabbitmq-delayed-message-exchange插件,或消息加TTL然后在死信队列中处理),投递到回调服务器中,消费端处理完消息后生成一条确认消息,发送到回调服务器,而回调服务器收到生产端的延迟投递的消息后查找是否有对应的确认消息,如果没有则启用补偿机制,与生产端进行RPC通信,要求重发消息

对比:

  1. 第一种方案的缺点是多次操作数据库,导致性能不好,而且无法得知消费端是处理完消息后才故障还是接收到消息就故障了,还需再次对消息做幂等的处理

  2. 第二种方案的缺点是如果延迟的时间太短,可能消息还没处理完,回调就已经发送了RPC指令要求重发,可能有重复消费的问题,优点是把对消息状态的打标放到了回调服务器中,减轻了生产端的压力

幂等

方案一:利用数据库主键唯一性实现,可以通过唯一ID+指纹码做主键,同时还可以通过对ID进行分片解决性能瓶颈

方案二:利用Redis原子特性实现(比如自增),但是如果数据需要持久化,无法保证数据库和Redis同时成功或同时失败

其他问题

合并消息:使用序列化工具,把多条消息放到一个Map或自定义的对象中,在消费端再反序列化

顺序消息:自定义header告知消费端消息总数及id,消费端接收到消息后不立即处理,而是存储到数据库中,然后发一个给自己的延迟消息,等消息全部接收完成后,在延迟消息中检测消息是否完整,如果不完整,做补偿机制,完整则开始处理消息(或者把多条顺序消息合并再发送也可以实现)

消息监听

生产端使用ConfirmListener监听消费端的ack应答,使用ReturnListener监听错误的routingkey或其他原因导致消息不可达的情况

消费端自定义继承自DefaultConsumer的类可以通过回调的方式监听接收的消息

class MyConsumer extends DefaultConsumer {
    public MyConsumer(Channel channel) {
        super(channel);
    }

    public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body) throw IOException {
        System.out.println(new String(body));
        System.out.println(envelope);
    }
}


//消费者
new Thread(new Runnable(){
    public void run(){
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("127.0.0.1");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");

        Connection connection = connectionFactory.newConnection();
        Channel channel = connection.createChannel();

        channel.exchangeDeclare("test_exchange", "topic", true, false, false, null);
        channel.queueDeclare("test_queue", true, false, false, null);
        channel.queueBind("test_queue", "test_exchange", "confirm.#");

        //第二个参数为是否自动ack
        channel.basicConsume("test_queue", true, new MyConsumer(channel));
    }
}).start();

//生产者
new Thread(new Runnable(){
    public void run(){
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("127.0.0.1");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");

        Connection connection = connectionFactory.newConnection();
        Channel channel = connection.createChannel();

        //启用消息确认监听
        channel.confirmSelect();

        //添加消息确认监听
        channel.addConfirmListener(new ConfirmListener(){
            public void handleNack(long deliveryTag, boolean multiple) throw IOException {
                System.out.println("Not ACK");
            }
            public void handleAck(long deliveryTag, boolean multiple) throw IOException {
                System.out.println("ACK");
            }
        });

        //添加不可达消息监听
        channel.addReturnListener(new ReturnListener(){
            public void handleReturn(int replyCode, String replyText, String exchange, String routingkey, BasicProperties properties, byte[] body) throw IOException {
                System.out.println("return");
            }
        });

        //发送正常消息
        channel.basicPublish("test_exchange", "confirm.save", null, "Hello confirm".getBytes());

        //发送不可达消息
        //第三个参数为mandatory:当消息不可达时是放到returnListener中(true)还是直接删除(false)
        channel.basicPublish("test_exchange", "abc.xxx", true, null, "Hello confirm".getBytes());


        //如果close就无法收到消息确认的应答了
        //channel.close();
        //connection.close();
    }
}).start();

消费端限流

如果RabbitMQ中堆积了很多的未处理消息,这时如果直接打开消费端会导致严重的线上故障,这时可以通过消费端限流保证接收的消息不超过一定数量

在非自动签收的前提下,可以通过给Channel或Consumer设置Qos(服务质量保证),来限制未被确认的消息超过一定数量就不再消费新的消息

class MyConsumer extends DefaultConsumer {
    private Channel channel;

    public MyConsumer(Channel channel) {
        super(channel);
        this.channel = channel;
    }

    public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body) throw IOException {
        System.out.println(new String(body));

        //手动签收
        //basicAck(deliveryTag, multiple /*是否批量签收*/)
        channel.basicAck(envelope.getDeliveryTag(), false);

        //拒签,一般放到catch中,表示处理失败
        //basicNack(deliveryTag, multiple, requeue /*是否重新放回消息队列中(尾端),如果有些代码确实无法成功执行,会导致一直尝试,一般不使用*/)
        //channel.basicNack(envelope.getDeliveryTag(), false, false);
    }
}


//消费者
new Thread(new Runnable(){
    public void run(){
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("127.0.0.1");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");

        Connection connection = connectionFactory.newConnection();
        Channel channel = connection.createChannel();

        channel.exchangeDeclare("test_exchange", "topic", true, false, null);
        channel.queueDeclare("test_queue", true, false, false, null);
        channel.queueBind("test_queue", "test_exchange", "qos.#");

        //开启限流
        //basicQos(prefetchSize /*消息大小限制(0表示不限制)*/, prefetchCount /*消息数量限制*/, global /*限制对当前channel有效(true)还是对consumer有效(false)*/);
        channel.basicQos(0, 1, false);

        //第二个参数为是否auto_ack,使用限流要设置为false
        channel.basicConsume("test_queue", false, new MyConsumer(channel));
}).start();

死信队列

死信队列(DLX,Dead-Letter-Exchange),当投递到当前queue的消息没有被正确消费时,会自动重新publish到一个被设置为DLX的exchange中(在queue的arguments中添加x-dead-letter-exchange参数,指定投递到当前queue的消息变成死信时重新publish到哪个exchange中)

消息变成死信有如下情况: 1. 消费端调用basicReject或basicNack,且requeue为false 2. 消息TTL过期 3. 队列达到最大长度

//消费者
new Thread(new Runnable(){
    public void run(){
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("127.0.0.1");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");

        Connection connection = connectionFactory.newConnection();
        Channel channel = connection.createChannel();

        Map<String, Object> arguments = new HashMap<>();
        arguments.put("x-dead-letter-exchange", "dlx.exchange");

        //声明普通exchange和queue
        channel.exchangeDeclare("test_exchange", "topic", true, false, null);
        channel.queueDeclare("test_queue", true, false, false, arguments);
        channel.queueBind("test_queue", "test_exchange", "dlx.#");
        
        //声明用于处理死信的exchange和queue
        channel.exchangeDeclare("dlx.exchange", "topic", true, false, null);
        channel.queueDeclare("dlx.queue", true, false, false, null);
        channel.queueBind("dlx.queue", "dlx.exchange", "#");

        channel.basicConsume("test_queue", true, new MyConsumer(channel));
    }
}).start();

集群搭建

RabbitMQ

RabbitMQ配置

# 停止所有节点(每个节点上都执行)
rabbitmqctl stop_app

# 把cookie复制到其他节点,同步数据,然后把文件权限还原成400(当前节点是192.168.1.71)
/etc/init.d/rabbitmq-server stop
scp /var/lib/rabbitmq/.erlang.cookie 192.168.1.72:/var/lib/rabbitmq/
scp /var/lib/rabbitmq/.erlang.cookie 192.168.1.73:/var/lib/rabbitmq/

# 所有节点用detached方式启动(每个节点上都执行)
rabbitmq-server -detached

# slave加入集群(node72是hostname)
node72: rabbitmqctl stop_app
node72: rabbitmqctl join_cluster rabbit@node71
node72: rabbitmqctl start_app

node73: rabbitmqctl stop_app
node73: rabbitmqctl join_cluster rabbit@node71
node73: rabbitmqctl start_app

# 修改集群名称(默认为第一个node名称),可以在集群中的任意一个节点中修改
#rabbitmqctl set_cluster_name rabbitmq_cluster1
# 删除节点
#rabbitmqctl forget_cluster_node rabbit@node73

# 查看集群状态
rabbitmqctl cluster_status

# 执行镜像策略,使用镜像队列的集群模式,让队列在节点之间复制
rabbitmqctl set_policy ha-all "^" '{"ha_mode":"all"}'

HAProxy

使用HAProxy做负载均衡

# 下载haproxy
wget http://www.haproxy.org/download/1.6/src/haproxy-1.6.5.tar.gz

# 解压
tar -zxvf haproxy-1.6.5.tar.gz -C /usr/local

# 进入目录、进行编译、安装
cd /usr/local/haproxy-1.6.5
make TARGET=linux31 PREFIX=/usr/local/haproxy
make install PREFIX=/usr/local/haproxy
mkdir /etc/haproxy

# 赋权
groupadd -r -g 149 haproxy
useradd -g haproxy -r -s /sbin/nologin -u 149 haproxy

# 创建haproxy配置文件
touch /etc/haproxy/haproxy.cfg

haproxy.cfg内容如下

#logging options
global
    log 127.0.0.1 local0 info
    maxconn 5120
    chroot /usr/local/haproxy
    uid 99
    gid 99
    daemon
    quiet
    nbproc 20
    pidfile /var/run/haproxy.pid

defaults
    log global
    #使用4层代理模式,”mode http”为7层代理模式
    mode tcp
    #if you set mode to tcp,then you nust change tcplog into httplog
    option tcplog
    option dontlognull
    retries 3
    option redispatch
    maxconn 2000
    contimeout 5s
    ##客户端空闲超时时间为 60秒 则HA发起重连机制
    clitimeout 60s
    ##服务器端链接超时时间为 15秒 则HA发起重连机制
    srvtimeout 15s
#front-end IP for consumers and producters

listen rabbitmq_cluster
    bind 0.0.0.0:5672
    #配置TCP模式
    mode tcp
    #balance url_param userid
    #balance url_param session_id check_post 64
    #balance hdr(User-Agent)
    #balance hdr(host)
    #balance hdr(Host) use_domain_only
    #balance rdp-cookie
    #balance leastconn
    #balance source //ip
    #简单的轮询
    balance roundrobin
    #rabbitmq集群节点配置 #inter 每隔五秒对mq集群做健康检查, 2次正确证明服务器可用,2次失败证明服务器不可用,并且配置主备机制
        server node71 192.168.1.71:5672 check inter 5000 rise 2 fall 2
        server node72 192.168.1.72:5672 check inter 5000 rise 2 fall 2
        server node73 192.168.1.73:5672 check inter 5000 rise 2 fall 2

#配置haproxy web监控,查看统计信息
listen stats
    bind 192.168.1.74:8100
    mode http
    option httplog
    stats enable
    #设置haproxy监控地址为http://localhost:8100/rabbitmq-stats
    stats uri /rabbitmq-stats
    stats refresh 5s

启动HAProxy,然后访问http://192.168.1.74:8100/rabbitmq-stats

/usr/local/haproxy/sbin/haproxy -f /etc/haproxy/haproxy.cfg

# 查看haproxy进程状态
ps -ef | grep haproxy

# 关闭
#killall haproxy

跨集群通信

要在多个集群之间发送消息,则需要使用federation插件

  1. 启用插件(在所有集群中的所有主机都启用该插件(需下载并放到插件目录))

    rabbitmq-plugins enable rabbitmq_federation
    
    # 开启web管理页面支持
    rabbitmq-plugins enable rabbitmq_federation_management
  2. 然后在下游RabbitMQ中创建用于跨集群通信的exchange和queue并绑定

  3. 再在下游RabbitMQ的web管理页面的Admin选项卡中创建新的Upstreams(指定上游的RabbitMQ的URL,消息从指定的上游RabbitMQ转发到下游的RabbitMQ)及Policies(匹配策略,Pattern使用正则匹配exchange及queue的名字,Definition设置federation-upstream-setall

  4. 这时就可以在上游的RabbitMQ中看到在下游创建的exchange,通过该exchange发送的消息就可以被下游的queue接收

  5. 如果发送的消息想在本地也能接收,只需在该exchange中绑定一个本地的queue即可

Shiro

Apache Shiro

Shiro是一个权限管理框架

几个概念

环境搭建

创建Spring MVC工程

src/main
├── java
│   └── controller
│       └── User.java
├── resources
│   ├── applicationContext.xml
│   ├── log4j.properties
│   └── shiro.ini
└── webapp
    ├── index.jsp
    └── WEB-INF
        ├── jsp
        │   ├── admin_info.jsp
        │   ├── login.jsp
        │   ├── unauthorized.jsp
        │   └── user_info.jsp
        └── web.xml

Shiro依赖(maven)

<!-- Shiro -->
<dependency>
    <groupId>net.sf.ehcache</groupId>
    <artifactId>ehcache</artifactId>
    <version>2.10.6</version>
</dependency>

<!--  Shiro uses SLF4J for logging. -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>1.6.1</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.6.1</version>
</dependency>

shiro.ini

# username = password, role1, role2, ..., roleN
# 分为管理员和普通用户
[users]
root = root, admin, user
user = 123456, user

# roleName = permission1, permission2, ..., permissionN
# 可以使用通配符*,表示匹配零个或多个字符
[roles]
admin = *
user = user

代码使用

controller中User.java

@Controller
@RequestMapping("/user")
public class User {

    @RequestMapping("/login")
    public String login() {
        return "login";
    }

    @RequestMapping(value = "/dologin", method = RequestMethod.POST)
    public String doLogin(String username, String password, HttpSession session) {
        System.out.println(username + "--" + password);

        Factory factory = new IniSecurityManagerFactory("classpath:shiro.ini");
        SecurityManager securityManager = (SecurityManager) factory.getInstance();
        SecurityUtils.setSecurityManager(securityManager);

        //每个用户就是一个subject
        Subject currentUser = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        //是否记住用户
        token.setRememberMe(true);

        try {
            //登录
            currentUser.login(token);

            //如果登录成功
            if (currentUser.isAuthenticated()) {
                //有管理员权限
                if (currentUser.isPermitted("admin")) {
                    //shiro的session和servlet的session是不一样的
                    //currentUser.getSession().setAttribute("role", "admin");

                    session.setAttribute("role","admin");
                    return "admin_info";
                }
                //普通用户
                if (currentUser.hasRole("user")) {
                    session.setAttribute("role","admin");
                    return "user_info";
                }
            }
        } catch (UnknownAccountException uae) {
            System.out.println("账户不存在");
        } catch (IncorrectCredentialsException ice) {
            System.out.println("密码不正确");
        } catch (LockedAccountException lae) {
            System.out.println("用户被锁定了 ");
        } catch (AuthenticationException ae) {
            //无法判断是什么错了
            System.out.println(ae.getMessage());
        }
        return "unauthorized";
    }

    @RequestMapping("/userinfo")
    public String userInfo(@SessionAttribute(required = false) String role) {
        if (role != null && (role.equals("user") || role.equals("admin")))
            return "user_info";
        else
            return "unauthorized";
    }

    @RequestMapping("/admininfo")
    public String adminInfo(@SessionAttribute(required = false) String role) {
        if (role != null && role.equals("admin"))
            return "admin_info";
        else
            return "unauthorized";
    }
}

与Spring MVC整合

目录结构

src/main
├── java
│   ├── controller
│   │   └── User.java
│   └── shiro
│       ├── FilterChainDefinitionMapBuilder.java
│       └── realms
│           ├── FirstRealm.java
│           └── SecondRealm.java
├── main.iml
├── resources
│   ├── applicationContext.xml
│   ├── ehcache.xml
│   └── log4j.properties
└── webapp
    ├── index.jsp
    └── WEB-INF
        ├── jsp
        │   ├── admin_info.jsp
        │   ├── info.jsp
        │   ├── login.jsp
        │   ├── unauthorized.jsp
        │   └── user_info.jsp
        └── web.xml

pom.xml

在maven添加对web、Spring、Ehcache的依赖

<properties>
    <shiro-version>1.4.0</shiro-version>
</properties>

<dependencies>
    <!-- Shiro -->
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-core</artifactId>
        <version>${shiro-version}</version>
    </dependency>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-web</artifactId>
        <version>${shiro-version}</version>
    </dependency>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <version>${shiro-version}</version>
    </dependency>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-ehcache</artifactId>
        <version>${shiro-version}</version>
    </dependency>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-cas</artifactId>
        <version>${shiro-version}</version>
    </dependency>

     <!--  Shiro uses SLF4J for logging. -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>1.6.1</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>1.6.1</version>
    </dependency>

    <!-- Ehcache -->
    <dependency>
        <groupId>net.sf.ehcache</groupId>
        <artifactId>ehcache</artifactId>
        <version>2.10.6</version>
    </dependency>
     <!--不加这个org.springframework.cache.ehcache.EhCacheManagerFactoryBean找不到-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context-support</artifactId>
        <version>${spring-version}</version>
    </dependency>

    <!--  Spring(省略) --->
</dependencies>

ehcache.xml

<ehcache>
    <!-- 缓存路径
     使用java.io.tmpdir是获取系统的/tmp目录,此时需要su权限,如果没有权限会报错
     使用user.home是当前用户的home目录,不需要su权限
     -->
    <diskStore path="user.home"/>
    <!-- 默认缓存区 -->
    <defaultCache
            maxElementsInMemory="10000"
            eternal="false"
            timeToIdleSeconds="120"
            timeToLiveSeconds="120"
            maxElementsOnDisk="10000000"
            diskExpiryThreadIntervalSeconds="120"
            memoryStoreEvictionPolicy="LRU">
        <!--
             localTempSwap: 当堆内存或者非堆内存里面的元素已经满了的时候,将其中的元素临时的存放在磁盘上,一旦重启就会消失
             localRestartable: 该策略只对企业版Ehcache有用。它可以在重启的时候将堆内存或者非堆内存里面的元素持久化到硬盘上,重启之后再从硬盘上恢复元素到内存中
             none: 不持久化缓存的元素
             distributed: 该策略不适用于单机,是用于分布式的
         -->
        <persistence strategy="localTempSwap"/>
    </defaultCache>
    <!-- 自定义缓存区 -->
    <cache name="bos"
           maxElementsInMemory="10000"
           eternal="false"
           timeToIdleSeconds="120"
           timeToLiveSeconds="120"
           maxElementsOnDisk="10000000"
           diskExpiryThreadIntervalSeconds="120"
           memoryStoreEvictionPolicy="LRU">
        <persistence strategy="localTempSwap"/>
    </cache>
    <!-- 自定义缓存区 -->
    <cache name="standard"
           maxElementsInMemory="10000"
           eternal="false"
           timeToIdleSeconds="120"
           timeToLiveSeconds="120"
           maxElementsOnDisk="10000000"
           diskExpiryThreadIntervalSeconds="120"
           memoryStoreEvictionPolicy="LRU">
        <persistence strategy="localTempSwap"/>
    </cache>
</ehcache>

log4j.properties

log4j.rootLogger=INFO, stdout, 

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m %n

# General Apache libraries
log4j.logger.org.apache=WARN

# Spring
log4j.logger.org.springframework=WARN

# Default Shiro logging
log4j.logger.org.apache.shiro=TRACE

# Disable verbose logging
log4j.logger.org.apache.shiro.util.ThreadContext=WARN
log4j.logger.org.apache.shiro.cache.ehcache.EhCache=WARN

web.xml

在web.xml中加入Shiro的过滤器

<filter>
    <filter-name>shiroFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    <init-param>
        <param-name>targetFilterLifecycle</param-name>
        <param-value>true</param-value>
    </init-param>
</filter>

<filter-mapping>
    <filter-name>shiroFilter</filter-name>
    <url-pattern>/*</url-pattern>
    <dispatcher>REQUEST</dispatcher>
    <dispatcher>FORWARD</dispatcher>
    <dispatcher>INCLUDE</dispatcher>
    <dispatcher>ERROR</dispatcher>
</filter-mapping>

applicationContext.xml

在Spring配置文件中配置Shiro(主要配置ShiroFilter、SecurityManager和Realm,CacheManager和SessionManager都是可选的)

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
                           http://www.springframework.org/schema/context
                           http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/mvc
                           http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <context:component-scan base-package="controller"/>

    <mvc:annotation-driven/>
    <mvc:default-servlet-handler/>

    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/jsp/"/>
        <property name="suffix" value=".jsp"/>
    </bean>

    <!--
    1. 配置 SecurityManager
    -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <!-- 认证器,可以配置Realm和认证策略 -->
        <property name="authenticator" ref="authenticator"/>
        <!-- 单个Realm的配置
        <property name="realm" ref="firstRealm"/>
        -->
        <!-- 多个Realm的配置 -->
        <property name="realms">
            <list>
                <ref bean="firstRealm"/>
                <ref bean="secondRealm"/>
            </list>
        </property>
        <property name="sessionManager" ref="sessionManager"/>
        <property name="cacheManager" ref="shiroCacheManager"/>
    </bean>

    <!--
    2. 配置 Realm
       直接配置实现了 org.apache.shiro.realm.Realm 接口的 bean
    -->
    <bean id="firstRealm" class="shiro.realms.FirstRealm">
        <property name="credentialsMatcher">
            <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
                <property name="hashAlgorithmName" value="MD5"/>
                <property name="hashIterations" value="1024"/>
            </bean>
        </property>
    </bean>

    <bean id="secondRealm" class="shiro.realms.SecondRealm">
        <property name="credentialsMatcher">
            <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
                <property name="hashAlgorithmName" value="SHA1"/>
                <property name="hashIterations" value="1024"/>
            </bean>
        </property>
    </bean>

    <!-- 认证器,配置自定义的Realm及认证策略,自定义的Realm也可以在securityManager中直接配置 -->
    <bean id="authenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
        <!-- 认证策略,如果有多个Realm,可以设置为只要一个Realm通过就认为通过认证,或者必须所有Realm都通过才认为通过认证 -->
        <property name="authenticationStrategy">
            <bean class="org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy"/>
        </property>
        <!-- 多个Realm也可以在这里配置
        <property name="realms">
            <list>
                <ref bean="firstRealm"/>
                <ref bean="secondRealm"/>
            </list>
        </property>
        -->
    </bean>

    <!--
     3. sessionManager
    -->
    <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
        <!-- 设置session的失效扫描间隔,单位为毫秒 -->
        <property name="sessionValidationInterval" value="100000"/>
        <!-- 设置全局会话超时时间,默认30分钟,即如果30分钟内没有访问会话将过期 1800000 -->
        <property name="globalSessionTimeout" value="1800000"/>
        <!-- 删除失效的session -->
        <property name="deleteInvalidSessions" value="true"/>
        <!-- 是否开启会话验证器,默认是开启的 -->
        <property name="sessionValidationSchedulerEnabled" value="true"/>
        <!-- session还支持持久化,可以实现SessionDAO来把session保存到数据库或文件
        <property name="sessionDAO" ref=""/> -->

        <property name="sessionIdCookie" ref="sessionIdCookie"/>
        <property name="sessionIdCookieEnabled" value="true"/>
    </bean>

    <!-- Shiro的session是独立于servlet的session的
         如果设置了把Shiro的session保存在cookie中,默认的名字是JSESSIONID,会和servlet的session冲突,需要另起名字 -->
    <bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
        <constructor-arg name="name" value="shiroSession"/>
    </bean>

    <!--
    4. 配置 CacheManager.
       使用Ehcache需要加入Ehcache的jar包及配置文件.
    -->
    <!--
    <bean id="ehCacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
    <property name="configLocation" value="classpath:ehcache.xml"/>
    </bean>

    <bean id="shiroCacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
    <property name="cacheManager" ref="ehCacheManager"/>
    </bean>
    -->
    <bean id="shiroCacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
        <property name="cacheManagerConfigFile" value="classpath:ehcache.xml"/>
    </bean>

    <!-- 如果需要使用注解,则配置5、6 -->
    <!--
    5. 配置 LifecycleBeanPostProcessor. 可以自定的来调用配置在 Spring IOC 容器中 shiro bean 的生命周期方法.
    -->
    <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>

    <!--
    6. 启用 IOC 容器中使用 shiro 的注解. 但必须在配置了 LifecycleBeanPostProcessor 之后才可以使用.
    -->
    <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
          depends-on="lifecycleBeanPostProcessor"/>
    <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
        <property name="securityManager" ref="securityManager"/>
    </bean>

    <!--
    7. 配置 ShiroFilter.
       id 必须和 web.xml 文件中配置的 DelegatingFilterProxy 的 <filter-name> 一致.
    若不一致, 则会抛出: NoSuchBeanDefinitionException. 因为 Shiro 会来 IOC 容器中查找和 <filter-name> 名字对应的 filter bean.
    -->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager"/>
        <property name="loginUrl" value="/user/login"/>
        <property name="successUrl" value="/user/info"/>
        <property name="unauthorizedUrl" value="/user/unauthorized"/>
        <!-- 通过xml配置各路径所需的权限
            配置哪些页面需要受保护,以及访问这些页面需要的权限.
            Shiro会按顺序搜索页面及对应的权限,前面的会覆盖后面的。所以全通配 /** 必须放在最后
            1). anon 可以被匿名访问
            2). authc 必须认证(即登录)后才可能访问的页面.
            3). logout 登出.
            4). roles 角色过滤器
        <property name="filterChainDefinitions">
            <value>
                / = anon
                index.jsp = anon
                /user/login = anon
                /user/dologin = anon
                /user/logout = logout

                /user/userinfo = roles[user]
                /user/admininfo = roles[admin]

                /** = authc
            </value>
        </property>
        -->
        <!-- 也可以通过代码查询数据库,获取各路径对应的权限 -->
        <property name="filterChainDefinitionMap" ref="filterChainDefinitionMap"/>

    </bean>

    <!-- 生成各个路径对应权限的Builder,其实就是生成一个LinkedHashMap<String, String> -->
    <bean id="filterChainDefinitionMap" class="shiro.FilterChainDefinitionMapBuilder" factory-method="build"/>

</beans>

FilterChainDefinitionMapBuilder.java

public class FilterChainDefinitionMapBuilder {
    public static LinkedHashMap<String, String> build(){
        LinkedHashMap<String, String> map = new LinkedHashMap<>();
        map.put("/", "anon");
        map.put("/index.jsp", "anon");
        map.put("/user/login", "anon");
        map.put("/user/dologin", "anon");
        map.put("/user/logout", "logout");

        map.put("/user/userinfo", "authc,roles[user]");
        map.put("/user/admininfo", "authc,roles[admin]");

        map.put("/**", "authc");
        return map;
    }
}

JSP

在jsp中使用Shiro的标签来为不同用户显示不同内容

info.jsp

<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
         pageEncoding="ISO-8859-1"%>
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
    <title>Insert title here</title>
</head>
<body>

<h4>List Page</h4>

Welcome: <shiro:principal></shiro:principal>

<shiro:hasRole name="admin">
    <br/>
    <a href="admininfo">Admin Page</a>
</shiro:hasRole>

<shiro:hasRole name="user">
    <br/>
    <a href="userinfo">User Page</a>
</shiro:hasRole>

<br/>
<a href="logout">Logout</a>

</body>
</html>

index.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>index</title>
</head>
<body>

<h1>Welcome!!</h1>
<hr/>

<a href="/user/login">login</a><br/>
<a href="/user/userinfo">user info</a><br/>

<a href="/user/admininfo">admin info</a><br/>
<a href="/logout">logout</a>

</body>
</html>

login.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>user login</title>
</head>
<body>

Login<br/>
<form action="dologin" method="post">
    username: <input type="text" name="username"/><br/>
    password: <input type="password" name="password"/><br/>
    <input type="submit"/>
</form>

</body>
</html>

Realm

FirstRealm.java

//使用md5的Realm,即需要认证又需要授权验证则继承AuthorizingRealm
public class FirstRealm extends AuthorizingRealm {

    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("[FirstRealm] doGetAuthenticationInfo");

        //1. 把 AuthenticationToken 转换为 UsernamePasswordToken
        UsernamePasswordToken upToken = (UsernamePasswordToken) token;

        //2. 从 UsernamePasswordToken 中来获取 username
        String username = upToken.getUsername();

        //3. 调用数据库的方法, 从数据库中查询 username 对应的用户记录
        System.out.println("从数据库中获取 username: " + username + " 所对应的用户信息.");

        //4. 若用户不存在, 则可以抛出 UnknownAccountException 异常
        if ("unknown".equals(username)) {
            throw new UnknownAccountException("用户不存在!");
        }

        //5. 根据用户信息的情况, 决定是否需要抛出其他的 AuthenticationException 异常.
        if ("monster".equals(username)) {
            throw new LockedAccountException("用户被锁定");
        }

        //6. 根据用户的情况, 来构建 AuthenticationInfo 对象并返回. 通常使用的实现类为: SimpleAuthenticationInfo
        //以下信息是从数据库中获取的.
        //1). principal: 认证的实体信息. 可以是 username, 也可以是数据表对应的用户的实体类对象.
        Object principal = username;
        //2). credentials: 密码.
        //以下模拟从数据库查询出md5加密后的结果
        Object credentials = null; //"fc1709d0a95a6be30bc5926fdb7f22f4";
        if ("admin".equals(username)) {
            credentials = "4ec847db9bc2bad60e4279cce1fad5db";
        } else if ("user".equals(username)) {
            credentials = "098d2c478e9c11555ce2823231e02ec1";
        }

        //3). realmName: 当前 realm 对象的 name. 调用父类的 getName() 方法即可
        String realmName = getName();

        //直接使用md5
        //SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(principal, credentials, realmName);

        //4). 使用md5加盐,即使不同用户的密码相同,它们的MD5也不一样
        ByteSource credentialsSalt = ByteSource.Util.bytes(username);
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName);
        return info;
    }

    //用于计算MD5,用于模拟从数据库查询出MD5加密的密码
    public static void main(String[] args) {
        String hashAlgorithmName = "MD5";
        Object credentials = "root";
        Object salt = ByteSource.Util.bytes("admin");
        int hashIterations = 1024;

        Object result = new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations);
        System.out.println(result);
    }


    //授权验证
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        //1. 从 PrincipalCollection 中来获取登录用户的信息
        Object principal = principals.getPrimaryPrincipal();

        //2. 利用登录的用户的信息来设置用户当前用户的角色或权限(可能需要查询数据库)
        Set<String> roles = new HashSet<>();
        roles.add("user");
        if("admin".equals(principal)){
            roles.add("admin");
        }

        //3. 创建 SimpleAuthorizationInfo
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles);

        //4. 返回 SimpleAuthorizationInfo 对象.
        return info;
    }

}

SecondRealm.java

//使用sha1的Realm,只需要认证则继承AuthenticatingRealm
public class SecondRealm extends AuthenticatingRealm {

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("[SecondRealm] doGetAuthenticationInfo");

        //1. 把 AuthenticationToken 转换为 UsernamePasswordToken
        UsernamePasswordToken upToken = (UsernamePasswordToken) token;

        //2. 从 UsernamePasswordToken 中来获取 username
        String username = upToken.getUsername();

        //3. 调用数据库的方法, 从数据库中查询 username 对应的用户记录
        System.out.println("从数据库中获取 username: " + username + " 所对应的用户信息.");

        //4. 若用户不存在, 则可以抛出 UnknownAccountException 异常
        if ("unknown".equals(username)) {
            throw new UnknownAccountException("用户不存在!");
        }

        //5. 根据用户信息的情况, 决定是否需要抛出其他的 AuthenticationException 异常.
        if ("monster".equals(username)) {
            throw new LockedAccountException("用户被锁定");
        }

        //6. 根据用户的情况, 来构建 AuthenticationInfo 对象并返回. 通常使用的实现类为: SimpleAuthenticationInfo
        //以下信息是从数据库中获取的.
        //1). principal: 认证的实体信息. 可以是 username, 也可以是数据表对应的用户的实体类对象.
        Object principal = username;
        //2). credentials: 密码.
        Object credentials = null; //"fc1709d0a95a6be30bc5926fdb7f22f4";
        if ("admin".equals(username)) {
            credentials = "15a9aeaa034f1f65730feaffbdbe4260c02dd8aa";
        } else if ("user".equals(username)) {
            credentials = "073d4c3ae812935f23cb3f2a71943f49e082a718";
        }

        //3). realmName: 当前 realm 对象的 name. 调用父类的 getName() 方法即可
        String realmName = getName();
        //4). 盐值.
        ByteSource credentialsSalt = ByteSource.Util.bytes(username);

        SimpleAuthenticationInfo info = null; //new SimpleAuthenticationInfo(principal, credentials, realmName);
        info = new SimpleAuthenticationInfo("secondRealmName", credentials, credentialsSalt, realmName);
        return info;
    }

    //用于计算SHA1,用于模拟从数据库查询出SHA1加密的密码
    public static void main(String[] args) {
        String hashAlgorithmName = "SHA1";
        Object credentials = "root";
        Object salt = ByteSource.Util.bytes("admin");
        int hashIterations = 1024;

        Object result = new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations);
        System.out.println(result);
    }
}

Controller

@Controller
@RequestMapping("/user")
public class User {

    @RequestMapping("/login")
    public String login() {
        return "login";
    }

    @RequestMapping(value = "/dologin", method = RequestMethod.POST)
    public String doLogin(String username, String password, HttpSession session) {
        Subject currentUser = SecurityUtils.getSubject();
        if (!currentUser.isAuthenticated()) {
            // 把用户名和密码封装为 UsernamePasswordToken 对象
            UsernamePasswordToken token = new UsernamePasswordToken(username, password);
            // rememberme
            token.setRememberMe(true);
            try {
                // 执行登录.
                currentUser.login(token);
            }
            // 所有认证时异常的父类.
            catch (AuthenticationException ae) {
                //unexpected condition?  error?
                System.out.println("登录失败: " + ae.getMessage());
            }
            return "info";
        }
        return "unauthorized";
    }

    @RequestMapping("/userinfo")
    public String userInfo() {
        return "user_info";
    }

    @RequestMapping("/admininfo")
    public String adminInfo() {
        return "admin_info";
    }

    @RequestMapping("/info")
    public String info() {
        return "info";
    }

    @RequestMapping("unauthorized")
    public String unauthorized() {
        return "unauthorized";
    }

    @RequestMapping("logout")
    public String logout() {
        return "/index.jsp";
    }
}

注解

如果Service层使用了其他代理,使用Shiro的注解可能会报错,这时可以选择在Controller使用Shiro的注解

Quartz

Quartz是一个任务调度框架

几个概念

pom.xml

<!-- Quartz -->
<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz</artifactId>
    <version>2.2.3</version>
</dependency>
<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz-jobs</artifactId>
    <version>2.2.3</version>
</dependency>

<!-- log4j -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>1.6.1</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.6.1</version>
</dependency>

使用

public class Main {
    public static void main(String[] args) throws SchedulerException {
        //任务的数据,其实是一个Map
        JobDataMap jobDataMap = new JobDataMap();
        jobDataMap.put("user", new User("小明"));

        //任务详情,用于设置任务所需的数据
        JobDetail jobDetail = JobBuilder.newJob(MyJob.class)
                .withIdentity("jobDetail1", "d_group1")//设置jobDetail的id
                .usingJobData("key1", "data1")//设置简单的数据
                .usingJobData(jobDataMap)//设置复杂的数据
                .build();

        //触发器,设置任务何时触发,是否重复触发等
        Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("trigger1", "t_group1")//设置trigger的id
                .usingJobData("key2", "data2")//trigger也可以设置简单数据和复杂数据
                .usingJobData("key1","trigger_data")
                .withPriority(2)//优先级
                .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                        .withIntervalInSeconds(10)//隔10秒执行一次
                        .withRepeatCount(5))//或者.repeatForever()),重复执行,设置为5表示开始执行1次,然后在重复5次,总共执行6次
                .startAt(new Date(System.currentTimeMillis() + 5000))//5秒后开始执行,或者.startNow()
                .build();

        //调度器,用于执行任务
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
        scheduler.scheduleJob(jobDetail, trigger);
        scheduler.start();
    }
}

class User {
    String name;

    public User(String name) {
        this.name = name;
    }
}

Job

public class MyJob implements Job {

    //重复任务每次都会new,而不是使用之前的任务
    //底层使用了jobClass.newInstance(),所以必须要有public的无参构造器
    public MyJob() {
        System.out.println("new MyJob()");
    }

    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        //任务开始时间
        DateFormat format = new SimpleDateFormat("yyyy-mm-dd--HH:mm:ss");
        System.out.println("start time: "+format.format(jobExecutionContext.getFireTime()));

        //获取从jobDetail或trigger处设置的数据,使用MergedJobDataMap时,如果key有重复,trigger会覆盖jobDetail的
        System.out.println("key1: "+jobExecutionContext.getMergedJobDataMap().getString("key1"));
        User user = (User) jobExecutionContext.getMergedJobDataMap().get("user");
        System.out.println("user.name: "+user.name);

        //也可以获取jobDetail和trigger,这样即使key有重复也可以获取
        //直接set的数据其实是set到了map中
        System.out.println("jobDetail key1: "+jobExecutionContext.getJobDetail().getJobDataMap().get("key1"));
        System.out.println("trigger key1: "+jobExecutionContext.getTrigger().getJobDataMap().get("key1"));
        System.out.println("trigger key2: "+jobExecutionContext.getTrigger().getJobDataMap().get("key2"));

        System.out.println("-----------------------------");
    }
}

Job

设计成JobDetail+Job的原因:重复的任务每次执行,都会新建一个Job的实例,这样可以避免并发产生的问题,但是有时候又需要设置一些共享的数据,于是就有了JobDetail,用于存储共享的数据

@DisallowConcurrentExecution

Job是有可能并发执行的,比如一个任务要执行10秒中,而调度算法是每5秒中触发1次,那么就有可能多个任务被并发执行。有时候我们并不想任务并发执行,比如Job是对系统做检测,重复的检测是没有意义的,这时就可以使用@DisallowConcurrentExecution。使用@DisallowConcurrentExecution后,会等待任务执行完毕以后再重新执行(这样会导致任务的执行不是按照我们预先定义的时间间隔执行)

@DisallowConcurrentExecution
public class MyJob implements Job {
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        //do something
    }
}

@DisallowConcurrentExecution是对JobDetail实例生效的,也就是如果你定义两个JobDetail实例,引用同一个Job类,仍然是可以并发执行的

@PersistJobDataAfterExecution

表示当正常执行完Job后,JobDataMap中的数据应该被改动,以被下一次调用时用(在默认情况下,也就是没有设置@PersistJobDataAfterExecution的时候 每个Job都拥有独立JobDataMap

当使用@PersistJobDataAfterExecution注解时,为了避免并发时,存储数据造成混乱,建议把@DisallowConcurrentExecution注解也加上

JobDetail

除了identity、description、jobData等属性外,还有两个属性:

Trigger

SimpleTrigger

指定从某一个时间开始,以一定的时间间隔(单位可以是毫秒、秒、分钟、小时)执行的任务,它可以指定重复的次数

SimpleTrigger simpleTrigger = TriggerBuilder.newTrigger()
        .withIdentity("trigger")
        .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                .withMisfireHandlingInstructionFireNow()
                .withIntervalInSeconds(5)
                .withRepeatCount(5))
        .startNow()
        .build();

CalendarIntervalTrigger

指定从某一个时间开始,以一定的时间间隔执行的任务,它支持的时间单位比SimpleTrigger多,CalendarIntervalTrigger支持的间隔单位有秒、分钟、小时、天、星期、月,年,但是CalendarIntervalTrigger不能指定重复次数

CalendarIntervalTrigger calendarIntervalTrigger = TriggerBuilder.newTrigger()
        .withIdentity("trigger")
        .withSchedule(CalendarIntervalScheduleBuilder.calendarIntervalSchedule()
                .withIntervalInWeeks(1))
        .startNow()
        .build();

DailyTimeIntervalTrigger

指定每天的某个时间段内,以一定的时间间隔执行任务,它还可以支持指定星期几

DailyTimeIntervalTrigger dailyTimeIntervalTrigger = TriggerBuilder.newTrigger()
        .withIdentity("trigger")
        .withSchedule(DailyTimeIntervalScheduleBuilder.dailyTimeIntervalSchedule()
                .startingDailyAt(TimeOfDay.hourAndMinuteOfDay(9, 0)) //每天9:00开始
                .endingDailyAt(TimeOfDay.hourAndMinuteOfDay(16, 0)) //16:00 结束
                .onDaysOfTheWeek(MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY) //周一至周五执行
                .withIntervalInHours(1) //每间隔1小时执行一次
                .withRepeatCount(100)) //最多重复100次(实际执行100+1次)
        .startNow()
        .build();

CronTrigger

适合于更复杂的任务,它支持类型于Linux Cron的语法,可以通过cron表达式设置任务的执行时间

CronTrigger cronTrigger = TriggerBuilder.newTrigger()
        .withIdentity("trigger")
        .withSchedule(CronScheduleBuilder.cronSchedule("0 0/2 8-17 * * ?")) //每天8:00-17:00,每隔2分钟执行一次
        .startNow()
        .build();

Cron表达式:

位置 时间域 允许值 特殊值
1 0-59 , - * /
2 分钟 0-59 , - * /
3 小时 0-23 , - * /
4 日期 1-31 , - * ? / L W C
5 月份 1-12 , - * /
6 星期 1-7 , - * ? / L C #
7 年份(可选) 1-31 , - * /

Cron表达式对特殊字符的大小写不敏感,对代表星期、月份的缩写英文大小写也不敏感(星期使用数字时,1表示星期天,2才是星期一;星期可以使用英文缩写代替,如MON、FRI;月份也可以使用英文缩写,如JAN、DEC)

Calendar

Trigger可以使用Quartz的Calendar来执行周期较长的任务

AnnualCalendar cal = new AnnualCalendar(); //定义一个每年执行Calendar,精度为天
java.util.Calendar excludeDay = new GregorianCalendar();
excludeDay.setTime(newDate().inMonthOnDay(2, 25).build());
cal.setDayExcluded(excludeDay, true); //设置排除2.25这个日期
scheduler.addCalendar("FebCal", cal, false, false); //scheduler加入这个Calendar

Trigger trigger = TriggerBuilder.newTrigger().withIdentity("trigger1", "group1")
        .startNow()//一旦加入scheduler,立即生效
        .modifiedByCalendar("FebCal") //使用Calendar !!
        .withSchedule(simpleSchedule()
                .withIntervalInSeconds(1)
                .repeatForever())
        .build();

一共有如下Calendar

Misfire(错失策略)

Trigger trigger = TriggerBuilder.newTrigger()
    .withIdentity("trigger1", "t_group1")
    .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                  //设置错失策略,对应SimpleTrigger.MISFIRE_INSTRUCTION_FIRE_NOW;
                  .withMisfireHandlingInstructionFireNow()
                  .withIntervalInSeconds(5)
                  .withRepeatCount(5))
    .usingJobData(map)
    .startNow()
    .build();

通用的错失策略

SimpleTrigger的错失策略

CronTrigger的错失策略

Scheduler

SchedulerSchedulerFactory创建,SchedulerFactory有两种实现,一种是DirectSchedulerFactory,可以在代码中设置Scheduler的各种参数;另一种是StdSchdulerFactory,通过读取classpath下的quartz.properties来配置Scheduler,如果文件不存在,则取默认值

一个简单的配置文件如下

# scheduler
org.quartz.scheduler.instanceName = DefaultQuartzScheduler
org.quartz.scheduler.instanceId = AUTO

# ThreadPool
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount = 10 
org.quartz.threadPool.threadPriority = 5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = true

# jobStore
org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore

Scheduler包含一个两个重要组件: JobStore和ThreadPool

JobStore

JobStore是会来存储运行时信息的,包括Trigger、Schduler、JobDetail、业务锁等,它有多种实现:RAMJob(内存实现)、JobStoreTX(JDBC,事务由Quartz管理)、JobStoreCMT(JDBC,使用容器事务)、ClusteredJobStore(集群实现)、TerracottaJobStore

ThreadPool

ThreadPool就是线程池,Quartz有自己的线程池实现。所有任务的都会由线程池执行

其他

自动设置

如果在Job中有成员变量,且设置了set方法,可以自动设置

public class MyJob implements Job {

    private String jobData;

    public void setJobData(String jobData) {
        this.jobData = jobData;
    }
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        System.out.println("jobData: " + jobData);
    }
}

使用时

JobDetail jobDetail = JobBuilder.newJob(MyJob.class)
    .withIdentity("jobDetail1", "d_group1")
    .usingJobData("jobData","任务数据")//可以自动调用setJobData
    .build();

JobListener

public class MyJobListener implements JobListener {
    @Override
    public String getName() {
        return "MyJobListener";
    }

    //执行前
    @Override
    public void jobToBeExecuted(JobExecutionContext context) {
        System.out.println(getName() + "正在监听"+context.getJobDetail().getJobClass().getName());
    }

    //JobDetail被否决,否决是由TriggerListener执行的
    @Override
    public void jobExecutionVetoed(JobExecutionContext context) {
        System.out.println("jobExecutionVetoed");
    }

    //执行完
    @Override
    public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
        System.out.println(getName() + "完成对监听"+context.getJobDetail().getJobClass().getName());
    }
}

注册

可以给单个JobDetail注册监听,也可以给所有JobDetail注册监听

JobDetail jobDetail = JobBuilder.newJob(MyJob.class)
        .withIdentity("jobDetail1", "d_group1")
        .build();

Trigger trigger = TriggerBuilder.newTrigger()
        .withIdentity("trigger1", "t_group1")
        .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                .withIntervalInSeconds(10)
                .withRepeatCount(5))
        .startNow()
        .build();

Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
scheduler.scheduleJob(jobDetail, trigger);

MyJobListener listener = new MyJobListener();

//给单个jobDetail注册一个全局的Job Listener
//scheduler.getListenerManager().addJobListener(listener, KeyMatcher.keyEquals(JobKey.jobKey("jobDetail1", "d_group1")));

//注册一个全局的Job Listener
scheduler.getListenerManager().addJobListener(listener);

scheduler.start();

给单个jobDetail注册时使用的Matcher除了KeyMatcher,还有OrMatcherAndMatcherGroupMatcherEverythingMatcher

TriggerListener

public class MyTriggerListener implements TriggerListener {

    //相当于设置触发器的名称
    @Override
    public String getName() {
        return "MyTriggerListener";
    }

    //当与监听器相关联的Trigger被触发,即Job上的execute()方法将被执行时,Scheduler就调用该方法
    @Override
    public void triggerFired(Trigger trigger, JobExecutionContext context) {  }

    //在 Trigger 触发后,Job 将要被执行时由 Scheduler 调用这个方法。TriggerListener 给了一个选择去否决 Job 的执行。假如这个方法返回 true,这个 Job 将不会为此次 Trigger 触发而得到执行
    @Override
    public boolean vetoJobExecution(Trigger trigger, JobExecutionContext context) {
        return false;
    }

    //Scheduler 调用这个方法是在 Trigger 错过触发时。你应该关注此方法中持续时间长的逻辑:在出现许多错过触发的 Trigger 时,长逻辑会导致骨牌效应。你应当保持这上方法尽量的小
    @Override
    public void triggerMisfired(Trigger trigger) {  }

    //Trigger 被触发并且完成了 Job 的执行时,Scheduler 调用这个方法
    @Override
    public void triggerComplete(Trigger trigger, JobExecutionContext context, Trigger.CompletedExecutionInstruction triggerInstructionCode) {  }
}

注册

注册和JobListener类似

//单个
scheduler.getListenerManager().addTriggerListener(new MyTriggerListener(),KeyMatcher.keyEquals(TriggerKey.triggerKey("trigger","group")));
//全局
scheduler.getListenerManager().addTriggerListener(new MyTriggerListener());

SchedulerListener

SchedulerListener接口定义如下

public interface SchedulerListener {
    //用于部署JobDetail时调用
    void jobScheduled(Trigger trigger);

    //用于卸载JobDetail时调用
    void jobUnscheduled(TriggerKey triggerKey);

    //当一个 Trigger 来到了再也不会触发的状态时调用这个方法。除非这个 Job 已设置成了持久性,否则它就会从 Scheduler 中移除
    void triggerFinalized(Trigger trigger);

    //当一个 Trigger 或一组 Trigger 被暂停时调用triggerPaused
    void triggerPaused(TriggerKey triggerKey);

    void triggersPaused(String triggerGroup);

    //当一个 Trigger 或一组 Trigger 从暂停中恢复时调用triggerResumed
    void triggerResumed(TriggerKey triggerKey);

    void triggersResumed(String triggerGroup);

    void jobAdded(JobDetail jobDetail);

    void jobDeleted(JobKey jobKey);

    //当一个或一组 JobDetail 暂停时调用jobPaused
    void jobPaused(JobKey jobKey);

    void jobsPaused(String jobGroup);

    //当一个或一组 Job 从暂停上恢复时调用jobResumed
    void jobResumed(JobKey jobKey);

    void jobsResumed(String jobGroup);

    void schedulerError(String msg, SchedulerException cause);

    //当Scheduler处于StandBy模式时,调用该方法
    void schedulerInStandbyMode();

    //当Scheduler 开启时,调用该方法
    void schedulerStarted();

    void schedulerStarting();

    //当Scheduler停止时,调用该方法
    void schedulerShutdown();

    void schedulerShuttingdown();

    //当Scheduler中的数据被清除时,调用该方法
    void schedulingDataCleared();
}

注册

注册和前面的一样,但是Scheduler只有一个,所以没有区分单个和全局

scheduler.getListenerManager().addSchedulerListener(new MySchedulerListener());

MyCat

Mycat官网

Mycat是一个数据库分库分表中间件,通过Mycat连接MySql,能够把分布在不同服务器上的数据库虚拟成一个数据库,这样在代码中可以以访问一个数据库的方式来访问分布式的数据库,这样对于业务的升级,可以减少代码的修改量

安装

安装JDK并配置环境变量(MyCat依赖JDK)

vim ~/.profile
JAVA_HOME=/usr/lib/jdk1.8.0_101
CLASSPATH=.:$JAVA_HOME/lib/tools.jar:$JAVA_HOME/lib/dt.jar 
PATH=$JAVA_HOME/bin:$HOME/bin:$HOME/.local/bin:$PATH
source ~/.profile

启动mysqlservice mysql start

mycat

下载、解压

wget http://dl.mycat.io/1.6.5/Mycat-server-1.6.5-release-20180122220033-linux.tar.gz
tar -zxvf Mycat-server-1.6.5-release-20180122220033-linux.tar.gz
mv mycat /usr/local/

配置启动参数

vim wrapper.conf

添加mycat用户组

groupadd mycat
adduser -r -g mycat mycat
passwd mycat
chown -R mycat.mycat /usr/local/mycat  //修改mycat目录所属mycat用户

在schema.xml中设置mysql的地址、用户名和密码

<writeHost host="hostM1" url="localhost:3306" user="root" password="root">
    <readHost host="hostS1" url="localhost:3306" user="root" password="root" />
</writeHost>

在server.xml中设置mycat的用户名密码

<user name="root">
   <property name="password">123456</property>
   <property name="schemas">testdb</property>
</user>

启动mycat、连接mycat

/usr/local/mycat/bin/mycat start
mysql -uroot -p123456 -h127.0.0.1 -P8066 -Dtestdb

配置

server.xml

用于配置系统参数、防火墙、用户信息

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mycat:server SYSTEM "server.dtd">
<mycat:server xmlns:mycat="http://io.mycat/">
    <system>
        <property name="nonePasswordLogin">0</property> <!-- 0为需要密码登陆、1为不需要密码登陆 ,默认为0,设置为1则需要指定默认账户-->
        <property name="useHandshakeV10">1</property>
        <property name="useSqlStat">0</property>  <!-- 1为开启实时统计、0为关闭 -->
        <property name="useGlobleTableCheck">0</property>  <!-- 1为开启全局一致性检测、0为关闭 -->

        <!-- 全局序列号
        0表示使用本地文件方式,需配置sequence_conf.properties文件
        1表示数据库方式,此时需要配置数据库和sequence_db_conf.properties文件
        2表示本地时间戳方式,需配置sequence_time_conf.properties文件
        3表示使用分布式ZK ID生成器,需配置sequence_distributed_conf.properties文件,ZK的连接信息统一在myid.properties的zkURL属性中配置
        4表示使用Zk递增方式,需配置sequence_conf.properties文件,ZK的连接信息统一在myid.properties的zkURL属性中配置
        -->
        <property name="sequnceHandlerType">2</property>

        <property name="subqueryRelationshipCheck">false</property> <!-- 子查询中存在关联查询的情况下,检查关联字段中是否有分片字段 .默认 false -->
        <!--  <property name="useCompression">1</property>--> <!--1为开启mysql压缩协议-->
        <!--  <property name="fakeMySQLVersion">5.6.20</property>--> <!--设置模拟的MySQL版本号-->
        <!-- <property name="processorBufferChunk">40960</property> -->
        <!-- 
        <property name="processors">1</property> 
        <property name="processorExecutor">32</property> 
         -->
        <!--默认为type 0: DirectByteBufferPool | type 1 ByteBufferArena | type 2 NettyBufferPool -->
        <property name="processorBufferPoolType">0</property>
        <!--默认是65535 64K 用于sql解析时最大文本长度 -->
        <!--<property name="maxStringLiteralLength">65535</property>-->
        <!--<property name="sequnceHandlerType">0</property>-->
        <!--<property name="backSocketNoDelay">1</property>-->
        <!--<property name="frontSocketNoDelay">1</property>-->
        <!--<property name="processorExecutor">16</property>-->
        <!-- MyCat的服务端口及管理端口,如果不配置,就默认是8066和9066 -->
        <property name="serverPort">8066</property>
        <property name="managerPort">9066</property> 
        <!--
        <property name="idleTimeout">300000</property>
        <property name="bindIp">0.0.0.0</property> 
        <property name="frontWriteQueueSize">4096</property>
        <property name="processors">32</property>
        -->
        <!--分布式事务开关,0为不过滤分布式事务,1为过滤分布式事务(如果分布式事务内只涉及全局表,则不过滤),2为不过滤分布式事务,但是记录分布式事务日志-->
        <property name="handleDistributedTransactions">0</property>

        <property name="useOffHeapForMerge">1</property><!-- off heap for merge/order/group/limit  1开启 0关闭 -->

        <property name="memoryPageSize">64k</property><!-- 默认单位为m -->

        <property name="spillsFileBufferSize">1k</property><!-- 默认单位为k -->

        <property name="useStreamOutput">0</property>

        <property name="systemReserveMemorySize">384m</property><!-- 默认单位为m -->

        <property name="useZKSwitch">false</property><!-- 是否采用zookeeper协调切换 -->

        <!--<property name="XARecoveryLogBaseDir">./</property>--><!-- XA Recovery Log日志路径 -->
        <!--<property name="XARecoveryLogBaseName">tmlog</property>--><!-- XA Recovery Log日志名称 -->
    </system>
    
    <!-- 全局SQL防火墙设置 -->
    <!--白名单可以使用通配符%或者*-->
    <!--例如<host host="127.0.0.*" user="root"/>-->
    <!--例如<host host="127.0.*" user="root"/>-->
    <!--例如<host host="127.*" user="root"/>-->
    <!--例如<host host="1*7.*" user="root"/>-->
    <!--这些配置情况下对于127.0.0.1都能以root账户登录-->
    <!--
    <firewall>
       <whitehost>
          <host host="1*7.0.0.*" user="root"/>
       </whitehost>
       <blacklist check="true">
           <property name="selelctAllow">false</property>
       </blacklist>
    </firewall>
    -->

    <user name="root" defaultAccount="true">
        <property name="password">123456</property>
        <property name="schemas">testdb</property>

        <!-- 表级 DML 权限设置 -->
        <!--        
        <privileges check="false">
            <schema name="TESTDB" dml="0110" >
                <table name="tb01" dml="0000"></table>
                <table name="tb02" dml="1111"></table>
            </schema>
        </privileges>       
         -->
    </user>

    <user name="user">
        <property name="password">user</property>
        <property name="schemas">testdb</property>
        <property name="readOnly">true</property>
    </user>

</mycat:server>
user配置

配置用户名、密码、可访问的表(逻辑表)

<user name="test">
    <property name="password">123456</property>
    <property name="schemas">testdb</property><!-- 用户可访问的表 -->
    <!--<property name="schemas">db1,db2,db3</property>--><!-- 多个表用逗号分隔 -->
    <property name="readOnly">false</property>
</user>

给每个 数据库/表 配置具体权限

<user name="test">
    <property name="password">123456</property>
    <!-- 给每个 数据库/表 配置具体权限 -->
    <privileges check="false">
        <!-- dml四个数字分别代表insert、update、select、delete权限 -->
        <schema name="testdb" dml="0110" >
            <table name="tb01" dml="0010"></table>
            <!-- 如果表不配置权限,则使用数据库的权限 -->
            <table name="tb02"></table>
        </schema>
    </privileges>
</user>

密码可以加密存储,可以通过java -cp Mycat-server-1.6.5-release.jar io.mycat.util.DecryptUtil 0:test:123456生成密码,命令中的0:test:123456,0表示通过前端应用进行加密,test为用户名,123456为密码

<user name="test">
    <property name="usingDecrypt">1</property><!-- 使用加密 -->
    <property name="password">F8VKn5UHlz5J3pPQSEL/xQiTh9HmKvgd94MOxfPGcvhv1gbrSB8Iw7Hh8eHfS5NlFB2K81UnxOLotNF9ATkYKA==</property>
</user>
防火墙配置

SQL配置

配置项 缺省值 描述
selelctAllow true 是否允许执行
selectAllColumnAllow true 是否允许执行SELECT * FROM T这样的语句。如果设置为false,不允许执行select * from t,但select * from (select id, name from t) a。这个选项是防御程序通过调用select *获得数据表的结构信息
selectIntoAllow true SELECT查询中是否允许INTO字句
deleteAllow true 是否允许执行DELETE语句
updateAllow true 是否允许执行UPDATE语句
insertAllow true 是否允许执行INSERT语句
replaceAllow true 是否允许执行REPLACE语句
mergeAllow true 是否允许执行MERGE语句,这个只在Oracle中有用
callAllow true 是否允许通过jdbc的call语法调用存储过程
setAllow true 是否允许使用SET语法
truncateAllow true truncate语句是危险,缺省打开,若需要自行关闭
createTableAllow true 是否允许创建表
alterTableAllow true 是否允许执行Alter Table语句
dropTableAllow true 是否允许修改表
commentAllow false 是否允许语句中存在注释,Oracle 的用户不用担心,Wall 能够识别 hints和注释的区别
noneBaseStatementAllow false 是否允许非以上基本语句的其他语句,缺省关闭,通过这个选项就能够屏蔽 DDL
multiStatementAllow false 是否允许一次执行多条语句,缺省关闭
useAllow true 是否允许执行mysql的use语句,缺省打开
describeAllow true 是否允许执行mysql的describe语句,缺省打开
showAllow true 是否允许执行mysql的show语句,缺省打开
commitAllow true 是否允许执行commit操作
rollbackAllow true 是否允许执行roll back操作

如果把 selectIntoAllow、deleteAllow、updateAllow、insertAllow、mergeAllow 都设置为 false,这就是一 个只读数据源了

拦截配置-永真条件

配置项 缺省值 描述
selectWhereAlwayTrueCheck true 检查 SELECT 语句的 WHERE 子句是否是一个永真条件
selectHavingAlwayTrueCheck true 检查 SELECT 语句的 HAVING 子句是否是一个永真条件
deleteWhereAlwayTrueCheck true 检查 DELETE 语句的 WHERE 子句是否是一个永真条件
deleteWhereNoneCheck false 检查 DELETE 语句是否无 where 条件,这是有风险的,但不是 SQL 注入类型的风险
updateWhereAlayTrueCheck true 检查 UPDATE 语句的 WHERE 子句是否是一个永真条件
updateWhereNoneCheck false 检查 UPDATE 语句是否无 where 条件,这是有风险的,但不是SQL 注入类型的风险
conditionAndAlwayTrueAllow false 检查查询条件(WHERE/HAVING 子句)中是否包含 AND 永真条件
conditionAndAlwayFalseAllow false 检查查询条件(WHERE/HAVING 子句)中是否包含 AND 永假条件
conditionLikeTrueAllow true 检查查询条件(WHERE/HAVING 子句)中是否包含 LIKE 永真条件

其他拦截配置

配置项 缺省值 描述
selectIntoOutfileAllow false SELECT...INTO OUTFILE是否允许,这个是mysql注入攻击的常见手段,缺省是禁止的
selectUnionCheck true 检测SELECT UNION
selectMinusCheck true 检测SELECT MINUS
selectExceptCheck true 检测SELECT EXCEPT
selectIntersectCheck true 检测SELECT INTERSECT
mustParameterized false 是否必须参数化,如果为true,则不允许类似WHERE ID = 1这种不参数化的SQL
strictSyntaxCheck true 是否进行严格的语法检测,Druid SQL Parser在某些场景不能覆盖所有的SQL语法,出现解析SQL出错,可以临时把这个选项设置为false,同时把SQL反馈给Druid的开发者。
conditionOpXorAllow false 查询条件中是否允许有XOR条件。XOR不常用,很难判断永真或者永假,缺省不允许。
conditionOpBitwseAllow true 查询条件中是否允许有”&“、”~“、”
conditionDoubleConstAllow false 查询条件中是否允许连续两个常量运算表达式
minusAllow true 是否允许SELECT * FROM A MINUS SELECT * FROM B这样的语句
intersectAllow true 是否允许SELECT * FROM A INTERSECT SELECT * FROM B这样的语句
constArithmeticAllow true 拦截常量运算的条件,比如说WHERE FID = 3 - 1,其中”3 - 1”是常量运算表达式
limitZeroAllow false 是否允许limit 0这样的语句
tableCheck true 检测是否使用了禁用的表
schemaCheck true 检测是否使用了禁用的 Schema
functionCheck true 检测是否使用了禁用的函数
objectCheck true 检测是否使用了“禁用对对象”
variantCheck true 检测是否使用了“禁用的变量”
readOnlyTables 指定的表只读,不能够在 SELECT INTO、DELETE、UPDATE、INSERT、MERGE
拦截器

拦截器并不是用于拦截SQL,而是用于记录SQL,以进行日志分析(如果使用MySql的日志记录,会导致日志分布在各个MySql服务器上,不方便统一管理),真正要拦截SQL应该在防火墙中设置

<system> 
    <property name="sqlInterceptor">
        io.mycat.server.interceptor.impl.StatisticsSqlInterceptor
    </property> 
    <property name="sqlInterceptorType">
        update,insert,delete
    </property> 
    <property name="sqlInterceptorFile">/tmp/sql.txt</property> 
</system>

update、insert、delete的SQL就会被记录到/tmp/sql.txt

全局序列号

如果使用水平分片,MySql的AUTO_INCREMENT无法满足在分布式的情况下保持主键的唯一性,这时可以使用MyCat提供的生成全局序列号的功能

全局序列号的生成方式有多种,可以通过sequnceHandlerType指定

<property name="sequnceHandlerType">0</property>

然后在schema.xml的table中使用autoIncrement="true"让全局序列生效,此时该table的主键由MyCat生成

#default global sequence
GLOBAL.HISIDS=
GLOBAL.MINID=10001
GLOBAL.MAXID=20000
GLOBAL.CURID=10000

# self define sequence
MY_TABLE.HISIDS=
MY_TABLE.MINID=1001
MY_TABLE.MAXID=2000
MY_TABLE.CURID=1000

HISIDS: 表示使用过的历史分段(一般无特殊需要可不配置)

MINID: 最小ID值

MAXID: 表示最大ID值

CURID 表示当前ID 值

sequence_conf.properties的配置名字与表名一致的时候sql可以不包含ID字段(此处表名为MY_TABLE

缺点:

  1. mycat重新发布时,seq文件需要替换,集群部署无法用此方式,路由到不同的mycat上无法保证id唯一

  2. 使mycat变成了有状态的中间件

原理是在数据库中建立一张表,存放sequence名称(name),sequence当前值(current_value),步长(increment)等信息

sequence_db_conf.properties:

#sequence stored in datanode
# MY_TABLE是表名,要和插入的序列名一致,必须大写
MY_TABLE=node1

MY_TABLE是使用该序列的逻辑表的表名(表示该表的主键使用该序列生成),要和后面插入的序列名一致,node1是schema.xml中配置的dataNodename

schema.xml:

<schema name="TESTDB" checkSQLschema="false" sqlMaxLimit="100">
    <!-- 这里要使用autoIncrement="true" -->
    <table name="my_table" dataNode="node1" primaryKey="id" autoIncrement="true" rule="my_rule" />
</schema>

<dataNode name="node1" dataHost="localhost1" database="MY_DB" />

在该dataNode对应的主机的对应的数据库中导入mycat安装包下conf/dbseq.sql文件,创建存放序列的表和用于生成序列的函数,该文件内容如下:

# 创建用于存储序列信息的表
DROP TABLE IF EXISTS MYCAT_SEQUENCE;
CREATE TABLE MYCAT_SEQUENCE (  name VARCHAR(64) NOT NULL,  current_value BIGINT(20) NOT NULL,  increment INT NOT NULL DEFAULT 1, PRIMARY KEY (name) ) ENGINE=InnoDB;

# 创建存储函数,用于生成序列
# 获取当前sequence的值(返回当前值,增量)
DROP FUNCTION IF EXISTS `mycat_seq_currval`;
DELIMITER ;;
CREATE FUNCTION `mycat_seq_currval`(seq_name VARCHAR(64)) RETURNS varchar(64) CHARSET latin1
    DETERMINISTIC
BEGIN
    DECLARE retval VARCHAR(64);
    SET retval="-1,0";
    SELECT concat(CAST(current_value AS CHAR),",",CAST(increment AS CHAR) ) INTO retval FROM MYCAT_SEQUENCE  WHERE name = seq_name;
    RETURN retval ;
END
;;
DELIMITER ;

# 获取下一个sequence值
DROP FUNCTION IF EXISTS `mycat_seq_nextval`;
DELIMITER ;;
CREATE FUNCTION `mycat_seq_nextval`(seq_name VARCHAR(64)) RETURNS varchar(64) CHARSET latin1
    DETERMINISTIC
BEGIN
    DECLARE retval VARCHAR(64);
    DECLARE val BIGINT;
    DECLARE inc INT;
    DECLARE seq_lock INT;
    set val = -1;
    set inc = 0;
    SET seq_lock = -1;
    SELECT GET_LOCK(seq_name, 15) into seq_lock;
    if seq_lock = 1 then
      SELECT current_value + increment, increment INTO val, inc FROM MYCAT_SEQUENCE WHERE name = seq_name for update;
      if val != -1 then
          UPDATE MYCAT_SEQUENCE SET current_value = val WHERE name = seq_name;
      end if;
      SELECT RELEASE_LOCK(seq_name) into seq_lock;
    end if;
    SELECT concat(CAST((val - inc + 1) as CHAR),",",CAST(inc as CHAR)) INTO retval;
    RETURN retval;
END
;;
DELIMITER ;

# 获取跳过指定count后的sequence值
DROP FUNCTION IF EXISTS `mycat_seq_nextvals`;
DELIMITER ;;
CREATE FUNCTION `mycat_seq_nextvals`(seq_name VARCHAR(64), count INT) RETURNS VARCHAR(64) CHARSET latin1
    DETERMINISTIC
BEGIN
    DECLARE retval VARCHAR(64);
    DECLARE val BIGINT;
    DECLARE seq_lock INT;
    SET val = -1;
    SET seq_lock = -1;
    SELECT GET_LOCK(seq_name, 15) into seq_lock;
    if seq_lock = 1 then
        SELECT current_value + count INTO val FROM MYCAT_SEQUENCE WHERE name = seq_name for update;
        IF val != -1 THEN
            UPDATE MYCAT_SEQUENCE SET current_value = val WHERE name = seq_name;
        END IF;
        SELECT RELEASE_LOCK(seq_name) into seq_lock;
    end if;
    SELECT CONCAT(CAST((val - count + 1) as CHAR), ",", CAST(val as CHAR)) INTO retval;
    RETURN retval;
END
;;
DELIMITER ;

# 设置sequence值
DROP FUNCTION IF EXISTS `mycat_seq_setval`;
DELIMITER ;;
CREATE FUNCTION `mycat_seq_setval`(seq_name VARCHAR(64), value BIGINT) RETURNS varchar(64) CHARSET latin1
    DETERMINISTIC
BEGIN
    DECLARE retval VARCHAR(64);
    DECLARE inc INT;
    SET inc = 0;
    SELECT increment INTO inc FROM MYCAT_SEQUENCE WHERE name = seq_name;
    UPDATE MYCAT_SEQUENCE SET current_value = value WHERE name = seq_name;
    SELECT concat(CAST(value as CHAR),",",CAST(inc as CHAR)) INTO retval;
    RETURN retval;
END
;;
DELIMITER ;

往表中插入序列

# 创建一个名为my_table的序列,初始值是1,步长是1
INSERT INTO MYCAT_SEQUENCE(name,current_value,increment) VALUES ('my_table', 1, 1);

缺点:当配置节点的部署是主从复制,当主挂了切从后会有重复

#sequence depend on TIME
# WORKID和DATAACENTERID都是 0-31 任意整数
WORKID=01
DATAACENTERID=01

此时序列的计算方式为:64 位二进制 = 42(毫秒)+5(机器 ID)+5(业务编码)+12(重复累加) 。此时要求主键字段长度大于18位

myid.properties:

# 使用zk管理mycat和ID
loadZk=true
# zk服务器的地址和端口
zkURL=192.168.0.10:2181
# 本机房mycat集群的ID
clusterId=mycat-cluster-1
# 集群内mycat的ID
myid=mycat_fz_01
# mycat节点的名称
clusterNodes=mycat_fz_01

sequence_distributed_conf.properties:

#代表使用zk
INSTANCEID=ZK
#与myid.properties中的CLUSTERID设置的值相同
CLUSTERID=mycat-cluster-1

sequence_distributed_conf.properties和myid.properties配置和3一样

sequence_conf.properties:

# self define sequence
#可以不填写
ACCOUNT.HISIDS=
#某线程当前区间内最小值
ACCOUNT.MINID=1
#某线程当前区间内最大值
ACCOUNT.MAXID=2000
#某线程当前区间内当前值
ACCOUNT.CURID=0

log4j2.xml

日志配置

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
    <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d [%-5p][%t] %m %throwable{full} (%C:%F:%L) %n"/>
        </Console>

        <RollingFile name="RollingFile" fileName="${sys:MYCAT_HOME}/logs/mycat.log"
                     filePattern="${sys:MYCAT_HOME}/logs/$${date:yyyy-MM}/mycat-%d{MM-dd}-%i.log.gz">
        <PatternLayout>
                <Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %5p [%t] (%l) - %m%n</Pattern>
            </PatternLayout>
            <Policies>
                <OnStartupTriggeringPolicy/>
                <SizeBasedTriggeringPolicy size="250 MB"/>
                <TimeBasedTriggeringPolicy/>
            </Policies>
        </RollingFile>
    </Appenders>
    <Loggers>
        <!--<AsyncLogger name="io.mycat" level="info" includeLocation="true" additivity="false">-->
            <!--<AppenderRef ref="Console"/>-->
            <!--<AppenderRef ref="RollingFile"/>-->
        <!--</AsyncLogger>-->
        <asyncRoot level="info" includeLocation="true">
            <!--<AppenderRef ref="Console" />-->
            <AppenderRef ref="RollingFile"/>
        </asyncRoot>
    </Loggers>
</Configuration>

Pattern%d{yyyy-MM-dd HH:mm:ss.SSS} %5p [%t] (%l) - %m%n指定日志格式,其中%d{yyyy-MM-dd HH:mm:ss.SSS}表示日期格式,%5p表示日志级别,其中5表示最多显示5个字符,%t表示记录线程的名称,%m表示输出代码中提取的消息,%n表示输出换行符

asyncRootlevel可以指定日志级别,mycat支持的日志级别有All < Trace < Debug < Info < Warm < Error < Fatal < Off

rule.xml

用于配置表的分片规则,分片分为垂直分片和水平分片

垂直分片是按照数据结构进行的切分,它把一个表分成多个更小的表,该表存储的字段更少(比如本来一张表有id、name、addr,现在拆分成两个表,一个表存储id和name,另一个存储id和addr,这样一张表存储的字段变少了,索引也就更快了,而且还可以存储到不同的服务器上)

水平分片是按照数据行的切分,它把一张表拆分成多个部分存储,每个部分放到不同的服务器上,而且只负责存储一部分信息(比如将id为1w-2w存储在服务器1上,id为2w-3w存储在服务器2上,…)

rule.xml中主要定义水平分片的规则

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mycat:rule SYSTEM "rule.dtd">
<mycat:rule xmlns:mycat="http://io.mycat/">
    <tableRule name="rule-mod-long-3">
        <rule>
            <columns>id</columns>
            <algorithm>mod-long</algorithm>
        </rule>
    </tableRule>

    <!-- 分片算法 -->
    <!-- 一致性hash -->
    <function name="murmur"
        class="io.mycat.route.function.PartitionByMurmurHash">
        <property name="seed">0</property><!-- 默认是0 -->
        <property name="count">2</property><!-- 要分片的数据库节点数量,必须指定,否则没法分片 -->
        <property name="virtualBucketTimes">160</property><!-- 一个实际的数据库节点被映射为这么多虚拟节点,默认是160倍,也就是虚拟节点数是物理节点数的160倍 -->
        <!-- <property name="weightMapFile">weightMapFile</property> 节点的权重,没有指定权重的节点默认是1。以properties文件的格式填写,以从0开始到count-1的整数值也就是节点索引为key,以节点权重值为值。所有权重值必须是正整数,否则以1代替 -->
        <!-- <property name="bucketMapPath">/etc/mycat/bucketMapPath</property> 
            用于测试时观察各物理节点与虚拟节点的分布情况,如果指定了这个属性,会把虚拟节点的murmur hash值与物理节点的映射按行输出到这个文件,没有默认值,如果不指定,就不会输出任何东西 -->
    </function>

    <function name="crc32slot"
              class="io.mycat.route.function.PartitionByCRC32PreSlot">
        <property name="count">2</property><!-- 要分片的数据库节点数量,必须指定,否则没法分片 -->
    </function>

    <!-- 枚举法 -->
    <function name="hash-int"
        class="io.mycat.route.function.PartitionByFileMap">
        <property name="mapFile">partition-hash-int.txt</property>
    </function>

    <!-- 范围约定 -->
    <!-- autopartition-long.txt
        # range start-end ,data node index
        # K=1000,M=10000.
        0-500M=0
        500M-1000M=1
        1000M-1500M=2

        0-10000000=0
        10000001-20000000=1
     -->
    <function name="rang-long"
        class="io.mycat.route.function.AutoPartitionByLong">
        <property name="mapFile">autopartition-long.txt</property>
    </function>

    <!-- 求模法 -->
    <function name="mod-long" class="io.mycat.route.function.PartitionByMod">
        <!-- how many data nodes -->
        <property name="count">3</property>
    </function>

    <!-- 固定分片hash算法 -->
    <function name="func1" class="io.mycat.route.function.PartitionByLong">
        <property name="partitionCount">8</property>
        <property name="partitionLength">128</property>
    </function>

    <function name="latestMonth"
        class="io.mycat.route.function.LatestMonthPartion">
        <property name="splitOneDay">24</property>
    </function>

    <function name="partbymonth"
        class="io.mycat.route.function.PartitionByMonth">
        <property name="dateFormat">yyyy-MM-dd</property>
        <property name="sBeginDate">2015-01-01</property>
    </function>
    
    <function name="rang-mod" class="io.mycat.route.function.PartitionByRangeMod">
            <property name="mapFile">partition-range-mod.txt</property>
    </function>
    
    <function name="jump-consistent-hash" class="io.mycat.route.function.PartitionByJumpConsistentHash">
        <property name="totalBuckets">3</property>
    </function>
    
    <!-- 日期列分区法 -->
    <!--
        2014-01-01为0
        2014-01-10为1
        ...
        2014-05-01为12
        ...
    -->
    <function name="sharding-by-date" class="io.mycat.route.function.PartitionByDate">
        <property name="dateFormat">yyyy-MM-dd</property>
        <property name="sBeginDate">2014-01-01</property>
        <property name="sPartionDay">10</property>
    </function>

    <!-- 通配取模 -->
    <!-- partition-pattern.txt 
        # id partition range start-end ,data node index
        1-32=0
        33-64=1
        65-96=2
        97-128=3
        129-160=4
        161-192=5
        193-224=6
        225-256=7
        0-0=7
    -->
    <function name="sharding-by-pattern" class="io.mycat.route.function.PartitionByPattern">
        <property name="patternValue">256</property><!-- 求模基数 -->
        <property name="defaultNode">7</property><!-- 默认节点,如果不配置,则默认是0;如果分片字段不是整形,也会放到默认节点 -->
        <property name="mapFile">partition-pattern.txt</property>
    </function>
</mycat:rule>

tableRule定义根据哪个字段进行分片,以及使用什么分片算法,分片算法由function标签定义

<tableRule name="rule-mod-long-3"><!-- 这里的name会在schema.xml中用到 -->
    <rule>
        <columns>id</columns><!-- 根据id进行分片 -->
        <algorithm>mod-long</algorithm><!-- 这里指定function的name -->
    </rule>
</tableRule>

常用的分片算法有:

<!-- 简单取模,一般用于分片字段为整数类型 -->
<function name="mod-long" class="io.mycat.route.function.PartitionByMod">
    <property name="count">3</property><!-- 分成3个表,下标从0开始 -->
</function>

<!-- 枚举分片,一般用于分片字段只有有限个可选值的情况,比如地区编码、性别等 -->
<!-- partition-hash-int.txt:
1000=0
1001=1
DEFAULT_NODE=0
-->
<function name="hash-int" class="io.mycat.route.function.PartitionByFileMap">
    <property name="mapFile">partition-hash-int.txt</property>
    <property name="type">0</property><!-- 0表示分片字段为int类型,1表示为String类型 -->
    <property name="defaultNode">0</property><!-- 是否使用default node,小于0表示不设置默认节点,大于等于0表示设置默认节点;不设置默认节点时,如果碰到没有匹配的值会报错 -->
</function>

<!-- ASCII码求模通配,用于分片字段为字符串类型;该算法会取字符串的前n个字符,对其ASCII求和再模以指定值,最后得出的数再在指定的文件中按照范围匹配 -->
<!-- partition-pattern.txt:
0-63=0
64-127=1
-->
<function name="sharding-by-pattern" class="io.mycat.route.function.PartitionByPrefixPattern">
    <property name="patternValue">128</property><!-- 取模基数 -->
    <property name="prefixLength">5</property><!-- 取字符串的前5个字符 -->
    <property name="mapFile">partition-pattern.txt</property>
</function>

可以使用编程来对分片算法进行测试

PartitionByString rule = new PartitionByString();
rule.setPartitionLength("512");
rule.setPartitionCount("2");
rule.init();
rule.setHashSlice("0:2");
Assert.assertEquals(true, 0 == rule.calculate("45a"));

schema.xml

用于配置逻辑库、逻辑表以及对应的物理库(逻辑表直接对应物理库中的同名物理表,不再另外配置)

<?xml version="1.0"?>
<!DOCTYPE mycat:schema SYSTEM "schema.dtd">
<mycat:schema xmlns:mycat="http://io.mycat/">
    <!-- 逻辑库、逻辑表(逻辑表的名字要和物理表的名字一样) -->
    <schema name="testdb" checkSQLschema="false" sqlMaxLimit="100">
        <table name="customer" primaryKey="id" dataNode="dn1,dn2,dn3" rule="rule-mod-long-3" />
        <!-- <table name="dual" primaryKey="id" dataNode="dn3" type="global" autoIncrement="true" needAddLimit="false"/> -->
        <!-- <table name="customer" primaryKey="ID" dataNode="dn1,dn2" rule="sharding-by-intfile">
            <childTable name="orders" primaryKey="ID" joinKey="customer_id" parentKey="id">
                <childTable name="order_items" joinKey="order_id" parentKey="id" />
            </childTable>
            <childTable name="customer_addr" primaryKey="ID" joinKey="customer_id" parentKey="id" />
        </table> -->
    </schema>
    
    <dataNode name="dn1" dataHost="localhost1" database="db1" />
    <dataNode name="dn2" dataHost="localhost1" database="db2" />
    <dataNode name="dn3" dataHost="localhost1" database="db3" />
    
    
    <dataHost name="localhost1" maxCon="1000" minCon="10" balance="0" writeType="0" dbType="mysql" dbDriver="native" switchType="1" slaveThreshold="100">
        <heartbeat>select user()</heartbeat>

        <writeHost host="hostM1" url="localhost:3306" user="root" password="123456">
            <readHost host="hostS2" url="192.168.1.200:3306" user="root" password="xxx" />
        </writeHost>
        <!-- 如果hostM1宕机,则自动把hostS2变为writeHost -->
        <writeHost host="hostS2" url="192.168.1.200:3306" user="root" password="xxx" />

        <writeHost host="hostS1" url="localhost:3316" user="root" password="123456" />
    </dataHost>
</mycat:schema>

schema

table

childTable(用于定义E-R分片的子表)

dataNode

dataHost

heartbeat: 心跳检测语句

writeHostreadHost: 读主机、写主机,如果后端数据库宕机,那么这个writeHost绑定的所有readHost都将不可用

垂直分库案例

需求:从192.168.1.2中垂直分库,192.168.1.3负责存储customer数据,192.168.1.4负责存储order数据,192.168.1.5负责存储product数据

192.168.1.2

创建用于主从复制的用户,这里要使用--master-data=2记录事务日志点

create user 'repl_user'@'192.168.1.%' identified by '123456';
grant replication slave on *.* to 'repl_user'@'192.168.1.%';

使用mysqldump导出数据,然后使用scp命令把导出的数据拷贝到三台从机上

mysqldump --master-data=2 --single-transaction --routines --triggers --events -uroot -p main_db > back_main_db.sql

scp back_main_db.sql root@192.168.1.3:/root
scp back_main_db.sql root@192.168.1.4:/root
scp back_main_db.sql root@192.168.1.5:/root

192.168.1.3/5

创建customer_db数据库,从主数据库导出的文件恢复数据

然后使用change master to配置复制链路,开启主从复制

由于我们要垂直分库,所以从机的数据库名称和主机不一致,所以还需要使用change replication filter配置数据库名转换

最后创建Mycat连接MySql所用的用户

其他两台从机同理

mysql -uroot -p -e"create database customer_db"
mysql -uroot -p customer_db <  back_main_db.sql
change master to  master host='192.168.1.2',master_user='repl_user',master_password='123456',MASTER_LOG_FILE='mysql-bin.000001',MASTER_LOG_POS=4532519;
change replication filter replicate rewrite db=((main_db,customer_db));

start slave;
show slave stauts \G

create user 'mycat'@'192.168.1.%' identified by '123456';
grant select,insert,update,delete,execute on *.* to 'mycat'@'192.168.1.%'

MASTER_LOG_FILEMASTER_LOG_POS是通过在192.168.1.2中使用more back_main_db.sql查看的

至此,主从复制已经配置好了,然后需要配置schema.xml和server.xml(由于我们只需要垂直分库,不需要配置rule.xml)

192.168.1.2

schema.xml

<?xml version="1.0"?>
<!DOCTYPE mycat:schema SYSTEM "schema.dtd">
<mycat:schema xmlns:mycat="http://io.mycat/">

    <!-- schema的name就是我们最后模拟成一个逻辑库的名字 -->
    <schema name="website_db" checkSQLschema="false" sqlMaxLimit="100">
        <!-- MyCat跨分片的关联查询速度很慢,一个解决方案是把共用的数据冗余到各个数据库中,这时就需要使用全局表,但是如果以后需要修改全局表的数据,必须通过MyCat修改,否则会出现数据不一致的情况 -->
        <table name="region_info" primaryKey="region_id" dataNode="cusdb,orddb,prodb" type="global" />

        <table name="customer_inf" primaryKey="customer_inf_id" dataNode="cusdb" />
        <table name="customer_login" primaryKey="login_id" dataNode="cusdb" />
        <table name="customer_level" primaryKey="customer_level_id" dataNode="cusdb" />

        <table name="order_detail" primaryKey="order_detail_id" dataNode="orddb" />
        <table name="order_master" primaryKey="login_id" dataNode="orddb" />
        <table name="order_cart" primaryKey="cart_id" dataNode="orddb" />
        <table name="order_customer_addr" primaryKey="customer_addr_id" dataNode="orddb" />

        <table name="product_category" primaryKey="category_id" dataNode="prodb" />
        <table name="product_brand_infos" primaryKey="brand_id" dataNode="prodb" />
        <table name="product_info" primaryKey="product_id" dataNode="prodb" />
    </schema>

    <!-- database就是刚才创建的库 -->
    <dataNode name="cusdb" dataHost="mysql0103" database="customer_db" />
    <dataNode name="orddb" dataHost="mysql0104" database="order_db" />
    <dataNode name="prodb" dataHost="mysql0105" database="product_db" />
    
    <!-- dataHost命名最后使用ip相关的名字,提高可读性 -->
    <dataHost name="mysql0103" maxCon="1000" minCon="10" balance="3" writeType="0" dbType="mysql" dbDriver="native" switchType="1">
        <heartbeat>select user()</heartbeat>
        <writeHost host="192.168.1.3" url="192.168.1.3:3306" user="mycat" password="123456"/>
    </dataHost>
    <dataHost name="mysql0104" maxCon="1000" minCon="10" balance="3" writeType="0" dbType="mysql" dbDriver="native" switchType="1">
        <heartbeat>select user()</heartbeat>
        <writeHost host="192.168.1.4" url="192.168.1.4:3306" user="mycat" password="123456"/>
    </dataHost>
    <dataHost name="mysql0103" maxCon="1000" minCon="10" balance="3" writeType="0" dbType="mysql" dbDriver="native" switchType="1">
        <heartbeat>select user()</heartbeat>
        <writeHost host="192.168.1.5" url="192.168.1.5:3306" user="mycat" password="123456"/>
    </dataHost>
</mycat:schema>

server.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mycat:server SYSTEM "server.dtd">
<mycat:server xmlns:mycat="http://io.mycat/">
    <system>
        <property name="serverPort">8066</property>
        <property name="managerPort">9066</property>
        <property name="nonePasswordLogin">0</property>
        <property name="bindIp">0.0.0.0</property>
        <property name="frontWriteQueueSize">2048</property>

        <property name="charset">utf8</property>
        <property name="txIsolation">2</property>
        <property name="processors">8</property>
        <property name="idleTimeout">1800000</property>
        <property name="sqlExecuteTimeout">300</property>
        <property name="useSqlStat">0</property>
        <property name="useGlobleTableCheck">0</property>
        <property name="sequnceHandlerType">2</property>
        <property name="defaultMaxLimit">100</property>
        <property name="maxPacketSize">104857600</property>
    </system>
    <user name="mycat_root" defaultAccount="true">
        <property name="password">123456</property>
        <!-- 这里的schemas是前面指定的逻辑库的名字 -->
        <property name="schemas">website_db</property>
    </user>
</mycat:server>

最后启动MyCat,通过Mycat的端口、账号、密码登陆MySql

/usr/local/mycat/bin/mycat start
mysql -umycat_root -p -h192.168.1.2 -P8066 -Dwebsite_db

192.168.1.3/5

停止主从复制,删除冗余数据,其他两个从库同理

stop slave;
reset slave all;

drop table order_detail;drop table order_master;drop table order_cart;drop table order_customer_addr;drop table product_category;drop table product_brand_infos;drop table product_info;

MyCat+ZooKeeper

前面垂直分库中只使用了一个MyCat,但真实环境中我们需要使用MyCat集群,这时就需要通过ZooKeeper同步配置

为了测试方便,需要关闭防火墙和selinux,如果熟悉它们的配置,也可以修改对应的规则,不用关闭

# 关闭防火墙
systemctl stop firewalld.service
systemctl disable firewalld.service
 
# 关闭SELINUX
sed -i 's/SELINUX=enforcing/SELINUX=disabled/g' /etc/sysconfig/selinux
setenforce 0

首先配置ZooKeeper集群(略,参考ZooKeeper章节),并启动各ZooKeeper

修改所有MyCat主机的mycat/conf/myid.properties,配置ZooKeeper所在的服务器(以其中一台为例,其余的同理,不同MyCat主机只是myid不同)

# 是否使用ZooKeeper
loadZk=true
# zookeeper集群节点,逗号分隔
zkURL=192.168.1.6:2181,192.168.1.7:2181,192.168.1.8:2181
# mycat集群名字,会在ZooKeeper的mycat节点下创建指定clusterId的名称节点
clusterId=mycat-cluster-1
# 当前mycat节点名字,每台MyCat主机都不同
myid=mycat_01
# MyCat主机数量,这里假设有两台MyCat主机
clusterSize=2
# mycat集群节点成员,和前面的myid对应
clusterNodes=mycat_01,mycat_02
#server  booster  ;   booster install on db same server,will reset all minCon to 2
type=server
boosterDataHosts=dataHost1

选择其中一台MyCat主机,把mycat的conf目录下的server.xmlrule.xmlschema.xml移动到conf/zkconf,如果使用全局序列号,还需要复制对应的文件

cp server.xml rule.xml schema.xml ./zkconf
cp sequence_db_conf.properties ./zkconf/

在该MyCat主机中使用mycat/bininit_zk_data.sh脚本把配置同步到ZooKeeper中(要求配置的zookeeper集群节点必须全部在线)

 mycat/bin/init_zk_data.sh

此时登录其中一台ZooKeeper客户端,使用ls /mycat/mycat-cluster-1如果可以看到mycat对应的节点,说明配置成功

然后再重启各服务器上的MyCat就可以获取到配置

如果以后要修改MyCat配置,可以通过ZooKeeper的ZooInspector工具直接修改ZooKeeper中对应的节点,然后等待1分钟左右就会同步配置了;又或者直接修改MyCat的配置文件,然后再次运行init_zk_data.sh脚本进行同步也可以

HAProxy+xinetd+KeepAlived

HAProxy

HAProxy可以实现MyCat的负载均衡

安装HAProxy(最好在安装MyCat的服务器上安装,因为要对MyCat进行存活检测;如果安装多个HAProxy,它们的配置是一样的)

sudo apt install haproxy

配置HAProxy:vim /etc/haproxy/haproxy.cfg,配置如下

# 全局参数的设置(一般不用改,也可以根据需要改)
global
    # log语法:log <address_1> [max_level_1]
    # 比如 log 127.0.0.1 local2 info
    log /dev/log    local0
    log /dev/log    local1 notice
    # chroot为工作目录
    chroot /var/lib/haproxy
    # pid文件位置
    pidfile     /var/run/haproxy.pid
    stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners
    stats timeout 30s
    maxconn 4000
    # user和group会自动创建linux用户
    user haproxy
    group haproxy
    # 后台运行
    daemon
    # 启动1个实例,可以启多个来提高效率
    nbproc 1

    # 定义连接后端服务器的失败重连次数(失败超过指定次数时将服务器标记为不可用)
    retries 3
    # http请求超时时间
    timeout http-request    10s
    # 一个请求在队列里的超时时间
    timeout queue   1m
    # 连接超时
    timeout connect 10s
    # 客户端超时
    timeout client  1m
    # 服务器端超时
    timeout server  1m
    # 设置http-keep-alive的超时时间
    timeout http-keep-alive 10s
    # 检测超时
    timeout check   10s

    # 每次请求完毕后主动关闭http通道(禁用长连接)
    option http-server-close
    # 当serverId对应的服务器挂掉后,强制定向到其他健康的服务器
    option redispatch
    # 当服务器负载很高的时候,自动结束掉当前队列处理比较久的链接
    option abortonclose
    # 保证HAProxy不记录上级负载均衡发送过来的用于检测状态没有数据的心跳包 
    option dontlognull

defaults
    log global
    mode    http
    option  httplog
    option  dontlognull
        timeout connect 5000
        timeout client  50000
        timeout server  50000
    errorfile 400 /etc/haproxy/errors/400.http
    errorfile 403 /etc/haproxy/errors/403.http
    errorfile 500 /etc/haproxy/errors/500.http

# 下面是我们的配置
# HAProxy的状态信息统计页面
listen admin_status
    # 用于使用keepalived监控HaProxy的虚拟ip及端口,使用keepalived时,只有绑定了虚拟网卡的主机能够对外提供服务,其余的HAProxy主机是备用机;需要使用ifconfig eth0:1 192.168.1.10/24配置虚拟网卡
    #bind 192.168.1.10:48800
    # 使用0.0.0.0可以绑定所有的IP,就不需要每个服务器都配置了,同时还可用避免虚拟ip被切走就无法启动的情况
    bind 0.0.0.0:48800
    # 监控的url
    stats uri /admin-status
    # 使用用户名admin密码admin登录监控页面,如果要设置多个,另起一行写入即可
    stats auth admin:admin
    mode http
    # 启用日志记录HTTP请求
    option httplog

# MyCat对外提供服务的端口,此时要通过8096端口登录mysql
listen allmycat_servicce
    #bind 192.168.1.10:8096
    bind 0.0.0.0:8096
    mode tcp
    # 使用tcplog的日志格式
    option tcplog
    option httpchk OPTIONS * HTTP/1.1\r\nHost:\ www
    # 负载均衡算法
    balance roundrobin
    # 使用48700端口对MyCat进行存活检测,inter:每隔5秒检测一次,fail:失败后重试3次,rise:如果连续两次检测成功,则认为服务从离线状态恢复成正常状态;48700会在后面使用xinetd通过检测脚本检测
    server mycat_01 192.169.1.3:8066 checkport 48700 inter 5s rise 2 fail 3
    server mycat_01 192.169.1.4:8066 checkport 48700 inter 5s rise 2 fail 3

# 管理mycat
listen allmycat_admin
    #bind 192.168.1.10:8097
    bind 0.0.0.0:8097
    mode tcp
    option tcplog
    option httpchk OPTIONS * HTTP/1.1\r\nHost:\ www
    balance roundrobin
    server mycat_01 192.169.1.3:9066 checkport 48700 inter 5s rise 2 fail 3
    server mycat_01 192.169.1.4:9066 checkport 48700 

配置虚拟IPifconfig eth0:1 192.168.1.10/24(只需配置其中一台主机即可,使用keepalived时,只有绑定了虚拟IP的主机能对外提供服务,其余主机作为备用机,当出现故障时,keepalived会把虚拟IP从一台主机迁移到另一台主机)

设置HAProxy开机启动

# 拷贝开机启动文件
cp /usr/local/src/haproxy-1.5.16/examples/haproxy.init /etc/rc.d/init.d/haproxy
chmod +x /etc/rc.d/init.d/haproxy

ln -s /usr/local/haproxy/sbin/haproxy /usr/sbin

# 设置HAProxy开机启动
chkconfig --add haproxy
chkconfig haproxy on

启动HAProxy:haproxy -f /etc/haproxy/haproxy.cfg

xinetd

在安装HAProxy的主机上使用xinetd通过48700端口返回MyCat的存活检测结果(每台主机上的配置都是一样的)

sudo apt install xinetd

检查/etc/xinetd.conf的末尾是否有includedir /etc/xinetd.d,没有就加上,并检查/etc/xinetd.d目录是否存在,不存在则创建

# Simple configuration file for xinetd
#
# Some defaults, and include /etc/xinetd.d/
defaults
{
# Please note that you need a log_type line to be able to use log_on_success
# and log_on_failure. The default is the following :
# log_type = SYSLOG daemon info
}

includedir /etc/xinetd.d

增加MyCat存活状态检测服务配置:touch /etc/xinetd.d/mycatchk,配置如下

service mycatchk
{
    flags = REUSE
    socket_type = stream
    port = 48700
    wait = no
    user = root
    server = /user/local/bin/mycat_status
    log_on_failure += USERID
    disable = no
} 

创建检测脚本:touch /user/local/bin/mycat_status,内容如下

#!/bin/bash
#/usr/local/bin/mycat_status.sh
# This script checks if a mycat server is healthy running on localhost.
# It will return:
#
# "HTTP/1.x 200 OK\r" (if mycat is running smoothly)
#
# "HTTP/1.x 503 Internal Server Error\r" (else)
mycat=`/user/local/bin/mycat status | grep 'not running' | wc -l`
if [ "$mycat" = "0" ];
then
    /bin/echo -en "HTTP/1.1 200 OK\r\n"
    /bin/echo -en "Content-Type: text/plain\r\n"
    /bin/echo -en "Connection: close\r\n"
    /bin/echo -en "Content-Length: 40\r\n"
    /bin/echo -en "\r\n"
    /bin/echo -en "MyCat Cluster Node is synced.\r\n"
    exit 0
else
    /bin/echo -e "HTTP/1.1 503 Service Unavailable\r\n"
    /bin/echo -en "Content-Type: text/plain\r\n"
    /bin/echo -en "Connection: close\r\n"
    /bin/echo -en "Content-Length: 44\r\n"
    /bin/echo -en "\r\n"
    /bin/echo -en "MyCat Cluster Node is not synced.\r\n"
    exit 1
fi

给脚本添加执行权限:chmod a+x /user/local/bin/mycat_status

如果打开了防火墙,可以修改/etc/sysconfig/iptables,添加-A INPUT -m state --state NEW -m tcp -p tcp --dport 48700 -j ACCEPT,然后重启防火墙service iptables restart(HAProxy的信息统计页面对应的端口以及MyCat对应的服务、管理端口同理)

修改/etc/services,添加

mycatchk 48700/tcp # mycatchk

重启xinetd:service xinetd restart

查看是否启动成功:netstat -nltp | grep 48700ps -ef | grep 48700

测试HAProxy+xinetd

重启HAProxy:systemctl reload haproxyservice haproxy restart

测试mysql是否连接成功mysql -utest -p -h192.168.1.10 -P8096(如果没有使用虚拟网卡,把ip改成HAProxy主机的ip即可)

登录HAProxy信息统计页面:http://192.168.1.10:48800/admin_status

如果连接失败,检查:

  1. 是否关闭了selinux
  2. iptables中是否允许范围对应的端口(或者直接关闭iptables再测试)

KeepAlived

要保证HAProxy的高可用,需要使用KeepAlived,在安装HAProxy的主机上安装KeepAlived:

 sudo apt install keepalived

创建keepalived的配置文件touch /etc/keepalived/keepalived.conf,配置如下(备用机的state要修改为BACKUP;如果希望在MASTER下线后再上线时重新保持MASTER,可以修改priority,把其他主机的priority调低;如果不希望抢占VIP,让当前的MASTER继续保持,可以使用nopreempt

! Configuration File for keepalived
vrrp_script chk_port
{
     script "/etc/keepalived/check_haproxy.sh"
     interval 2
     timeout 2
     fall 3
     weight 2
}
vrrp_instance VI_1 {
    #可以指定MASTER和BACKUP
    state MASTER
    # 网卡
    interface eth0
    # VRRP组名,同一集群的keepalived的主、备机的virtual_router_id必须相同,取值0-255
    virtual_router_id 51
    # 优先级,MASTER的优先级应该比BACKUP的高
    priority 100
    # 设置为不抢夺VIP,可以避免频繁的切换主机
    nopreempt
    advert_int 1
    authentication {
        auth_type PASS
        auth_pass 1111
    }
    # 要执行的脚本,就是上面设置的vrrp_script
    track_script {
         chk_port
    }
    # 如果有多个IP,换行写
    virtual_ipaddress {
        192.168.1.10 dev eth0 scope global
    }
}

KeepAlived通过检测主机上的KeepAlived是否存活来决定是否需要转移VIP,我们需要编写脚本检测HAProxy是否存活,如果HAProxy挂了,就把当前的KeepAlived停掉,让KeepAlived转移VIP到其他KeepAlived备用机上

创建用于检测HAProxy是否存活的脚本touch /etc/keepalived/check_haproxy.sh,如果HAProxy挂了,再次尝试启动,如果启动失败,则停止当前KeepAlived,让其他机器上的KeepAlived转移VIP

#!/bin/bash
STARTHAPROXY="/user/sbin/haproxy -f /etc/haproxy/haproxy.cfg"
STOPKEEPALIVED="/etc/init.d/keepalived stop"
#STOPKEEPALIVED="/usr/bin/systemctl stop keepalived"
LOGFILE="/var/log/keepalived-haproxy-state.log"

echo "[check_haproxy status]" >> $LOGFILE
A=`ps -C haproxy --no-header | wc -l`
echo "[check_haproxy status]" >> $LOGFILE
date >> $LOGFILE

if [ $A -eq 0 ];then
    echo $STARTHAPROXY >> $LOGFILE
    $STARTHAPROXY >> $LOGFILE 2>&1
    sleep 5

    if [ `ps -C haproxy --no-header | wc -l` -eq 0];then
        $STOPKEEPALIVED >> $LOGFILE
        exit 0
    else
        exit 1
    fi
fi

给脚本添加执行权限:chmod a+x /etc/keepalived/check_haproxy.sh

设置KeepAlived开机启动:chkconfig keepalived on

前面已经配置过虚拟IP了,这里只需要直接启动KeepAlived即可:/etc/init.d/keepalived startservice keepalived start

测试KeepAlived

在有虚拟IP的主机上关闭KeepAlived:/etc/init.d/keepalived stop

查看当前主机是否有我们之前配置的虚拟IP:ip addr,如果没有,说明虚拟IP已被迁移;再查看其他主机是否有被自动迁移的虚拟IP:ip addr,如果有任意一台有,则说明配置成功,最后把原来的KeepAlived启动,如果MASTER的priority最高,那么重启后会重新获得虚拟IP

MyCat管理

使用server.xml中定义的用户通过指定的管理端口(默认为9066)登录mysql:mysql -uroot -p123456 -P9066 -h127.0.0.1

常用命令:

MyCat优化

linux优化

1、修改/etc/sysctl.conf优化内核相关参数,可以把tcp缓存池容量、连接数等调大

2、通过/etc/security/limits.conf修改资源限制,一般在最后增加如下两行(意思是控制打开文件数量的限制)

* soft nofile 65535
* hard nofile 65535

*表示对所有用户有效,soft表示当前系统生效的设置,hard表示系统中可以设置的最大值(soft的值要小于等于hard),nofile表示要控制的是打开文件的最大数,该数目就是65535

MyCat优化

1、修改/conf/wrapper.conf优化JVM参数,主要是修改直接内存的大小wrapper.java.additional.5=-XX:MaxDirectMemorySize=4G,对于独占服务器,最好设置为系统内存的一半或者三分之二

2、server.xml的system标签中的参数,主要有如下:

MySQL优化

修改my.cnf优化MySQL配置

Solr

安装

下载地址:Apache SolrSolr-7.5文档

Solr对中文支持不太好,所以需要另外安装中文分词器,这里用的是IKAnalyzer2012或者ik-analyzer-solr7

Solr安装

下载完Solr后,解压,运行

unzip solr-7.5.0.zip
sudo mv solr-7.5.0 /opt/solr
cd /opt/solr/bin/
solr start -p 8983

此时可以通过http://127.0.0.1:8983/solr访问Solr的web界面,我们可以使用web界面创建core,也可以通过命令行创建core(core就是solr的一个实例,一个solr服务下可以有多个core,每个core下都有自己的索引库和与之相应的配置文件,类似数据库中的database和table的关系),下面以命令行为例

solr create -c corename

命令说明

中文分词器安装

解压分词器的压缩包,把ik-analyzer-7.5.0.jar复制到Solr安装目录下的server/solr-webapp/webapp/WEB-INF/lib中,把IKAnalyzer.cfg.xmlext.dicstopword.dicik.confdynamicdic.txt五个文件复制到server/solr-webapp/webapp/WEB-INF/classes中,其中classes文件夹需要手动创建

server/solr/configsets/_default/conf/managed-schema中配置

<!-- ik分词器 -->
<fieldType name="text_ik" class="solr.TextField">
    <analyzer type="index" useSmart="false" class="org.wltea.analyzer.lucene.IKAnalyzer"/>
    <analyzer type="query" useSmart="false" class="org.wltea.analyzer.lucene.IKAnalyzer"/>
</fieldType>

<!-- 使用ik.conf配置文件可以这样配 -->
<fieldType name="text_ik" class="solr.TextField">
  <analyzer type="index">
      <tokenizer class="org.wltea.analyzer.lucene.IKTokenizerFactory" useSmart="false" conf="ik.conf"/>
      <filter class="solr.LowerCaseFilterFactory"/>
  </analyzer>
  <analyzer type="query">
      <tokenizer class="org.wltea.analyzer.lucene.IKTokenizerFactory" useSmart="true" conf="ik.conf"/>
      <filter class="solr.LowerCaseFilterFactory"/>
  </analyzer>
</fieldType>

这时使用solr create -c corename创建的core就会使用该配置创建;如果是使用web端创建,会提示创建失败,但server/solr下确实多了一个叫corename的文件夹,这时我们手动把server/solr/configsets/_default下的conf文件夹复制到server/solr/corename中即可;如果是之前就创建好的core,直接修改server/solr/corename下的配置文件即可

重启Solr,然后登录web端,找到刚才创建的corename,选择Analysis标签,即可测试分词器

如果想要扩展词库,可以在ext.dic文件中配置自定义的中文词组(编码是UTF-8无BOM模式),在stopword.dic中配置禁止使用的词

配置

field相关

前面都是使用默认配置,Solr的配置在server/solr/configsets中,默认配置的文件夹是_default,其中managed-schema是主要的配置文件,它负责管理字段相关的配置

下面是一个简单使用的例子

<field name="id" type="string" indexed="true" stored="true" required="true" multiValued="false" />
<field name="_text_" type="text_general" indexed="true" stored="false" multiValued="true"/>

<dynamicField name="*_t" type="text_general" indexed="true" stored="true" multiValued="false"/>

<copyField source="*_name" dest="destinationFieldName" maxChars="30000"/>
<copyField source="*_content" dest="destinationFieldName" maxChars="30000"/>

<uniqueKey>id</uniqueKey>

<fieldType name="string" class="solr.StrField" sortMissingLast="true" docValues="true" />
<fieldType name="text_general" class="solr.TextField" positionIncrementGap="100" multiValued="true">
    <analyzer type="index">
        <tokenizer class="solr.StandardTokenizerFactory"/>
        <filter class="solr.StopFilterFactory" ignoreCase="true" words="stopwords.txt" />
        <filter class="solr.LowerCaseFilterFactory"/>
    </analyzer>
</fieldType>

如果直接修改上面的配置需要重启Solr,在实际应用中,我们需要动态修改配置而不重启,这时可以使用Solr的Schema API上传配置

Schema API其实就是用post请求向Solr服务器发送携带json参数的请求,所有操作内容都封装在json中,如果是linux系统直接使用curl工具,如果是windows系统推荐使用Postman

Solr的配置上传可以使用web界面,也可以使用命令行,在Solr安装目录下的example/exampledocs有一个post.jar可以通过该jar包上传数据

java -jar post.jar -help

# corename为core的名字,*.xml为要上传的文件
# 通过java参数上传
java -Durl=http://localhost:8983/solr/corename/update -jar post.jar *.xml
# 或者
java -Dtype=text/csv -Dc=corename -jar post.jar *.csv

# 通过curl工具上传
curl http://localhost:8983/solr/corename/update --data-binary @*.xml -H 'Content-type:text/xml; charset=utf-8'
# 或者使用json数据,具体参考文档中Documents, Fields, and Schema Design一章中Schema API小节
curl -X POST -H 'Content-type:application/json' --data-binary '{
  "add-field":{
     "name":"sell_by",
     "type":"pdate",
     "stored":true }
}' http://localhost:8983/solr/corename/schema

curl -X POST -H 'Content-type:application/json' --data-binary '{
  "delete-field" : { "name":"sell_by" }
}' http://localhost:8983/solr/corename/schema

curl -X POST -H 'Content-type:application/json' --data-binary '{
  "replace-field":{
     "name":"sell_by",
     "type":"date",
     "stored":false }
}' http://localhost:8983/solr/corename/schema

DHI

DIH全称是Data Import Handler数据导入处理器,大部分情况下我们需要连接数据库来对数据进行索引,来提升我们的查询效率

下载mysql连接驱动mysql-connector-java-8.0.13.jar,并找到Solr安装目录下的dist文件夹下的solr-dataimporthandler-7.5.0.jarsolr-dataimporthandler-extras-7.5.0.jar两个jar包,复制到server\solr-webapp\webapp\WEB-INF\lib

数据库的配置可以参考example/example-DIH/solr/db/conf

修改server/solr/corename/conf下的solrconfig.xml,指定数据库配置文件的位置,其中/dataimport是指当请求以http://localhost:8983/solr/corename/dataimport/开头时(localhost:8983可以是其他url),匹配该数据库配置文件(Solr命令执行的方式就是通过发起命令请求)

<requestHandler name="/dataimport" class="solr.DataImportHandler">
    <lst name="defaults">
        <str name="config">db-data-config.xml</str>
    </lst>
</requestHandler>

修改db-data-config.xml,配置entity与数据库中字段的对应关系(document相当于数据库中的database,entity相当于数据库中的一个表),比如

<dataConfig>
    <dataSource driver="com.mysql.jdbc.Driver" url="jdbc:mysql://127.0.0.1:3306/solr?charactorEncoding=utf-8" user="root" password="root" />
    <document>
        <entity name="student" query="select * from student" pk="id">
            <field column="id" name="id" />
            <field column="name" name="name" />
            <field column="code" name="code" />
            <field column="create_time" name="createTime" />
        </entity>
    </document>
</dataConfig>

数据库的建表语句如下

CREATE TABLE `student` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(20) DEFAULT NULL,
  `code` varchar(20) DEFAULT NULL,
  `create_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

insert into student value('1','小明','hello world','2018-10-01');
insert into student value('2','小红','hello 小明','2018-10-01');

修改managed-schema,配置entity的索引,要和上面的配置对应

<field name="id" required="true" type="string" indexed="true" stored="true"/>
<field name="name" type="string" indexed="true" stored="true"/>
<field name="code" type="string" indexed="false" stored="true"/>
<field name="createTime" type="pdate" indexed="false" stored="true"/>

<uniqueKey>id</uniqueKey>

配置完成后重启Solr,访问 http://127.0.0.1:8983/solr/#/corename/dataimport//dataimport ,里面的Entity中有我们设置的student,点击Refresh Status导入数据

上面的导入方式是手动的全量导入,如果要自动增量导入,需要使用solr-dataimportscheduler

全量导入其实就是发送http://localhost:8983/solr/corename/dataimport?command=full-import指令,增量导入就是发送http://localhost:8983/solr/corename/dataimport?command=delta-import指令

修改server/solr-webapp/webapp/WEB-INF下的web.xml,添加监听器配置

<listener>
    <listener-class>
        org.apache.solr.handler.dataimport.scheduler.ApplicationListener
    </listener-class>
</listener>

server/solr下创建conf文件夹,在此文件夹下创建dataimport.properties文件,内容如下

#################################################
#       dataimport scheduler properties         #
#                                               #
#################################################
#  to sync or not to sync
#  1 - active; anything else - inactive
syncEnabled=1
#  which cores to schedule
#  in a multi-core environment you can decide which cores you want syncronized
#  leave empty or comment it out if using single-core deployment
#  修改成你所使用的core
syncCores=test01
#  solr server name or IP address
#  [defaults to localhost if empty]
#  这个一般都是localhost不会变
server=172.0.0.1
#  solr server port
#  [defaults to 80 if empty]
#  安装solr的tomcat端口,如果你使用的是默认的端口,就不用改了,否则改成自己的端口就好了
port=8983
#  application name/context
#  [defaults to current ServletContextListener's context (app) name]
#  这里默认不改
webapp=/
  
#  URL params [mandatory]
#  remainder of URL
#  这里改成下面的形式,solr同步数据时请求的链接
params=/dataimport?command=delta-import&clean=false&commit=true
#  schedule interval
#  number of minutes between two runs
#  [defaults to 30 if empty]
#  这里是设置定时任务的,单位是秒,也就是多长时间你检测一次数据同步,根据项目需求修改
#  开始测试的时候为了方便看到效果,时间可以设置短一点
interval=30
  
#  重做索引的时间间隔,单位分钟,默认7200,即5天; 
#  为空,为0,或者注释掉:表示永不重做索引
#reBuildIndexInterval=7200
  
#  重做索引的参数
#reBuildIndexParams=/select?qt=/dataimport&command=full-import&clean=true&commit=true
  
#  重做索引时间间隔的计时开始时间,第一次真正执行的时间=reBuildIndexBeginTime+reBuildIndexInterval*60*1000;
#  两种格式:2012-04-11 03:10:00 或者  03:10:00,后一种会自动补全日期部分为服务启动时的日期
#reBuildIndexBeginTime=03:10:00
#  延迟执行时间 默认是30分钟
initialDelay=1
#  执行线程数 默认是10
threadPoolCount=10

server/solr/corename/conf/db-data-config.xml中添加deltaImportQuerydeltaQuery,指定增量时使用的SQL

<dataConfig>
    <dataSource driver="com.mysql.jdbc.Driver" url="jdbc:mysql://127.0.0.1:3306/solr?charactorEncoding=utf-8" user="root" password="root" />
    <document>
        <entity name="student" query="select * from student" pk="id"
            deltaImportQuery="select * from student where id='${dataimporter.delta.id}'"
            deltaQuery="select * from student where `create_time` > '${dataimporter.last_index_time}'">
            <field column="id" name="id" />
            <field column="name" name="name" />
            <field column="code" name="code" />
            <field column="create_time" name="createTime" />
        </entity>
    </document>
</dataConfig>

${dataimporter.delta.id}是根据主键的名字写的,如果主键id配置的不叫id,如叫studentId,那么,这个取变量就应该写成${dataimporter.delta.studentId},而${dataimporter.last_index_time}是固定写法

solr/server/solr/corename/conf中创建dataimport.properties文件(和前面的配置文件名字相同但路径不同),内容如下

# 往前推迟的数量,单位秒
before_second:1
# 最后更新时间,系统默认添加,每次系统自己记录
student.last_index_time:2018-10-01 00\:00\:00
last_index_time:2018-10-01 00\:00\:00

添加数据

使用web端添加数据可以在core的Documents页面使用xml格式添加

<!-- Solr没有更新,如果id已存在则是更新,否则是插入 -->
<add> 
   <doc> 
      <field name = "id">1</field>
      <field name = "name">小明</field>
      <field name = "age">14</field>
   </doc>
</add>
<!-- 不要忘了提交 -->
<commit />

<!-- 根据id删除 -->
<delete>
   <id>1</id>
</delete>
<commit />

<!-- 根据查询删除 -->
<delete> 
   <query>name:小明</query> 
</delete>
<commit />

查询语法

web端的Query页面有如下查询条件

关于查询语句,可以使用如下的检索运算符

如果要使用+-&&||!( ){ }[ ]^"~*?:/这些特殊字符,需要加反斜杠进行转义,比如 \(1\+1\):2

可以在q中更改查询参数,比如q={!q.op=AND df=title}jakarta apache,更改参数的语句要以{!开头,以}结束,中间是任意用空格分隔的查询参数(key=value形式)

Solr的日期的格式是yyyy-MM-ddTHH:mm:ssZ,日期和小时之间用T分隔,最后以Z结束(Z代表时区是UTC)

日期的内建表达式(时间单位都可以带S,也可以不带,意义一样,比如HOUR或HOURS):

比如

JAVA客户端

maven依赖

<dependency>
    <groupId>org.apache.solr</groupId>
    <artifactId>solr-solrj</artifactId>
    <version>7.5.0</version>
</dependency>

java代码(一般配置DHI后只需直接查询,不需要增删改)

String solrUrl = "http://127.0.0.1:8983/solr/corename";
HttpSolrClient solrClient = new HttpSolrClient.Builder(solrUrl).build();
// ------------ 插入 ------------
Collection<SolrInputDocument> docs = new ArrayList<SolrInputDocument>();

SolrInputDocument doc1 = new SolrInputDocument();
doc1.addField("id", 3);
doc1.addField("name", "小王");
doc1.addField("code", "hello");
doc1.addField("createTime", "2018-10-01");

docs.add(doc1);
UpdateResponse rsp1 = solrclient.add(docs);
UpdateResponse rspcommit1 = solrclient.commit();

// ------------ 修改 ------------
HashMap<String, Object> map = new HashMap<String, Object>();
// 多值更新方法
//List<String> mulitValues = new ArrayList<String>();
//mulitValues.add("小花");
map.put("set", "小花");
SolrInputDocument doc2 = new SolrInputDocument();
doc2.addField("id", id);
doc2.addField("name", map);
UpdateResponse rsp2 = solrclient.add(doc2);
UpdateResponse rspCommit2 = solrclient.commit();

// ------------ 查询 ------------
// 创建搜索对象
SolrQuery query = new SolrQuery();
// 设置搜索条件
query.set("q","name:小明");
// 分页参数
query.setStart(0);
// 设置每页显示多少条
query.setRows(10);
//发起搜索请求
QueryResponse response = solrClient.query(query);
// 查询结果
SolrDocumentList docs = response.getResults();
// 查询结果总数
long count= docs.getNumFound();
System.out.println("总条数为"+count+"条");
for (SolrDocument doc : docs) {
    System.out.println("id:"+ doc.get("id") + ",name:"+ doc.get("name"));
}

// ------------ 删除 ------------
UpdateResponse rsp3 = solrclient.deleteById(id);
solrclient.commit();
// 或者
UpdateResponse rsp4 = client.deleteByQuery("name:小明");

solrClient.close();

通过JavaBean操作

public class Student{
    @Field("id")
    private int id;

    @Field("name")
    private int name;

    @Field("code")
    private int code;
    
    @Field("createTime")
    private int createTime;
    
    //省略构造器、get、set
    //...
}

public class Main{
    public static void main(String[] args){
        String solrUrl = "http://127.0.0.1:8983/solr/corename";
        HttpSolrClient solrClient = new HttpSolrClient.Builder(solrUrl).build();
        Student student = new Student(5,"小明","hello","2018-10-01");
        solrClient.addBean(student);
        solrClient.commit();

        SolrQuery query = new SolrQuery();
        query.set("q","*:*");
        QueryResponse response = solrClient.query(query);
        SolrDocumentList docs = response.getResults();
        for (SolrDocument doc : docs) {
            Student s = solrClient.getBinder().getBean(Student.class, doc);
            System.out.println(s);
        }
        
    }
}

集群搭建

安装ZooKeeper集群(略)

从安装包中解压脚本并执行,--strip-components=2是指去掉前两层目录,是tar命令的参数,该脚本的作用是把Solr安装成系统服务,以便运行,默认安装目录是/opt目录+压缩包名,建议先把压缩包重命名为solr.tgz,方便以后升级(以后升级只需替换/opt/solr的文件即可)

tar xzf solr-7.5.0.tgz solr-7.5.0/bin/install_solr_service.sh --strip-components=2
sudo ./install_solr_service.sh solr-7.5.0.tgz

# 也可以指定安装参数
sudo ./install_solr_service.sh solr-7.5.0.tgz -d /var/solr -i /opt/ -p 8983 -s solr -u solr -n

sudo service solr start
sudo service solr restart
sudo service solr status
sudo service solr stop

参数说明

安装完成后,到/var/solr/data目录,修改solr.xml文件,修改host参数,在后面填入本机ip

<solr>
  <solrcloud>
    <str name="host">${host:192.168.1.2}</str>
    <int name="hostPort">${jetty.port:8983}</int>
    <str name="hostContext">${hostContext:solr}</str>

    <bool name="genericCoreNodeNames">${genericCoreNodeNames:true}</bool>

    <int name="zkClientTimeout">${zkClientTimeout:30000}</int>
    <int name="distribUpdateSoTimeout">${distribUpdateSoTimeout:600000}</int>
    <int name="distribUpdateConnTimeout">${distribUpdateConnTimeout:60000}</int>
    <str name="zkCredentialsProvider">${zkCredentialsProvider:org.apache.solr.common.cloud.DefaultZkCredentialsProvider}</str>
    <str name="zkACLProvider">${zkACLProvider:org.apache.solr.common.cloud.DefaultZkACLProvider}</str>
  </solrcloud>

  <shardHandlerFactory name="shardHandlerFactory"
    class="HttpShardHandlerFactory">
    <int name="socketTimeout">${socketTimeout:600000}</int>
    <int name="connTimeout">${connTimeout:60000}</int>
  </shardHandlerFactory>
</solr>

再到/etc/default/下修改solr.in.sh文件,配置ZK_HOST参数

ZK_HOST="192.168.1.2:2181,192.168.1.3:2181,192.168.1.4:2181"

然后上传配置到ZooKeeper中

./solr zk upconfig -d [要上传的配置文件目录] -n [zookeeper上保存配置文件的节点名称] -z [zookeeper的集群地址]

# 使用/opt/solr/server/scripts/cloud-scripts/zkcli.sh也可以,三个参数也是对应的
 ./zkcli.sh -zkhost 192.168.1.2:2181,192.168.1.3:2181,192.168.1.4:2181 -cmd upconfig -confdir /opt/solr/server/solr/corename/conf -confname myconf

创建集合

./solr create-collection -c [新建集合的名字] -n [zookeeper上配置文件的节点名称,上一步设置的那个n] -shards 2 [分两块] -replicationFactor 2 [replic数量]

如果没有把Solr安装为系统服务,而是直接解压,那么solr.xml就应该在/opt/solr/server/solr目录配置,solr.in.sh应该在/opt/solr/bin下配置,无论是否安装为系统服务,它们的配置是一样的,只是路径不同

集群版Solr的JAVA客户端连接

String ZK_HOST = "192.168.1.2:2181,192.168.1.3:2181,192.168.1.4:2181";
CloudSolrClient solr=new CloudSolrClient.Builder().withZkHost(ZK_HOST).build();
solr.setDefaultCollection("mycollection");
solr.setParser(new XMLResponseParser());
solr.setRequestWriter(new BinaryRequestWriter());

//id存在就是改,不存在就是增
SolrInputDocument document = new SolrInputDocument();
document.setField("id", 1L);
document.setField("name", "小明");
solr.add(document);
solr.commit();

solr.close();

或者

ConcurrentUpdateSolrClient solr = new ConcurrentUpdateSolrClient.Builder(url).withQueueSize(20).build();
solr.setParser(new XMLResponseParser());
solr.setConnectionTimeout(10000);
solr.setRequestWriter(new BinaryRequestWriter());

SolrInputDocument document = new SolrInputDocument();
document.setField("id", 1L);
document.setField("name", "小明");
/*
UpdateResponse response = solr.add(d);
commit();
*/
//或者
UpdateRequest request = new UpdateRequest();
request.setAction(ACTION.COMMIT, false, false);
request.add(document);

UpdateResponse response1 = request.process(solr);


UpdateResponse response2 = solr.deleteById(1);
solr.commit();

solr.close();

Rxjava

reactivex官网

rxjava是一个提供响应式编程的框架,它是基于观察者模式的

无背压

无背压问题的可以使用Observable作为观察者

Observable.create(new ObservableOnSubscribe<String>() {
    public void subscribe(ObservableEmitter<String> emitter) throws Exception {
        emitter.onNext("1");
        emitter.onNext("2");
        emitter.onNext("3");
        emitter.onComplete();
    }
})
    .map(new Function<String, Integer>() {
        public Integer apply(String s) throws Exception {
            System.out.println("mapping " + s);
            return Integer.valueOf(s);
        }
    })
    .subscribe(new Observer<Integer>() {
        public void onSubscribe(Disposable d) {
            System.out.println("onSubscribe");
        }

        public void onNext(Integer integer) {
            System.out.println("onNext i=" + integer);
        }

        public void onError(Throwable e) {
            System.out.println("onError e=" + e.getMessage());
        }

        public void onComplete() {
            System.out.println("onComplete");
        }
    });

背压

当被观察者产出远高于观察者的处理能力时,会产生背压问题,此时需要使用支持背压策略的被观察者(Flowable),同时观察者也需指定最大消费能力,当超出消费能力后,数据会根据背压策略进行处理

Flowable.create(new FlowableOnSubscribe<String>() {
    public void subscribe(FlowableEmitter<String> flowableEmitter) throws Exception {
        flowableEmitter.onNext("hello");
        flowableEmitter.onNext("world");
    }
}, BackpressureStrategy.BUFFER)
    .subscribe(new Subscriber<String>() {
        public void onSubscribe(Subscription subscription) {
            System.out.println("onSubscribe");
            subscription.request(Long.MAX_VALUE);//消费能力,超出指定数量后数据会被丢弃
        }

        public void onNext(String s) {
            System.out.println("onNext-- " + s);
        }

        public void onError(Throwable throwable) {
            System.out.println("onError" + throwable.getMessage());

        }

        public void onComplete() {
            System.out.println("onComplete");
        }
    });

调用Emitter的方法其实会调用Subscriber对应的方法:

public void subscribe(FlowableEmitter<String> emitter) throws Exception {
    emitter.onNext("hello");
    int i = 1 / 0;
    emitter.onNext("world");
    emitter.onComplete();
    emitter.onComplete();
}

还可以指定工作的线程

Observable.create(emitter -> {
    //在io线程工作
    for (int i = 0; i < 5; i++) {
        System.out.println(Thread.currentThread().getName() + "--发射中-- i = " + i);
        emitter.onNext(i);
    }
})
    .subscribeOn(Schedulers.io())
    .observeOn(Schedulers.newThread())
    .subscribe(//在newThread工作
    i -> System.out.println(Thread.currentThread().getName() + "-- " + i));