tomcat 集群 Redis MySQL 分布式消息发送 Redis锁,MySQL锁问题

大憨熊 提交于 2019-12-06 22:18:15

开发过程中,遇到消息发送功能,将要发送的消息对象先保存在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();// 释放表资源
				}
			}
		});

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!