Spring整合Mybatis的乐观锁与悲观锁详情

Spring整合Mybatis的乐观锁与悲观锁详情

一、概述

前面一篇《Spring和Mybatis整合详解》介绍了Spring如何结合mybatis进行数据库访问操作。上一篇《Spring和SpringDataJpa整合的乐观锁与悲观锁详情》也介绍了Spring-data-jpa如何进行乐观锁和悲观锁的使用。这一篇介绍下springmvc环境下Mybatis如何进行乐观锁、悲观锁的使用。

悲观锁和乐观锁的概念:

  • 悲观锁:就是独占锁,不管读写都上锁了。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。

  •  

  • 乐观锁:不上锁,读取的时候带版本号,写入的时候带着这个版本号,如果不一致就失败,乐观锁适用于多读的应用类型,因为写多的时候会经常失败。

代码可以在Spring组件化构建https://www.pomit.cn/java/spring/spring.html中的MybatisLock组件中查看,并下载。

首发地址:

  品茗IT-同步发布

品茗IT提供在线支持:

  一键快速构建Spring项目工具

  一键快速构建SpringBoot项目工具

  一键快速构建SpringCloud项目工具

  一站式Springboot项目生成

  Mysql一键生成Mybatis注解Mapper

  Mysql一键生成SpringDataRest项目

如果大家正在寻找一个java的学习环境,或者在开发中遇到困难,可以加入我们的java学习圈,点击即可加入,共同学习,节约学习时间,减少很多在学习中遇到的难题。

二、环境配置

本文假设你已经引入Spring必备的一切了,已经是个Spring项目了,如果不会搭建,可以打开这篇文章看一看《Spring和Spring Mvc 5整合详解》

2.1 maven依赖

和前面的《Spring和Mybatis整合详解》的配置一样,
使用Mybatis需要引入mybatis和mybatis-spring,已经数据源和connector。

<?xml version="1.0"?>
<project
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
    xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>cn.pomit</groupId>
        <artifactId>SpringWork</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <artifactId>MybatisLock</artifactId>
    <packaging>jar</packaging>
    <name>MybatisLock</name>
    <url>http://maven.apache.org</url>
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-orm</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-dbcp2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
        </dependency>
    </dependencies>
    <build>
        <finalName>MybatisLock</finalName>
    </build>
</project>


父模块可以在https://www.pomit.cn/spring/SpringWork/pom.xml获取。

2.2 Spring配置

需要配置数据源、jdbcTemplate、sqlSessionFactory、transactionManager和MapperScannerConfigurer。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
                    http://www.springframework.org/schema/beans
                    http://www.springframework.org/schema/beans/spring-beans.xsd
                    http://www.springframework.org/schema/tx 
                    http://www.springframework.org/schema/tx/spring-tx.xsd
                    http://www.springframework.org/schema/aop 
                    http://www.springframework.org/schema/aop/spring-aop.xsd
                    http://www.springframework.org/schema/context      
                    http://www.springframework.org/schema/context/spring-context.xsd">

    <bean id="annotationPropertyConfigurerMybatisLock"
        class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
        <property name="order" value="1" />
        <property name="ignoreUnresolvablePlaceholders" value="true" />
        <property name="locations">
            <list>
                <value>classpath:mybatis.properties</value>
            </list>
        </property>
    </bean>
    
    <bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="${db.dirverClass}"></property>
        <property name="url" value="${db.url}" />
        <property name="username" value="${db.username}" />
        <property name="password" value="${db.password}" />

        <property name="initialSize" value="1" />
        <property name="minIdle" value="1" />
        <property name="maxTotal" value="20" />

        <property name="validationQuery" value="SELECT 1" />
        <property name="testWhileIdle" value="true" />
        <property name="testOnBorrow" value="false" />
        <property name="testOnReturn" value="false" />
    </bean>

    <bean id="transactionManager"
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <tx:annotation-driven transaction-manager="transactionManager" />

    <!-- jdbcTemplate -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <!-- mybatis -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="configLocation" value="classpath:mybatis/mybatis-config.xml" />
        
        <!-- 如果用xml方式,而不是用注解去写sql,这地方要注明xml文件的路径 -->
    </bean>
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="cn.pomit.springwork.mybatislock.mapper" />
    </bean>
</beans>

这里面,需要注意的是:

  • dataSource,这里用的是dbcp2数据源。

  • sqlSessionFactory,是mybatis的连接信息配置。

  • MapperScannerConfigurer,指明mapper的路径,要使用Mybatis的注解sql必须指明。

  • transactionManager,事务处理器。

  • tx:annotation-driven:开启事务注解。

mybatis.properties中存放数据库的地址端口等连接信息。

mybatis.properties:

db.url=jdbc:mysql://127.0.0.1:3306/boot?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
db.username=cff
db.password=123456
db.dirverClass=com.mysql.cj.jdbc.Driver

mybatis/mybatis-config.xml:

<?xml version="1.0" encoding="UTF-8" ?>  
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">  
<configuration> 
<settings>    
        <setting name="logImpl" value="STDOUT_LOGGING" />      
    </settings> 
</configuration>  

这里我只配置了日志打印功能。

三、悲观锁

悲观锁在数据库的访问中使用,表现为:前一次请求没执行完,后面一个请求就一直在等待。

3.1 Dao层

数据库要实现悲观锁,就是将sql语句带上for update即可。 for update 是行锁

所在mybatis的查询sql加上for update,就实现了对当前记录的锁定,就实现了悲观锁。

UserInfoDao :

package cn.pomit.springwork.mybatislock.mapper;

import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;

import cn.pomit.springwork.mybatislock.domain.UserInfo;

@Mapper
public interface UserInfoDao {
    
    @Select({
        "<script>",
            "SELECT ",
            "user_name as userName,passwd,name,mobile,valid, user_type as userType, version as version",
            "FROM user_info_test",
            "WHERE user_name = #{userName,jdbcType=VARCHAR} for update",
       "</script>"})
    UserInfo findByUserNameForUpdate(@Param("userName") String userName);
    
    @Update({
        "<script>",
        " update user_info_test set",
        " name = #{name, jdbcType=VARCHAR}, mobile = #{mobile, jdbcType=VARCHAR},version=version+1 ",
        " where user_name=#{userName}",
        "</script>"
    })
    int update(UserInfo userInfo);

    @Insert({
        "<script>",
        "INSERT INTO user_info_test",
        "( user_name,",
        "name ,",
        "mobile,",
        "passwd,",
        "version",
         ") ",
        " values ",
         "( #{userName},",
         "#{name},",
         "#{mobile},",
         "#{passwd},",
         "#{version}",
        " ) ",
        "</script>"
    })
    int save(UserInfo entity);
}

这里,findByUserNameForUpdate的sql中加上了for update。update就是普通的更新而已。

3.2 Service层

更新数据库前,先调用findByUserNameForUpdate方法,使上面的配置的悲观锁锁定表记录,然后再更新。

UserInfoService :

package cn.pomit.springwork.mybatislock.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import cn.pomit.springwork.mybatislock.domain.UserInfo;
import cn.pomit.springwork.mybatislock.mapper.UserInfoDao;

@Service
public class UserInfoService {
    @Autowired
    UserInfoDao userInfoDao;

    public void save(UserInfo entity) {
        entity.setVersion(0);
        userInfoDao.save(entity);
    }
    

    @Transactional
    public UserInfo getUserInfoByUserNamePessimistic(String userName) {
        return userInfoDao.findByUserNameForUpdate(userName);
    }
    
    @Transactional
    public void updateWithTimePessimistic(UserInfo entity, int time) throws InterruptedException {      
        UserInfo userInfo = userInfoDao.findByUserNameForUpdate(entity.getUserName());
        if (userInfo == null)
            return;

        if (!StringUtils.isEmpty(entity.getMobile())) {
            userInfo.setMobile(entity.getMobile());
        }
        if (!StringUtils.isEmpty(entity.getName())) {
            userInfo.setName(entity.getName());
        }
        Thread.sleep(time * 1000L);

        userInfoDao.update(userInfo);
    }
    
    @Transactional
    public void updatePessimistic(UserInfo entity) {
        UserInfo userInfo = userInfoDao.findByUserNameForUpdate(entity.getUserName());
        if (userInfo == null)
            return;

        if (!StringUtils.isEmpty(entity.getMobile())) {
            userInfo.setMobile(entity.getMobile());
        }
        if (!StringUtils.isEmpty(entity.getName())) {
            userInfo.setName(entity.getName());
        }

        userInfoDao.update(userInfo);
    }

}

测试中,我们在update方法中sleep几秒,其他线程的update将一直等待。

3.3 测试Web层

可以先调用/update/{time}接口,延迟执行,然后马上调用/update接口,会发现,/update接口一直在等待/update/{time}接口执行完成。

MybatisPessLockRest :

package cn.pomit.springwork.mybatislock.web;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import cn.pomit.springwork.mybatislock.domain.UserInfo;
import cn.pomit.springwork.mybatislock.service.UserInfoService;

/**
 * 测试乐观锁
 * 
 * @author fufei
 *
 */
@RestController
@RequestMapping("/mybatispesslock")
public class MybatisPessLockRest {

    @Autowired
    UserInfoService userInfoService;

    @RequestMapping(value = "/detail/{name}", method = { RequestMethod.GET })
    public UserInfo detail(@PathVariable("name") String name) {
        return userInfoService.getUserInfoByUserNamePessimistic(name);
    }

    @RequestMapping(value = "/save")
    public String save(@RequestBody UserInfo userInfo) throws InterruptedException {
        userInfoService.save(userInfo);
        return "0000";
    }

    @RequestMapping(value = "/update/{time}")
    public String update(@RequestBody UserInfo userInfo, @PathVariable("time") int time) throws InterruptedException {
        userInfoService.updateWithTimePessimistic(userInfo, time);

        return "0000";
    }

    @RequestMapping(value = "/update")
    public String update(@RequestBody UserInfo userInfo) throws InterruptedException {
        userInfoService.updatePessimistic(userInfo);
        return "0000";
    }
}


四、乐观锁

数据库访问dao层还是3.1那个UserInfoDao。

4.1 Dao层

UserInfoDao更新时,需要携带version字段进行更新:and version = #{version}。如果version不一致,是不会更新成功的,这时候,我们的select查询是不能带锁的。

UserInfoDao :

package cn.pomit.springwork.mybatislock.mapper;

import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;

import cn.pomit.springwork.mybatislock.domain.UserInfo;

@Mapper
public interface UserInfoDao {
    @Select({
        "<script>",
            "SELECT ",
            "user_name as userName,passwd,name,mobile,valid, user_type as userType, version as version",
            "FROM user_info_test",
            "WHERE user_name = #{userName,jdbcType=VARCHAR}",
       "</script>"})
    UserInfo findByUserName(@Param("userName") String userName);
    
    @Update({
        "<script>",
        " update user_info_test set",
        " name = #{name, jdbcType=VARCHAR}, mobile = #{mobile, jdbcType=VARCHAR},version=version+1 ",
        " where user_name=#{userName} and version = #{version}",
        "</script>"
    })
    int updateWithVersion(UserInfo userInfo);

    @Insert({
        "<script>",
        "INSERT INTO user_info_test",
        "( user_name,",
        "name ,",
        "mobile,",
        "passwd,",
        "version",
         ") ",
        " values ",
         "( #{userName},",
         "#{name},",
         "#{mobile},",
         "#{passwd},",
         "#{version}",
        " ) ",
        "</script>"
    })
    int save(UserInfo entity);
}

4.2 Service层

service层我们做一下简单的调整。更新数据库前,先调用findByUserName方法,查询出当前的版本号,然后再更新。

UserInfoService :

package cn.pomit.springwork.mybatislock.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import cn.pomit.springwork.mybatislock.domain.UserInfo;
import cn.pomit.springwork.mybatislock.mapper.UserInfoDao;

@Service
public class UserInfoService {
    @Autowired
    UserInfoDao userInfoDao;
    public UserInfo getUserInfoByUserName(String userName){
        return userInfoDao.findByUserName(userName);
    }

    public void save(UserInfo entity) {
        entity.setVersion(0);
        userInfoDao.save(entity);
    }
    @Transactional
    public void updateWithTimeOptimistic(UserInfo entity, int time) throws Exception {      
        UserInfo userInfo = userInfoDao.findByUserName(entity.getUserName());
        if (userInfo == null)
            return;

        if (!StringUtils.isEmpty(entity.getMobile())) {
            userInfo.setMobile(entity.getMobile());
        }
        if (!StringUtils.isEmpty(entity.getName())) {
            userInfo.setName(entity.getName());
        }
        Thread.sleep(time * 1000L);

        int ret = userInfoDao.updateWithVersion(userInfo);
        if(ret < 1)throw new Exception("乐观锁导致保存失败");
    }

    @Transactional
    public void updateOptimistic(UserInfo entity) throws Exception {
        UserInfo userInfo = userInfoDao.findByUserName(entity.getUserName());
        if (userInfo == null)
            return;

        if (!StringUtils.isEmpty(entity.getMobile())) {
            userInfo.setMobile(entity.getMobile());
        }
        if (!StringUtils.isEmpty(entity.getName())) {
            userInfo.setName(entity.getName());
        }

        int ret = userInfoDao.updateWithVersion(userInfo);
        if(ret < 1)throw new Exception("乐观锁导致保存失败");
    }

}

4.2 测试Web层

可以先调用/update/{time}接口,延迟执行,然后马上调用/update接口,会发现,/update接口不会等待/update/{time}接口执行完成,读取完版本号能够成功更新数据,但是/update/{time}接口等待足够时间以后,更新的时候会失败,因为它的版本和数据库的已经不一致了。

注意: 这里更新失败不会抛异常,但是返回值会是0,即更新不成功,需要自行判断。jpa的乐观锁可以抛出异常,手动catch到再自行处理。

MybatisOptiLockRest :

package cn.pomit.springwork.mybatislock.web;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import cn.pomit.springwork.mybatislock.domain.UserInfo;
import cn.pomit.springwork.mybatislock.service.UserInfoService;

/**
 * 测试乐观锁
 * @author fufei
 *
 */
@RestController
@RequestMapping("/mybatislock")
public class MybatisOptiLockRest {

    @Autowired
    UserInfoService userInfoService;

    @RequestMapping(value = "/detail/{name}", method = { RequestMethod.GET })
    public UserInfo detail(@PathVariable("name") String name) {
        return userInfoService.getUserInfoByUserName(name);
    }

    @RequestMapping(value = "/save")
    public String save(@RequestBody UserInfo userInfo) throws InterruptedException {
        userInfoService.save(userInfo);
        return "0000";
    }

    @RequestMapping(value = "/update/{time}")
    public String update(@RequestBody UserInfo userInfo, @PathVariable("time") int time) throws Exception {
        userInfoService.updateWithTimeOptimistic(userInfo, time);

        return "0000";
    }

    @RequestMapping(value = "/update")
    public String update(@RequestBody UserInfo userInfo) throws Exception {
        userInfoService.updateOptimistic(userInfo);
        return "0000";
    }
}

五、过程中用到的完整实体和Service

UserInfo:




UserInfoService :


UserInfoDao:


详细完整的实体和逻辑,可以访问品茗IT-博客《Spring整合Mybatis的乐观锁与悲观锁详情》进行查看

品茗IT-博客专题:https://www.pomit.cn/lecture.html汇总了Spring专题Springboot专题SpringCloud专题web基础配置专题。

快速构建项目

Spring组件化构建

SpringBoot组件化构建

SpringCloud服务化构建

喜欢这篇文章么,喜欢就加入我们一起讨论Spring技术吧!


品茗IT交流群
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容

  • 这部分主要是开源Java EE框架方面的内容,包括Hibernate、MyBatis、Spring、Spring ...
    杂货铺老板阅读 1,343评论 0 2
  • web service 相关 什么是Web Service? 答:从表面上看,Web Service就是一个应用程...
    niuben阅读 903评论 0 3
  • 本期维权公告,公布简书作者念伊伊、没文化的野狐狸、莫嫡Morettie、小万PPT、蓝胖说说的文章,在微信公众号平...
    行距版君阅读 3,307评论 21 70
  • 一直到月上中天,夜才渐渐冷清下来。 灯下影子已散尽,只剩一个还在彷徨。冷子文愁苦地望着星,吐着烟圈,思索着。 夜越...
    霑露饮冰小先生阅读 465评论 0 3
  • 你离开的第1097天。 突然想起前天一整夜的失眠,回想起来大概是你在提醒我又到了7.6的日子…… 我在努力的开心生...
    一个天真的情绪疯子0704阅读 206评论 0 0