前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Mysql二阶段锁与死锁、连接池与临时表 & Redis为何缓存大批量错误数据

Mysql二阶段锁与死锁、连接池与临时表 & Redis为何缓存大批量错误数据

作者头像
吴就业
发布2020-07-10 11:52:47
6310
发布2020-07-10 11:52:47
举报
文章被收录于专栏:Java艺术Java艺术

这期要分享的内容,是上周我在项目中遇到的问题。我选了三个我认为比较重要的分享给大家。

  1. 数据库死锁异常;
  2. 临时表不存在异常;
  3. 缓存中出现大量脏数据导致收益下降。

Mysql二阶段锁与死锁

不知道你们有没有遇到过这样的业务场景:由一个定时任务去更新一个表中的数据,在某种情况下,就是全表更新(每一条记录都需要更新),这是最坏的情况。如果表里面有十几万的数据,那更新时间是需要很长的。

问题重现。

sched服务,定时任务

代码语言:javascript
复制
{
    定时任务类          Service类          Mapper接口
    开启事务
    for(...){
        更新一条记录 --->调用Mapper ------> sql="update table set statu=1 where id=#{id};"
    }
    结束,提交事务
}

场景一:

管理后台服务

代码语言:javascript
复制
{
    接口              Service类                    Mapper接口
    批量更新记录---->  开启事务
                     idArray := [] int {9,15,1};
                     for _,id = range idArray{
                        调用Mapper更新记录  ------> sql = “update table set statu=0 where id=#{id}”;
                     }
                     结束,提交事务
    接口响应 <-----
}

场景二:

管理后台服务

代码语言:javascript
复制
{
    接口               Service类           Mapper接口   
    批量更新记录 ---->  开启事务
                        调用Mapper ------> sql="update table set status=0 where id in (....)"
                      结束,提交事务
    接口响应 <--------
}

这里的例子所有update语句都是使用主键id去更新记录,所以使用的是行级锁,当然,并不是根据id的顺序去更新的。

场景一:

如果sched服务更新十几万的数据需要几分钟,此时如果有运营在管理后台操作更新这个表的某些记录,就有可能会出现死锁情况。如果不清楚二阶段锁,那你可能只想到,管理后台会因锁等待超时而更新失败,想不到为什么会出现死锁情况。

二阶段锁:

即事务开启后,并不会获取任何锁,只有第一条"写"类型的sql执行才会去获取相应的锁,并且只要在事务提交后才会释放所有当前事务获取到的锁。

在sched服务中,开启事务后,执行第一条update语句,假设这条记录的id是15,那只会取得id=15的行锁,在其它事务中更新其它id不等于15的记录是可以更新成功的。

假设,此时管理后台当前更新到id=9的记录,接着想更新id=15的记录,sched服务接着想更新id=9的记录,就会出现这样的情况,管理后台的事务,等待sched服务的事务释放id=15的行锁,而sched服务的事务,等待管理后台的事务释放id=9的行锁,这就出现了死锁。

场景二:

定时任务1不变,并且对于场景一管理后台的更新操作改用了批量更新sql(事务2)。假设管理后台需要更新的记录的id为{232,121,324},sched服务定时任务依然是几乎全表更新(事务1)。

假设此时:

定时任务取得的行锁:{100,102,121};

管理后台需要获取锁:{232,121,324},但发现121被其它事务取得了,所以需要等待锁,等待锁超时默认为50s,如果是因为等待锁超时不会抛出死锁异常,而是抛出等待超时异常。

那么此时,如果定时任务需要对232这条记录进行更新,就会出现,事务1等待事务2释放232的行锁,事务2等待事务1释放121的行锁,所以就会出现死锁的情况。

不信?有图有真像。

左边是事务1,右边是事务2。当左边执行两条sql获取id=3000和id=2987的行锁时,事务2执行更新操作,想要获取id in (3001,2987,1999)这三条记录的行锁,但发现id=2987的行锁被事务1占有了,就会进入等待锁的状态。

当事务1继续执行,想要更新id=1999这条记录的时候,发现需要获取的id=1999的行锁与事务2的互斥。此时,事务2等待事务1释放id=1999的行锁,而事务2已经取得了id=3001、1999的行锁,正等待事务1释放id=2987的行锁。由于msql有死锁检测机制,将事务2抛出了死锁异常,让事务1继续执行。

死锁检测机制我记得不太清楚了。隐约记得是使用一种利用有向图的数据结构的算法,当发现有向图即将出现回路时,会删除其中的一个节点,让当前事务可执行。想要了解具体的算法实现可以自行谷歌。

相信大家早已看出了问题,为什么在sched服务中使用这么大范围的事务,不瞒大家,这是新的项目,这些代码是我在项目初期的时候写的,现在是时候该还债了。我之所会使用大范围的事务,当初的业务要求是要么全部更新成功,其它未更新到的记录标志为不可用状态,要么全部更新失败,好吧,我又找借口了。

总结:不要使用大范围的事务。在微服务下也尽量不要使用大批量更新的sql。

连接池与临时表

再给大家说一个mysql相关的坑吧。一个老项目,在一个定时任务模块中,使用了临时表统计报表数据,定时任务每个小时执行一次,由于重构后,使用了动态数据源(可根据平台、数据库类型动态切换数据源),偶尔会发现定时任务执行失败,抛出临时表不存在的异常。

其实是因为临时表的生命周期是同一个会话(session),或者说同一个连接,而动态数据源使用了连接池,所以就不能保证每次执行sql都使用同一个连接,所以就会抛出表xxx不存在的异常。建议不要在使用数据库连接池的项目中使用mysql临时表。直接点,建议不要使用临时表。

Redis为何缓存了大批量错误数据

在项目中,hash类型的使用很频繁。如果使用不当,会导致缓存中存储了大量过时的数据,甚至会影响到业务逻辑的正确性。真的会有这么严重吗?

举个栗子:假设,我们需要存储电影的评论到缓存,需要存储的信息有,电影名称,用户名,评论内容。对应到hash类型,就是电影名称作为key,用户名作为field,评论内容作为value。这个例子确实不好,不过只是用以说明问题,不用去纠结这个问题。

伪代码如下

代码语言:javascript
复制
{
    Map<String,Map<String,String>> cacheHashMap = 数据库查询结果转化成;
    cacheHashMap.entrySet().stream().forEcho(
        entry->redis.hmset(entry.getKey(),entry.getValue()); 
    );
}

假设,缓存是由定时任务去更新的,每次都是批量更新。第一次更新时,没有任何问题,此时缓存中存储了key为“哪吒”,有用户“syy001”、"syy002"、"syy003"的留言。下一次更新时,如果没有做任何数据清理,数据也没有到过期过期时间。当前查询数据库获取到的留言数据需要更新到缓存,只有key为“悟空”的数据,留言用户有“s11”,"s22",那么缓存中就存有一份key为“哪吒”和“悟空”的数据。但实际“哪吒”的相关留言数据已经被删除,“哪吒”不在此次定时任务同步的数据内,所以是过期的数据,却没有移除缓存,key为“哪吒”的缓存数据就是脏数据。

上面是一种情况。第二种情况,假设“哪吒”这部电影,用户“syy001”删除了评论,用户“syy004”添加了评论。那么下一次执行定时任务全量更新的时候,发现从数据库中加载出来的并没有“syy001”的用户的评论,所以不会更新,也不会删除,而是更新了用户“syy002”、“syy003”和新增了用户"syy004"的用户的评论。那么缓存中用户“syy001”的数据就是脏数据。

所以使用redis缓存hash类型数据时,一定要注意两个问题:

1、设置key的过期时间,并且不能远大于下一次定时更新缓存的时间。

伪代码如下

代码语言:javascript
复制
{
    Map<String,Map<String,String>> cacheHashMap = 数据库查询结果转化成;
    cacheHashMap.entrySet().stream().forEcho(
        entry->redis.hmset(entry.getKey(),entry.getValue()); 
        redis.exists(entry.getKey(),比定时任务的周期长一点的过期时间);
    );
}

2、在更新key的数据时,需要删除key的全部field,或者先删除key,再进行set。但这可能出现短暂的key为null的情况,如果field很大的时间,set是需要时间的,所以不推荐,可以先存一个临时的key,再将key改名,这是最理想的。

伪代码如下

代码语言:javascript
复制
{
    Map<String,Map<String,String>> cacheHashMap = 数据库查询结果转化成;
    cacheHashMap.entrySet().stream().forEcho(
        entry->redis.hmset(entry.getKey()+"tmp",entry.getValue()); 
        redis.rename(entry.getKey()+"tmp",entry.getKey())
    );
}
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-08-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Java艺术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云数据库 SQL Server
腾讯云数据库 SQL Server (TencentDB for SQL Server)是业界最常用的商用数据库之一,对基于 Windows 架构的应用程序具有完美的支持。TencentDB for SQL Server 拥有微软正版授权,可持续为用户提供最新的功能,避免未授权使用软件的风险。具有即开即用、稳定可靠、安全运行、弹性扩缩等特点。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档