单锁的分拆降低冲突概率
+-----------+
--> | counter 1 |
+-----------+
--> | counter 2 |
+-----------+ -> sum(subCounter)
--> | counter 3 |
+-----------+
--> | counter 4 |
+-----------+
JAVA 中的 ConcurrentHashMap 和 LongAdder 的应用
在内部都有多个计数器,这样在并发更新对象的时候,随机选择一个,就可以将锁冲突的概率降低到 1/n
mysql 计数器的应用
拿文章的喜欢计数来做例子,当有n个人同时点击喜欢的时候,INNODB会锁住这条记录,这篇文章的喜欢计数器将是串行增长的。
pre_posts
+-------+-----+----------+
| id | ... | favs |
+-------+-----+----------+
| 10000 | ... | 8001 |
+-------+-----+----------+
再看下面的,可以将喜欢数从 posts 表拆分出来,一个文章 id,对应多个子计数器,在更新计数器的时候,可以随机选择一条记录增加,在取文章喜欢数的时候,可以sum一下累加起来;这样就降低了并发时候的冲突。
pre_post_favs
+-----+----------+----------+
| id | post_id | favs |
+-----+----------+----------+
| 1 | 10000 | 2001 |
| 2 | 10000 | 1994 |
| 3 | 10000 | 2010 |
| 4 | 10000 | 1898 |
+-----+----------+----------+
对于计数器的缓存
显然上面的数据解决了数据并发写入的问题,但是查询的代价却是提高了,想到的办法就是对计数器进行缓存。
读取的时候被动缓存的话,就会产生计数器的延迟;所以这里可以采用 canal 监听 mysql binlog 主动触发 cache 的更新。
那么问题又来了,既然是存到 redis 中,该用什么类型的;如果使用大量的 string 的话,key 就会占用大量的内存,可以部分改用 hash,但是 redis 的设定中,只有 数量 < hash-max-ziplist-entries
的时候才会采用优化的存储方式;根据多位大牛的研究 1000 是比较合适的。
favs:10
000 -> 8001
001 -> 0
...
999 -> 0