之前,个人博客网站里的文章访问量都是存在MySQL中的。每次访问都需要通过文章id查数据库,获得浏览量后+1操作。之后在更新数据库,为了实时显示文章浏览量还要在读一遍数据库。访问量多了之后就带来了线程安全问题:两个线程同时获得数量,各自+1后更新,这个访问量就有问题了。对于这个情况,之前直接用synchronized粗暴的对操作加锁。这带来一个问题,多个线程只能有一个线程来获取文章信息,其他线程阻塞,。后来又想到另一个解决方案,浏览量的增加独立出来,不和读取文章信息绑定。每读取一个文章信息后返回调用一个接口来增加数量。这有一个缺陷就是显示的文章访问量不是实时的。
最后决定使用redis来存储文章的访问量,redis的关键语句是incr key。作用是key的value+1;key和文章id绑定。由于redis是单线程的,单个语句的执行是线程安全的。
理想很丰满,但操作的时候还是遇到了不少问题。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
设置redis配置
redis:
database: 0
# Redis服务器地址 写你的ip
host: XXX.XX.XX.XXX
# Redis服务器连接端口
port: 你的端口号
# Redis服务器连接密码(默认为空)
password: 你的密码
timeout: 6000
lettcue:
pool:
# 连接池中的最大空闲连接,默认值也是8。
max-idle: 100
# 连接池中的最小空闲连接,默认值也是0。
min-idle: 10
# 如果赋值为-1,则表示不限制;如果pool已经分配了maxActive个jedis实例,则此时pool的状态为exhausted(耗尽)
max-active: 8
# 等待可用连接的最大时间,单位毫秒,默认值为-1,表示永不超时。如果超过等待时间,则直接抛出JedisConnectionException
max-wait: 2000
如果不设置redis的连接池属性的话默认只有一个连接,多线程来使用redisTemplate会报”redis连接异常“。
设置redisTemplate类模板
@Bean
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory connectionFactory) {
// 配置redisTemplate
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());//key序列化
redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<Object>(Object.class));//value序列化
return redisTemplate;
}
JUnit测试类:
@Resource
RedisUtils redisUtils;
@Test
public void redisTest(){
try {
Runnable runnable = new Runnable(){
@SneakyThrows
@Override
public void run() {
for(int i=0;i<500;i++){
Thread.sleep(1);
System.out.println(Thread.currentThread().getName()+": "+addReadNums());
}
}
};
ExecutorService executorService = Executors.newFixedThreadPool(5);
executorService.execute(runnable);//线程1
executorService.execute(runnable);//线程2
System.out.println("@Test线程执行完毕");
} catch (Exception e) {
System.out.println(e);
}
}
public Long addReadNums(){
return redisUtils.incrBy("test", 1);
}
进行多线程模拟测试的时候就报错了,lettuce线程池在多线程下有问题,查找资料后都说用Jedis作为redis的线程池。
修改后的依赖:在spring-boot-starter-data-redis去除lettuce线程池,导入jedis
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
<exclusions>
<exclusion>
<groupId>io.lettucegroupId>
<artifactId>lettuce-coreartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-pool2artifactId>
dependency>
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
<version>3.3.0version>
dependency>
这里要注意Jedis的版本和spring-boot-starter-data-redis版本,版本不一致会导致redisTemplate创建失败。
参考:【异常】nested exception is java.lang.NoClassDefFoundError: redis/clients/jedis/util/SafeEncoder
还要修改redisTemplate类创建方式
@Bean
public RedisTemplate<String, Object> redisTemplate(JedisConnectionFactory connectionFactory) {
// 配置redisTemplate
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());//key序列化
redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<Object>(Object.class));//value序列化
return redisTemplate;
}
这时候测试发现,线程运行一会就打印:Shutting down ExecutorService ‘applicationTaskExecutor‘ 。之后线程就停止了。发现是JUnit单元测试的坑:它会在主线程结束后调用相关的System.exit()方法,将JVM关闭,所以,子线程被动挂了。
参考:JUnit单元测试中多线程的坑
修改后的测试类:
@Test
public void redisTest(){
try {
Runnable runnable = new Runnable(){
@SneakyThrows
@Override
public void run() {
for(int i=0;i<500;i++){
Thread.sleep(1);
System.out.println(Thread.currentThread().getName()+": "+addReadNums());
}
}
};
ExecutorService executorService = Executors.newFixedThreadPool(5);
executorService.execute(runnable);//线程1
executorService.execute(runnable);//线程2
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("@Test线程执行完毕");
} catch (Exception e) {
System.out.println(e);
}
}
public Long addReadNums(){
return redisUtils.incrBy("test", 1);
}
发现两个线程下新增500还是没有问题的。
看一下redis确实是1000。
最后对于文章浏览量,只要key传入的是文章的id即可。如果担心key与其他的内容重复,添加标识符区别即可。