开发过程中,遇到消息发送功能,将要发送的消息对象先保存在Redis 队列 +内存队列(双保险),通过多个tomcat集群Quartz 轮询Job(两个集群都有)将消息存储到MySQL(5.5.51)发送表中,再通过多个tomcat集群Quartz 轮询发送Job 查询数据库,进行消息发送。其中遇到的几个问题:
1、多个实例job读取Redis,保存的发送数据重复保存。此问题使用Redis的分布式锁setnx 解决;通过java实现分布式锁RedisLock(部分代码参考网络);
import java.util.Random;
import org.apache.log4j.Logger;
import com.vstation.sys.config.FilesConfig;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.Transaction;
public class RedisLock {
private static final Logger log = Logger.getLogger(RedisLock.class);
//加锁标志
public static final String LOCKED = "TRUE";
public static final long ONE_MILLI_NANOS = 1000000L;
//默认超时时间(毫秒)
public static final long DEFAULT_TIME_OUT = 3000;
public static JedisPool pool;
public static final Random r = new Random();
//锁的超时时间(秒),过期删除
public static final int EXPIRE = 5 * 60;
private Jedis jedis;
private String key;
//锁状态标志
private boolean locked = false;
public RedisLock(String key) {
this.key = key;
this.jedis = new Jedis(FilesConfig.mainProp.get("redisHost"),
FilesConfig.mainProp.getInt("redisPort"),
FilesConfig.mainProp.getInt("redisMaxWaitMillis"));
}
/**
* 通过jedis.setnx实现锁
* @param timeout
* @return
*/
public boolean lock(long timeout) {
long nano = System.nanoTime();
timeout *= ONE_MILLI_NANOS;
try {
while ((System.nanoTime() - nano) < timeout) {
if (jedis.setnx(key, LOCKED) == 1) {
jedis.expire(key, EXPIRE);
locked = true;
log.info("["+key+"] locked is true");
return locked;
}
// 短暂休眠,nano避免出现活锁
Thread.sleep(3, r.nextInt(500));
}
} catch (Exception e) {
log.error("Lock error,"+e.toString());
}
log.info("["+key+"] locked is false");
return false;
}
public boolean lock() {
return lock(DEFAULT_TIME_OUT);
}
// 无论是否加锁成功,必须调用
public void unlock() {
log.info("begin unlock key["+key+"]");
try {
if (this.jedis.del(key) == 1) {
this.locked = false;//解锁
log.info("key["+key+"] unlock success!");
}else{
log.info("key["+key+"] unlock fail!");
}
} catch (Exception e) {
log.error ("unlock error,"+e.toString());
}
}
}
2、多个发送Jobs同时读取MySQL的发送表数据,造成重复发送。该问题主要是因为不同的实例同时读取mysql是可并发的,从而导致重复发送,解决方案,使用mysql的排他锁,排他读取发送消息。java实现mysql的排他锁(基于Jfinal相关事物实现),这里锁定的并非当前真正查询的表,而是新建一张锁表t_db_lock,使用要锁定的表名作为主键,通过锁定主键实现行锁:
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import org.apache.log4j.Logger;
import com.jfinal.plugin.activerecord.Db;
import com.jfinal.plugin.activerecord.DbKit;
import com.jfinal.plugin.activerecord.IAtom;
import com.vstation.model.pojo.BaseTDbLock;
import com.vstation.redis.RedisClient;
/**
* 锁表操作
* @author JamesCho
* @date 2018-8-1
*
*/
@SuppressWarnings("serial")
public class TDbLock extends BaseTDbLock<TDbLock> {
private final Logger log = Logger.getLogger(RedisClient.class);
private static final int LOCK_WAIT_TIMEOUT = 600;//表锁定超时时间
private Connection conn=null;
/**
* 锁表方法
* @param tableName
* @return
* @throws Exception
*/
public boolean lockTable(final String tableName) {
/**
* 事务管理
*/
return Db.tx(new IAtom() {
@Override
public boolean run() throws SQLException {
String sql = "select 1 from t_db_lock where id=? for update ";
Long beginTime = System.currentTimeMillis();
try{
conn=DbKit.getConfig().getDataSource().getConnection();
DbKit.getConfig().setThreadLocalConnection(conn);
conn.setAutoCommit(false);//
Statement state = conn.createStatement();
state.execute("SET GLOBAL innodb_lock_wait_timeout="+LOCK_WAIT_TIMEOUT);//锁定超时时间
Db.find(sql, tableName);
log.info(Thread.currentThread().getName()+"已锁定表["+tableName+"]:"+((System.currentTimeMillis()-beginTime))/1000);
return true;//可正常执行SQL,表示对应的表没有被锁定;
}catch(Exception ex){
log.error(Thread.currentThread().getName()+",锁定表记录行出错,"+ex.toString());
return false;
}
}
});
}
/**
* 释放连接
*/
public void unlockTable(){
try {
if(conn!=null) conn.commit();conn.close();
log.info(Thread.currentThread().getName()+" connection commit!");
} catch (SQLException e) {
log.error("commit connection error,"+e.toString());
}finally{
try {
conn.close();
log.info(Thread.currentThread().getName()+" close connection");
} catch (SQLException e) {
log.error("关闭连接出错,"+e.toString());
}
}
}
}
遗留问题:
当MySQL开启 log-bin (主从数据库)可能还是会存在重复发送的问题,此问题需要进一步了解主从配置相关技术点进行解决。在不配置主从情况下已经不会再重复发送!
遗留问题解决:
当MySQL开启log-bin (主从数据库)模式时,lockTable 不能有单独的事物(Jfinal中用 Db.tx实现),因为单独的事物会让外面调用LockTable的连接与lockTable方法中的连接不一样(多次实验得出),从而导致行锁无效,使得消息又重复读取发送。解决方案,从外部调用方法中传入Connection对象给lockTable使用,改造如下:
说明:先前的lockTable方法改名未了lockTableByRow 更贴切
/**
* 通过行锁锁定表
* @param tableName
* @return
* @throws Exception
*/
public void lockTableByRow(final String tableName,Connection conn){
/**
* 事务管理
*/
String sql = "select 1 from t_db_lock where id=? for update ";
Long beginTime = System.currentTimeMillis();
try{
this.conn = conn;
Statement state = conn.createStatement();
state.execute("SET GLOBAL innodb_lock_wait_timeout="+LOCK_WAIT_TIMEOUT);//锁定超时时间
Db.find(sql, tableName);
log.info(Thread.currentThread().getName()+"行锁已锁定表["+tableName+"]:"+((System.currentTimeMillis()-beginTime))/1000);
}catch(Exception ex){
log.error(Thread.currentThread().getName()+",表行锁出错,"+ex.toString());
// throw ex;
}
}
外部调用示例:
final TDbLock dbLock = new TDbLock();
final String threadName = Thread.currentThread().getName();
Db.tx(new IAtom() {
@Override
public boolean run() throws SQLException {
Connection conn = null;
try {
conn=DbKit.getConfig().getDataSource().getConnection();
conn.setAutoCommit(false);
/**
* 锁定后,后续进来的线程会在此 pending
* 直到事物处理完成
*/
dbLock.lockTableByRow("t_msg_send_his",conn);
List<TMsgSend> list = TMsgSend.dao.findSendFinish(count);// 查找已经发送完成的消息
if (list != null && list.size() > 0) {
String[] arrayIds = null;
log.info(threadName+", handle data" + JsonKit.toJson(list));
arrayIds = new String[list.size()];
for (int i = 0; i < list.size(); i++) {
TMsgSend msg = list.get(i);
Db.deleteById("t_msg_send", msg.getId());// 及时删除发送表数据,避免重复发送
arrayIds[i] = msg.getId();
Record msgRecord = msg.toRecord();
msgRecord.set("id", UUID.randomUUID().toString());
msgRecord.set("sendId", msg.getId());
Db.save("t_msg_send_his", msgRecord);// 保存到历史包
}
return true;
} else {
return false;
}
} catch (Exception ex) {
log.error(threadName+",handle data error," + ex.toString());
return false;
} finally {
dbLock.unlock();// 释放表资源
}
}
});
来源:oschina
链接:https://my.oschina.net/u/2948579/blog/1922175