利用MySQL中的乐观锁和悲观锁实现分布式锁
背景
对于一些并发量不是很高的场景,使用MySQL的乐观锁实现会比较精简且巧妙。
下面就一个小例子,针对不加锁、乐观锁以及悲观锁这三种方式来实现。
主要是一个用户表,它有一个年龄的字段,然后并发地对其加一,看看结果是否正确。
一些基础实现类
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private Integer age;
    private String name;
    private Long id;
    private Long version;
}
public interface UserMapper {
    @Results(value = {
            @Result(property = "id", column = "id", javaType = Long.class, jdbcType = JdbcType.BIGINT),
            @Result(property = "age", column = "age", javaType = Integer.class, jdbcType = JdbcType.INTEGER),
            @Result(property = "name", column = "name", javaType = String.class, jdbcType = JdbcType.VARCHAR),
            @Result(property = "version", column = "version", javaType = Long.class, jdbcType = JdbcType.BIGINT),
    })
    @Select("SELECT id, age, name, version FROM user WHERE id = #{id}")
    User getUser(Long id);
    @Update("UPDATE user SET age = #{age}, version=version+1 WHERE id = #{id} AND version = #{version}")
    Boolean compareAndSetAgeById(Long id, Long version, Integer age);
    @Update("UPDATE user SET age = #{age} WHERE id = #{id}")
    Boolean setAgeById(Long id, Integer age);
    @Select("SELECT id, age, name, version FROM user WHERE id = #{id} for update")
    User getUserForUpdate(Long id);
}
private static void exe(CountDownLatch countDownLatch, int threads, Runnable runnable) {
    ExecutorService executorService = Executors.newCachedThreadPool();
    for (int i = 0; i < threads; i++) {
        executorService.execute(() -> {
            runnable.run();
            countDownLatch.countDown();
        });
    }
    executorService.shutdown();
}
private static User getUser(long id) {
    try (SqlSession session = openSession(getDataSource1())) {
        UserMapper userMapper = session.getMapper(UserMapper.class);
        return userMapper.getUser(id);
    }
}
public static SqlSession openSession(DataSource dataSource) {
    TransactionFactory transactionFactory = new JdbcTransactionFactory();
    Environment environment = new Environment("development", transactionFactory, dataSource);
    Configuration configuration = new Configuration(environment);
    configuration.addMapper(UserMapper.class);
    configuration.setCacheEnabled(false);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
    return sqlSessionFactory.openSession();
}
private static DataSource getDataSource1() {
    MysqlConnectionPoolDataSource dataSource = new MysqlConnectionPoolDataSource();
    dataSource.setUser("root");
    dataSource.setPassword("root");
    dataSource.setUrl("jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8");
    return dataSource;
}
不加锁
private static Boolean addAge(long id, int value) {
    Boolean result;
    try (SqlSession session = openSession(getDataSource1())) {
        UserMapper userMapper = session.getMapper(UserMapper.class);
        User user = userMapper.getUser(id);
        result = userMapper.setAgeById(user.getId(), user.getAge() + value);
        session.commit();
    }
    return result;
}
public static void main(String[] args) throws Exception {
    long id = 1L;
    int threads = 50;
    CountDownLatch countDownLatch = new CountDownLatch(threads);
    log.info("user:{}", getUser(id));
    long start = System.currentTimeMillis();
    exe(countDownLatch, threads, () -> addAge(1, 1));
    countDownLatch.await();
    long end = System.currentTimeMillis();
    log.info("end - start : {}", end - start);
    log.info("user:{}", getUser(id));
}
865  [main] INFO  cn.eagle.li.mybatis.UpdateMain - user:User(age=0, name=3, id=1, version=0)
1033 [main] INFO  cn.eagle.li.mybatis.UpdateMain - end - start : 164
1046 [main] INFO  cn.eagle.li.mybatis.UpdateMain - user:User(age=9, name=3, id=1, version=0)
从输出可以看出,50个并发,但是执行成功的只有9个,这种实现很明显是有问题的。
乐观锁
private static Boolean compareAndAddAge(long id, int value, int times) {
    int time = 0;
    Boolean result = false;
    while (time++ < times && BooleanUtils.isFalse(result)) {
        result = compareAndAddAge(id, value);
    }
    return result;
}
private static Boolean compareAndAddAge(long id, int value) {
    try (SqlSession session = openSession(getDataSource1())) {
        UserMapper userMapper = session.getMapper(UserMapper.class);
        User user = userMapper.getUser(id);
        Boolean result = userMapper.compareAndSetAgeById(id, user.getVersion(), user.getAge() + value);
        session.commit();
        return result;
    }
}
public static void main(String[] args) throws Exception {
    long id = 1L;
    int threads = 50;
    CountDownLatch countDownLatch = new CountDownLatch(threads);
    log.info("user:{}", getUser(id));
    long start = System.currentTimeMillis();
    exe(countDownLatch, threads, () -> addAge(1, 1, 20));
    countDownLatch.await();
    long end = System.currentTimeMillis();
    log.info("end - start : {}", end - start);
    log.info("user:{}", getUser(id));
}
758  [main] INFO  cn.eagle.li.mybatis.UpdateMain - user:User(age=0, name=3, id=1, version=0)
1270 [main] INFO  cn.eagle.li.mybatis.UpdateMain - end - start : 509
1277 [main] INFO  cn.eagle.li.mybatis.UpdateMain - user:User(age=50, name=3, id=1, version=50)
从输出可以看出,并发的情况下,结果是没问题的。
悲观锁
private static Boolean addAgeForUpdate(long id, int value) {
    Boolean result;
    try (SqlSession session = openSession(getDataSource1())) {
        UserMapper userMapper = session.getMapper(UserMapper.class);
        User user = userMapper.getUserForUpdate(id);
        result = userMapper.setAgeById(id, user.getAge() + value);
        session.commit();
    }
    return result;
}
public static void main(String[] args) throws Exception {
    long id = 1L;
    int threads = 50;
    CountDownLatch countDownLatch = new CountDownLatch(threads);
    log.info("user:{}", getUser(id));
    long start = System.currentTimeMillis();
    exe(countDownLatch, threads, () -> addAgeForUpdate(1, 1));
    countDownLatch.await();
    long end = System.currentTimeMillis();
    log.info("end - start : {}", end - start);
    log.info("user:{}", getUser(id));
}
631  [main] INFO  cn.eagle.li.mybatis.UpdateMain - user:User(age=0, name=3, id=1, version=50)
829  [main] INFO  cn.eagle.li.mybatis.UpdateMain - end - start : 196
837  [main] INFO  cn.eagle.li.mybatis.UpdateMain - user:User(age=50, name=3, id=1, version=50)
从输出可以看出,并发的情况下,结果是没问题的。
总结
从以上来看,乐观锁和悲观锁实现都是没有问题的,至于选哪一种,还是要看业务的场景,比如说并发量的多少,加锁时长等等。
利用MySQL中的乐观锁和悲观锁实现分布式锁的更多相关文章
- mysql中的乐观锁和悲观锁
		mysql中的乐观锁和悲观锁的简介以及如何简单运用. 关于mysql中的乐观锁和悲观锁面试的时候被问到的概率还是比较大的. mysql的悲观锁: 其实理解起来非常简单,当数据被外界修改持保守态度,包括 ... 
- 图解Janusgraph系列-并发安全:锁机制(本地锁+分布式锁)分析
		图解Janusgraph系列-并发安全:锁机制(本地锁+分布式锁)分析 大家好,我是洋仔,JanusGraph图解系列文章,实时更新~ 图数据库文章总目录: 整理所有图相关文章,请移步(超链):图数据 ... 
- SpringBoot--防止重复提交(锁机制---本地锁、分布式锁)
		防止重复提交,主要是使用锁的形式来处理,如果是单机部署,可以使用本地缓存锁(Guava)即可,如果是分布式部署,则需要使用分布式锁(可以使用zk分布式锁或者redis分布式锁),本文的分布式锁以red ... 
- Spring Boot 2实现分布式锁——这才是实现分布式锁的正确姿势!
		参考资料 网址 Spring Boot 2实现分布式锁--这才是实现分布式锁的正确姿势! http://www.spring4all.com/article/6892 
- 老司机带大家领略MySQL中的乐观锁和悲观锁
		原文地址:https://cloud.tencent.com/developer/news/227982 为什么需要锁 在并发环境下,如果多个客户端访问同一条数据,此时就会产生数据不一致的问题,如何解 ... 
- MYSQL中的乐观锁实现(MVCC)简析
		https://segmentfault.com/a/1190000009374567#articleHeader2 什么是MVCC MVCC即Multi-Version Concurrency Co ... 
- 分布式锁(一) Zookeeper分布式锁
		什么是Zookeeper? Zookeeper(业界简称zk)是一种提供配置管理.分布式协同以及命名的中心化服务,这些提供的功能都是分布式系统中非常底层且必不可少的基本功能,但是如果自己实现这些功能而 ... 
- 分布式锁之三:Redlock实现分布式锁
		之前写过一篇文章<如何在springcloud分布式系统中实现分布式锁?>,由于自己仅仅是阅读了相关的书籍,和查阅了相关的资料,就认为那样的是可行的.那篇文章实现的大概思路是用setNx命 ... 
- 【面试普通人VS高手系列】请说一下你对分布式锁的理解,以及分布式锁的实现
		一个工作了7年的Java程序员,私信我关于分布式锁的问题. 一上来就两个灵魂拷问: Redis锁超时怎么办? Redis主从切换导致锁失效怎么办? 我说,别着急,这些都是小问题. 那么,关于" ... 
随机推荐
- JS基础代码
			1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="U ... 
- Java中时间类中的Data类与Time类
			小简博客 - 小简的技术栈,专注Java及其他计算机技术.互联网技术教程 (ideaopen.cn) Data类 Data类中常用方法 boolean after(Date date) 若当调用此方法 ... 
- Java创建boolean型数组
			Java如何声明并初始化一个boolean型的数组? public class Main{ static boolean[] arr1 = new boolean[20]; public static ... 
- 微信小程序订阅消息
			概述 消息能力是小程序能力中的重要组成,我们为开发者提供了订阅消息能力,以便实现服务的闭环和更优的体验. 订阅消息推送位置:服务通知 订阅消息下发条件:用户自主订阅 订阅消息卡片跳转能力:点击查看详情 ... 
- Git (常用命令)
			某程序猿退休后决定练习书法,于是花重金买下文房四宝.某日,饭后突生雅兴,一番磨墨拟纸 并点上上好檀香.定神片刻,泼墨挥毫,郑重地写下一行:Hello World 斯~ 有被冷到吗哈哈哈 Git常用命令 ... 
- Shell脚本实战:日志关键字监控+自动告警
			一个执着于技术的公众号 该程序使用场景说明:主要用于Linux服务器监控程序日志,如出现关键字异常则触发相应的动作或告警操作,通知到邮件联系人. 一.安装邮件服务 1.解压 tar -jxf mail ... 
- Redis设计与实现2.1:数据库和事件
			数据库和事件 这是<Redis设计与实现>系列的文章,系列导航:Redis设计与实现笔记 数据库 数据库的结构定义在 redis.h/redisServer 这个结构体中,这个结构体有许多 ... 
- unity---脚本创建按钮
			脚本创建按钮 新建文件夹 Resources 方便引用图片 在文件Resources中新建Images,并且下载一个图片 没有图片,按钮内容无法显示 图片需要处理一下 Textrue Type 改为 ... 
- Zookeeper安装学习(二)
			学习内容:Zookeeper集群安装(Zookeeper版本:Zookeeper3.5.7:注:master,s1,s2都需要部署) 解压安装: (1)在主机 master 解压 Zookeeper ... 
- 《回炉重造 Java 基础》——集合(容器)
			整体框架 绿色代表接口/抽象类:蓝色代表类. 主要由两大接口组成,一个是「Collection」接口,另一个是「Map」接口. 前言 以前刚开始学习「集合」的时候,由于没有好好预习,也没有学好基础知识 ... 
