面试反馈
1、redis除了缓存外其他用法
轻量级消息队列,点赞,抽奖,签到打卡,可能认识的人,附近的酒店,购物车,热点新闻、分布式锁,统计日活月活。
一、锁的种类
synchronized和lock是单机版JVM(即在同一个虚拟机上)使用的锁,对于分布式架构(存在多个不同的虚拟机)单机版的锁线程机制不再起作用
(1)折中的方案:使用setnx
多个订单模块去抢库存模块,这里就需要用到锁,setnx就是在库存模块和订单,模块之间加一个redis,在这个redis上执行setnx尝试获取锁
简单的方法
//使用redislock
setnx redislock 1
//使用完后归还redislock
del redislock
存在的问题:不高可用,存在死锁情况, 可能存在“乱抢”现象
(2)设计锁的要求
①独占性:onlyOne,任何时刻只有一个线程可以持有这把锁
②高可用:在redis集群环境下,不能因为某一个节点挂了而出现获取锁和释放锁失败的情况;高并发情况下,性能依然良好。
③防死锁:必须有个兜底的方案跳出死锁(如超时控制机制和撤销操作)
④不乱抢:一个线程不能去释放别人的锁。
线程A获得锁A后,设置了过期时间10s,如果线程A出现了故障,10s内没有完成任务释放锁A,10s时锁过期失效被删除,第11s线程B又获得了锁A并设置过期时间,而此时线程A又完成了操作把锁A删除了,等线程B完成操作想要释放锁A,这时候发现锁A没有了,这样就会出现问题。
⑤可重入性:同一个线程可以多次获得同一把锁
(3)业务实现
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
private Lock lock = new ReentrantLock();
public string sale{
String retMessage="";
lock.lock();
try{
//查询库存数量
String result = stringRedisTemplate.opsForValue.get("inventory001");
//数据类型转换
Integer inventoryNumber = result==null ? 0 : Integer.parseInt(result);
//根据库存执行业务
if(inventoryNumber>0){
stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余:"+inventoryNumber;
System.out.println(resMessage+"\t"+"服务端口号"+port);
}else{
retMessage="商品卖完了";
}
}finally{
lock.unlock();
}
return resMessage + "\t" + "服务端口号" + port;
}
二、分布式锁实现
(1)使用setnx实现分布式锁,保证不超卖(使用JMeter实现压测)
public String sale(){
String retMessage = "";
String key = "zzyyRedisLock";
String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
Boolean flag = string RedisTemplate.opsForValue().setIfAbsent(key, uuidValue);
if(!flag){
//不断尝试
try{TimeUnit.MILLISECONDS.sleep(20);}catch(InterruptedException e){
e.printStackTrace();
sale();
}
}else{
try{
//强锁成功后进行的业务代码
}finally{
//完成业务后一定要释放锁
stringRedisTemplate.delete(key);
}
}
}
存在的问题:
①高并发下禁止用递归,堆和栈本身容易溢出,用递归容易导致stackoverflow
②使用if判断在高并发环境下可能会导致虚假唤醒,应用while替换if,达到自旋的效果
(2)用while优化,实现高可用
public String sale(){
String retMessage = "";
String key = "zzyyRedisLock";
String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
//用while替换if,用自旋替换递归
while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue)){
//不断尝试
try{TimeUnit.MILLISECONDS.sleep(20);}catch(InterruptedException e){
e.printStackTrace();
}
}
try{
//强锁成功后进行的业务代码
}finally{
//完成业务后一定要释放锁
stringRedisTemplate.delete(key);
}
}
存在的问题:setIfAbsent里的key没有加过期时间。如果A建成功了,但微服务挂掉,即代码运行到业务代码且finally里的内容没运行,不设置过期时间的key一直存在,其他的线程或者客户就无法获得这把锁,造成死锁现象。
(3)加入过期时间来优化,实现防死锁
public String sale(){
String retMessage = "";
String key = "zzyyRedisLock";
String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
//加锁和设置过期时间必须同时进行保证原子性
while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue,30,TimeUnit.SECONDS)){
//不断尝试
try{TimeUnit.MILLISECONDS.sleep(20);}catch(InterruptedException e){
e.printStackTrace();
}
}
//stringRedisTemplate(key, 30,TimeUnit.SECONDS);
try{
//强锁成功后进行的业务代码
}finally{
//完成业务后一定要释放锁
stringRedisTemplate.delete(key);
}
}
存在的问题:线程A的第30秒key过期,第31秒线程B新建同名的key,第33秒线程A误删线程B建的key,线程B执行完,就发现要删的key没有了。
(4)添加限制,线程只能删除自己建的锁,不能删除别人的
public String sale(){
String retMessage = "";
String key = "zzyyRedisLock";
String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
//加锁和设置过期时间必须同时进行保证原子性
while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue,30,TimeUnit.SECONDS)){
//不断尝试
try{TimeUnit.MILLISECONDS.sleep(20);}catch(InterruptedException e){
e.printStackTrace();
}
}
//stringRedisTemplate(key, 30,TimeUnit.SECONDS);
try{
//强锁成功后进行的业务代码
}finally{
if(stringRedisTemplate.opsForValue().get(key).equalsIgnoreCase(uuidValue)){
//完成业务后一定要释放锁
stringRedisTemplate.delete(key);
}
}
}
存在问题:先if判断key的id,然后删除,这不是原子操作,仍然可能导致误删别人的key
解决方法:使用lua脚本
redis调用lua通过eval命令保证代码执行的原子性,直接return返回执行后的结果
eval luascript numkeys key1 key2 ... arg1 arg2 ...
luascript指定脚本的内容,在脚本里如果想调用redis命令,就要使用redis,call(),在脚本里使用return返回脚本的返回值给redis
双引号内要用单引号
除了最后一个业务分支外都要有then,if语句要用end作为结尾
(5)用lua实现判断删除的原子性(执行lua脚本的过程是原子的),实现不乱删
public String sale(){
String retMessage = "";
String key = "zzyyRedisLock";
String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
//加锁和设置过期时间必须同时进行保证原子性
while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue,30,TimeUnit.SECONDS)){
//不断尝试
try{TimeUnit.MILLISECONDS.sleep(20);}catch(InterruptedException e){
e.printStackTrace();
}
}
//stringRedisTemplate(key, 30,TimeUnit.SECONDS);
try{
//强锁成功后进行的业务代码
}finally{
String luascript = "if redis.call('get',KEYS[1])==ARGV[1] then"+
"return redis.call('del', KEYS[1])" +
"else" +
"return 0" +
"end";
//DefaultRedisScript构造器的第二个参数要执行返回值类型,不添加会报错
//execute()的第二个参数指定key列表,第三个指定val参数值
stringRedisTemplate.execute(new DefaultRedisScript(luascript, Boolean.class), Arrays.asList(key),uuidValue);
}
return retMessage
}
使用JMeter进行并发测试
存在的问题:锁不具有可重入性
public String sale(){
String retMessage = "";
String key = "zzyyRedisLock";
String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue,30,TimeUnit.SECONDS)){
try{TimeUnit.MILLISECONDS.sleep(20);}catch(InterruptedException e){
e.printStackTrace();
}
}
//stringRedisTemplate(key, 30,TimeUnit.SECONDS);
try{
//业务代码
//此处如果调用了某个方法,而这个方法又需要访问redis的某个key作为锁,
//如果不做额外处理,就会导致死锁,因为该key已经被外层方法获取了
}finally{
String luascript = "if redis.call('get',KEYS[1])==ARGV[1] then"+
"return redis.call('del', KEYS[1])" +
"else" +
"return 0" +
"end";
stringRedisTemplate.execute(new DefaultRedisScript(luascript, Boolean.class), Arrays.asList(key),uuidValue);
}
return retMessage
}
(6)实现可重入
JUC里的reentrantlock和synchronized都是可重入锁,使用reentranlock时lock几次就要unlock几次
详情参考:
为了记录每个线程对一把锁的重入次数,在redis中可以使用hash结构来记录
if redis.call('exists', 'key') == 0 then
redis.call('hset','key','uuid:threadid',1)
redis.call('expire','key',50)
return 1
elseif redis.call('hexists','key','uid:thread') == 1 then
redis.call('hincrby','key','uuid:threadid',1)
redis.call('expire','key',50)
return 1
else
return 0
end
//用hincrby替换hset
if redis.call('exists', 'key') == 0 or redis.call('hexists','key','uid:thread') == 1 then
redis.call('hincrby','key','uuid:threadid',1)
redis.call('expire','key',50)
return 1
else
return 0
end
//动态传参
if redis.call('exists',KEYS[1]) == 0 or if redis.call('hexists',KEYS[1],ARGV[1]) == 1 then
redis.call('hincrby',KEYS[1],ARGV[1],1)
redis.call('expire',KEYS[1],ARGV[2])
return 1
else
return 0
end