上面 session B 的修改结果,被 session A 之后的 select 语句用当前读看到,不能称为幻读。幻读仅专指新插入的行。
如果只从我们学到的事务可见性规则来分析的话,上面这三条 SQL 语句的返回结果都没有问题。
因为这三个查询都是加了 for update,都是当前读。而当前读的规则,就是要能读到所有已经提交的记录的最新值。并且,session B 和 sessionC 的两条语句,执行后就会提交,所以 Q2 和 Q3 就是应该看到这两个事务的操作效果,而且也看到了,这跟事务的可见性规则并不矛盾。
幻读有什么问题?
**首先是语义上的。**session A 在 T1 时刻就声明了,“我要把所有 d=5 的行锁住,不准别的事务进行读写操作”。所以我们假设只锁了id = 5这一行的语义与select * from t where d = 5 for update 不同。
其次,是数据一致性的问题。 **这个数据不一致到底是怎么引入的?**肯定是前面的假设有问题。
我们把扫描过程中碰到的行,也都加上写锁,再来看看执行效果。
由于 session A 把所有的行都加了写锁,所以 session B 在执行第一个 update 语句的时候就被锁住了。需要等到 T6 时刻 session A 提交以后,session B 才能继续执行。
这样对于 id = 0 这一行,在数据库里的最终结果还是 (0,5,5)。在 binlog 里面,执行序列是这样的:
1 2 3 4 5 6 7
insert into t values(1,1,5); /*(1,1,5)*/ update t set c=5where id=1; /*(1,5,5)*/ update t set d=100where d=5;/* 所有 d=5 的行,d 改成 100*/ update t set d=5where id=0; /*(0,0,5)*/ update t set c=5where id=0; /*(0,5,5)*/
因为字符比较是从右往左比较的,所以第一层循环 needle.length + 1 <= i < haystack.length。
1 2 3 4 5 6 7 8 9 10 11 12 13
start=needle.length - 1 end=haystack.length - 1 + + | | | | v-------------------------------------------------------------------v-> 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ | t | h | i | s | | i | s | | a | | s | i | m | p | l | e | | e | x | a | m | p | l | e | +-------------------------------+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +---------------------------+ | e | x | a | m | p | l | e | +---+---+---+---+---+---+---+
第二层循环中i变量表示坏字符的位置、j表示搜索坏字符开始位置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
i + | v 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ | t | h | i | s | | i | s | | a | | s | i | m | p | l | e | | e | x | a | m | p | l | e | +-------------------------------+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +---------------------------+ | e | x | a | m | p | l | e | +---+---+---+---+---+---+-+-+ ^ | + j
/** * Returns the index within this string of the first occurrence of the * specified substring. If it is not a substring, return -1. * * There is no Galil because it only generates one match. * * @param haystack The string to be scanned * @param needle The target string to search * @return The start index of the substring */ publicstaticintindexOf(char[] haystack, char[] needle) { if (needle.length == 0) { return0; } int charTable[] = makeCharTable(needle); int offsetTable[] = makeOffsetTable(needle); for (inti= needle.length - 1, j; i < haystack.length;) { for (j = needle.length - 1; needle[j] == haystack[i]; --i, --j) { if (j == 0) { return i; } } // i += needle.length - j; // For naive method i += Math.max(offsetTable[needle.length - 1 - j], charTable[haystack[i]]); } return -1; }
/** * Makes the jump table based on the mismatched character information. */ privatestaticint[] makeCharTable(char[] needle) { finalintALPHABET_SIZE= Character.MAX_VALUE + 1; // 65536 int[] table = newint[ALPHABET_SIZE]; for (inti=0; i < table.length; ++i) { table[i] = needle.length; } for (inti=0; i < needle.length - 2; ++i) { table[needle[i]] = needle.length - 1 - i; } return table; }
/** * Makes the jump table based on the scan offset which mismatch occurs. * (bad character rule). */ privatestaticint[] makeOffsetTable(char[] needle) { int[] table = newint[needle.length]; intlastPrefixPosition= needle.length; for (inti= needle.length; i > 0; --i) { if (isPrefix(needle, i)) { lastPrefixPosition = i; } table[needle.length - i] = lastPrefixPosition - i + needle.length; } for (inti=0; i < needle.length - 1; ++i) { intslen= suffixLength(needle, i); table[slen] = needle.length - 1 - i + slen; } return table; }
/** * Is needle[p:end] a prefix of needle? */ privatestaticbooleanisPrefix(char[] needle, int p) { for (inti= p, j = 0; i < needle.length; ++i, ++j) { if (needle[i] != needle[j]) { returnfalse; } } returntrue; }
/** * Returns the maximum length of the substring ends at p and is a suffix. * (good suffix rule) */ privatestaticintsuffixLength(char[] needle, int p) { intlen=0; for (inti= p, j = needle.length - 1; i >= 0 && needle[i] == needle[j]; --i, --j) { len += 1; } return len; }
它的做法是给每一个生产者一个唯一的 ID,并且为生产的每一条消息赋予一个唯一 ID,消息队列的服务端会存储 < 生产者 ID,最后一条消息 ID> 的映射。当某一个生产者产生新的消息时,消息队列服务端会比对消息 ID 是否与存储的最后一条 ID 一致,如果一致,就认为是重复的消息,服务端会自动丢弃。
而在消费端,幂等性的保证会稍微复杂一些,你可以从通用层和业务层两个层面来考虑。
在通用层面,你可以在消息被生产的时候,使用发号器给它生成一个全局唯一的消息 ID,消息被处理之后,把这个 ID 存储在数据库中,在处理下一条消息之前,先从数据库里面查询这个全局 ID 是否被消费过,如果被消费过就放弃消费。
你可以看到,无论是生产端的幂等性保证方式,还是消费端通用的幂等性保证方式,它们的共同特点都是为每一个消息生成一个唯一的 ID,然后在使用这个消息的时候,先比对这个 ID 是否已经存在,如果存在,则认为消息已经被使用过。所以这种方式是一种标准的实现幂等的方式,**你在项目之中可以拿来直接使用,**它在逻辑上的伪代码就像下面这样:
**布隆过滤器不支持删除元素的缺陷也和 Hash 碰撞有关。**给你举一个例子,假如两个元素 A 和 B 都是集合中的元素,它们有相同的 Hash 值,它们就会映射到数组的同一个位置。这时我们删除了 A,数组中对应位置的值也从 1 变成 0,那么在判断 B 的时候发现值是 0,也会判断 B 是不在集合中的元素,就会得到错误的结论。
**那么我是怎么解决这个问题的呢?**我会让数组中不再只有 0 和 1 两个值,而是存储一个计数。比如如果 A 和 B 同时命中了一个数组的索引,那么这个位置的值就是 2,如果 A 被删除了就把这个值从 2 改为 1。这个方案中的数组不再存储 bit 位,而是存储数值,也就会增加空间的消耗。**所以,你要依据业务场景来选择是否能够使用布隆过滤器,**比如像是注册用户的场景下,因为用户删除的情况基本不存在,所以还是可以使用布隆过滤器来解决缓存穿透的问题的。
**其次,就是一致性 Hash 算法的脏数据问题。为什么会产生脏数据呢?**比方说,在集群中有两个节点 A 和 B,客户端初始写入一个 Key 为 k,值为 3 的缓存数据到 Cache A 中。这时如果要更新 k 的值为 4,但是缓存 A 恰好和客户端连接出现了问题,那这次写入请求会写入到 Cache B 中。接下来缓存 A 和客户端的连接恢复,当客户端要获取 k 的值时,就会获取到存在 Cache A 中的脏数据 3,而不是 Cache B 中的 4。