问题描述
记录一下这次线上问题排查的思路及收获,以及仍没有解决的点。
服务背景:这属于一个控量的服务,我们会对需要平均投放的数据进行更新分时处理。我们开启了两个单线程,并且部署在不同的机器上,但同时只会有一个线程在处理(通过在redis里设置一个key作为互斥锁)。问题出现在其中一台机上的线程死了,但其RedisLock这个线程并没有关闭掉(???这是一个疑问点,按正常流程,应该执行finally方法,然后该线程被interrupt掉),导致这个key一直被持有,从而另一个线程执行不了更新操作。
问题总结
针对这个问题,总结出了几个知识点:
线程不执行finally方法
- 线程try里面死锁
- 线程catch里面System.exit()
- 出现Error级别的异常,但我们只捕捉到了Exception异常级别
反思
- 日志的error级别一定要和info日志打印到不同的文件里。这个问题为什么排查不出来,因为线上日志被冲掉了。而这是一个偶发性问题,很难复现。目前现有的结论都是推测的。
- 对于程序中一定要用线程池取代单线程,防止线程死掉,造成难以察觉错误。
// 一个class类的部分代码,其作用是取redis中的当天数据,然后进行分时操作;
private Thread thread;
private boolean enable = true;
//启动线程
public void start() {
if(null == thread || !thread.isAlive()){
Worker worker = new Worker();
thread = new Thread(worker);
thread.start();
}
}
//关闭该线程
public void stop(){
if(null != thread){
thread.interrupt();
}
enable = false;
}
class Worker implements Runnable{
@Override
public void run() {
RedisLock rl = null;
try {
rl = new RedisLock("job_name", 30);
} catch (URISyntaxException e) {
LOGGER.error("", e);
}
while (enable){
RedisConnection jedis = null;
Pipeline pipeline = null;
try {
Thread.sleep(1000 * 60 * 1);
// redis锁, 每日job只能有一个执行
boolean lock = rl.lock();
if(lock){
// 获得锁
jedis = new RedisConnection(...);
pipeline = jedis.pipelined();
// 针对redis的一些操作
......
}
}
catch (InterruptedException interrupted){
break;
}
catch (Exception e) {
LOGGER.error("",e);
}
finally {
if(null != pipeline){
try {
pipeline.close();
} catch (IOException e) {
}
}
if(null != jedis){
jedis.close();
}
if(null != rl){
rl.unlock();
}
}
}
if(null != rl){
rl.close();
}
}
}
//redis 竞争锁
public class RedisLock {
private static final Logger LOGGER = LoggerFactory.getLogger(RedisLock.class);
private String lockKey;
private long keyValue;
private int expire;
private volatile boolean isLocked; // 现有锁状态
private Thread thread;
/**
*
* @param lockKey 锁名
* @param expire 锁过期时间
* @throws URISyntaxException
*/
public RedisLock(String lockKey, int expire) throws URISyntaxException {
this.lockKey = lockKey;
this.expire = expire;
this.isLocked = false; // 未锁
}
public void close(){
unlock();
}
/**
* 请求锁
* @return
*/
public boolean lock() {
if(isLocked){
return isLocked;
}
synchronized (this){
isLocked = lock(lockKey, expire);
if(isLocked){
if(null == thread || !thread.isAlive()){
thread = new Thread(new Worker());
thread.start();
}
}
LOGGER.debug(String.format("%s lock %s!", lockKey, isLocked));
return isLocked;
}
}
/**
* 释放锁
*/
public void unlock() {
if (isLocked) {
synchronized (this) {
// 锁
if (null != thread) {
thread.interrupt();
try {
thread.join();
} catch (InterruptedException e) {
}
thread = null;
LOGGER.debug(String.format("%s unlock!", lockKey));
}
}
}
}
private boolean lock(String lockKey, int expireSec){
Jedis jedis = null;
try{
jedis = Environment.getRedisPool().getResource();//new RedisConnection(RedisInfo.CAP);
// 获得key的值
String value = jedis.get(lockKey);
if (null == value) {
// 如果key没有值,则添加key同时累加1
keyValue = jedis.incrBy(lockKey, 1);
if (1 == keyValue) {
// 如果累加值等于1,则说明是第一个使用者,则获得逻辑锁,并添加过期时间
jedis.expire(lockKey, expireSec);
return true;
}
else {
// 如果累加值不等于1,则说明不是第一个使用者,则放弃处理
}
}
else {
// 如果key有值,说明已经被占用,则放弃处理
}
}
catch(Exception ex){
}
finally {
if(null != jedis){
jedis.close();
}
}
return false;
}
class Worker implements Runnable {
@Override
public void run() {
while(true){
Jedis jedis = null;
try {
Thread.sleep(1000);
if (isLocked) {
// 锁且未释放
jedis = Environment.getRedisPool().getResource();//new RedisConnection(RedisInfo.CAP);
expire(jedis, lockKey, expire);
}
}
catch (InterruptedException e) {
break;
}
catch (Exception e){
LOGGER.error("", e);
}
finally {
if(null != jedis){
jedis.close();
}
}
}
Jedis jedis = null;
try{
jedis = Environment.getRedisPool().getResource();//new RedisConnection(RedisInfo.CAP);
releaseSource(jedis);
}
catch (Exception e){
LOGGER.error("", e);
}
finally {
if(null != jedis){
jedis.close();
}
}
LOGGER.debug(String.format("%s release lock thread!", lockKey));
}
private void releaseSource(Jedis jedis){
del(jedis, lockKey);
isLocked = false; // 未锁
}
private void del(Jedis jedis, String key){
if(null != jedis) {
jedis.del(key);
}
}
private void expire( Jedis jedis, String key, int expire){
if(null != jedis) {
jedis.expire(key, expire);
}
}
}