Coding & Life

求知若饥,虚心若愚

更新缓存策略

  • 失效:应用程序从cache取数据,没有得到,则从数据库取数据,然后将数据放入cache
  • 命中:应用从cache取数据,若取到则返回
  • 更新:把数据更新到数据库,然后使cache失效

借用网上两张图

有没有其他策略?

读操作容易理解,现在讨论一下写的操作

如果先使cache失效,后更新数据库,是否可行

这么做引发的问题是,如果A,B两个线程同时要更新数据,并且A,B已经都做完了删除缓存这一步,接下来,A先更新了数据库,C线程读取数据,由于缓存没有,则查数据库,并把A更新的数据,写入了缓存,最后B更新数据库。那么缓存和数据库的值就不一致了。

如果先把数据库更新,然后把cache更新(不是失效),是否可行

这么做引发的问题是,如果A,B两个线程同时做数据更新,A先更新了数据库,B后更新数据库,则此时数据库里存的是B的数据。而更新缓存的时候,是B先更新了缓存,而A后更新了缓存,则缓存里是A的数据。这样缓存和数据库的数据也不一致。

如果更新数据时,更新数据库成功,而失效cache时失败怎么解决?

  • 对删除缓存进行重试,数据的一致性要求越高,我越是重试得快。
  • 定期全量更新,简单地说,就是我定期把缓存全部清掉,然后再全量加载。
  • 给所有的缓存一个失效期。(绝招,任何不一致,都可以靠失效期解决,失效期越短,数据一致性越高。但是失效期越短,查数据库就会越频繁。因此失效期应该根据业务来定)

不要缓存那些对数据一致性要求很高的数据

activemq安装

下载

点击下载

解压启动

1
tar -zxvf apache-activemq-5.14.0-bin.tar.gz

进去bin目录 cd apache-activemq-5.14.0/bin 启动 ./activemq start

打开web管理页面

访问http://IP:8161/admin

启动后,activeMQ会占用两个端口,一个是负责接收发送消息的tcp端口:61616,一个是基于web负责用户界面化管理的端口:8161。这两个端口可以在conf下面的xml中找到。http服务器使用了jetty

项目集成activemq

配置文件设置application.properties

1
2
3
4
spring.activemq.broker-url=tcp://localhost:61616 
spring.activemq.user=admin
spring.activemq.password=admin
spring.activemq.pool.enabled=false

生产者

1
2
3
4
5
6
7
8
9
10
11
@Service
public class Producer {
@Resource
private JmsMessagingTemplate jmsMessagingTemplate;

public void sendMsg(String destName, String message) {
Destination destination = new ActiveMQQueue(destName);
jmsMessagingTemplate.convertAndSend(destination, message);
}
}

消费者

1
2
3
4
5
6
7
@Service
public class Consumer {
@JmsListener(destination = "test.queue")
public void receiveMsg(String text) {
System.out.println(">>>>->>>收到消息:" + text);
}
}

发布者

1
2
3
4
5
6
7
8
9
10
11
@Service
public class Publisher {
@Resource
private JmsMessagingTemplate jmsMessagingTemplate;

public void publish(String destName, String message) {
Destination destination = new ActiveMQTopic(destName);

jmsMessagingTemplate.convertAndSend(destination, message);
}
}

订阅者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Service
public class Subscriber {
@JmsListener(destination = "test.topic", containerFactory = "myJmsContainerFactory")
public void subscribe(String text) {
System.out.println("===<<<<收到订阅消息:" + text);
}

@Bean
JmsListenerContainerFactory myJmsContainerFactory(ConnectionFactory connectionFactory) {
SimpleJmsListenerContainerFactory factory = new SimpleJmsListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setPubSubDomain(true);
return factory;
}
}

在pub/sub模式中,对消息的监听需要对containerFactory的配置

代码参考github地址

跨站请求伪造(CSRF)

原理

危害是攻击者可以盗用你的身份,以你的名义发送恶意请求。比如可以盗取你的账号,以你的身份发送邮件,购买商品等

例子

在某个论坛管理页面,管理员可以在list.php页面执行删除帖子操作,根据URL判断删除帖子的id,像这样的一个URL

1
http://localhost/list.php?action=delete&id=12

当恶意用户想管理员发送包含CSFR的邮件,骗取管理员访问 http://test.com/csrf.php 在这个恶意网页中只要包含这样的html语句就可以利用让管理员在不知
情的情况下删除帖子了

1
<img alt="" src="http://localhost/list.php?action=delete&id=12"/>

使用post修改信息就安全了么?

1
2
3
4
5
<?php
$action=$_POST['action'];
$id=$_POST['id'];
delete($action,$id);
?>

同样可以攻击

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html>
  <body>
    <iframe display="none">
      <form method="post" action="http://localhost/list.php">
        <input type="hidden" name="action" value="delete">
        <input type="hidden" name="id" value="12">
<input id="csfr" type="submit"/>
      </form>
    </iframe>

<script type="text/javascript">
     document.getElementById('csfr').submit();
    </script>
  </body>
</html>

如何防范

  1. 使用post,不使用get修改信息
  2. 验证码,所有表单的提交需要验证码,但是貌似用起来很麻烦,所以一些关键的操作可以
  3. 在表单中预先植入一些加密信息,验证请求是此表单发送

跨站脚本攻击(XSS)

原理

跨站脚本攻击(Cross Site Script为了区别于CSS简称为XSS)指的是恶意攻击者往Web页面里插入恶意html代码,当用户浏览该页之时,嵌入其中Web里面的htm
l代码会被执行,从而达到恶意用户的特殊目的

例子

我们有个页面用于允许用户发表留言,然后在页面底部显示留言列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<!DOCTYPE html>
<html>
<head>
<?php include('/components/headerinclude.php');?></head>
<style type="text/css">
.comment-title{
font-size:14px;
margin: 6px 0px 2px 4px;
}

.comment-body{
font-size: 14px;
color:#ccc;
font-style: italic;
border-bottom: dashed 1px #ccc;
margin: 4px;
}
</style>
<script type="text/javascript" src="/js/cookies.js"></script>
<body>
<form method="post" action="list.php">
<div style="margin:20px;">
<div style="font-size:16px;font-weight:bold;">Your Comment</div>
<div style="padding:6px;">
Nick Name:
<br/>
<input name="name" type="text" style="width:300px;"/>
</div>
<div style="padding:6px;">
Comment:
<br/>
<textarea name="comment" style="height:100px; width:300px;"></textarea>
</div>
<div style="padding-left:230px;">
<input type="submit" value="POST" style="padding:4px 0px; width:80px;"/>
</div>
<div style="border-bottom:solid 1px #fff;margin-top:10px;">
<div style="font-size:16px;font-weight:bold;">Comments</div>
</div>
<?php
require('/components/comments.php');
if(!empty($_POST['name'])){
addElement($_POST['name'],$_POST['comment']);
}
renderComments();
?>
</div>
</form>
</body>
</html>

addElement()方法用于添加新的留言,而renderComments()方法用于展留言列表,网页看起来是这样的

因为我们完全信任了用户输入,但有些别有用心的用户会像这样的输入

这样无论是谁访问这个页面的时候控制台都会输出“Hey you are a fool fish!”,如果这只是个恶意的小玩笑,有些人做的事情就不可爱了,有些用户会利用
这个漏洞窃取用户信息、诱骗人打开恶意网站或者下载恶意程序等

如何防范

上面演示的是一个非常简单的XSS攻击,还有很多隐蔽的方式,但是其核心都是利用了脚本注入,因此我们解决办法其实很简单,不信赖用户输入,对特殊字符如”<”,”>”转义,就可以从根本上防止这一问题,当然很多解决方案都对XSS做了特定限制,如上面这中做法在ASP.NET中不幸不同,微软validateRequest对表单提交自动做了XSS验证。但防不胜防,总有些聪明的恶意用户会到我们的网站搞破坏,对自己站点不放心可以看看这个XSS跨站测试代码大全试试站点是否安全。

1
nohup java -jar yatai_pro-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod >/dev/null &

/dev/null 表示将标准输出信息重定向到”黑洞”

轮询(默认策略)

每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,能自动剔除

指定权重

指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的情况。

1
2
3
4
upstream servername { 
server 192.168.0.1 weight=1;
server 192.168.0.2 weight=12;
}

IP绑定 ip_hash

每个请求按访问ip的hash结果分配,这样每个访客固定访问一个后端服务器,可以解决session的问题。

1
2
3
4
5
upstream backserver { 
ip_hash;
server 192.168.0.14:88;
server 192.168.0.15:80;
}

fair(第三方)

按后端服务器的响应时间来分配请求,响应时间短的优先分配。

1
2
3
4
5
upstream backserver { 
server server1;
server server2;
fair;
}

这种策略具有很强的自适应性,但是实际的网络环境往往不是那么简单,因此须慎用。

url_hash(第三方)

按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,后端服务器为缓存时比较有效。

1
2
3
4
5
6
upstream backserver { 
server squid1:3128;
server squid2:3128;
hash $request_uri;
hash_method crc32;
}

问题:CentOS安装nginx后,通过外网不能访问

怀疑:nginx使用80端口,被防火墙拦截

解决办法

编辑防火墙配置文件

1
vim /etc/firewalld/zones/public.xml

增加80端口的开放

1
<port protocol="tcp" port="80"/>

重启防火墙

1
systemctl restart firewalld.service

删除老版本

1
sudo apt-get remove firefox

下载最新软件包

1
http://www.firefox.com.cn/download/

解压到本地

移动文件到/opt文件夹下

1
sudo mv firefox /opt

创建桌面图标

在/usr/share/applications下创建firefox.desktop文件

1
sudo touch firefox.desktop

编辑文件

1
2
3
4
5
6
7
8
9
[Desktop Entry]
Name=Firefox
Name[zh_CN]=火狐浏览器
Comment=this is firefox
Exec=/opt/firefox/firefox
Icon=/opt/firefox/icons/fox.png
Terminal=false
Type=Application
Categories=Application;Network;

pic

购买vultr服务

注册vultr,购买服务(建议地址选Tokyo,操作系统选择CentOS,配置选择最小配置),然后部署服务器

安装shadowsocks

本文安装是是使用CentOS7系统

连接服务器

依次执行以下命令

1
2
3
wget –no-check-certificate https://raw.githubusercontent.com/teddysun/shadowsocks_install/master/shadowsocks.sh
chmod +x shadowsocks.sh
./shadowsocks.sh 2>&1 | tee shadowsocks.log

等几秒钟,根据提示输入shadowsocks密码及端口号

安装ss

成功后出现以下页面,妥善保存信息

安装成功后

配置维护Shadowsocks

配置多用户多端口

修改/etc/shadowsock.json文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"server":"my_server_ip",
"local_address": "127.0.0.1",
"local_port":1080,
"port_password": {
"8381": "foobar1",
"8382": "foobar2",
"8383": "foobar3",
"8384": "foobar4"
},
"timeout":300,
"method":"aes-256-cfb",
"fast_open": false
}

其中,port_password就是将之前的服务器端口和对应的密码结合起来。然后通过使用以下命令,就可以启动多端口多用户了。

1
2
3
4
启动:systemctl start shadowsocks.service
停止:systemctl stop shadowsocks.service
重启:systemctl restart shadowsocks.service
状态:systemctl status shadowsocks.service

centos默认的防火墙机制,会阻隔掉我们的多端口配置。所以,解决方法就是,将这个端口打开tcp和udp通信。这里需要说明的事,在centos版本的更新迭代过程中,centos7和centos6之间的差异性较大。在centos7,采用的是最新的防火墙filewall而不是传统的iptables。
具体操作为,先进入firewalled的配置端口目录,路径为etc/firewalled/zones打开public.xml文件进行端口的编辑.例如

1
2
<port protocol="tcp" port="449"/>   
<portprotocol="udp" port="443"/>

即添加了tcp和udp的权限.然后systemctl restart firewalld.service 重启防火墙,端口就添加到防火墙的白名单中啦。

下载(Shadowsocks客户端)

mac和windows

访问

1
https://github.com/shadowsocks

client
Shadowsocks-windows为windows客户端
ShadowsocksX-NG为mac客户端

iphone

商店搜索super wingy
super_wingy

KCPTun加速

搭建好了Shadowsocks相当于搭建成功了梯子,但是梯子太长,即时梯子带宽足够宽,线路质量也是不忍直视,此时就需要KCPTun了。

KCPTun是一个使用可信UDP来加速TCP传输速度的网络软件。

KCP 是一个快速可靠协议,能以比 TCP浪费10%-20%的带宽的代价,换取平均延迟降低 30%-40%,且最大延迟降低三倍的传输效果。

KCPTun服务安装

1
2
3
4
5
6
// 下载脚本
wget https://raw.githubusercontent.com/kuoruan/kcptun_installer/master/kcptun.sh
// 赋予权限
chmod +x ./kcptun.sh
// 执行脚本
./kcptun.sh

vultr购买推荐地址

1
https://www.vultr.com/?ref=7259520

最近发现购买的vps总是有很多次失败尝试登录的记录,感觉很不安全,遂想到修改ssh默认端口来降低风险

修改配置文件

1
vim /etc/ssh/sshd_config

在**#Port 22下增加端口Port 10001**,去掉22端口的注释(以防修改端口失败,默认端口也不能登录)

SSH默认监听端口就是22,上面我保留了22端口,以防修改端口失败,默认端口也不能登录

增加10001端口,大家修改端口时候最好挑10000~65535之间的端口号,10000以下容易被系统或一些特殊软件占用,或是以后新应用准备占用该端口的时候,却被你先占用了,导致软件无法运行。

修改SELinux配置

查看是否开启了SELinux

1
sestatus -v

如果没有开启,则跳过这一步

查看SELinux开放给ssh使用的端口

1
semanage port -l|grep ssh

ssh_port_t tcp 22

SELinux开放10001端口给ssh

1
semanage port -a -t ssh_port_t -p tcp 10001

完成后再次查看

1
semanage port -l|grep ssh

ssh_port_t tcp 22,10001

防火墙放开10001端口

修改防火墙配置文件

1
vim /etc/firewalld/zones/public.xml

增加100001的端口放开

1
<port protocol="tcp" port="10001"/>

重启

1
2
3
systemctl restart sshd   重启ssh
systemctl restart firewalld.service 重启防火墙
shutdown -r now 重启服务器

重新登录

1
ssh root@localhost -p 10001

如果登录成功注释掉上边的22端口配置

修改了默认端口之后尝试登录的攻击显著降低

pic

问题描述

最近搞微信公众平台接口时,我们获得access_token的处理方式是:先在缓存中查找,如果找到,返回;否则,调用微信接口获得,放入缓存(有时效),然后返回;

此处的逻辑并不严谨

如果有两个线程 同时 获得access_token,而且此时恰好access_token在缓存中因为过期而删除了,那么两个线程都会调用微信接口请求access_token,这时就会出现第二次请求的结果把第一次的结果覆盖掉的情况。

最初实现方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public JSONObject getAccessToken(String appid, String appsecret) {
String reqestUrl = ACCESS_TOKEN_URL.replace("APPID", appid).replace("APPSECRET", appsecret);

RedisUtils<String> redisUtils = new RedisUtils<>(redisTemplate);

String accessToken = redisUtils.get(ACCESS_TOKEN_CACHE_KEY);

if (StrUtil.isNotEmpty(accessToken)) {
return JSONObject.parseObject(accessToken);
}

JSONObject resultJSON = JSONObject.parseObject(HttpUtil.get(reqestUrl));

if (StrUtil.isNotEmpty(resultJSON.getString("errcode"))) {
log.info(resultJSON.getString("errmsg"));
return null;
}

redisUtils.set(ACCESS_TOKEN_CACHE_KEY, resultJSON.toJSONString(), 7000L);

return resultJSON;
}

解决方法

把请求微信接口的方法加入同步代码块中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public JSONObject getAccessToken(String appid, String appsecret) {
String reqestUrl = ACCESS_TOKEN_URL.replace("APPID", appid).replace("APPSECRET", appsecret);

RedisUtils<String> redisUtils = new RedisUtils<>(redisTemplate);

String accessToken = redisUtils.get(ACCESS_TOKEN_CACHE_KEY);

if (StrUtil.isNotEmpty(accessToken)) {
return JSONObject.parseObject(accessToken);
}

// 防止高并发情况下,access_token覆盖的问题
synchronized (appid) {
// 再次验证缓存中的数据
String accessToken2 = redisUtils.get(ACCESS_TOKEN_CACHE_KEY);
if (StrUtil.isNotEmpty(accessToken2)) {
log.info("multi thread hits!");
return JSONObject.parseObject(accessToken2);
}

JSONObject resultJSON = JSONObject.parseObject(HttpUtil.get(reqestUrl));

if (StrUtil.isNotEmpty(resultJSON.getString("errcode"))) {
log.info(resultJSON.getString("errmsg"));
return null;
}

redisUtils.set(ACCESS_TOKEN_CACHE_KEY, resultJSON.toJSONString(), 7000L);

return resultJSON;
}
}
0%