面试题总结
1、在分布式服务中,一个生成订单的接口别一次性点击了很多次,在集群服务器中生成了很多的订单怎么办?
答:这个用redis分布式锁来解决。
这其实是一个解决幂等性(数学和计算机领域的一个概念,就是对某一个动作进行多次操作,都只能产生一种结果。例如,在http协议的访求方式中,同样的提交请求,PUT方法就是幂等性的,POST方法就是非幂等性)的问题。对于同一个用户,通过生成订单接口,我们希望一次只能生成一个订单,即使是通过集群订单微服务,即使用户能够多次点击生成订单接口。那么,这个现象我们换一种说法就是,一个身份,我要求在一个短暂的时间内只能生成一个订单,集群微服务在进行事物之前,都去检验某个相同的地方,满足这三个条件就能解决这个问题。这个身份就是用户的token,很简单,时间可以定义在三秒或四秒,放的位置就在缓存nosql数据库redis就最好。
具体可以使用redis的两个方法 getset 和 getnx(SET if Not eXists) 两个方法来解决。两个方法的特点是,前者:在设置一个(key:value)值的时候返回这个key对应的旧的value;后者:在设置一个(key:value)值的时候,如果这个key已经存在,那就设置不进去,返回0,如果可不存在则设置进去了,返回1,而且这个方法能直接设置有效期。这两个方法相比来讲,后者更加适合当作redis分布式锁。
getnx:主要代码部分
// 返回1表示锁获取成功,返回0表示锁取失败。lockKey是通过token得来的,
//例如String lockKey = MD5Util.md5Of32(token).append("_lock").toString(),它是key值
//nowTime是此刻时间,它是value值,expire值的意义是,在这个时间内本应该生成订单的,如果超过这个时间这个键值对还在的花,后面的也可以替换掉它。
String result = jedisCommands.set(lockKey, nowTime, "NX", "PX", expire);
if ("1".equals(result)) {
return true; //返回true说明拿到了这个锁,可以进行事物(例如生单)操作
} else {
String oldTime = redisTemplate.opsForValue().get(lockKey);
if (null != oldTime) {
// 检查锁lockKey的值是不是超过了设定的时间,如2秒钟,如果超过了则继续尝试获取锁,
// 直到获取到锁,或者数据未超期时退出,循环判断可以解决死锁的问题
if (now - Long.parseLong(oldTime) >= expire) {// 数据已经过期了
con = true;
}
}
getset:主要代码部分
long now = System.currentTimeMillis();
// Redis的GetSet返回的值必须是字符串,否则会抛异常,因而将其转换为字符串
String nowTime = String.valueOf(now);
String oldTime = null;
// 判断用于锁定的key是否已经被设置了值,如果被设置了值,则用于控制后续的处理逻辑不再进行
if ((oldTime = redisTemplate.opsForValue().getAndSet(lockKey, nowTime)) != null) {
// 检查锁lockKey的值是不是超过了设定的时间,如2秒钟,没有超过则返回,不继续处理后续的任务;
// 针对正常的业务请求这个是可以约定的,针对非正常的请求,被拦截也很正常,所以这个问题不是问题。
if (now - Long.parseLong(oldTime) < timeout) {
return false;
}
return true;
}
return true;
最终:redis分布式锁,其实就是借用redis的缓存一用,我做事物的时候,给个标记,相同就别进来了,事物做完了,我自己删除那个标记。如果,我没做完的过程中死掉了,没删掉这个标记,那个标记的过期时间就是我的死亡时间。
2、Synchronized的原理是什么?“锁”到底是什么?怎么确定锁?可重入锁和不可重入锁是什么概念?
1、其实被加了所的代码块在编译成字节码之后会看到,在代码块的前面又一个monitorenter指令,代码块末尾有个moniterexit指令,这些指令都是有个Reference类型参数,其实这个参数就是锁。Synchronized的原理就是,当一个线程通过monitorenter时,当前的对象没有被锁定,或者当前线程已经拥有了这个对象的锁,会给当前的对象锁的计数器+1,到monitorexit时,该锁的及计数器-1。只有当对象的计数器为0的时候,锁才会被释放。当锁计数器不为0的时候,别的线程拿到该对象的锁,则进入阻塞等待状态,知道别线程释放这个锁
2、锁就是Reference类型的对象,它是monitorenter和monitorexit指令的参数。当synchronized有传入对象做参数的时候,锁就是这个参数,没有传入对象的时候,那就分被修饰的方法是静态还是非静态。非静态时,锁就是调用该方法的当前对象,就是this;静态的时候,那锁就是该静态方法所属的静态类了。
3、可重入锁就是在已经持有一个对象锁的情况下还可以再次进入该对象锁的情况,反之就是不可重入锁。进入一个持有对象锁的代码块,该锁计数器+1,在代码块里面再进入还是此锁的代码块,那该所计数器再+1,就成了2了,以此类推,只要是可重入锁,就继续往上加,只要到最后释放一次次-1就行。Synchronized和ReentrantLock是可重入锁。
3、用过ThreadLocal吗?它是什么?在什么场景下使用
ThreadLocal其实是一个带有泛型的变量,它是用来存放不同线程所需要的不同成员变量的,就是多个线程公用一个成员变量,还希望它存储的东西不同,那就用ThreadLocal来存储。因为这个类型的底层是一个map,这个类里面有Thread.currentThread()获取当前的线程,没存入一个对象,都把线程和对象以键值对的形式存放在map以供后面get提取调用。例如在订单模块,购物车模块,那些明确需要用户信息的模块,一般在一个拦截器里面,拦截器里面一个静态get方法提取ThreadLocal里面的对象,后面直接可以用拦截器.方法得到同一个线程的对象。
4、一个类,重写equals方法后一定要重写hashCode方法吗?
是的。
如果一个类重写了equals方法但是没有重写hashCode方法,那么该类无法结合所有基于散列的集合(HashMap,HashSet)一起正常运作。解释一下就是,重写了equals方法,说明程序对该对象有自己的等值比较方法,但是如果想往在hashMap等散列集合中存入该对象当作key值得话,它是先比较hashCode值的,所以也要重写hashCode方法。
例如:一个Person类的对象,只要他们名字一样就代表这两个对象是一样的,要这样重写
static class Person {
private String name;
public Person(String name) {
this.name = name;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Person) {
Person person = (Person) obj;
return name.equals(person.name);
}
return false;
}
@Override
public int hashCode() {
return name.hashCode();
}
}
直接将name的hashCode的值返回,当成对象的hashCode的值。