直接写入Mysql是最简单的做法,1.数据库优化之定
redis设计部分:
前言
点赞其实是一个很有意思的功能。基本的设计思路有大致两种, 一种自然是用mysql(写了几百行的代码都还没写完,有毒)啦
数据库直接落地存储, 另外一种就是利用点赞的业务特征来扔到redis(或memcache)中, 然后离线刷回mysql等。
我这里所讲的功能都是基于我之前的项目去说的,所以有些地方可以不用管的,我主要是记录这个功能的实现思路,当你理解了,基本想用什么鬼语言写都一样的。
Linux:Linux 的配置文件为 my.cnf ,一般在 /etc 下
在 my.ini 增加几行:
long_query_time : 设定慢查询的阀值,超出次设定值的SQL即被记录到慢查询日志,缺省值为10s
slow_query_log : 指定是否开启慢查询日志
log_slow_queries : 指定是否开启慢查询日志(该参数要被slow_query_log取代,做兼容性保留)
slow_query_log_file : 指定慢日志文件存放位置,可以为空,系统会给一个缺省的文件host_name-slow.log
min_examined_row_limit:查询检查返回少于该参数指定行的SQL不被记录到慢查询日志
log_queries_not_using_indexes: 不使用索引的慢查询日志是否记录到索引
4.数据库优化之分析慢查询
使用explain慢查询语句,来详细分析慢查询语句
mysql>explain select * from dept where loc='aaa' G
*********************1.row ********************
id:1 //查询序列号
select_type:SIMPLE //查询类型
table:dept //查询的表名
type:ALL 所描的方式,all表示全表扫描这列很重要,显示了连接使用了哪 种类别,有无使用索引.
从最好到最差的连接类型为const、eq_reg、ref、range、indexhe和ALL
possible_keys:NULL//可能使用到的索引
key:NULL//实际使用到的索引
key_len:NULL//
rows:10//该sql语句扫描了多少行,可能得到的记录
Extra:Using where //sql语句的额外信息,比如排序方式filesort
5.数据库优化之遵循范式
数据库表设计时需要遵循范式
表的范式,是首先符合1NF,才能满足2NF,进一步满足3NF
1NF:即表的列具有原子性不可再分解,即列的信息,不能分解,只要数据库是关系型数据库,就自动的满足1NF,关系型数据库中是不允许分割列的
2NF:表中的记录是唯一的,通常我们设计一个主键来实现
3NF:即表中不要有冗余的数据,就是说,表的信息,如果能够被推导出来,就不应该单独的设计一个字段来存放(外键)
反3NF:没有冗余的数据库未必是最好的数据库,有时为了提高运行效率,就必须降低范式标准,适当的保留冗余数据,具体做法是:在概念数据库设计时遵循第三范式,降低范式标准的工作放到物理数据模型设计时考虑,降低范式就是增加字段,允许冗余,订单和订单项,相册浏览次数和照片的浏览次数等
6.数据库优化之选择合适的存储引擎
在开发中,我们经常使用的存储引擎myisam/innodb/memory
MyISAM存储引擎
如果表对事物要求不高,同时是以查询和添加为主的,我们考虑使用myisam存储引擎,比如bbs中的发帖表,回复表。
INNODB存储引擎
对事务要求高,保存的数据都是重要数据,我们建议使用INNODB存储引擎,比如订单表,账号表
Memory存储引擎
我们数据变化频繁,不需要入库,同时又频繁的查询和修改,我们考虑使用Memmory存储引擎,速度极快
MyISAM存储引擎与INNODB存储引擎的主要区别:
1.事务安全 myisam不支持事务而innodb支持
2.查询和添加速度 myisam不用支持事务就不用考虑同步锁,查询和添加的速度快
3.锁机制 myisam只支持表锁,innodb支持行锁
4.外键 myisam不支持外键,innodb支持外键
5.支持全文索引 myisam支持全文索引,innodb不支持全文索引
7.数据库优化之创建合适的索引
索引(Index)是帮助DBMS高效获取数据的数据结构
分类:普通索引/唯一索引/主键索引/全文索引
普通索引:允许重复的值出现
唯一索引:除了不能有重复的值外,其他的和普通索引一样
主键索引:是随着设定主键而创建的,也就是把某个列设为主键的时候,数据库就会给该列自动创建索引,这就是主键索引,唯一并且没有null值
全文索引:用来对表中的文本域(char,varchar,text)进行索引,全文索引针对myisam
8.数据库优化之索引使用小技巧
索引弊端
1.占用磁盘空间
2.对dml(插入,修改,删除)操作有影响,变慢
使用场景
a:肯定在where条件经常使用,如果不做查询就没有意义
b:该字段的内容不是唯一的几个值
c:字段内容不是频繁变化,不会出现在where语句中字段不该创建索引
具体技巧
1.对于创建的多列索引(复合索引),不是使用的第一部分就不会使用索引
alert table dept add index my_index(dname,loc);//dname左边的列,loc就是右边的列
explain select * from dept where dname='aaa' G会使用到索引
explain select * from dept where loc='aaa' G就不会使用到索引
2.对于使用like查询,查询如果是'%aaa' 就不会使用到索引,而‘aaa%'会使用到索引
explain select * from dept where dname like '%aaa' G 不会使用到索引
explain select * from dept wehre dname like 'aaa%' G 会使用到索引
所以在like查询时,关键字的最前面不能使用%或者_这样的字符,如果一定要前面有变化的值,则考虑使用全文索引->sphinx
3.如果条件中有or,有条件没有使用索引,即使其中有条件带索引也不会使用,换言之,就是要求使用的所有字段,都必须单独使用时能使用索引
4.如果列类型是字符串,那一定要在条件中将数据库使用引号引起来,否则不使用索引
explain select * from dept where dname='111'//使用索引
explain select * from dept where dname =111//不使用索引
5.如果myslq估计使用全表扫描比使用索引快,则不用索引
如:表里面只有一条记录
9.数据库优化之分表
分表分为水平(按行)分表和垂直(按列)分表
水平分表:
根据经验,mysql表数据一般达到百万级别,查询效率会很低,容易造成表锁,甚至堆积很多连接,直接挂掉;水平分表能够很大程度减少这些压力,按行数据进行分表
垂直分表:
如果一张表中某个字段非常多(长文本,二进制),而且只有在很少的情况下会查询,这时候就可以把字段多个单独放到一个表,通过外键关联起来。按列进行分表
比如:考试详情表,一般我们只关注分数,不关注详情
水平分表策略
1.按时间分表
这种分表方式有一定的局限性,当数据有较强的实效性,如微博发送记录,微信消息记录等,这种数据很少有用户会查询几个月前的数据,如就可以按月分表
2.按区间范围分表
一般在有严格的自增id需求上,如按照user_id 水平分表:
table_1 user_id 从1~100w
table_2 user_id 从101~200w
table_3 user_id 从201~300w
3.hash分表******(用的最多)
通过一个原始目标的ID或者名称通过一定的hash算法计算出数据存储表的表名,然后访问相应的表
10.数据库优化之读写分离
当一台数据库支持的最大并发连接数是有限的,如果用户并发访问太多,一台服务器满足不了要求时,就可以集群群里,mysql的集群处理技术最常用的就是读写分离。
1.主从同步
数据库最终会把数据持久化到磁盘,如果集群必须确保每个数据库服务器的数据是一直的,能改变数据库的操作都往主数据库去写,而其他的数据库从主数据库上同步数据
2.读写分离
使用 负载均衡来实现写的操作都往主数据库去,而读的操作都往从服务器去
11.数据库优化之缓存
在持久层(dao)和数据(db)之间添加一个缓存层,如果用户访问的数据已经缓存起来时,在用户访问数据库直接从缓存中获取,不用访问数据库,而缓存是在内存级别的,访问速度快。
作用:减少数据库服务器压力,减少访问时间。
java中常用的缓存有reids,memcache
可以使用redis作为中央缓存
将用户的点赞/取消赞的情况记录在redis中, 具体为:
直接写入Mysql
直接写入Mysql是最简单的做法。
做三个表即可,
comment_info
记录文章的主要内容,主要有like_count,hate_count,score这三个字段是我们本次功能的主要字段。
comment_like
记录文章被赞的次数,已有多少人赞过这种数据就可以直接从表中查到;
user_like_comment
记录用户赞过了哪些文章, 当打开文章列表时,显示的有没有赞过的数据就在这里面;
开启慢查询方式一:修改配置文件
对每个post维护一个计数器, 用来记录当前在redis中的点赞数,
这次的开篇,算是总结下这段时间来的积累吧,废话不多说,直接干!
Windows:Windows 的配置文件为 my.ini,一般在 MySQL 的安装目录下或者 c:Windows 下。
将用户点赞数据, 例如赞状态, post_id, user_id, ctime(操作时间),
mtime(修改时间)写入post_user_like_{$post_id}_{$user_id}
中
缺点
数据库读写压力大
热门文章会有很多用户点赞,甚至是短时间内被大量点赞, 直接操作数据库从长久来看不是很理想的做法
mysql数据库性能优化
个人博客网站:
mysql数据库的优化,其他数据库类似
1.数据库优化之定位
查找定位慢查询
2.数据库优化手段
- 创建索引:创建合适的索引,我们就可以先在索引中查询,查询以后直接找对应的记录
- 分表:当一张表的数据比较多或者一张表的某些字段的值比较多并且很少使用时,采用水平分表或者垂直分表来优化
- 读写分离:当一台服务器不能满足需求时,采用读写分离的方式进行集群
- 缓存:采用redis来进行缓存
3.数据库优化之定位慢查询的方式
在项目自验项目转测试之前,在启动mysql数据库时开启慢查询,并且把执行慢查询的语句写到日志中,在
运行一定时间后,通过查看日志找到慢查询语句
前言
redis存储随后批量刷回数据库
redis主要的特点就是快, 毕竟主要数据都在内存嘛;
另外为啥我选择redis而不是memcache的主要原因在于redis支持更多的数据类型, 例如hash, set, zset等。
下面具体的会用到这几个类型。
如果原来是取消赞的情况, 本次是点赞, counter加一。
缺点
开发复杂
这个比直接写mysql的方案要复杂很多, 需要考虑的地方也很多;
不能保证数据安全性
redis挂掉的时候会丢失数据, 同时不及时同步redis中的数据, 可能会在redis内存置换的时候被淘汰掉;
不过对于我们点赞而已, 稍微丢失一点数据问题不大;
其实上面第二点缺点是可以避免的,这就涉及到redis 的一些设计模式,不懂没关系,我尽量详细的写,后面我会给出如何解决这个缺点。
设计功能前知识准备
1.将要用到的redis数据类型(具体的类型说明,请看底部链接,有详细说明):
zset 这个类型主要用来做排序或者数字的增减,这里被用作like 和hate的数字记录,以及热度的记录。
set 这个是无序集合,主要用来记录今天需不需要更新,将今天被点赞(包括点讨厌)过的文章id记录下来,方便晚上或者有时间对这部分数据更新。
- hash 这个是散列,主要用来存储数据以及索引。这里被用来记录用户对哪个文章点了什么,方便下次判断(我看过一些网上的介绍使用set来记录,那个也可以,但是本人觉得这样做更省空间,以及方便管理,再有就是hash的速度快)。
- list 这个是队列大佬,我们的数据能不能 安全 回到mysql就靠它了。
2.关于热度如何去判断:
大家都知道,文章获得点赞数越高,文章的热度就越高,那么怎么判断呢?不就直接记录点赞数就行啦,但是对于最新的文章怎么办?例如有一篇文章一年前发布的,获得50个赞,有篇最新文章获得49个赞,但是按照上面所说的一年前的文章热度还比最新的高,这就不合理了,文章都是时效性,谁都想看最新最热的。
so!我们要换个方法去处理这个时效性,绝大部分语言都有 时间戳 生成的方法,时间戳随着时间越新,数字越大,直接将时间戳初始化赋值给文章的score,这样最新的文章相比以前的文章就会靠前了。接着是点赞对score的影响,我们假设一天得到20个赞算是一天最热,一天60*60*24=86400秒,然后得到一个赞就是得到86400 / 20 = 4320分。具体数字看自己的业务需求定,我只是举例子而已。点hate当然也会减去相应的数字。
激动时刻!直接上代码了!里面有详细注释!
1 <?php
2
3 class Good
4 {
5 public $redis = null;
6
7 //60*60*24/20=4320,每个点赞得到的分数,反之即之。
8 public $score = 4320;
9
10 //点赞增加数,或者点hate增加数
11 public $num = 1;
12
13 //init redis
14 public $redis_host = "127.0.0.1";
15 public $redis_port = "6379";
16 public $redis_pass = "";
17
18 public function __construct()
19 {
20 $this->redis = new Redis();
21 $this->redis->connect($this->redis_host,$this->redis_port);
22 $this->reids->auth($this->redis_pass);
23 }
24
25 /**
26 * @param int $user_id 用户id
27 * @param int $type 点击的类型 1.点like,2.点hate
28 * @param int $comment_id 文章id
29 * @return string json;
30 */
31 public function click($user_id,$type,$comment_id)
32 {
33 //判断redis是否已经缓存了该文章数据
34 //使用:分隔符对redis管理是友好的
35 //这里使用redis zset-> zscore()方法
36 if($this->redis->zscore("comment:like",$comment_id))
37 {
38 //已经存在
39 //判断点的是什么
40 if($type==1)
41 {
42 //判断以前是否点过,点的是什么?
43 //redis hash-> hget()
44 $rel = $this->redis->hget("comment:record",$user_id.":".$comment_id);
45 if(!$rel)
46 {
47 //什么都没点过
48 //点赞加1
49 $this->redis->zincrby("comment:like",$this->num,$comment_id);
50 //增加分数
51 $this->redis->zincrby("comment:score",$this->score,$comment_id);
52 //记录上次操作
53 $this->redis->hset("comment:record",$user_id.":".$comment_id,$type);
54
55 $data = array(
56 "state" => 1,
57 "status" => 200,
58 "msg" => "like+1",
59 );
60 }
61 else if($rel==$type)
62 {
63 //点过赞了
64 //点赞减1
65 $this->redis->zincrby("comment:like",-($this->num),$comment_id);
66 //增加分数
67 $this->redis->zincrby("comment:score",-($this->score),$comment_id);
68 $data = array(
69 "state" => 2,
70 "status" => 200,
71 "msg" => "like-1",
72 );
73 }
74 else if($rel==2)
75 {
76 //点过hate
77 //hate减1
78 $this->redis->zincrby("comment:hate",-($this->num),$comment_id);
79 //增加分数
80 $this->redis->zincrby("comment:score",$this->score+$this->score,$comment_id);
81 //点赞加1
82 $this->redis->zincrby("comment:like",$this->num,$comment_id);
83 //记录上次操作
84 $this->redis->hset("comment:record",$user_id.":".$comment_id,$type);
85
86 $data = array(
87 "state" => 3,
88 "status" => 200,
89 "msg" => "like+1",
90 );
91 }
92 }
93 else if($type==2)
94 {
95 //点hate和点赞的逻辑是一样的。参看上面的点赞
96 $rel = $this->redis->hget("comment:record",$user_id.":".$comment_id);
97 if(!$rel)
98 {
99 //什么都没点过
100 //点hate加1
101 $this->redis->zincrby("comment:hate",$this->num,$comment_id);
102 //减分数
103 $this->redis->zincrby("comment:score",-($this->score),$comment_id);
104 //记录上次操作
105 $this->redis->hset("comment:record",$user_id.":".$comment_id,$type);
106
107 $data = array(
108 "state" => 4,
109 "status" => 200,
110 "msg" => "hate+1",
111 );
112 }
113 else if($rel==$type)
114 {
115 //点过hate了
116 //点hate减1
117 $this->redis->zincrby("comment:hate",-($this->num),$comment_id);
118 //增加分数
119 $this->redis->zincrby("comment:score",$this->score,$comment_id);
120
121 $data = array(
122 "state" => 5,
123 "status" => 200,
124 "msg" => "hate-1",
125 );
126 return $data;
127 }
128 else if($rel==2)
129 {
130 //点过like
131 //like减1
132 $this->redis->zincrby("comment:like",-($this->num),$comment_id);
133 //增加分数
134 $this->redis->zincrby("comment:score",-($this->score+$this->score),$comment_id);
135 //点hate加1
136 $this->redis->zincrby("comment:hate",$this->num,$comment_id);
137
138 $data = array(
139 "state" => 6,
140 "status" => 200,
141 "msg" => "hate+1",
142 );
143 return $data;
144 }
145 }
146 }
147 else
148 {
149 //未存在
150 if($type==1)
151 {
152 //点赞加一
153 $this->redis->zincrby("comment:like",$this->num,$comment_id);
154 //分数增加
155 $this->redis->zincrby("comment:score",$this->score,$comment_id);
156 $data = array(
157 "state" => 7,
158 "status" => 200,
159 "msg" => "like+1",
160 );
161 }
162 else if($type==2)
163 {
164 //点hate加一
165 $this->redis->zincrby("comment:hate",$this->num,$comment_id);
166 //分数减少
167 $this->redis->zincrby("comment:score",-($this->score),$comment_id);
168
169 $data = array(
170 "state" => 8,
171 "status" => 200,
172 "msg" => "hate+1",
173 );
174 }
175 //记录
176 $this->redis->hset("comment:record",$user_id.":".$comment_id,$type);
177 }
178
179 //判断是否需要更新数据
180 $this->ifUploadList($comment_id);
181
182 return $data;
183 }
184
185 public function ifUploadList($comment_id)
186 {
187 date_default_timezone_set("Asia/Shanghai");
188 $time = strtotime(date('Y-m-d H:i:s'));
189
190 if(!$this->redis->sismember("comment:uploadset",$comment_id))
191 {
192 //文章不存在集合里,需要更新
193 $this->redis->sadd("comment:uploadset",$comment_id);
194 //更新到队列
195 $data = array(
196 "id" => $comment_id,
197 "time" => $time,
198 );
199 $json = json_encode($data);
200 $this->redis->lpush("comment:uploadlist",$json);
201 }
202 }
203 }
204
205 //调用
206 $user_id = 100;
207 $type = 1;
208 $comment_id= 99;
209 $good = new Good();
210 $rel = $good->click($user_id,$type,$comment_id);
211 var_dump($rel);
温馨提示:
1.上面代码只是一个实现的方法之一,里面的代码没精分过,适合大部分小伙伴阅读。用心看总有收获。
2.对于第三方接口,应该在外面包装多一层的,但是边幅有限,我就不做这么详细,提示,大家可以作为参考。
3.剩下的将数据返回数据的方法,等下篇再继续了。欢迎大家来交流心得。
redis手册中文版传送门:
写压力对于前面的方案确实是不大好使。
优点
性能高
缓解数据库读写压力
其实我更多的在于缓解写压力, 真的读压力, 通过mysql主从甚至通过加入redis对热点数据做缓存都可以解决,
写压力对于前面的方案确实是不大好使。
2、写入post_user_like_set_{$post_id}
post_user_like_{$post_id}_{$user_id}
下面具体的会用到这几个类型。
另外为啥我选择redis而不是memcache的主要原因在于redis支持更多的数据类型, 例如hash, set, zset等。
根据post_id
, user_id
,
直接获取对应的hash表的内容(post_user_like_{$post_id}_{$user_id}
redis主要的特点就是快, 毕竟主要数据都在内存嘛;
做两个表即可,
将每个用户对每个post的点赞情况放到一个hash里面去, hash的字段就
当然有同学会说用key, value也可以, 将所有的数据序列化(json_encode
等)
点赞其实是一个很有意思的功能。基本的设计思路有大致两种, 一种自然是用mysql等
热门文章会有很多用户点赞,甚至是短时间内被大量点赞, 直接操作数据库从长久来看不是很理想的做法。
优点
页面展示
Mysql设计
这一块和写入写mysql是一样的,毕竟是要落地存储的。
这个比直接写mysql的方案要复杂很多, 需要考虑的地方也很多;
但没有针对用户量较大的场景考虑分表的设计, 可以考虑针对user_id或者post_id进行分表
缓存和数据库都要查询, 缓存没有再查询数据库。
所以还是同样的需要post_like
,
user_like_post
这两表存储文章被点赞的个数(等统计),
用户对那些文章点了赞(取消赞)。
2、缓解数据库读写压力
将post_id
写入post_set