记毕设过程中遇到的一个InnoDB的"坑"

2017-04-09  本文已影响0人  Coselding

情景描述

  1. Mybatis映射文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.coselding.docsearcher.catcher.dao.UrlCatcherDao">
    <select id="getTop1" resultType="UrlCatcher">
        select * from url_catcher where status=#{status} limit 0,1;
    </select>
    <update id="setStatus">
        update url_catcher set status= #{newStatus},err_msg=#{errMsg} where id = #{id} and status= #{oldStatus}
    </update>
</mapper>
  1. Dao只是个接口:
public interface UrlCatcherDao {

    UrlCatcher getTop1(@Param("status") Integer status);

    int setStatus(@Param("id") Integer id,
                  @Param("oldStatus") Integer oldStatus,
                  @Param("newStatus") Integer newStatus,
                  @Param("errMsg") String errMsg);
}
  1. url状态枚举:
    public enum CatcherStatus {

        NO_CATCH(0, "还没爬取"),
        CATCHING(1, "正在爬取"),
        CATCHED(2, "爬取过了"),
        FAILED(3, "爬取失败");

        private int code;
        private String description;
    }
  1. 抢锁关键Service
    @Transactional(isolation = Isolation.REPEATABLE_READ)
    public UrlCatcher findTopAndLock() {
        UrlCatcher catcher = null;
        int count = 0;
        while (count <= 0) {
            //(1)获取表中status为NO_CATCH的第一条记录,sql语句见上面的Mybatis映射文件
            catcher = urlCatcherDao.getTop1(CatcherStatus.NO_CATCH.getCode());
            System.out.println("catcher = " + catcher + ",count = " + count);
            //(2)爬虫停止
            if (catcher == null) {
                break;//容器中没url了
            }
            //(3) 修改状态,确认锁定:将对应id的记录,如果状态为NO_CATCH就修改为CATCHING,否则不修改,返回值count为这条sql执行后对表中影响的行数
            count = urlCatcherDao.setStatus(catcher.getId(), CatcherStatus.NO_CATCH.getCode(), CatcherStatus.CATCHING.getCode(), "");
            //count小于等于0表示没抢到,接着循环抢下一个,抢到了就返回
        }
        logger.info("抢锁成功:catcher = {}", catcher);
        return catcher;
    }
  • 说明:
  1. 第一步:获取表中status为NO_CATCH的第一条记录,sql语句见上面的Mybatis映射文件,很好理解
  2. 第二步:获取的第一条记录catcher为空,表示表中没有可用url,退出循环返回,这不是重点,这是爬虫停止条件
  3. 第三步:对第一步获取的catcher对象id进行CAS抢锁(如果状态为NO_CATCH就修改为CATCHING,否则不修改,返回值count为这条sql执行后对表中影响的行数),这样的结果就是如果该线程抢到了,状态修改成功(即加锁),count>0退出循环返回,否则就是被其他线程抢了,count=0继续外层while循环
  4. @Transactional(isolation = Isolation.REPEATABLE_READ)设定该操作的事务隔离级别

死循环了吧~
看见id为1450了吗?我让这个死循环接着运行着,然后控制台sql查一下这条记录的status:

select id,status from url_catcher WHERE id=1450

结果如下:


sql-result.png

对比上面的枚举类,可以知道该url当前的状态为CATCHING,被哪个线程抢了我不管,但是已经被抢了,这样在抢锁逻辑中urlCatcherDao.getTop1(CatcherStatus.NO_CATCH.getCode());这句肯定不应该获取这条记录的,我们把死循环的程序停了,看看打的日志:
不在公司,屏幕比较小,没法完成截图,复制其中一行看看:

catcher = UrlCatcher{id=1450, docId=1, fullUrlPath='http://hadoop.apache.org/docs/r2.6.5/hadoop-mapreduce-client/hadoop-mapreduce-client-hs/images/logos/', rootUrlPath='http://hadoop.apache.org/docs/r2.6.5/hadoop-mapreduce-client/hadoop-mapreduce-client-hs', rootFilePath='/Users/coselding/test1', parentPath='/images/logos', filename='', createTime=1491723334618, status=0, errMsg='null'},count = 0
20

看重点!!!id=1450,status=0(对应枚举NO_CATCH
这尼玛,同样在运行中,死循环中查询出的1450记录status=0,而我用控制台sql得到的status=1,心中千万只草泥马奔腾而过!!!

解决过程

  1. 首先想到的自然是事务隔离级别,我就在草稿纸上画两个事务线程的可能的执行轨迹,不论怎么画,都想不到有怎样的轨迹能够达到这种执行结果!!!这些不是重点,不贴图了,然后我就不想理论的了,直接把@Transactional(isolation = Isolation.REPEATABLE_READ)换着测试,一开始还挺顺利,换成SERIALIZABLE就不死循环了,但是因为没有理论支撑,我多执行了几次,然后死循环依然出现了,看来是锁粒度变大导致了死循环发生频率降低了,但是至少说明了我对这些事务隔离级别的理解还是对的,世界观没崩塌,还好还好~
  2. 然后还是查资料:mysql缓存?就算是缓存也有有效时间,不可能死循环
  3. 先查查事务隔离级别,找思路,直到找到了这几篇博客:
  1. 重新回顾了一下二段锁协议,也了解了InnoDB的行级锁基于索引的,没建立索引的字段无法触发行级锁,还有间隙锁,感觉自己了解的还是不够,之后还是得花时间好好补补
  2. 重点来了:InnoDB的乐观锁实现MVCC,规则如下:
    innoDB-MVCC.png

InnoDB基于事务版本号对select实现了快照读,insert、delete、update是当前读,简单说呢,就是select在并发环境下可能读取到的就是历史纪录(和死循环的现象吻合),具体详解可以参照Innodb中的事务隔离级别和锁的关系,有了这个思路,我们来分析一下上面的死循环原因~

原因过程分析

还是id=1450作为例子,初始创建版本号createVersion为0,删除版本号deleteVersion为null

过程 事务线程1 事务线程2
事务开始 createVersion=0,deleteVersion=null createVersion=0,deleteVersion=null
事务版本号 version=1 version=2
1 getTop1执行:createVersion<version,deleteVersion=null
2 获取status=0
3 getTop1执行:createVersion<version,deleteVersion=null
4 获取status=0
5 setStatus执行,update规则:新纪录(status=1,createVersion=2,deleteVersion=null),原记录快照(status=0,createVersion=0,deleteVersion=2)
6 抢锁成功,commit
7 返回,该事务线程结束
8 setStatus抢锁失败
9 下次循环:getTop1执行:createVersion<version,deleteVersion=null,查找到了刚才的原记录快照(status=0)
10 setStatus抢锁失败,接着循环
11 该事务线程永远无法commit,version版本号永远不变,永远查找到原记录快照,status永远为0,陷入死循环

最终代码

    public UrlCatcher findTopAndLock() {
        UrlCatcher catcher = null;
        int count = 0;
        while (count <= 0) {
            catcher = urlCatcherDao.getTop1(CatcherStatus.NO_CATCH.getCode());
            System.out.println("catcher = " + catcher + ",count = " + count);
            if (catcher == null) {
                break;//容器中没url了
            }
            //抢分布式锁:开始抢锁
            //修改状态,确认锁定
            count = urlCatcherDao.setStatus(catcher.getId(), CatcherStatus.NO_CATCH.getCode(), CatcherStatus.CATCHING.getCode(), "");
            //count小于等于0表示没抢到,接着循环抢下一个,抢到了就返回
        }
        logger.info("抢锁成功:catcher = {}", catcher);
        return catcher;
    }
public class UrlCatcherServiceImpl implements UrlCatcherService {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());
    @Autowired
    private UrlCatcherDao urlCatcherDao;
    private static ConcurrentHashMap<Integer, Long> urlLockMap = new ConcurrentHashMap<Integer, Long>();

    public void prepareLockMap() {
        urlLockMap.clear();
    }

    public UrlCatcher findTopAndLock() {
        UrlCatcher catcher = null;
        int count = 0;
        Long currentThreadId = Thread.currentThread().getId();
        while (count <= 0) {
            catcher = urlCatcherDao.getTop1(CatcherStatus.NO_CATCH.getCode());
            logger.info("catcher = " + catcher + ",count = " + count);
            if (catcher == null) {
                break;//容器中没url了
            }
            //抢分布式锁:开始抢锁
            Long beforeThreadId = urlLockMap.putIfAbsent(catcher.getId(), currentThreadId);
            if (beforeThreadId == null) {
                //抢到了:之前该id为key没有映射关系
                //修改状态,确认锁定
                count = urlCatcherDao.setStatus(catcher.getId(), CatcherStatus.NO_CATCH.getCode(), CatcherStatus.CATCHING.getCode(), "");
            } else {
                //没抢到:之前已经有映射关系了
                count = 0;
            }
            //count小于等于0表示没抢到,接着循环抢下一个,抢到了就返回
        }
        logger.info("抢锁成功:catcher = {}", catcher);
        return catcher;
    }
}

总结

上一篇 下一篇

猜你喜欢

热点阅读