redis学习日记(一)-在CentOS上部署redis服务

之前公司项目用过redis做消息队列,但是没有深入探究过,趁着现在有空重新系统学习一下。

第一步先在服务器上安装一下redis服务,这个没啥技术含量,直接转一下别人的链接,我照着安装的没有问题。

https://www.cnblogs.com/heqiuyong/p/10463334.html

接下来还需要配置一下外网访问

1.确认配置文件bind的ip是否正确,一般想要外网能访问,需要填写为0.0.0.0,表示监听任何ip

注意任何人都能访问,一定要开启密码 requirepass 你的密码

2.确认protected-mode 是否为 no

3.修改完配置文件后重启Redis

service redis restart

另外为了避免redis密码被暴力破解,我在服务器上配置了6379端口入站策略,只有我的测试机器可以访问。

Spring事务异常rollback-only深入探究

首先rollback-only出现的原因先简单带过一下吧:在使用了Propagation.REQUIRED的事务传递中,若本层的service捕获了下层service的异常,则本层中的事务也无法提交,在方法结束,事务尝试提交时会报出Transaction rolled back because it has been marked as rollback-only错误。这是因为REQUIRED是同一个事务,具有原子性。当内层的方法出现异常的时候,会标记一个rollback-only,这样外层方法提交的时候,判断rollback-only为true,那么整个事务都不允许提交。参考代码如下:(以下代码全部基于service层方法级别注解,且事务传播机制为REQUIRED)

service1
public String getName(String name) {
    try {
        demoService2.insertUser();
  }catch (Exception e) {
    e.printStackTrace();
  }
  demoService3.insertUser();
  return "1";
}
service2
public void insertUser()  {
    userDao.insertUserError();//这里报错,字段超长
    
}
service3
public void insertUser() {
    userDao.insertUser1(); //这里正常插入一条数据
  }

这样执行service1.getName后,service3的insertUser也是无法提交的。原因就是上边说的。

接下来对service2进行一下修改

service2
public void insertUser()  {
    try {
        userDao.insertUserError();//这里报错,字段超长
    }catch (Exception e) {
    e.printStackTrace();
  }
    
}

这样执行一下,可以看到service3提交了。这里就不再分析源代码了。研究一下原因是:DAO没有标记事务,只是使用了当前的连接,因此当DAO异常,没有标记当前事务的roll-back。而异常在service2里边进行catch了,因此在service1中没有异常,因此对于整个事务来说,spring认为这个事务没有出现异常,因此事务正常提交。

在修改一下

service2
public void insertUser()  {
    try {
        demoService4.insertUser();
      userDao.insertUserError();//这里报错,字段超长
    }catch (Exception e) {
    e.printStackTrace();
  }
    
}
service4
public void insertUser()  {
    userDao.insertUserError();//这里报错,字段超长
    
}
这样再执行service1.getName还是和第一种情况一样,roll-back only了。

考虑到spring的事务传播机制,我们再对代码进行try catch时,应该多一些思考,事务传播往往和异常有着千丝万缕的关系。check exception不用多说了,主要是对于runtimeException,一定要慎重。 比如,在最内层catch异常,会对整个事务的原子性造成污染,即try的DAO可能执行失败了,但是外层方法的事务还是提交了。 当存在需要单独开辟事务的场景时,需要考虑其他的事务传播属性。

实际开发中,对于异常的处理更是一门学问,另找时间研究。

又一次栽倒arraylist浅拷贝的坑

先记下两次的解决方法。

  1. 实现Cloneable 接口,重写clone方法。

场景:现有一个类Person[sex,age],存放在ArrayList中。要求得到一个ArrayList,存放sex=男和sex=女的年龄总和。

思路:重写Person的hashCode和equals方法,使用sex判断。遍历ArrayList l1,将ArrayList存到Set中,再转回ArrayList l2,获取去重后的性别。再遍历l2,内部遍历l1,判断sex相等的,将age加到l2中。

for(int i=0;i<l2.size();i++){

  for (int j=0;i<l1.size();f++){

    if(l2.i.sex=l1.j.sex){

      l2.age=l2.age+l1.age;

    }  

}

}

问题:在改变l2中person对象的age时,l1的person对象也发生了变化。

原因:经查看HashSet的add方法,内部调用了HashMap的put方法。

private static final Object PRESENT = new Object();

public boolean add(E e) {

        return map.put(e, PRESENT)==null;

    }

查看HashMap的put方法

   public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

可以看到上述代码,在hashmap中找到hash相同的Entry,然后判断key相等的,对value进行交换,即hashMap后边put的会覆盖之前put的value。而对于hashSet而言,传入的value是空的Object,因此Key没有发生变化,即遍历ArrayList l1,将ArrayList存到Set中,这一步,Set中存放的sex=男 的这个对象是l1中第一条sex=男 的对象,后边的add直接跳过。而再Set转回l2后,对l2的person对象的age属性进行操作的同时,l1中的相同的person对象也发生了变化。

错误思路:使用Collections.copy(newList, oldList);创建一个新的l3代替l1,然后进行转set。结果:无效,经查看

Collections.copy创建了新的list对象,但是list中存放的person对象还是原person对象,因此无效。

正确思路:重写person的clone方法。在set转l2的时候,存入l2的存放person的克隆对象,这样修改l2的

person对象的时候不会影响到l1的person对象。

注意:默认的clone方法,其实现还是person层的浅拷贝,若person中有引用类型的属性,而对这个

引用类型的属性进行操作时,还应注意。由此引出下一条。

2.new 一个新对象,挨着set值。

不再赘述,一句话总结:浅拷贝,包含clone的默认实现或者Collections.copy,获取的对象是

新的对象,但是对象内部的引用类型属性的地址是相同的。可以重新指定新对象的引用对象的地址,但是

直接修改引用对象的属性,则原对象的引用对象的属性也会改变。

cron表达式 每个月的最后一个工作日的另一种解法

近日有个需求,要求某项定时任务在每个月的最后一个工作日执行(仅考虑周末的情况),但是没有好的cron表达式写法,最后在stackoverflow找到了思路

cron每天执行一次,然后在执行定时任务的方法加个判断

public static boolean checkLastWorkDay() {
Calendar cal1 = Calendar.getInstance();
int month = cal1.get(Calendar.MONTH) + 1;
int year = cal1.get(Calendar.YEAR);
int day = cal1.get(Calendar.DATE);
LOG.warn(year + "年" + month + "月" + day + "日");
int week = 0;
Calendar cal = Calendar.getInstance();
cal.set(Calendar.YEAR, year);
cal.set(Calendar.MONTH, month - 1);
int lastDay = cal.getActualMaximum(Calendar.DAY_OF_MONTH);// 获取该月最大一天
cal.set(Calendar.DAY_OF_MONTH, lastDay);
week = cal.get(Calendar.DAY_OF_WEEK) - 1 == 0 ? 7 : cal.get(Calendar.DAY_OF_WEEK) - 1;// 获得最后一天是星期几
if (week == 7) {
lastDay = lastDay - 2;
} else if (week == 6) {
lastDay = lastDay - 1;
}
if (day == lastDay) {
LOG.warn("今天是本月的最后一个工作日");
return true;
} else {
LOG.warn("今天不是是本月的最后一个工作日");
return false;
}
}

问题解决。

悲观锁

悲观锁简而言之就是查询的时候就把数据锁上,修改完提交后释放锁。这样如果别的session有事务没提交的话,查询就会处在等待状态。锁一旦释放,这个session争夺到资源后立即上锁。的确暴力而有效啊。