诸子百家

王选的博客


  • 首页

  • 标签

  • 分类

  • 归档

Jdk8新特性

发表于 2018-04-24 | 更新于 2019-08-28 | 分类于 Java基础 , Jdk8

stream

stream分组去重合并

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public static void main(String[] args) {
List<Buss> bussList = new ArrayList<>();
bussList.add(new Buss("a",10,0.3));
bussList.add(new Buss("b",3,0.8));
bussList.add(new Buss("c",5,2.0));
bussList.add(new Buss("b",30,3.2));
bussList.add(new Buss("c",20,0.1));

List<Buss> st = new ArrayList<>();
bussList.stream().collect(Collectors.groupingBy(Buss::getName)) //分组(Name can't be null)
.forEach((k,v) -> {
Optional<Buss> sum = v.stream().reduce((v1,v2) -> { //合并
v1.setCount(v1.getCount() + v2.getCount());
v1.setValue(v1.getValue() + v2.getValue());
return v1;
});
st.add(sum.orElse(new Buss("other",0,0.0)));
});

System.out.println(st);
}

class Buss {
private String name;
private int count;
private double value;

public Buss(String name, int count, double value) {
this.name = name;
this.count = count;
this.value = value;
}
//省略get、set、toString方法
}

输出:
[Buss{name='a', count=10, value=0.3}, Buss{name='b', count=33, value=4.0}, Buss{name='c', count=25, value=2.1}]

如何根据两个字段排序,可以再model里面,写一个类似的方法即可,下例,分组的key 就是name+value

1
2
3
public String groupField() {
return getName()+"-"+getValue();
}

stream分组去重求最大值

List对象筛选学生年龄和性别一样的进行分组,并且挑选出身高最高的学生

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class Student {
private String name;
private int age;
private Long hight;
private int sex;

public Student(String name, int age, long hight, int sex) {
this.name = name;
this.age = age;
this.hight = hight;
this.sex = sex;
}
//省去相应get/set方法
//设置年龄和性别的拼接,得出相应分组
public Long getIwantStudent(){
return Long.valueOf(this.sex + this.age);
}
}

public static void main(String[] args) {
List<Student> allList = new ArrayList<>();
allList.add(new Student("韩梅梅",20,178L,1));
allList.add(new Student("马冬梅",20,168L,1));
allList.add(new Student("李磊",21,179L,2));
allList.add(new Student("小李",21,189L,2));

//以年龄和性别分组,并选取最高身高的学生
Map<Long, Optional<Student>> allMapTask = allList.stream().collect(
Collectors.groupingBy(Student::getIwantStudent,
Collectors.maxBy((o1, o2) ->
o1.getHight().compareTo(o2.getHight()))));

//遍历获取对象信息
for (Map.Entry<Long,Optional<Student>> entry: allMapTask.entrySet()) {
Student student = entry.getValue().get();
System.out.println(student.toString());
}
}

其中代码可以改写:

1
2
3
Map<Long, Optional<Student>> allMapTask = allList.stream().collect(
Collectors.groupingBy(Student::getIwantStudent,
Collectors.maxBy(Comparator.comparing(Student::getHight))));

注意:
Collectors.groupingBy方法根据Student对象中方法作为分组条件
Collectors.maxBy方法筛选每个分组中符合条件的数据

stream分组

思考一下Employee对象流,每个对象对应一个名字、城市和销售数量,如下表所示:

1
2
3
4
5
6
7
8
+----------+------------+-----------------+
| Name | City | Number of Sales |
+----------+------------+-----------------+
| Alice | London | 200 |
| Bob | London | 150 |
| Charles | New York | 160 |
| Dorothy | Hong Kong | 190 |
+----------+------------+-----------------+

在Java8中,你可以使用groupingBy收集器,像这样:
Map<String, List<Employee>> map = employees.stream().collect(groupingBy(Employee::getCity));
结果如下面的map所示:
{New York=[Charles], Hong Kong=[Dorothy], London=[Alice, Bob]}
还可以计算每个城市中雇员的数量,只需传递一个计数收集器给groupingBy收集器。第二个收集器的作用是在流分类的同一个组中对每个元素进行递归操作。
Map<String, Long> map = employees.stream().collect(groupingBy(Employee::getCity, counting()));
结果如下面的map所示:
{New York=1, Hong Kong=1, London=2}
另一个例子是计算每个城市的平均销售,这可以联合使用averagingInt和groupingBy 收集器:
Map<String, Double> map = employees.stream().collect(groupingBy(Employee::getCity, averagingInt(Employee::getNumSales)));
结果如下map所示:
{New York=160.0, Hong Kong=190.0, London=175.0}

Java8的foreach()中使用return,不能使用break/continue

使用foreach()处理集合时不能使用break和continue这两个方法,也就是说不能按照普通的for循环遍历集合时那样根据条件来中止遍历,而如果要实现在普通for循环中的效果时,可以使用return来达到,也就是说如果你在一个方法的lambda表达式中使用return时,这个方法是不会返回的,而只是执行下一次遍历。
代码:

1
2
3
4
5
6
7
List<String> list = Arrays.asList("123", "45634", "7892", "abch", "sdfhrthj", "mvkd");  
list.stream().forEach(e ->{
if(e.length() >= 5){
return;
}
System.out.println(e);
});

输出:

1
2
3
4
123
7892
abch
mvkd

return起到的作用和continue是相同的

Mybatis异常集锦

发表于 2018-04-24 | 更新于 2019-08-28 | 分类于 MyBatis

判断非空的坑

Mybatis中,alarmType是int类型。如果alarmType为0的话,条件判断返回结果为false,其它值的话,返回true。

1
2
3
<if test="alarmType != null and alarmType != ''">
alarm_type=#{alarmType},
</if>

其实对于条件判断alarmType如果为0,条件判断结果为true
<if test="alarmType == ''">
其实如果alarmType 是int类型的话,不用进行非空判断。

注意:判空操作是对字符串而言,不要对非字符串判空操作;其中int、long类型,条件判断alarmType如果为0,条件判断结果为true

java类型不匹配数据库类型

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
<select id="getSingleLevelBom" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM
material_bom
WHERE
enterprise_id = #{enterpriseId}
AND deleted = '00'
AND pid = #{id}
ORDER BY
create_time DESC
</select>

其中#{enterpriseId}的是去掉了java类型校验是可以的,添加java校验是这样的#{enterpriseId,jdbcType=BIGINT}。
但是在传参的时候如果将enterpriseId传入非Long类型如String,将会报错,错误信息如下:

1
No operator matches the given name and argument type(s). You might need to add explicit type casts.

MyBatis批量操作

发表于 2018-04-23 | 更新于 2019-08-28 | 分类于 MyBatis

Mapper.xml需要注意:
1.当parameterType为java.util.List时,foreach的collection值必须为list
2.当parameterType为java.util.Map时,foreach的collection值必须与Map中的键值一样
3.当parameterType为Bean时,foreach的collection值必须与Bean中的属性名一样

1. 批量添加

session.insert(String string, Object o)
mapper.batchInsertStudent(List stuList)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public void batchInsertStudent(){  
List<Student> ls = new ArrayList<Student>();
for(int i = 5;i < 8;i++){
Student student = new Student();
student.setId(i);
student.setName("maoyuanjun" + i);
student.setSex("man" + i);
student.setTel("tel" + i);
student.setAddress("浙江省" + i);
ls.add(student);
}
SqlSession session = SessionFactoryUtil.getSqlSessionFactory().openSession();
session.insert("mybatisdemo.domain.Student.batchInsertStudent", ls);
session.commit();
session.close();
}

public void batchInsertStudent(List<Student> stuList);

<!--List包裹Map或者Bean对象,此处collection必须填list-->
<insert id="batchInsertStudent" parameterType="java.util.List">
INSERT INTO STUDENT (id,name,sex,tel,address)
VALUES
<foreach collection="list" item="item" index="index" separator="," >
(#{item.id},#{item.name},#{item.sex},#{item.tel},#{item.address})
</foreach>
</insert>

2. 批量修改

实例1:

session.update(String string, Object o)
mapper.batchUpdateStudent(List idList)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void batchUpdateStudent(){  
List<Long> ls = new ArrayList<Long>();
for(int i = 2;i < 8;i++){
ls.add(i);
}
SqlSession session = SessionFactoryUtil.getSqlSessionFactory().openSession();
session.update("mybatisdemo.domain.Student.batchUpdateStudent", ls);
session.commit();
session.close();
}

public void batchUpdateStudent(List<Long> idList);

<update id="batchUpdateStudent" parameterType="java.util.List">
UPDATE STUDENT SET name = "5566" WHERE id IN
<foreach collection="list" item="item" index="index" open="(" separator="," close=")" >
#{item}
</foreach>
</update>

实例2:

session.update(String string, Object o)
mapper.batchUpdateStudentWithMap(Map<String, Object> map)
注意:collection=”idList”的值idList必须与Map中的键值一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void batchUpdateStudentWithMap(){  
List<Long> ls = new ArrayList<Long>();
for(int i = 2;i < 8;i++){
ls.add(i);
}
Map<String,Object> map = new HashMap<String,Object>();
map.put("idList", ls);
map.put("name", "mmao789");
SqlSession session = SessionFactoryUtil.getSqlSessionFactory().openSession();
session.update("mybatisdemo.domain.Student.batchUpdateStudentWithMap", map);
session.commit();
session.close();
}

public void batchUpdateStudentWithMap(Map<String, Object> map);

<!--collection的值idList必须与Map中的属性值一样-->
<update id="batchUpdateStudentWithMap" parameterType="java.util.Map" >
UPDATE STUDENT SET name = #{name} WHERE id IN
<foreach collection="idList" index="index" item="item" open="(" separator="," close=")">
#{item}
</foreach>
</update>

这里也可以将Map转换成bean对象,如:
session.update(String string, Object o)
mapper.batchUpdateStudentWithMap(StudentVO vo)
注意:collection=”idList”的值idList必须与Bean中的属性名一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public Class StudentVO {
private String name;
private List<Long> idList;
//get、set方法省略
}

public void batchUpdateStudentWithMap(StudentVO vo);

<!--collection的值idList必须与Bean中的属性名一样-->
<update id="batchUpdateStudentWithMap" parameterType="com.StudentVO" >
UPDATE STUDENT SET name = #{name} WHERE id IN
<foreach collection="idList" index="index" item="item" open="(" separator="," close=")">
#{item}
</foreach>
</update>

3. 批量删除

session.delete(String string,Object o)
mapper.batchDeleteStudent(List idList)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void batchDeleteStudent(){  
List<Long> ls = new ArrayList<Long>();
for(int i = 4;i < 8;i++){
ls.add(i);
}
SqlSession session = SessionFactoryUtil.getSqlSessionFactory().openSession();
session.delete("mybatisdemo.domain.Student.batchDeleteStudent",ls);
session.commit();
session.close();
}

public void batchDeleteStudent(List<Long> idList);

<delete id="batchDeleteStudent" parameterType="java.util.List">
DELETE FROM STUDENT WHERE id IN
<foreach collection="list" index="index" item="item" open="(" separator="," close=")">
#{item}
</foreach>
</delete>

数据库索引失效的条件

发表于 2018-04-22 | 更新于 2019-08-28 | 分类于 数据库 , MySQL

几乎所有的小伙伴都可以随口说几句关于创建索引的优缺点,也知道什么时候创建索引能够提高我们的查询性能,什么时候索引会更新,但是你有没有注意到,即使你设置了索引,有些时候索引他是不会生效的!这不仅考察了大家对索引的了解程度,还要让大家在使用的时候能够正确的使用。以下介绍了一些可能会造成索引失效的特殊情况,希望大家在平时开发和面试的时候能够注意到!

如何判断数据库索引是否生效

首先在接着探讨之前,我们先说一下,如何判断数据库的索引是否生效!相信大家应该猜到了,就是explain!explain显示了MySQL如何使用索引来处理select语句以及连接表。他可以帮助选择更好的索引和写出更优化的查询语句。
例如我们有一张表user,为name列创建索引name_index,如下所示:

使用explain分析语句如下:

可以看到,使用explain显示了很多列,各个关键字的含义如下:

  • table:顾名思义,显示这一行的数据是关于哪张表的;
  • type:这是重要的列,显示连接使用了何种类型。从最好到最差的连接类型为:const、eq_reg、ref、range、indexhe和ALL;
  • possible_keys:显示可能应用在这张表中的索引。如果为空,没有可能的索引。可以为相关的域从where语句中选择一个合适的语句;
  • key: 实际使用的索引。如果为NULL,则没有使用索引。很少的情况下,MySQL会选择优化不足的索引。这种情况下,可以在Select语句中使用USE INDEX(indexname)来强制使用一个索引或者用IGNORE INDEX(indexname)来强制MySQL忽略索引;
  • key_len:使用的索引的长度。在不损失精确性的情况下,长度越短越好;
  • ref:显示索引的哪一列被使用了,如果可能的话,是一个常数;
  • rows:MySQL认为必须检查的用来返回请求数据的行数;
  • Extra:关于MySQL如何解析查询的额外信息。

具体的各个列所能表示的值以及含义可以参考MySQL官方文档介绍,地址:https://dev.mysql.com/doc/refman/5.7/en/explain-output.html

哪些场景会造成索引生效

  1. 应尽量避免在 where 子句中使用 != 或 <> 操作符,否则引擎将放弃使用索引而进行全表扫描;
  2. 尽量避免在 where 子句中使用 or 来连接条件,否则将导致引擎放弃使用索引而进行全表扫描,即使其中有条件带索引也不会使用,这也是为什么尽量少用 or 的原因;
  3. 对于多列索引,不是使用的第一部分,则不会使用索引;
  4. 如果列类型是字符串,那一定要在条件中将数据使用引号引用起来,否则不会使用索引;
  5. like的模糊查询以 % 开头,索引失效;
  6. 应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描;
    如:
    select id from t where num/2 = 100;
    以abc开头的,应改成:
    select id from t where num = 100*2;
  7. 应尽量避免在 where 子句中对字段进行函数操作,这将导致引擎放弃使用索引而进行全表扫描;
    例如:
    select id from t where substring(name,1,3) = 'abc' – name;
    以abc开头的,应改成:
    select id from t where name like 'abc%';
    例如:
    select id from t where datediff(day, createdate, '2005-11-30') = 0 – '2005-11-30';
    应改为:
    select id from t where createdate >= '2005-11-30' and createdate < '2005-12-1';
  8. 不要在 where 子句中的 “=” 左边进行函数、算术运算或其他表达式运算,否则系统将可能无法正确使用索引;
  9. 如果MySQL估计使用全表扫描要比使用索引快,则不使用索引;
  10. 不适合键值较少的列(重复数据较多的列)

假如索引列TYPE有5个键值,如果有1万条数据,那么 WHERE TYPE = 1将访问表中的2000个数据块。再加上访问索引块,一共要访问大于200个的数据块。如果全表扫描,假设10条数据一个数据块,那么只需访问1000个数据块,既然全表扫描访问的数据块少一些,肯定就不会利用索引了。

阿里云Redis开发规范

发表于 2018-04-13 | 更新于 2019-08-28 | 分类于 Redis

摘要: 本文介绍了在使用阿里云Redis的开发规范,从键值设计、命令使用、客户端使用、相关工具等方面进行说明,通过本文的介绍可以减少使用Redis过程带来的问题。

一、键值设计

1. key名设计

  • (1)【建议】: 可读性和可管理性
    以业务名(或数据库名)为前缀(防止key冲突),用冒号分隔,比如业务名:表名:id
    ugc:video:1
  • (2)【建议】:简洁性
    保证语义的前提下,控制key的长度,当key较多时,内存占用也不容忽视,例如:
    user:{uid}:friends:messages:{mid}简化为u:{uid}:fr:m:{mid}。
  • (3)【强制】:不要包含特殊字符
    反例:包含空格、换行、单双引号以及其他转义字符

2. value设计

  • (1)【强制】:拒绝bigkey(防止网卡流量、慢查询)
    string类型控制在10KB以内,hash、list、set、zset元素个数不要超过5000。
    反例:一个包含200万个元素的list。
    非字符串的bigkey,不要使用del删除,使用hscan、sscan、zscan方式渐进式删除,同时要注意防止bigkey过期时间自动删除问题(例如一个200万的zset设置1小时过期,会触发del操作,造成阻塞,而且该操作不会不出现在慢查询中(latency可查)),查找方法和删除方法
  • (2)【推荐】:选择适合的数据类型。
    例如:实体类型(要合理控制和使用数据结构内存编码优化配置,例如ziplist,但也要注意节省内存和性能之间的平衡)
    反例:

    1
    2
    3
    set user:1:name tom
    set user:1:age 19
    set user:1:favor football

    正例:

    1
    hmset user:1 name tom age19 favor football

3.【推荐】:控制key的生命周期,redis不是垃圾桶

建议使用expire设置过期时间(条件允许可以打散过期时间,防止集中过期),不过期的数据重点关注idletime。

二、命令使用

1.【推荐】 O(N)命令关注N的数量

例如hgetall、lrange、smembers、zrange、sinter等并非不能使用,但是需要明确N的值。有遍历的需求可以使用hscan、sscan、zscan代替。

2.【推荐】:禁用命令

禁止线上使用keys、flushall、flushdb等,通过redis的rename机制禁掉命令,或者使用scan的方式渐进式处理。

3.【推荐】合理使用select

redis的多数据库较弱,使用数字进行区分,很多客户端支持较差,同时多业务用多数据库实际还是单线程处理,会有干扰。

4.【推荐】使用批量操作提高效率

原生命令:例如mget、mset。
非原生命令:可以使用pipeline提高效率。
但要注意控制一次批量操作的元素个数(例如500以内,实际也和元素字节数有关)。
注意两者不同:
1.原生是原子操作,pipeline是非原子操作。
2.pipeline可以打包不同的命令,原生做不到。
3.pipeline需要客户端和服务端同时支持。

5.【建议】Redis事务功能较弱,不建议过多使用

Redis的事务功能较弱(不支持回滚),而且集群版本(自研和官方)要求一次事务操作的key必须在一个slot上(可以使用hashtag功能解决)

6.【建议】Redis集群版本在使用Lua上有特殊要求:

  • 1.所有key都应该由 KEYS 数组来传递,redis.call/pcall 里面调用的redis命令,key的位置,必须是KEYS array, 否则直接返回error,”-ERR bad lua script for redis cluster, all the keys that the script uses should be passed using the KEYS arrayrn”

  • 2.所有key,必须在1个slot上,否则直接返回error, “-ERR eval/evalsha command keys must in same slotrn”

7.【建议】必要情况下使用monitor命令时,要注意不要长时间使用。

三、客户端使用

1.【推荐】

避免多个应用使用一个Redis实例
正例:不相干的业务拆分,公共数据做服务化。

2.【推荐】

使用带有连接池的数据库,可以有效控制连接,同时提高效率,标准使用方式:
执行命令如下:

1
2
3
4
5
6
7
8
9
10
11
12
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
//具体的命令
jedis.executeCommand()
} catch(Exception e) {
logger.error("op key {} error: " + e.getMessage(), key, e);
} finally {
//注意这里不是关闭连接,在JedisPool模式下,Jedis会被归还给资源池。
if(jedis !=null)
jedis.close();
}

下面是JedisPool优化方法的文章:

  • Jedis常见异常汇总
  • edisPool资源池优化

3.【建议】

高并发下建议客户端添加熔断功能(例如netflix hystrix)

4.【推荐】

设置合理的密码,如有必要可以使用SSL加密访问(阿里云Redis支持)

5.【建议】

根据自身业务类型,选好maxmemory-policy(最大内存淘汰策略),设置好过期时间。
默认策略是volatile-lru,即超过最大内存后,在过期键中使用lru算法进行key的剔除,保证不过期数据不被删除,但是可能会出现OOM问题。
其他策略如下:

  • allkeys-lru:根据LRU算法删除键,不管数据有没有设置超时属性,直到腾出足够空间为止。
  • allkeys-random:随机删除所有键,直到腾出足够空间为止。
  • volatile-random:随机删除过期键,直到腾出足够空间为止。
  • volatile-ttl:根据键值对象的ttl属性,删除最近将要过期数据。如果没有,回退到noeviction策略。
  • noeviction:不会剔除任何数据,拒绝所有写入操作并返回客户端错误信息”(error) OOM command not allowed when used memory”,此时Redis只响应读操作。

四、相关工具

1.【推荐】:数据同步

redis间数据同步可以使用:redis-port

2.【推荐】:big key搜索

redis大key搜索工具

3.【推荐】:热点key寻找(内部实现使用monitor,所以建议短时间使用)

facebook的redis-faina

五 附录:删除bigkey

1.下面操作可以使用pipeline加速。
2.redis 4.0已经支持key的异步删除,欢迎使用。

1. Hash删除: hscan + hdel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void delBigHash(String host, int port, String password, String bigHashKey) {
Jedis jedis = new Jedis(host, port);
if(password != null && !"".equals(password)) {
jedis.auth(password);
}
ScanParams scanParams = new ScanParams().count(100);
String cursor = "0";
do {
ScanResult<Entry<String,String>> scanResult = jedis.hscan(bigHashKey, cursor, scanParams);
List<Entry<String,String>> entryList = scanResult.getResult();
if(entryList != null && !entryList.isEmpty()) {
for(Entry<String,String> entry : entryList) {
jedis.hdel(bigHashKey, entry.getKey());
}
}
cursor = scanResult.getStringCursor();
} while(!"0".equals(cursor));
//删除bigkey
jedis.del(bigHashKey);
}

2. List删除: ltrim

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void delBigList(String host, int port, String password, String bigListKey) {
Jedis jedis = new Jedis(host, port);
if(password != null && !"".equals(password)) {
jedis.auth(password);
}
long llen = jedis.llen(bigListKey);
int counter = 0;
int left = 100;
while(counter < llen) {
//每次从左侧截掉100个
jedis.ltrim(bigListKey, left, llen);
counter += left;
}
//最终删除key
jedis.del(bigListKey);
}

3. Set删除: sscan + srem

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void delBigSet(String host, int port, String password, String bigSetKey) {
Jedis jedis = new Jedis(host, port);
if(password != null && !"".equals(password)) {
jedis.auth(password);
}
ScanParams scanParams = new ScanParams().count(100);
String cursor = "0";
do {
ScanResult<String> scanResult = jedis.sscan(bigSetKey, cursor, scanParams);
List<String> memberList = scanResult.getResult();
if(memberList != null && !memberList.isEmpty()) {
for(String member : memberList) {
jedis.srem(bigSetKey, member);
}
}
cursor = scanResult.getStringCursor();
} while(!"0".equals(cursor));
//删除bigkey
jedis.del(bigSetKey);
}

4. SortedSet删除: zscan + zrem

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void delBigZset(String host, int port, String password, String bigZsetKey) {
Jedis jedis = new Jedis(host, port);
if(password != null && !"".equals(password)) {
jedis.auth(password);
}
ScanParams scanParams = new ScanParams().count(100);
String cursor = "0";
do {
ScanResult<Tuple> scanResult = jedis.zscan(bigZsetKey, cursor, scanParams);
List<Tuple> tupleList = scanResult.getResult();
if(tupleList != null && !tupleList.isEmpty()) {
for(Tuple tuple : tupleList) {
jedis.zrem(bigZsetKey, tuple.getElement());
}
}
cursor = scanResult.getStringCursor();
} while(!"0".equals(cursor));
//删除bigkey
jedis.del(bigZsetKey);
}

本文作者:carlosfu
原文链接:https://yq.aliyun.com/articles/531067

List接口的方法subList的使用注意事项

发表于 2018-03-31 | 更新于 2019-08-28 | 分类于 Java基础 , 集合

JDK中,List接口有一个实例方法List subList(int fromIndex, int toIndex),其作用是返回一个以fromIndex为起始索引(包含),以toIndex为终止索引(不包含)的子列表(List)。
但值得注意的是,返回的这个子列表的幕后其实还是原列表;也就是说,修改这个子列表,将导致原列表也发生改变;反之亦然。
下面是一段实例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import java.util.ArrayList;  
import java.util.List;

public class TestSubList {
public static void main(String[] args) {
List<Integer> test = new ArrayList<Integer>();
//init list
for (int i = 0; i < 5; i++) {
test.add(i);//auto boxing
}
//display the list
System.out.print("the orginal list: ");
for (int i = 0; i < test.size(); i++) {
System.out.print(test.get(i) + " ");
}
System.out.println();

//sub list
List<Integer> sub = test.subList(1, 3);//sub list contains elements: 1, 2
sub.remove(1);//remove element "2" from sub list

//display the list again
System.out.print("the orginal list after sublist modified: ");
for (int i = 0; i < test.size(); i++) {
System.out.print(test.get(i) + " ");
}
System.out.println();
}
}

程序运行结果:
the orginal list: 0 1 2 3 4
the orginal list after sublist modified: 0 1 3 4

在这段代码中,我们并没有改变原始列表“test”其中的元素。然而,当删除由subList方法得到的子列表中的元素时,原始列表中的该元素也被删除了。

SpringCloud中Hystrix线程隔离导致ThreadLocal数据丢失

发表于 2018-03-30 | 更新于 2019-08-28 | 分类于 Java基础 , 多线程

在Spring Cloud中我们用Hystrix来实现断路器,Zuul中默认是用信号量(Hystrix默认是线程)来进行隔离的,我们可以通过配置使用线程方式隔离。

在使用线程隔离的时候,有个问题是必须要解决的,那就是在某些业务场景下通过ThreadLocal来在线程里传递数据,用信号量是没问题的,从请求进来,但后续的流程都是通一个线程。

当隔离模式为线程时,Hystrix会将请求放入Hystrix的线程池中去执行,这个时候某个请求就有A线程变成B线程了,ThreadLocal必然消失了。

下面我们通过一个简单的列子来模拟下这个流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class CustomThreadLocal {
static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
CustomThreadLocal.threadLocal.set("猿天地");
new Service().call();
}
}).start();
}
}

class Service {
public void call() {
System.out.println("Service:" + Thread.currentThread().getName());
System.out.println("Service:" + CustomThreadLocal.threadLocal.get());
new Dao().call();
}
}

class Dao {
public void call() {
System.out.println("==========================");
System.out.println("Dao:" + Thread.currentThread().getName());
System.out.println("Dao:" + CustomThreadLocal.threadLocal.get());
}
}

我们在主类中定义了一个ThreadLocal用来传递数据,然后起了一个线程,在线程中调用Service中的call方法,并且往Threadlocal中设置了一个值,在Service中获取ThreadLocal中的值,然后再调用Dao中的call方法,也是获取ThreadLocal中的值,我们运行下看效果:

1
2
3
4
5
Service:Thread-0
Service:猿天地
==========================
Dao:Thread-0
Dao:猿天地

可以看到整个流程都是在同一个线程中执行的,也正确的获取到了ThreadLocal中的值,这种情况是没有问题的。

接下来我们改造下程序,进行线程切换,将调用Dao中的call重启一个线程执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class CustomThreadLocal {
static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
CustomThreadLocal.threadLocal.set("猿天地");
new Service().call();
}
}).start();
}
}

class Service {
public void call() {
System.out.println("Service:" + Thread.currentThread().getName());
System.out.println("Service:" + CustomThreadLocal.threadLocal.get());
//new Dao().call();
new Thread(new Runnable() {
@Override
public void run() {
new Dao().call();
}
});
}
}

class Dao {
public void call() {
System.out.println("==========================");
System.out.println("Dao:" + Thread.currentThread().getName());
System.out.println("Dao:" + CustomThreadLocal.threadLocal.get());
}
}

再次运行,看效果:

1
2
3
4
5
Service:Thread-0
Service:猿天地
==========================
Dao:Thread-1
Dao:null

可以看到这次的请求是由2个线程共同完成的,在Service中还是可以拿到ThreadLocal的值,到了Dao中就拿不到了,因为线程已经切换了,这就是开始讲的ThreadLocal的数据会丢失的问题。

那么怎么解决这个问题呢,其实也很简单,只需要改一行代码即可:
static ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
将ThreadLocal改成InheritableThreadLocal,我们看下改造之后的效果:

1
2
3
4
5
Service:Thread-0
Service:猿天地
==========================
Dao:Thread-1
Dao:猿天地

值可以正常拿到,InheritableThreadLocal就是为了解决这种线程切换导致ThreadLocal拿不到值的问题而产生的。

要理解InheritableThreadLocal的原理,得先理解ThreadLocal的原理,我们稍微简单的来介绍下ThreadLocal的原理:

  • 每个线程都有一个 ThreadLocalMap 类型的 threadLocals 属性,ThreadLocalMap 类相当于一个Map,key 是 ThreadLocal 本身,value 就是我们设置的值。

    1
    2
    3
    public class Thread implements Runnable {     
    ThreadLocal.ThreadLocalMap threadLocals = null;
    }
  • 当我们通过 threadLocal.set(“猿天地”); 的时候,就是在这个线程中的 threadLocals 属性中放入一个键值对,key 是 当前线程,value 就是你设置的值猿天地。

    1
    2
    3
    4
    5
    6
    7
    8
    public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
    map.set(this, value);
    else
    createMap(t, value);
    }
  • 当我们通过 threadlocal.get() 方法的时候,就是根据当前线程作为key来获取这个线程设置的值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
    ThreadLocalMap.Entry e = map.getEntry(this);
    if (e != null) {
    @SuppressWarnings("unchecked")
    T result = (T)e.value;
    return result;
    }
    }
    return setInitialValue();
    }

通过上面的介绍我们可以了解到threadlocal能够传递数据是用Thread.currentThread()当前线程来获取,也就是只要在相同的线程中就可以获取到前方设置进去的值。

如果在threadlocal设置完值之后,下步的操作重新创建了一个线程,这个时候Thread.currentThread()就已经变了,那么肯定是拿不到之前设置的值。具体的问题复现可以参考上面我的代码。

那为什么InheritableThreadLocal就可以呢?

InheritableThreadLocal这个类继承了ThreadLocal,重写了3个方法,在当前线程上创建一个新的线程实例Thread时,会把这些线程变量从当前线程传递给新的线程实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
/**
* Computes the child's initial value for this inheritable thread-local
* variable as a function of the parent's value at the time the child
* thread is created. This method is called from within the parent
* thread before the child is started.
* <p>
* This method merely returns its input argument, and should be overridden
* if a different behavior is desired.
*
* @param parentValue the parent thread's value
* @return the child thread's initial value
*/
protected T childValue(T parentValue) {
return parentValue;
}

/**
* Get the map associated with a ThreadLocal.
*
* @param t the current thread
*/
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}

/**
* Create the map associated with a ThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the table.
*/
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}

通过上面的代码我们可以看到InheritableThreadLocal 重写了childValue, getMap,createMap三个方法,当我们往里面set值的时候,值保存到了inheritableThreadLocals里面,而不是之前的threadLocals。

关键的点来了,为什么当创建新的线程池,可以获取到上个线程里的threadLocal中的值呢?原因就是在新创建线程的时候,会把之前线程的inheritableThreadLocals赋值给新线程的inheritableThreadLocals,通过这种方式实现了数据的传递。

源码最开始在Thread的init方法中,如下:

1
2
if(parent.inheritableThreadLocals != null)
this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

createInheritedMap如下:

1
2
3
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap){       
return new ThreadLocalMap(parentMap);
}

赋值代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}

到此为止,通过inheritableThreadLocals我们可以在父线程创建子线程的时候将Local中的值传递给子线程,这个特性已经能够满足大部分的需求了,但是还有一个很严重的问题是如果是在线程复用的情况下就会出问题,比如线程池中去使用inheritableThreadLocals 进行传值,因为inheritableThreadLocals 只是会再新创建线程的时候进行传值,线程复用并不会做这个操作,那么要解决这个问题就得自己去扩展线程类,实现这个功能。

不要忘记我们是做Java的哈,开源的世界有你需要的任何东西,下面我给大家推荐一个实现好了的Java库,是阿里开源的transmittable-thread-local。
GitHub地址:https://github.com/alibaba/transmittable-thread-local

主要功能就是解决在使用线程池等会缓存线程的组件情况下,提供ThreadLocal值的传递功能,解决异步执行时上下文传递的问题。

JDK的InheritableThreadLocal类可以完成父线程到子线程的值传递。但对于使用线程池等会缓存线程的组件的情况,线程由线程池创建好,并且线程是缓存起来反复使用的;这时父子线程关系的ThreadLocal值传递已经没有意义,应用需要的实际上是把 任务提交给线程池时的ThreadLocal值传递到任务执行时。

transmittable-thread-local使用方式分为三种,修饰Runnable和Callable,修饰线程池,Java Agent来修饰JDK线程池实现类

接下来给大家演示下线程池的修饰方式,首先来一个非正常的案例,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class CustomThreadLocal {
static ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
static ExecutorService pool = Executors.newFixedThreadPool(2);
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
int j = i;
pool.execute(new Thread(new Runnable() {
@Override
public void run() {
CustomThreadLocal.threadLocal.set("猿天地" + j);
new Service().call();
}
}));
}
}
}

class Service {
public void call() {
CustomThreadLocal.pool.execute(new Runnable() {
@Override
public void run() {
new Dao().call();
}
});
}
}

class Dao {
public void call() {
System.out.println("Dao:" + CustomThreadLocal.threadLocal.get());
}
}

运行上面的代码出现的结果是不正确的,输出结果如下:

1
2
3
4
5
6
7
Dao:猿天地99
Dao:猿天地99
Dao:猿天地99
Dao:猿天地99
Dao:猿天地99
Dao:猿天地99
Dao:猿天地99

正确的应该是从1到100,由于线程的复用,值被替换掉了才会出现不正确的结果

接下来使用transmittable-thread-local来改造有问题的代码,添加transmittable-thread-local的Maven依赖:

1
2
3
4
5
<dependency>   
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.2.0</version>
</dependency>

只需要修改2个地方,修饰线程池和替换InheritableThreadLocal:

1
2
static TransmittableThreadLocal<String> threadLocal = new TransmittableThreadLocal<>();
static ExecutorService pool = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(2));

正确的结果如下:

1
2
3
4
5
6
7
Dao:猿天地93
Dao:猿天地94
Dao:猿天地95
Dao:猿天地96
Dao:猿天地97
Dao:猿天地98
Dao:猿天地99

到这里我们就已经可以完美的解决线程中,线程池中ThreadLocal数据的传递了,各位看官又疑惑了,标题不是讲的Spring Cloud中如何解决这个问题么,我也是在Zuul中发现这个问题的,解决方案已经告诉大家了,至于怎么解决Zuul中的这个问题就需要大家自己去思考了,后面有时间我再分享给大家。

本文作者:尹吉欢
原文链接:https://mp.weixin.qq.com/s/NxBLhGo6MzClGpQP9WXctA

Chrome命令

发表于 2018-03-29 | 更新于 2019-08-28 | 分类于 工具
非常有用的chrome://命令
  • chrome://downloads 等同于从菜单中查看下载内容,其快捷键是Ctrl+J
  • chrome://extensions 等同于菜单-工具-扩展
  • chrome://plugins 显示已安装插件
  • chrome://bookmarks 等同于菜单-书签-书签管理器,快捷键Ctrl+Shift+O
  • chrome://history 等同于从菜单-历史直接访问,快捷键 Ctrl+H
  • chrome://restart 重启chrome浏览器
  • chrome://apps 打开chrome网上应用商店
  • chrome://flags 可用来启用或者关闭某些chrome的体验特性
  • chrome://dns 显示浏览器预抓取的主机名列表,让你随时了解DNS状态
  • chrome://memory 重定向到chrome://memory-redirect/,显示系统运行中的浏览器内存使用情况,以及浏览器中进程的详细信息。
  • chrome://net-internals 显示网络相关信息,用来捕获浏览器生成的网络事件,可导出数据,可查看DNS主机解析缓存。
  • chrome://quota-internals 用来显示浏览器所使用磁盘空间配额的情况。
  • chrome://sessions 该命令用来显示当前运行的浏览器的会话信息数以及详细列表
  • chrome://settings 该命令可通过菜单-选项直接访问,可用来控制浏览器各项设置值
  • chrome://sync-internals 用来显示 chrome 的同步状态
  • chrome://about/ 查看 chrome 所有的命令

Macbook上好用的工具

发表于 2018-03-01 | 更新于 2019-08-28 | 分类于 工具

软件包管理工具:Homebrew
Homebrew能在Macbook中方便的安装软件或者卸载软件,相当于linux下的apt-get、yum神器;Homebrew可以在Macbook上安装一些OS X没有的UNIX工具,Homebrew将这些工具统统安装到了/usr/local/Cellar 目录中,并在 /usr/local/bin 中创建符号链接。
文本编辑器:Atom 免费、github出品
解压软件:The Unarchiver(免费)
清理软件:cleanmymac(收费);Dr.cleaner(免费)

12
wangxuan

wangxuan

诗意生活,如诗如画!

19 日志
14 分类
13 标签
GitHub E-Mail
© 2019 wangxuan
由 Hexo 强力驱动 v3.9.0
|
主题 – NexT.Gemini v6.7.0